3

I'm pretty new to D3 so I'm sorry if this is redundant with other posts. I'm attempting to make a map with points that the user selects by clicking on a barplot. Currently I have a map of the world, and the points enter and exit based on the clicked bar. I was wondering how to also include a zoom effect that would crop the globe to only include the point area of the selected bar. For instance: the first bar should result in a map of the United States of America.

enter image description here

        var data = [{
                "name": "Apples",
                "value": 20,
                "latlong": [
  {"latitude": 32.043478, "longitude": -110.7851017},
  {"latitude": 40.49, "longitude": -106.83},
  {"latitude": 39.1960652, "longitude": -120.2384172},
  {"latitude": 36.137076, "longitude": -81.183722},
  {"latitude": 35.1380976, "longitude": -90.0611644},
  {"latitude": 33.76875, "longitude": -84.376217},
  {"latitude": 32.867153, "longitude": -79.9678813},
  {"latitude": 39.61078, "longitude": -78.79830099999},
  {"latitude": 40.8925, "longitude": -89.5074},
  {"latitude": 44.1862, "longitude": -85.8031},
  {"latitude": 35.48759154, "longitude": -86.10236359},
  {"latitude": 37.9342807, "longitude": -107.8073787999},
  {"latitude": 41.3530864, "longitude": -75.6848074},
  {"latitude": 38.423137, "longitude": -80.021118},
  {"latitude": 43.5121, "longitude": -72.4021},
  {"latitude": 48.4070083, "longitude": -114.2827366}           
  ]
        },
            {
                "name": "Oranges",
                "value": 26,
                "latlong": [
                  {"latitude": -36.8506158, "longitude": 174.7678785},
  {"latitude": -27.4510454, "longitude": 153.0319808},
  {"latitude": -33.867111, "longitude": 151.217941},
  {"latitude": -34.8450381, "longitude": 138.4985548},
  {"latitude": -37.7928386, "longitude": 144.9051327},
  {"latitude": -32.0582947, "longitude": 115.7460244},
  {"latitude": 29.934926599999, "longitude": -90.0615085},
  {"latitude": -34.4829472, "longitude": -58.518548},
  {"latitude": -33.460464, "longitude": -70.656868},
  {"latitude": 4.8007362, "longitude": -74.0373992},
  {"latitude": 4.9375556, "longitude": -73.9649426},
  {"latitude": -23.701185, "longitude": -46.7001431},
  {"latitude": 33.678023, "longitude": -116.23754},
  {"latitude": 51.8451208, "longitude": 5.6872547},
  {"latitude": 40.3688321, "longitude": -3.6866294},
  {"latitude": 40.4817271, "longitude": -3.6330802},
  {"latitude": 40.4642, "longitude": -3.6131},
  {"latitude": 52.327353537, "longitude": 1.67658117421},
                ]
        }
        ];

        //set up svg using margin conventions - we'll need plenty of room on the left for labels
        var margin = {
            top: 15,
            right: 25,
            bottom: 15,
            left: 60
        };

        var width = 400 - margin.left - margin.right,
            height = 500 - margin.top - margin.bottom;

        var svg = d3.select("#graphic").append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .append("g")
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

        var x = d3.scale.linear()
            .range([0, width])
            .domain([0, d3.max(data, function (d) {
                return d.value;
            })]);

        var y = d3.scale.ordinal()
            .rangeRoundBands([height, 0], .1)
            .domain(data.map(function (d) {
                return d.name;
            }));

        //make y axis to show bar names
        var yAxis = d3.svg.axis()
            .scale(y)
            //no tick marks
            .tickSize(0)
            .orient("left");

        var gy = svg.append("g")
            .attr("class", "y axis")
            .call(yAxis)

        var bars = svg.selectAll(".bar")
            .data(data)
            .enter()
            .append("g")

        //append rects
        bars.append("rect")
            .attr("class", "bar")
            .attr("y", function (d) {
                return y(d.name);
            })
            .attr("height", y.rangeBand())
            .attr("x", 0)
            .attr("width", function (d) {
                return x(d.value);
            })
      .on("click", function (d) {
      // d3.select("#chart circle")
      // .attr("fill", function () { return "rgb(0, 0, " + Math.round(d.key * 10) + ")"; });
      d3.selectAll('rect').style('fill', '#5f89ad');
      d3.select(this).style("fill", "#012B4E");
      
    var circle =  svg_map.select("g").selectAll("circle")
         .data(d.latlong);
         circle.exit().remove();//remove unneeded circles
         circle.enter().append("circle")
         .attr("r",0);//create any new circles needed
         //update all circles to new positions
         circle.transition()
         .duration(500)
         .attr("cx", function(d){ return projection([d.longitude, d.latitude])[0] })
        .attr("cy", function(d){ return projection([d.longitude, d.latitude])[1] })
        .attr("r", 7)
        .attr("class", "circle")
        .style("fill", "#012B4E")
        .attr("stroke", "#012B4E")
        .style("opacity", 0.7)
        .attr("r", 4)
      })
      

/////////////////////// WORLD MAP ////////////////////////////
   
      var width = window.innerWidth,
    height = window.innerHeight,
    centered,
    clicked_point;

var projection = d3.geoMercator()
   // .translate([width / 2.2, height / 1.5]);
    
var plane_path = d3.geoPath()
        .projection(projection);

