I'm working on a D3 project that involves a large zoomable map. Everything looks good and works fine except that when I zoom and pan the view can get a little laggy.
I'm wondering if D3 has a way for me to avoid projecting the map data that extends beyond the frame. I want to improve performance without sacrificing detail, if possible. Any hints or tips would be greatly appreciated!
There are a few options here. I'll take the question broadly as ways to optimize displaying large amounts of geographic data with d3.
projection.clipExtent(extent)
This method takes an array marking the top left and bottom right of the drawn extent of features (in projected/pixel coordinate space). Features are clipped to the specified extent. Every point is still run through the projection, but you won't have to draw them or do great circle sampling along the edges. Strictly speaking, this is close to what the question asks for.
projection.clipAngle(angle)
Whereas the above works in projected space, this method works in unprojected space. This method takes a value representing an angle (in degrees) and clips features based on this angle and the projection's rotational center. Depending on your projection, rotation may be a suitable method of translating and centering the map. You'd have to calculate the maximum clip angle for a given zoom level and centering, this would then clip the map prior to projecting to the small circle specified by the clip angle. This improves on the above by avoiding the need to project points prior to clipping, but as small circles rarely conform to the viewport, there will be some features drawn beyond the desired extent (though much less). The above could be combined with this approach.
A custom preclipping function could be made to get better results. This would be easier for a cylindrical projection than others in order to clip features to a specified box prior to projection, which would increase efficiency.
Other options
Vector tiles are available from a number of sources - though I don't particularily care for d3-tile (I've been working on an alternative, but have been too busy to really focus on it). You'd still have to load the tiles each zoom, and some tiles every pan, but this options gives you the greatest range of detail and appropriate resolutions of data.
An alternative to the above would be to store bounding box information on every polygon (don't do it on the fly). When drawing the map, filtering the polygons for those that have any overlap with the viewport would enable selective rendering of only relevant features. Depending on projection and zoom mechanics, this could be done in either projected or unprojected coordinate space.
This could be complimented by the loading of feature at two or more scales: zoomed out all features are drawn regardless of visibility, but at a resolution appropriate when zoomed out. When zooming in past a certain depth, use more detailed features, but filter based on bounding box.
I include this here simply because SVG can be the pinch point at times, depending on how many and the type of features being drawn. Switching to Canvas could be am improvement.
Projection type does affect speed, equirectangular is fast, some azimuthal projections slow. While I have only tested projection.invert()
in any detail, projection type is a factor.
Preprojection geometry is fastest of all though, which bypasses the projection altogether: feature coordinate space is in pixels. See here for a bit more on that topic.
Combined with the bounding box approach and/or a function to clip based on current extent, this would likely be a very fast approach.
I add this based on the text of your question: projecting the features should take the same time zoomed in or not. Drawing may be variable, but the projection runs the same either way. This suggests you may be using .on("zoom"
event. This is not very efficient: every tick of a pan event would require a redrawing, in other words, a single pan could trigger many zoom events. This means you need to project the features almost constantly during a pan.
A simple solution to this is to use an .on("end"
event as the event on which to redraw the map. This way we only redraw the map once per zoom.
However, you could get more complex. If you want the map to follow the mouse throughout a pan event, you can use a transform to translate the map, avoiding the need to project anything, and then at the end of the zoom, reproject the map and remove the transform. This would almost certainly require a rethink of your zoom functions. I've taken a somewhat similar approach before in determining if a pan has occured to avoid redrawing clusters when zoom scale doesn't change (example).