ReactJS D3 - 如何放大 D3 地理世界地图

jas*_*son 3 d3.js reactjs

我正在开发一个使用 ReactJS 的网络应用程序。我在 React 组件中使用 D3 渲染了一张世界地图。它具有基本功能,例如每当单击世界地图上的国家/地区时调整 geoMercator().fitSize()(因此“放大”)。但是,我希望用户能够通过滚动鼠标滚轮来放大自定义数量,但我不知道该怎么做。在网上搜索时,我遇到了 d3 缩放功能,但示例都是通过调整比例来完成的,但我不知道如何将其应用到 d3-geo 上,它似乎没有比例,只有投影。

到目前为止,这是我的代码

function GeoChart() {

    const svgRef = useRef();
    const wrapperRef = useRef();
    const [dimensions, setDimensions] = useState(useResizeObserver(wrapperRef));
    const [selectedCountry, setSelectedCountry] = useState(null);

    useEffect(() => {
        const svg = select(svgRef.current);
        const { width, height } = wrapperRef.current.getBoundingClientRect();

        const minProp = leadCount ? min(leadCount, count => count.count) : null
        const maxProp = leadCount ? max(leadCount, count => count.count) : null
        const colorScale = scaleLinear()
            .domain([minProp, maxProp])
            .range(["#ccc", "red"]);

        // projects geo-coordinates on a 2D plane
        const projection = geoMercator()
            .fitSize([width, height], selectedCountry || data)
            .precision(100);

        // takes geojson data,
        // transforms that into the d attribute of a path element
        const pathGenerator = geoPath().projection(projection);

        // render each country
        svg
            .selectAll(".country")
            .data(data.features)
            .join("path")
            .on("click", feature => {
                console.log(selectedCountry)
                setSelectedCountry(selectedCountry === feature ? null : feature);
            })
            .attr("class", "country")
            .transition()
            .attr("fill", feature => findLeadCount(feature.properties.name))
            .attr("d", feature => pathGenerator(feature))

}, [data, dimensions, selectedCountry]);

return (
        <div ref={wrapperRef} style={{ marginBottom: "2rem" }}>
            <svg id="geo-chart" ref={svgRef}></svg>
        </div>
) }

export default GeoChart;
Run Code Online (Sandbox Code Playgroud)

感谢所有帮助,我是 D3 的新手,因此可能有一些我不知道该怎么做的东西,请帮助指导我正确的方向,谢谢!

And*_*eid 13

我提供两个通用的 D3 解决方案;React 的适配应该相当简单。通过保持通用性,答案和片段更加清晰,尽管答案仍然很冗长......

坐标

在处理地理投影和缩放时,我们要处理几个坐标系:

  • 地理坐标(地球上的 3 维坐标,以度为单位)
  • 投影坐标(笛卡尔数据,以像素为单位,投影的结果)
  • SVG/Canvas 坐标(应用于投影坐标的平移和缩放,例如通过svg变换属性)

可以通过投影功能使用适当的平移和缩放,这样就不需要 SVG/Canvas 转换。例如,如果需要进一步放大地图,则将 D3 投影比例或 SVG/Canvas 比例加倍具有相同的效果。因此,我们可以通过确保设置投影参数而无需设置 SVG/Canvas 变换来有效地消除对第三坐标系的考虑。

如果我们想避免使用 SVG/Canvas 变换,我们将进行语义缩放:我们将根据更新的投影重新绘制所有数据。这让投影完成所有工作。

如果我们想操纵 SVG/Canvas 变换来实现缩放,我们将进行几何缩放。这仅需要在初始绘制要素时进行投影,然后使用 SVG/Canvas 变换来移动已绘制的要素并根据需要调整其大小。

两者都有不同的优点和缺点。我不会在这里谈论他们,但我会展示如何实现这两个目标。

语义缩放

对于您的用例,这可能是更容易实施的解决方案。

我们可以用来d3.zoom()跟踪当前的平移和缩放:

  let zoom = d3.zoom()
    .on("zoom", function() {
        let t = d3.event.transform;                 // get current zoom state
        projection.scale(t.k).translate([t.x,t.y]); // set scale and translate of projection.
        features.attr("d", path)                    // redraw the features
    })

  svg.call(zoom);
Run Code Online (Sandbox Code Playgroud)

上面的内容将让用户通过使用鼠标平移或缩放来与功能进行交互(通过投影)。D3.zoom 跟踪累积缩放平移和比例。我们将使用它来设置投影参数,然后使用更新的投影重新绘制所有特征,而不是使用它的值设置类似 SVG 变换的内容。

但是,我们需要设置缩放的初始值。缩放的初始比例为 1,对于 d3.geoMercator 投影,这将形成约 6x6 像素的世界。我们可以设置初始缩放状态selection.call(zoom.transform, transform)

 // Set up an initial projection translate and scale.
 svg.call(zoom.transform, d3.zoomIdentity.translate(width/2,height/2).scale(width/Math.PI/2));
Run Code Online (Sandbox Code Playgroud)

平移代表视口的中心。对于 D3 墨卡托投影,比例通常是经度的一个弧度所分布的像素数。墨卡托的默认中心是 [0,0],因此上面的代码将地图居中并将其缩放到视口

zoom.transform触发缩放事件,并且重要的是更新缩放状态,以便投影和缩放对齐。

最棘手的部分是使用fitSize()- 这会修改投影,但不会修改缩放状态。但是,fitSize()仅修改投影的平移和比例,而不修改其中心。因此,我们可以简单地提取当前比例并进行平移,并使用此数据以编程方式触发缩放事件:

 function centerOnFeature(feature) {
    projection.fitSize([width,height],feature);
    var k = projection.scale();
    var t = projection.translate();
    svg.call(zoom.transform, d3.zoomIdentity.translate(...t).scale(k));
  }
