🏠 back to Observable

Conversion of Zoom to Bounding Box to normal html

I’d like to run the Zoom to Bounding Box example outside of Observable - can anyone please help convert it to a normal html file + vanilla javascript, like the the earlier version of this great map zooming example Zoom to Bounding Box?

I know there are ways to download Observables and use special runtimes to run them - but I just want this simple example translated so that I can integrate it into my own web app and adapt it to pan and zoom around my own SVG image programmatically - like a prezi presentation.

Hi @abulka, and welcome to the forum!

Just for clarification - are you simply looking to update the example from D3v3 to D3v5?

I’ll see if I can make some headway. In my initial attempt at this, I opened the bl.ocks example and switched out the JS files for both D3 and topo-json. Following the changelog, I also converted d3.geo.albersUsa() to d3.geoAlbers() and d3.geo.path() to d3.geoPath().

This approach gets us up to D3v4 and topojson(v4):

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

.background {
  fill: none;
  pointer-events: all;
}

.feature {
  fill: #ccc;
  cursor: pointer;
}

.feature.active {
  fill: orange;
}

.mesh {
  fill: none;
  stroke: #fff;
  stroke-linecap: round;
  stroke-linejoin: round;
}

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

var width = 960,
    height = 500,
    active = d3.select(null);

var projection = d3.geoAlbers()
    .scale(1000)
    .translate([width / 2, height / 2]);

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

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

svg.append("rect")
    .attr("class", "background")
    .attr("width", width)
    .attr("height", height)
    .on("click", reset);

var g = svg.append("g")
    .style("stroke-width", "1.5px");

d3.json("https://cors-anywhere.herokuapp.com/https://bl.ocks.org/mbostock/raw/4090846/us.json", function(error, us) {
  if (error) throw error;

  g.selectAll("path")
      .data(topojson.feature(us, us.objects.states).features)
    .enter().append("path")
      .attr("d", path)
      .attr("class", "feature")
      .on("click", clicked);

  g.append("path")
      .datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
      .attr("class", "mesh")
      .attr("d", path);
});

function clicked(d) {
  if (active.node() === this) return reset();
  active.classed("active", false);
  active = d3.select(this).classed("active", true);

  var bounds = path.bounds(d),
      dx = bounds[1][0] - bounds[0][0],
      dy = bounds[1][1] - bounds[0][1],
      x = (bounds[0][0] + bounds[1][0]) / 2,
      y = (bounds[0][1] + bounds[1][1]) / 2,
      scale = .9 / Math.max(dx / width, dy / height),
      translate = [width / 2 - scale * x, height / 2 - scale * y];

  g.transition()
      .duration(750)
      .style("stroke-width", 1.5 / scale + "px")
      .attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}

function reset() {
  active.classed("active", false);
  active = d3.select(null);

  g.transition()
      .duration(750)
      .style("stroke-width", "1.5px")
      .attr("transform", "");
}

</script>

Unfortunately, something is missing for D3v5 :frowning: That is, I am getting no console errors, but for some reason the map won’t render… so I’ll keep trying. More soon!

Ah, got it! (should have paid more attention to the changelog):

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

.background {
  fill: none;
  pointer-events: all;
}

.feature {
  fill: #ccc;
  cursor: pointer;
}

.feature.active {
  fill: orange;
}

.mesh {
  fill: none;
  stroke: #fff;
  stroke-linecap: round;
  stroke-linejoin: round;
}

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

var width = 960,
    height = 500,
    active = d3.select(null);

var projection = d3.geoAlbers()
    .scale(1000)
    .translate([width / 2, height / 2]);

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

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

svg.append("rect")
    .attr("class", "background")
    .attr("width", width)
    .attr("height", height)
    .on("click", reset);

var g = svg.append("g")
    .style("stroke-width", "1.5px");

d3.json("https://cors-anywhere.herokuapp.com/https://bl.ocks.org/mbostock/raw/4090846/us.json").then(function(us) {

  g.selectAll("path")
      .data(topojson.feature(us, us.objects.states).features)
    .enter().append("path")
      .attr("d", path)
      .attr("class", "feature")
      .on("click", clicked);

  g.append("path")
      .datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
      .attr("class", "mesh")
      .attr("d", path);
});

function clicked(d) {
  if (active.node() === this) return reset();
  active.classed("active", false);
  active = d3.select(this).classed("active", true);

  var bounds = path.bounds(d),
      dx = bounds[1][0] - bounds[0][0],
      dy = bounds[1][1] - bounds[0][1],
      x = (bounds[0][0] + bounds[1][0]) / 2,
      y = (bounds[0][1] + bounds[1][1]) / 2,
      scale = .9 / Math.max(dx / width, dy / height),
      translate = [width / 2 - scale * x, height / 2 - scale * y];

  g.transition()
      .duration(750)
      .style("stroke-width", 1.5 / scale + "px")
      .attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}