var svg_map = d3.select("#graphic").append("svg")
    .attr("width", 900)
    .attr("height", 550)
    .attr("class", "map");
    
var g = svg_map.append("g");
var path = d3.geoPath()
    .projection(projection);
    
// load and display the World
d3.json("https://unpkg.com/world-atlas@1/world/110m.json", function(error, topology) {
    g.selectAll("path")
      .data(topojson.feature(topology, topology.objects.countries)
          .features)
      .enter()
      .append("path")
      .attr("d", path)
      ;
 });
      path {
  stroke: #2296F3;
  stroke-width: 0.25px;
  fill: #f8f8ff;
}
        body {
            font-family: "Arial", sans-serif;
        }
        
        .bar {
            fill: #5f89ad;
        }
        
        .axis {
            font-size: 13px;
        }
        
        .axis path,
        .axis line {
            fill: none;
            display: none;
        }
        
        .label {
            font-size: 13px;
        }
<!DOCTYPE html>
<html>

<head>
    <meta charset='utf-8' />
    <title>Simple Bar chart</title>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.js"></script>
  <script src="https://unpkg.com/topojson-client@3"></script>
  <script src="https://unpkg.com/topojson-client@3"></script>
  <script src="https://d3js.org/d3-array.v1.min.js"></script>
  <script src="https://d3js.org/d3-geo.v1.min.js"></script>
  <script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>


</head>

<body>

    <div id="graphic"></div>


</body>

</html>

1 Answer 1

4

This function does basically what you want. The use case is slightly different, as it is called after a map is redrawn with new data, but the result is the same. A selection of data appears in the map, and the map is zoomed to the extents of the dots rendered on it.

recenter() {
    console.log('recentering')
    try {
      // get a handle to the transform object, and reset it to the
      // zoom identity (resets to zoom out all the way)
      let ztrans = this.zoom.transform
      let t = this.d3.zoomIdentity 
      this.svg.transition().duration(DURATION*2).call(ztrans,t)

      // get the object and measurements to determine the virtual
      // "bounding" rectangle around the directional extremes of sites
      // i.e., north (topmost), east (rightmost), etc
      let circles    = this.g.selectAll('a > circle')
      let data       = circles.data()
      let long       = data.map(o => parseFloat(o.Longitude))
      let lat        = data.map(o => parseFloat(o.Latitude))
      let rightmost  = this.d3.max(long)
      let leftmost   = this.d3.min(long)
      let topmost    = this.d3.max(lat)
      let bottommost = this.d3.min(lat)
      // convert the lat/long to points in the projection
      let lt = this.projection([leftmost,topmost])
      let rb = this.projection([rightmost,bottommost])
      // calc the gaps (east - west, south - north) in pixels
      let g  = rb[0]-lt[0]
      let gh = rb[1]-lt[1]

      // get the dimensions of the panel in which the map sits
      let w  = this.svg.node().parentElement.getBoundingClientRect().width
      let h  = this.svg.node().parentElement.getBoundingClientRect().height

      // the goal here is to move the halfway point between leftmost and rightmost
      // on the projection sites to the halfway point of the panel in which the
      // svg element resides, and same for 'y'
      // the scale is the value 90% of the scale factor of either east-west to width
      // or south-north to height, whichever is greater
      let neoScale = 0.9 / Math.max(g/w, gh/h)

      // now recalculate what will be the difference between the current
      // center and the center of the scaled virtual rectangle
      // this finds the difference between the centers
      // the new center of the scaled rectangle is the average of the left and right
      // or top and bottom points
      let neoX = w/2 - (neoScale * ((lt[0]+rb[0])/2))
      let neoY = h/2 - (neoScale * ((lt[1]+rb[1])/2))

      // TRANSLATE FIRST!  then scale.
      t = this.d3.zoomIdentity.translate(neoX,neoY).scale(neoScale)
      this.svg.transition().duration(DURATION*2).call(ztrans, t)
    }
    catch(e)
    {
      console.log(e)
    }

UPDATE
I forked the op's fiddle, and created a working fiddle with the following changes:

  • The recenter function as depicted above was originally written in d3 v4. The op's fiddle was in v3. The new fiddle uses v4.
  • The recenter function above was copied and pasted "as is" from another project. It is modified in the fiddle to account for local variable names and scopes.

Also, the dots don't render on first click of a bar, but they do on subsequent clicks.

4
  • Wow thank you! I've been reading and re-reading this code before I asked for more help because I don't see exactly where to insert this logic into my use case. Do I call this function inside the bar plots on click function?
    – MayaGans
    Commented Feb 18, 2020 at 21:30
  • 1
    @MayaGans, yes call it from your click handler.
    – varontron
    Commented Feb 18, 2020 at 22:43
  • 1
    I've tried using this function in the context of my example, and even referred to observablehq.com/d/cc9e0b5016cdb6da as much as possible but I'm strill struggling to make the connection from your code to my application - if you have some time would you mind looking at my fiddle? jsfiddle.net/MayaGans/6uadw1m8/1 Thank you for your help!
    – MayaGans
    Commented Feb 19, 2020 at 1:36
  • thanks so much! I've spent a log time sifting through all the old d3 code zoom examples. Very few examples that zoom to a section! mine is now working well.
    – v3nt
    Commented Nov 2, 2020 at 18:44

Not the answer you're looking for? Browse other questions tagged or ask your own question.