SVG paths appears outside of DOM element when Zooming & Panning on map

Hi, I’ve created a choropleth map and am trying to work out the kinks for implementing zooming and panning. When I zoom on the map, SVG paths appear outside of the SVG element. I’ve tried to resolve this using a clipping path with no success. The zooming and panning interaction is a little off as well, and am not sure what I’m doing incorrectly.

thanks!

  • Chris

Hey Chris,

Sure - here’s a fixed-up version https://beta.observablehq.com/@tmcw/sf-bay-area-jobs-with-zooming

The gist is that the zoom event was changing the transform of the svg node, rather than a g node within that SVG. When you just transform the g node, the clip path is no longer necessary and everything seems to work as expected.

  • Tom
1 Like

Awesome, thanks Tom!

@tom quick follow up question, when I try to define the zoom function outside of the svg cell and then use it in the svg cell I get a “RuntimeError: circular definition”. Is this one of those cases where I need to do something tricky with yield or this ?

You’re not allowed to have circular cell definitions. You can’t have a zoom cell that depends on the svg cell (to set the “transform” attribute via selection.attr) and an svg cell that depends on the zoom cell (to apply the zoom behavior via selection.call).

You could define a standalone zoom behavior without an event listener:

zoom = d3.zoom()
    .scaleExtent([1, 18])
    .translateExtent([[0,0], [width, height]])

But then you have to modify the zoom instance inside the svg cell to add your zoom event listener, which isn’t clean. You could go the opposite way and have the zoom cell modify the svg element, but that isn’t very clean either (and requires removing the viewof from your svg definition):

{
  const s = d3.select(svg);
  s.call(d3.zoom()
      .on("zoom", () => s.select(".map-layers").attr("transform", d3.event.transform))
      .scaleExtent([1, 18])
      .translateExtent([[0,0], [width, height]]));
}

If you want to break this out into a separate cell, I’d probably do that by wrapping it in a function rather than using mutation, because the relationship between these two objects (the behavior and the selection) is inherently circular.

function zoom(svg) {
  svg.call(d3.zoom()
      .on("zoom", () => svg.select(".map-layers").attr("transform", d3.event.transform))
      .scaleExtent([1, 18])
      .translateExtent([[0,0], [width, height]]));
}

Then you can enable your zoom behavior in the svg cell simply as svg.call(zoom).

1 Like

The technique I suggest above (a zoom cell that’s a pure function rather than a mutable instance of d3.zoom) is similar to the technique I’ve been using to render D3 axes. Take a look at the definition of xAxis and yAxis here:

2 Likes

Thanks very much for the clarification @mbostock, that all makes sense. Will definitely check out how you used this technique for the x and y axises. Appreciate the work you all are doing here and how friendly and responsive you are when it comes to answering questions!