6
votes

We've adapted Mike Bostock's original D3 + Leaflet example: http://bost.ocks.org/mike/leaflet/ so that it does not redraw all paths on each zoom in Leaflet.

Our code is here: https://github.com/madeincluj/Leaflet.D3/blob/master/js/leaflet.d3.js

Specifically, the projection from geographical coordinates to pixels happens here: https://github.com/madeincluj/Leaflet.D3/blob/master/js/leaflet.d3.js#L30-L35

We draw the SVG paths on the first load, then simply scale/translate the SVG to match the map.

This works very well, except for one issue: D3's path resampling, which looks great at the first zoom level, but looks progressively more broken once you start zooming in.

Is there a way to disable the resampling?

As to why we're doing this: We want to draw a lot of shapes (thousands) and redrawing them all on each zoom is impractical.

Edit After some digging, seems that resampling happens here:

function d3_geo_pathProjectStream(project) {
   var resample = d3_geo_resample(function(x, y) {
     return project([ x * d3_degrees, y * d3_degrees ]);
   });
  return function(stream) {
    return d3_geo_projectionRadians(resample(stream));
  };
}

Is there a way to skip the resampling step?

Edit 2

What a red herring! We had switched back and forth between sending a raw function to d3.geo.path().projection and a d3.geo.transform object, to no avail.

But in fact the problem is with leaflet's latLngToLayerPoint, which (obviously!) rounds point.x & point.y to integers. Which means that the more zoomed out you are when you initialize the SVG rendering, the more precision you will lose.

The solution is to use a custom function like this:

function latLngToPoint(latlng) {
  return map.project(latlng)._subtract(map.getPixelOrigin());
};

var t = d3.geo.transform({
    point: function(x, y) {
      var point = latLngToPoint(new L.LatLng(y, x));
      return this.stream.point(point.x, point.y);
    }
  });

this.path = d3.geo.path().projection(t);

It's similar to leaflet's own latLngToLayerPoint, but without the rounding. (Note that map.getPixelOrigin() is rounded as well, so probably you'll need to rewrite it)

You learn something every day, don't you.

1
You would have to modify the source. - Lars Kotthoff
Have you tried simply changing the return to return d3_geo_projectionRadians(stream));, leaving out the resample? - Lars Kotthoff
Hi Lars, a little good night's sleep does wonders! The problem was with my Leaflet part (see Edit 2) - Dan Burzo

1 Answers

4
votes

Coincidentally, I updated the tutorial recently to use the new d3.geo.transform feature, which makes it easy to implement a custom geometric transform. In this case the transform uses Leaflet’s built-in projection without any of D3’s advanced cartographic features, thus disabling adaptive resampling.

The new implementation looks like this:

var transform = d3.geo.transform({point: projectPoint}),
    path = d3.geo.path().projection(transform);

function projectPoint(x, y) {
  var point = map.latLngToLayerPoint(new L.LatLng(y, x));
  this.stream.point(point.x, point.y);
}

As before, you can continue to pass a raw projection function to d3.geo.path, but you’ll get adaptive resampling and antimeridian cutting automatically. So to disable those features, you need to define a custom projection, and d3.geo.transform is an easy way to do this for simple point-based transformations.