Zoom to bounding box of path on externally loaded svg using D3

Viewed 503

I'm using D3 to load an external SVG map that isn't using topojson (as the map was hand created and non a traditional map). I'm trying to target the elements #lines path so that when clicked, each path zooms and fills its bounding box.

I'm trying to use this example from Mike Bostock but can't figure out how to replicate it with the data that isn't using topojson. See this line:

.data(topojson.feature(us, us.objects.states).features)

It this even possible?

Here is the code I'm using to load the SVG.

var mapContainer = $('.map');

d3.xml("../assets/subwaymap.svg", function(error, subwayMap) {
  if (error) throw error;
  $('.map').append(subwayMap.documentElement)

I've tried getting the bounding box using .getBBOX but am confused at how it connects. It seems like all examples I've seen use d3.create("svg") and then tack all the functionality within that but since my data is already appended to the DOM, would this be necessary? Fairly new to D3. Thanks!

1 Answers

Two initial considerations: d3.create("svg") is rarely used in real D3 codes. Also, you don't have data appended to the DOM, just SVG elements you loaded (unless you're calling that "data").

Back to your question, you don't need path.bounds to make your code work, actually you don't even need d3.zoom. All you need is get the element's box (with getBBox) and do the appropriate transform.

The real problem, though, is that you need to wrap all the elements in a <g>, because you cannot apply the transform to the root SVG in SVG 1.1 (apparently this is possible in SVG 2).

Here is a basic demo. In this demo I'm using an external SVG made with different elements (circle, rectangle, text...), which represents the SVG you're appending. You get this SVG with:

const svg = d3.select("svg");

Then, considering you somehow manage to fix the <g> problem I mentioned, you get that group...

const g = svg.select("g");

... and you select the elements you want to zoom in (here, everything), binding an event listener:

const elements = g.selectAll("*")
    .on("click", clicked);

In this demo I'm using Bostock's math, to save (my) time, but you can change it. Click the element to zoom in, click it again to zoom out.

const width = 500,
  height = 400;
const svg = d3.select("svg");
const g = svg.select("g");
const elements = g.selectAll("*")
  .each(function() {
    d3.select(this).datum({})
  })
  .on("click", clicked);

function clicked(d) {
  d.clicked = !d.clicked;
  const bounds = this.getBBox();
  const x0 = bounds.x;
  const x1 = bounds.x + bounds.width;
  const y0 = bounds.y;
  const y1 = bounds.y + bounds.height;
  g.transition().duration(1000).attr("transform", d.clicked ? "translate(" + (width / 2) + "," + (height / 2) + ") scale(" + (1 / Math.max((x1 - x0) / width, (y1 - y0) / height)) + ") translate(" + (-(x0 + x1) / 2) + "," + (-(y0 + y1) / 2) + ")" : "transform(0,0) scale(1)");
}
<svg width="500" height="400">
<g>
<circle cx="50" cy="50" r="30" stroke="black" stroke-width="3" fill="teal"></circle>
<rect x="300" y="20" rx="20" ry="20" width="150" height="150" style="fill:tomato;stroke:black;stroke-width:3"/>
<polygon points="200,100 250,190 160,210" style="fill:lavender;stroke:purple;stroke-width:3" />
<path d="M 140 350 q 150 -200 350 0" stroke="blue" stroke-width="5" fill="none" />
<text x="30" y="300" transform="rotate(-30, 30, 300)">Foo Bar Baz</text>
</g>
</svg>
<script src="https://d3js.org/d3.v5.min.js"></script>

Related