block by shawnbot 9240915

Uniformly scaled geo thumbnails

Full Screen

This example shows how to use d3 to create uniformly scaled geographic feature thumbnails. There are two interesting features of SVG at play here:

  1. The viewBox attribute is used to “zoom in” to the individual states.
  2. The use element is used to redraw a common background states path into each thumbnail, reducing DOM overhead.

The other nice thing here is that we didn’t have to define the SVG elements’ size in JavaScript, so they can be sized exclusively in CSS. The viewBox attribute tells the browser to fit the given rectangle within the SVG element’s bounds. You can also use the preserveAspectRatio attribute to change how the viewBox fits within its bounds.

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>d3 geo thumbnails</title>
    <script src="//d3js.org/d3.v3.min.js"></script>
    <script src="//d3js.org/topojson.v1.min.js"></script>
    <style>

      svg.thumb {
        width: 90px;
        height: 94px;
        display: block;
        float: left;
        margin: 5px 0 0 5px;
      }

      path {
        vector-effect: non-scaling-stroke;
        stroke-width: 1;
      }

      .thumb .bg,
      .thumb use {
        fill: #ddd;
        stroke: #ccc;
      }

      .thumb .fg {
        fill: #8cba3d;
        stroke: none;
      }

    </style>
  </head>
  <body>
    <div id="thumbnails"></div>
    <svg id="shared">
    </svg>
    <script>

      var root = d3.select("#thumbnails"),
          proj = d3.geo.albersUsa(),
          path = d3.geo.path()
            .projection(proj),
          // how much space to give the features from
          // the edge of their container
          margin = 10;

      d3.json("us-states.json", function(error, topology) {
        var collection = topojson.feature(topology, topology.objects["states"]);

        // draw the whole collection once to a path in the shared <defs>
        d3.select("#shared")
          .append("defs")
            .append("path")
              .datum(collection)
              .attr("id", "states-bg")
              .attr("d", path);

        // sort the states by name
        collection.features.sort(function(a, b) {
          return d3.ascending(a.properties.name, b.properties.name);
        });

        // filter out the territories & DC
        var states = collection.features.filter(function(d) {
          return +d.id <= 70 && d.id != 11;
        });

        var svg = root.selectAll(".thumb")
          .data(states)
          .enter()
          .append("svg")
            .attr("id", function(d) {
              return d.properties.name;
            })
            .attr("class", "thumb")
            .attr("viewBox", function(d) {
              // get the projected bounds
              var bounds = path.bounds(d),
                  width = bounds[1][0] - bounds[0][0],
                  height = bounds[1][1] - bounds[0][1],
                  // get the proportion of the bounds' longest side
                  // to the container's shortest side
                  scale = Math.max(width, height) / Math.min(this.offsetWidth, this.offsetHeight),
                  // and multiply the desired margin by this
                  m = margin * scale;
              return [
                bounds[0][0] - m,
                bounds[0][1] - m,
                width + m * 2,
                height + m * 2
              ].join(" ");
            });

        // place the shared states path here
        svg.append("use")
          .attr("xlink:href", "#states-bg");

        // and draw the individual states on top
        svg.append("path")
          .attr("class", "fg")
          .attr("d", path);
      });

    </script>
  </body>
</html>