7

I'm working of a fairly simple world globe interface using D3 and the D3.geo.projection to create a spinning globe with data points on it.

Everything worked fine (i.e. the points "eclipsed" when they rotated away behind the horizon) when I was just plotting the points with circles:

svg.append("g")
    .attr("class","points")
    .selectAll("text")
    .data(places.features)
  .enter()

  //for circle-point------------------------------
  .append("path")
  .attr("d", path.pointRadius(function(d) {
      if (d.properties)
        return 3+(4*d.properties.scalerank);
                    }))
    .attr("d", path)

    .attr("class", "point")
    .on("click",pointClick)
;

But now I'm trying to plot symbols instead of circles:

svg.append("g")
    .attr("class","points")
    .selectAll("text")
    .data(places.features)
  .enter()
    //for image-------------------------------------
    .append("image")
    .attr("xlink:href", "img/x_symbol.png")
    .attr("x", -12)
    .attr("y", -12)
    .attr("width", 24)
    .attr("height", 24)
    .attr("transform", function(d) {
        return "translate(" + projection([
          d.properties.longitude,
          d.properties.latitude
        ]) + ")"
      })

    .attr("class", "point")
    .on("click",pointClick)
;

And while this works, and the symbols plot in the right place on the globe, they persist even when they wrap to the back of the globe. I can hide them with a visibility property if I had a way to determine if they were eclipsed, but I don't see a method in d3.geo.projection to do that. Any ideas?

Craig Soich
  • 145
  • 10
  • Not necessarily related to your question but perhaps will stimulate you to create a fiddle...I noticed you are making two assignments for the d attribute of your circle points...is that just a copy/paste issue or your original code? – FernOfTheAndes Mar 13 '14 at 00:47
  • Ah yes, @FernOfTheAndes, that was apparently a superfluous line. I assumed that since I was setting the property `pointRadius`, I still had to call the `.attr("d", path)`. Omitting that line had no effect on the point style render. I'm not sure if I can extract just the relevant section into a fiddle. It's dependent on two data JSONs, and a few CSS. – Craig Soich Mar 13 '14 at 01:47
  • 1
    @mbostock , maybe you could shed some light? Can I use .png images as the points on a d3.geo.orthographic projection? – Craig Soich Mar 26 '14 at 23:01

3 Answers3

8

Calculate if the point is visible

If you have your projection:

const chartProjection = d3.geo.orthographic();

You can turn it into a path function:

const path = d3.geo.path()
  .projection(chartProjection);

Then you can evaluate each point for visibility. path will return undefined for values behind the projection.

function getVisibility(d) {
  const visible = path(
    {type: 'Point', coordinates: [d.longitude, d.latitude]});

  return visible ? 'visible' : 'hidden';
}

// Update all text elements.
svg.selectAll('text')
  .attr('visibility', getVisibility);
James
  • 2,488
  • 2
  • 28
  • 45
  • 2
    This is a much better answer, as it's universal for any projection. – zeroin Mar 14 '19 at 17:55
  • for canvas, I'm getting path strings that are off the edge. – antony.trupe Sep 25 '20 at 22:46
  • This is a solid approach to this problem. However, **WARNING**: If you're using canvas then presumably the `d3.geo.path()` instance you're using is linked to the canvas context (using `path.context(ctx)` method). If so, calling `path({type 'Point,...})` will always return undefined, because canvas drawing doesn't produce any strings the way SVG drawing does. To work around this, you need a second instance of the geoPath -- one without a context but otherwise identical to the first instance. – meetamit Jan 07 '21 at 00:06
6

OK, so I was able to at least simulate the images hiding around the back of the globe, by calculating the great circle distance between the projection center and the point in question. If the distance was greater than π/2, the point would be beyond the horizon:

        .attr("opacity", function(d) {
            var geoangle = d3.geo.distance(
                    d.geometry.coordinates,
                    [
                        -projection.rotate()[0],
                        projection.rotate()[1]
                    ]);
            if (geoangle > 1.57079632679490)
            {
                return "0";
            } else {
                return "1.0";
            }
        })

I'm sure I can get a bit fancier by fading them out as they approach the edge, disabling clicks, etc. but now I can move on...

Craig Soich
  • 145
  • 10
1

I know this is an old question but would like to add a new solution. You can use projection.stream to filter, parse, and then return the projected x,y in pixels. This method also works on filtering out points that outside of current (zoomed) extent.

Note this will filter (clip) out all points that are invisible under current projection. Add another loop to compare two arrays if you really want to retain all points. But this works very well in my case of using reactive properties.

let newStream = []
let stream = youCurrentProjection.stream({
  point: function (x, y) {
    newStream.push([x, y])
  }
})

arrayOfPoints.forEach(point => {
  stream.point(point.x, point.y)
})

Or build a closure to pass the index

let streamWrapper = function (x, y, index) {
  let stream = youCurrentProjection.stream({
    point: function (x, y) {
      arrayOfPoints[index].isVisible = true
    }
  })
  stream.point(x, y)
}

arrayOfPoints.forEach((point, index) => {
  streamWrapper(point.x, point.y, index)
})
KuN
  • 1,143
  • 18
  • 23
  • That's a nice solution, but does it account for points that are inside the projection area but beyond the horizon of the globe? (I'm asking; I really don't know the answer) – Craig Soich Aug 04 '17 at 20:08
  • 1
    'inside the projection area but beyond the horizon of the globe?', not sure if I understand it correctly. But this method sure filters out points which should be invisible yet ``projection(point)`` would yield valid x, y in pixel (points on the 'dark' side of the globe). – KuN Aug 05 '17 at 03:36