Run Code Online (Sandbox Code Playgroud)

请注意,这确实设置了两次投影参数:一次使用 fitSize,一次在 Zoom 事件函数中,这是可以避免的,但性能损失应该不存在,要求的是特征的绘制

以下是正在工作的这三个代码块(对最后一个代码块稍作调整):

  let zoom = d3.zoom()
    .on("zoom", function() {
        let t = d3.event.transform;                 // get current zoom state
        projection.scale(t.k).translate([t.x,t.y]); // set scale and translate of projection.
        features.attr("d", path)                    // redraw the features
    })

  svg.call(zoom);
Run Code Online (Sandbox Code Playgroud)
 // Set up an initial projection translate and scale.
 svg.call(zoom.transform, d3.zoomIdentity.translate(width/2,height/2).scale(width/Math.PI/2));
Run Code Online (Sandbox Code Playgroud)
 function centerOnFeature(feature) {
    projection.fitSize([width,height],feature);
    var k = projection.scale();
    var t = projection.translate();
    svg.call(zoom.transform, d3.zoomIdentity.translate(...t).scale(k));
  }
Run Code Online (Sandbox Code Playgroud)

几何缩放

这可能会导致更多混乱,因为很容易混合语义和几何,但应该很容易实现。

投影将使用一次:最初绘制特征。使用fitSize()缩放某些要素将修改投影的平移和比例。由于我们使用 SVG/Canvas 变换来适当缩放地图并使地图居中,因此我们无法使用此修改后的投影来绘制新要素:这将导致应用缩放和平移两次。这种情况下的解决方案是拥有一个不受 fitSize() 干扰的重复比例。

再次,让我们使用它d3.zoom()来跟踪当前的平移和缩放,但这次使用它来修改 SVG 上的变换:

  let zoom = d3.zoom()
    .on("zoom", function() {
        g.attr("transform", d3.event.transform);    // apply the current zoom to a parent holding our features
    })

   svg.call(zoom);
Run Code Online (Sandbox Code Playgroud)

同样,上述内容将让用户通过使用鼠标平移或缩放来与功能进行交互。

我们不需要设置初始缩放状态,只需在设置初始投影参数后绘制我们想要的特征即可:

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

let path = d3.geoPath(projection);

features.attr("d", path);
Run Code Online (Sandbox Code Playgroud)

并且,要以编程方式缩放到特定国家或功能,我们可以使用:

function centerOnFeature(feature) {
    projection.fitSize([width,height],feature);
    let k = projection.scale() / t0.k; // relative to initial scale.
    let x = projection.translate()[0] - t0.x * k; // relative to initial scale.
    let y = projection.translate()[1] - t0.y * k; // relative to initial scale.
    svg.call(zoom.transform, d3.zoomIdentity.translate(x,y).scale(k));
}
Run Code Online (Sandbox Code Playgroud)

在这里,我用来t0跟踪初始投影平移和缩放。我们想知道坐标值与初始投影的相对变化,然后我们获取该相对变化并将其应用到缩放中selection.call(zoom.transform,...

以下是正在工作的三个代码块(稍作调整 - 例如考虑缩放时的笔画宽度):

var width = 480;
var height = 480;

var svg = d3.select("svg");
var projection = d3.geoMercator();
var path = d3.geoPath(projection);

d3.json("https://d3js.org/world-110m.v1.json").then(function(world) {

  // Draw the world.
  let countries = topojson.feature(world, world.objects.countries).features;
  let features =  svg.selectAll("path")
    .data(countries)
    .enter()
    .append("path")

  // Let the zoom take care of modifying the projection:
  let zoom = d3.zoom()
    .on("zoom", function() {
        let t = d3.event.transform;  
        projection.scale(t.k).translate([t.x,t.y]);
        features.attr("d", path)
    })

  svg.call(zoom);
    
    
   
  // Set up an initial projection translate and scale.
  svg.call(zoom.transform, d3.zoomIdentity.translate(width/2,height/2).scale(width/Math.PI/2));
    
  // Let us click on a country:
  features.on("click", function(d) {
        projection.fitSize([width,height],d);
        var k = projection.scale();
        var t = projection.translate();
        svg.call(zoom.transform, d3.zoomIdentity.translate(...t).scale(k));
      })
  
  // Some buttons to programatically set the translate and scale:  
  d3.select("div")
      .selectAll(null)
      .data([{label:"Angola",id:"024"},{label:"New Zealand",id:"554"}])
      .enter()
      .append("button")
      .text(function(d) { return d.label; })
      .on("click", function(d) {
        projection.fitSize([width,height],getCountrybyID(d.id,countries));
        var k = projection.scale();
        var t = projection.translate();
        svg.call(zoom.transform, d3.zoomIdentity.translate(...t).scale(k));
      })

});

// Helper function:
function getCountrybyID(id,countries) {
  for(var i = 0; i < countries.length; i++) {
    if(id == countries[i].id) return countries[i];
  }
}
Run Code Online (Sandbox Code Playgroud)
path {
  stroke: #ccc;
  stroke-width: 1px;
  fill: #333;
}
Run Code Online (Sandbox Code Playgroud)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<div>.</div>
<svg width="480" height="480"></svg>
Run Code Online (Sandbox Code Playgroud)

混合方法

最好避免这种情况。

  • 这是一个令人难以置信的答案 - 我不敢相信你还没有被投票!谢谢@安德鲁·里德 (2认同)