function reset() {
  active.classed("active", false);
  active = d3.select(null);

  g.transition()
      .duration(750)
      .style("stroke-width", "1.5px")
      .attr("transform", "");
}

</script>

… and here it is hosted on GitHub:

https://aaronkyle.github.io/concept/data-visualization/sandbox/zoom-to-bounding,v5.html

Thanks for the code - still looks like the older example though.

The later example hosted in Observable is more sophisticated in that it allows the user to manually zoom and pan the map. The older vanilla version example you converted to v5 does not give the user this lovely freedom. I don’t mind which D3 version is used - as long as the more sophisticated example runs outside of Observable’s walled garden :wink:

:slight_smile: I see it now! Ok - I’ll have a crack at it, but I’m getting called away from the computer and might not return for some time.

On initial glance, it looks like, after getting it into D3v5, there’s not a whole lot left to do: just figuring out how to integrate the pan and zoom functions:

 const zoom = d3.zoom()
      .scaleExtent([1, 8])
      .on("zoom", zoomed);

 svg.call(zoom);

  function reset() {
    svg.transition().duration(750).call(
      zoom.transform,
      d3.zoomIdentity,
      d3.zoomTransform(svg.node()).invert([width / 2, height / 2])
    );
  }

  function clicked(d) {
    const [[x0, y0], [x1, y1]] = path.bounds(d);
    d3.event.stopPropagation();
    svg.transition().duration(750).call(
      zoom.transform,
      d3.zoomIdentity
        .translate(width / 2, height / 2)
        .scale(Math.min(8, 0.9 / Math.max((x1 - x0) / width, (y1 - y0) / height)))
        .translate(-(x0 + x1) / 2, -(y0 + y1) / 2),
      d3.mouse(svg.node())
    );
  }

  function zoomed() {
    const {transform} = d3.event;
    g.attr("transform", transform);
    g.attr("stroke-width", 1 / transform.k);
  }

Perhaps you might try taking it forward yourself in the meantime?

Hi again @abulka

I ended up having a few more minutes and was able to finish this… here you go (incl. a working version hosted on GitHub:

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

.background {
  fill: none;
  pointer-events: all;
}

.feature {
  fill: #ccc;
  cursor: pointer;
}

.feature.active {
  fill: orange;
}

.mesh {
  fill: none;
  stroke: #fff;
  stroke-linecap: round;
  stroke-linejoin: round;
}

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

var width = 960,
    height = 500,
    active = d3.select(null);

var zoom = d3.zoom()
   .scaleExtent([1, 8])
   .on("zoom", zoomed);

var projection = d3.geoAlbers()
    .scale(1000)
    .translate([width / 2, height / 2]);

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

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

svg.append("rect")
    .attr("class", "background")
    .attr("width", width)
    .attr("height", height)
    .on("click", reset);

var g = svg.append("g")
    .style("stroke-width", "1.5px");

d3.json("https://cors-anywhere.herokuapp.com/https://bl.ocks.org/mbostock/raw/4090846/us.json").then(function(us) {

  g.selectAll("path")
      .data(topojson.feature(us, us.objects.states).features)
    .enter().append("path")
      .attr("d", path)
      .attr("class", "feature")
      .on("click", clicked);

  g.append("path")
      .datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
      .attr("class", "mesh")
      .attr("d", path);
});

svg.call(zoom);

  function reset() {
    svg.transition().duration(750).call(
      zoom.transform,
      d3.zoomIdentity,
      d3.zoomTransform(svg.node()).invert([width / 2, height / 2])
    );
  }

  function clicked(d) {
    const [[x0, y0], [x1, y1]] = path.bounds(d);
    d3.event.stopPropagation();
    svg.transition().duration(750).call(
      zoom.transform,
      d3.zoomIdentity
        .translate(width / 2, height / 2)
        .scale(Math.min(8, 0.9 / Math.max((x1 - x0) / width, (y1 - y0) / height)))
        .translate(-(x0 + x1) / 2, -(y0 + y1) / 2),
      d3.mouse(svg.node())
    );
  }

  function zoomed() {
    const {transform} = d3.event;
    g.attr("transform", transform);
    g.attr("stroke-width", 1 / transform.k);
  }

</script>

NOTE: I didn’t bother cleaning up the CSS from the bl.ocks example; you might wish to do so.

1 Like

Thank you @aaronkyle - that works! :slightly_smiling_face: - brilliant!

I’ll try to modify it to accept an arbitrary SVG and post again. I’ve currently been using the snapsvg library with the zpd plugin zoom library however the snapsvg zoom plugin doesn’t allow simultaneous pan and zoom - whereas D3 does, which is a big advantage for the user experience.

1 Like