Search code examples
d3.jsgeojson

D3 Rolling map updates d3.geo.circle, but not arcs


I built a visualization on top of http://bl.ocks.org/patricksurry/6621971.

Basically I added d3.geo.circle and d3.svg.arc on the map.

Result

What I observed is when I pan/zoom the map, the circle remains intact, but the arc disappears. enter image description here

When I inspected the elements in chrome, I saw that the attribute 'd' of arc path vanished, but for circle path, it got updated appropriately.

Can anyone help me understand why the updated projection got applied to circle path element but not in arc. Is there a way to force re-projection of arcs without have to remove and re-create them?

UPDATE 1: Since this question seemed difficult to recreate, and jsfiddle won't not allow uploading a geo-data file , I am posting the source here:

<!DOCTYPE html>
<meta charset="utf-8">
<style>

    svg {
        background-color: lavender;
        border: 1px solid black;
    }

    path {
        fill: oldlace;
        stroke: #666;
        stroke-width: .5px;
    }

    path.circle {
        fill: red;
        stroke: #666;
        stroke-width: .5px;
    }

    path.arc1 {
        fill: green;
    }


</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>

    var width = 1600,
            height = 400,
            rotate = 60,        // so that [-60, 0] becomes initial center of projection
            maxlat = 83;        // clip northern and southern poles (infinite in mercator)

    var projection = d3.geo.mercator()
            .rotate([rotate,0])
            .scale(1)           // we'll scale up to match viewport shortly.
            .translate([width/2, height/2]);

    // find the top left and bottom right of current projection
    function mercatorBounds(projection, maxlat) {
        var yaw = projection.rotate()[0],
                xymax = projection([-yaw+180-1e-6,-maxlat]),
                xymin = projection([-yaw-180+1e-6, maxlat]);
        return [xymin,xymax];
    }

    // set up the scale extent and initial scale for the projection
    var b = mercatorBounds(projection, maxlat),
            s = width/(b[1][0]-b[0][0]),
            scaleExtent = [s, 10*s];

    projection.scale(scaleExtent[0]);

    var zoom = d3.behavior.zoom()
            .scaleExtent(scaleExtent)
            .scale(projection.scale())
            .translate([0,0])               // not linked directly to projection
            .on("zoom", redraw);

    var path = d3.geo.path()
            .projection(projection);

    var svg = d3.selectAll('body')
            .append('svg')
            .attr('width',width)
            .attr('height',height)
            .attr('id', 'svg')
            .call(zoom);

    d3.json("js/data/world-110m2.json", function ready(error, world) {

        // adding geo paths
        svg.selectAll('path')
                .data(topojson.feature(world, world.objects.countries).features)
                .enter().append('path')

        // adding a circle
        svg.append("path")
                .datum(d3.geo.circle().angle(2).origin([-10, 0]))
                .attr("d", path)
                .attr("class", "circle");

        redraw();


        // adding a pie arc
        var r = 10;
        var p = Math.PI * 2;
        var arc1 = d3.svg.arc()
                .innerRadius(r - 5)
                .outerRadius(r)
                .startAngle(0);

        var arcData = JSON.parse('[{ "lon" : "0", "lat":"0", "endAngle":"6.4" }]');
        var arcs1 = svg.selectAll("path.arc1");
        arcs1 = arcs1.data(arcData)
                .enter()
                .append("path")
                .attr("class", "arc1")
                .attr("fill", "green")
                .attr("transform", function(d, i) {     return "translate(" + projection([d.lon, d.lat])[0] + ", " + projection([d.lon, d.lat])[1] + ")"; })
                .attr("d", arc1);

    });

    // track last translation and scale event we processed
    var tlast = [0,0],
            slast = null;

    function redraw() {
        if (d3.event) {
            var scale = d3.event.scale,
                    t = d3.event.translate;

            console.log(d3.event.scale + " [" +d3.event.translate + "]");

            // if scaling changes, ignore translation (otherwise touch zooms are weird)
            if (scale != slast) {
                projection.scale(scale);
            } else {
                var dx = t[0]-tlast[0],
                        dy = t[1]-tlast[1],
                        yaw = projection.rotate()[0],
                        tp = projection.translate();

                // use x translation to rotate based on current scale
                projection.rotate([yaw+360.*dx/width*scaleExtent[0]/scale, 0, 0]);
                // use y translation to translate projection, clamped by min/max
                var b = mercatorBounds(projection, maxlat);
                if (b[0][1] + dy > 0) dy = -b[0][1];
                else if (b[1][1] + dy < height) dy = height-b[1][1];
                projection.translate([tp[0],tp[1]+dy]);
            }
            // save last values.  resetting zoom.translate() and scale() would
            // seem equivalent but doesn't seem to work reliably?
            slast = scale;
            tlast = t;
        }

        svg.selectAll('path').attr('d', path);
    }


</script>

Solution

  • I finally figured out what was going wrong. I was supposed to apply transformation on arc elements. So basically, in the redraw() method I did:

    var scaleRatio = 1;
    function redraw() {
    
        if (d3.event) {
            var scale = d3.event.scale,
                    t = d3.event.translate;
    
            //console.log(d3.event.scale + " [" +d3.event.translate + "]");
    
            // if scaling changes, ignore translation (otherwise touch zooms are weird)
            if (scale != slast) {
                projection.scale(scale);
            } else {
                var dx = t[0]-tlast[0],
                        dy = t[1]-tlast[1],
                        yaw = projection.rotate()[0],
                        tp = projection.translate();
    
                // use x translation to rotate based on current scale
                projection.rotate([yaw+360.*dx/width*scaleExtent[0]/scale, 0, 0]);
                // use y translation to translate projection, clamped by min/max
                var b = mercatorBounds(projection, maxlat);
                if (b[0][1] + dy > 0) dy = -b[0][1];
                else if (b[1][1] + dy < height) dy = height-b[1][1];
                projection.translate([tp[0],tp[1]+dy]);
            }
            // save last values.  resetting zoom.translate() and scale() would
            // seem equivalent but doesn't seem to work reliably?
            if(slast==null)
                scaleRatio=1;
            else
                scaleRatio = scaleRatio * (scale/slast);
            console.log(slast+'-' + scaleRatio);
            slast = scale;
            tlast = t;
        }
    
        svg.selectAll('path').attr('d', path);
        svg.selectAll("path.arc1")
            .attr("transform", function(d, i) {     return "translate(" + projection([d.lon, d.lat])[0] + ", " + projection([d.lon, d.lat])[1] + ")scale(" + scaleRatio + ")" })
            .attr("d", arc1);
    
    }
    

    But, I still have a knowledge gap, as to why svg.path elements require explicit re-projection, unlike d3.geo.path elements . Hope someone helps me on this.