Home > database >  D3 Zoomable choropleth map
D3 Zoomable choropleth map

Time:09-23

I am trying to make a very basic choropleth map with D3 that can be zoomed in and panned. Although this seems very trivial there are no correct example of this online. The example here has a non-bound panning and I find it quite cringe.

Here is what I have tried. I manage to get zoom factor updated, but the map does not change.

HTML STYLE

<!DOCTYPE html>
<meta charset="utf-8">
<style>
    body {
      margin: 0
    }

    svg {
      background: #fff;
    }

    .sphere {
      fill: #fff;
    }

    .land {
      fill: #000;
    }

    .map {
      fill: none;
      stroke: #fff;
      stroke-linejoin: round;
      stroke-linecap: round;
      vector-effect: non-scaling-stroke;
    }
    .country:hover{
      stroke: #ddd;
      stroke-width: 4px;
    }
    .legend{
        margin: 1%;
        padding: 1%;
    }
    .legendCells{
        padding: 10px;
        margin: 10px;
    }
    div.tooltip {
      color: #222; 
      background: #fff; 
      padding: .5em; 
      text-shadow: #f5f5f5 0 1px 0;
      border-radius: 2px; 
      box-shadow: 0px 0px 2px 0px #a6a6a6; 
      opacity: 0.9; 
      position: absolute;
    }
    .country{
        fill: "#000";
    }
</style>
<body>
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.js"></script>

JS

var svg = d3.select("svg");
    width =  svg.attr("width"),
    height =  svg.attr("height");
// Map and projection
var path = d3.geoPath();
var projection = d3.geoMercator(width / 2 / Math.PI).translate([width / 2, height / 2])
var path = d3.geoPath().projection(projection);

const zoom = d3.zoom()
    .on('zoom', function(){
        var event = d3.event.transform
        console.dir(event)
      svg.attr('transform', event.transform);

    })
    .scaleExtent([1, 40]);

// Load external data and boot
d3.queue()
    .defer(d3.json, "http://enjalot.github.io/wwsd/data/world/world-110m.geojson")
    .await(ready);

function ready(error, topo) {
    if (error) throw error;
    console.dir(topo)
    // Draw the map
    drawmap(topo)

}

function drawmap(topo){
    map = svg.append("g")
        .attr("class", "map")
        .selectAll("path")
        .data(topo.features)
        
    svg.call(zoom);

    map.enter().append("path")
        .attr("fill","#000")
        .attr("d", path)
        .attr("class","country")
}

I fail to see what I am not doing correctly. I am also not really sure it will produce the expected result (i.e., not the wrong behaviour of the example mentioned above). Any help appreciated.

CodePudding user response:

It sounds like setting the translateExtent on d3.zoom() is what you're looking for. Changing the value of zoom to

const zoom = d3.zoom()
    .scaleExtent([1, 8])
    .translateExtent([[0, 0], [width, height]])
    .on('zoom', zoomed);

should do the trick. This bounds the panning so that you cannot pan beyond the edge of the map.

I've updated the block you've linked to include this and to work with D3 v7:

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">

  <style>
    body {
      margin: 0
    }

    svg {
      background: #eee;
    }

    .sphere {
      fill: #fff;
    }

    .land {
      fill: #000;
    }

    .boundary {
      fill: none;
      stroke: #fff;
      stroke-linejoin: round;
      stroke-linecap: round;
      vector-effect: non-scaling-stroke;
    }
  </style>

  <script src="https://d3js.org/d3.v7.js"></script>
  <script src="https://unpkg.com/topojson@3"></script>
</head>

<body>
  <script>
    const width = 500;
    const height = 500;

    const projection = d3.geoMercator()
        .translate([width / 2, height / 2])
        .scale((width - 1) / 2 / Math.PI);

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

    const zoom = d3.zoom()
        .scaleExtent([1, 8])
        .translateExtent([[0, 0], [width, height]])
        .on('zoom', zoomed);

    const svg = d3.select('body').append('svg')
        .attr('width', width)
        .attr('height', height);

    const g = svg.append('g');

    svg.call(zoom);

    d3.json('https://unpkg.com/[email protected]/countries-110m.json')
      .then(world => {
        g.append('path')
            .datum({ type: 'Sphere' })
            .attr('class', 'sphere')
            .attr('d', path);

        g.append('path')
            .datum(topojson.merge(world, world.objects.countries.geometries))
            .attr('class', 'land')
            .attr('d', path);

        g.append('path')
            .datum(topojson.mesh(world, world.objects.countries, (a, b) => a !== b))
            .attr('class', 'boundary')
            .attr('d', path);
      });

    function zoomed(event) {
      g
        .selectAll('path') // To prevent stroke width from scaling
        .attr('transform', event.transform);
    }
  </script>
</body>

</html>

  • Related