D3:基于点缩放的地图缩放

3

我对D3还不是很熟悉,如果这与其他帖子重复了,我很抱歉。我试图制作一张地图,其中包含用户通过单击条形图选择的点。目前,我有一个世界地图,并且根据点击的条形图,点进入和退出。我想知道如何添加缩放效果,将地球仅裁剪为所选条形区域的点区域。例如:第一个条应导致美国的地图。

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个回答

4

这个函数基本上能够满足你的需求。使用场景有些微不同,因为它在地图以新数据重绘后被调用,但结果是相同的。数据的选择会出现在地图上,并且地图会缩放到上面渲染的点的范围内。

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)
    }

更新
我fork了原作者的fiddle,并做出以下更改,创建了一个可工作的fiddle

  • 如上所示的recenter函数最初是使用d3 v4编写的。原始的fiddle使用的是v3。新的fiddle使用v4。
  • 上面的recenter函数是“原样”从另一个项目中复制和粘贴的。在fiddle中被修改以考虑本地变量名称和作用域。

此外,在第一次单击柱形图时,点不会呈现,但在后续单击时会呈现。


哇,谢谢你!在我寻求更多帮助之前,我已经反复阅读了这段代码,因为我没有看到在我的用例中确切的逻辑插入位置。我应该在单击柱状图函数内调用这个函数吗? - MayaGans
1
@MayaGans,是的,请在您的点击处理程序中调用它。 - varontron
1
我尝试在我的示例上下文中使用此函数,甚至尽可能参考了https://observablehq.com/d/cc9e0b5016cdb6da,但我仍然在努力从您的代码到我的应用程序建立联系 - 如果您有时间,是否可以看看我的fiddle? https://jsfiddle.net/MayaGans/6uadw1m8/1/ 谢谢你的帮助! - MayaGans
非常感谢!我花了很长时间筛选所有旧的d3代码缩放示例。很少有缩放到特定部分的示例!我的现在运行良好。 - v3nt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接