D3 Donut chart projected to sphere/globe

Viewed 219

I want to use d3 for the next task: display rotating globe with donut chart in center of every country. It should be possible to interact with globe (select country, zoom, rotate). Seems d3 provide an easy way to implement every part of it but I can not get donuts part working as I need.

There is an easy way draw donut chart with the help of d3.arc:

var arc = d3.arc();
var data = [3, 23, 17, 35, 4];
var radius = 15/scale;
var _arc = arc.innerRadius(radius - 7/scale)
          .outerRadius(radius).context(donutsContext);
var pieData = pie(data);
for (var i = 0; i < pieData.length; i++) {
    donutsContext.beginPath();
    donutsContext.fillStyle = color(i);
    _arc(pieData[i]);
}

by with code as it is donuts are displayed on a plane on top of the globe, like: globe with donut ​

while I want them to be 'wrapped' around the globe

There is d3.geoCircle method that can be projected to globe correctly. I got 'ring' projected correctly to the globe with the help of two circles:

var circle = d3.geoCircle()
  .center(centroid)
  .radius(2);
var outerCircle = circle();
var circle = d3.geoCircle()
  .center(centroid)
  .radius(1);
var innerCircle = circle();

var interCircleCoordinates = [];
for (var i = innerCircle.coordinates[0].length - 1; i >= 0; i--) {
    interCircleCoordinates.push(innerCircle.coordinates[0][i]);
}
outerCircle.coordinates.push(interCircleCoordinates);

​globe with rings

but I really need to get a donut.

The other way I tried is getting image from donuts and wrapping this image around globe with the help of pixels manipulation:

var image = new Image;
image.onload = onload;
image.src = img;

function onload() {
    window.dx = image.width;
    window.dy = image.height;

    context.drawImage(image, 0, 0, dx, dy);  
    sourceData = context.getImageData(0, 0, dx, dy).data;
          target = context.createImageData(width, height);
          targetData = target.data;

    for (var y = 0, i = -1; y < height; ++y) {
        for (var x = 0; x < width; ++x) {
            var p = projection.invert([x, y]), λ = p[0], φ = p[1];
                if (λ > 180 || λ < -180 || φ > 90 || φ < -90) { i += 4; continue; }
                var q = ((90 - φ) / 180 * dy | 0) * dx + ((180 + λ) / 360 * dx | 0) << 2;
                var r = sourceData[q];
                var g = sourceData[++q];
                var b = sourceData[++q];
                targetData[++i] = r;
                targetData[++i] = g;
                targetData[++i] = b;
                targetData[++i] = 125;//
        }
    }   
    context.clearRect(0,0, width, height);
    context.putImageData(target, 0,  0);
};

by this way I get extremely slow rotating and interaction with a globe for a globe size I need (1000px)

So my questions are:

  1. Is there is some way to project donuts that are generated with the help of d3.arc to a sphere (globe, orthographic projection)?
  2. Is there is some way to get a donut from geoCircle?
  3. Maybe there is some other way to achieve my goal I do not see
1 Answers

There is one way that comes to mind to display donuts on a globe. The key challenge is that d3 doesn't project three dimensional objects very well - with one exception, geographic features. Consequently, an "easy" solution is to convert your pie charts into geographic features and project them with the rest of your features.

To do this you need to:

  • Use a pie/donut generator as you normally would
  • Go along the paths generated to get points approximating the pie shape.
  • Convert the points to long/lat points
  • Assemble those points into geojson
  • Project them onto the map.

The first point is easy enough, just make a pie chart with an inner radius.

Now you have to select each path and find points along its perimeter using path.getPointAtLength(), this will be dependent on path length, so path.getTotalLength() will be handy (and corners are important, so you might want to incorporate a little bit of complexity for these corner cases to ensure you get them)).

Once you have the points, you need the use of a second projection, azimuthal equidistant would be best. If the pie chart is centered on [0,0] in svg coordinate space, rotate the azimuthal (don't center), so that the centroid coordinate is located at [0,0] in svg space (you can use translates on the pies to position them, but it will just add extra steps). Take each point and run it through projection.invert() using the second projection. You will need to update the projection for each donut chart as each one will have a different geographic centroid.

Once you have lat long points, it's easy - you've already done it with the geo circle function - convert to geojson and project with the orthographic projection.


This approach gave me something like:

enter image description here

Notes: Depending on your data, it might be easiest to preprocess your data into geojson and store that as opposed to calculating the geojson each page load.

You are using canvas, while you don't need to actually use an svg, you need to still be able to access svg functions like getPointAtLength, you do not need to have an svg or display svg elements by using a custom element replicating a path :

document.createElementNS(d3.namespaces.svg, 'path');

Oh, and make sure the second projection's translate is set - the default is [480,250] for all (most?) d3 projections, that will throw things off if unaccounted for.

Related