修改 SVG 地图上标记的标记

Modify the markup for markers on the SVG map

我是 React 和 d3 的新手,我正在尝试使用 D3 v6.2 和 ReactJS 实现一个简单的 SVG 地图,这将指示一些城市的位置。每个城市都用圆圈表示,如下所示:

我需要更改此标记以放置带有城市名称的图钉(如果可能,请使用 div),如图所示:

但是点的符号形状由 D3-path 本身决定,我找不到任何可以更改或替换为其他标记的内容。

注意:我试图在点的 path 标签内放置一个 div,但我意识到它们没有被渲染,我唯一能做的就是改变pointRadius () 标记的半径,但我不想那样做。

代码:

import React, { useEffect, useRef, useState } from 'react';
import { select, geoPath, geoMercator } from "d3";
import useResizeObserver from 'use-resize-observer';

import drawMap from './mapa.json';
import dataMap from './city.json';

const svgRef = useRef(null);
const wrapperRef = useRef(null);

const { widthRef, heightRef } = useResizeObserver({ wrapperRef });

function map(){

    useEffect(() => {

        const svg = select(svgRef.current);
        const { width, height } = wrapperRef.current.getBoundingClientRect();
       
        const projection = geoMercator().fitSize([width, height], selectedEstados || drawMap);
        
        let pathGenerator = geoPath().projection(projection).pointRadius(4);
        console.log("path: " + pathGenerator)
        
         svg.selectAll(".estado")
            .data(drawMap.features)
            .join("path")
            .attr("class", "estado")
            .attr("d", features => pathGenerator(features));

         svg.selectAll(".city")
            .data(dataMap.features)
            .join("path")
            .attr("class", "city")
            .attr("d", features => pathGenerator(features))
                        
}, [drawMap, wrapperRef, widthRef, heightRef])

return(
    <div className='map' ref={wrapperRef}>
        <svg ref={svgRef}></svg>
    </div>
)
}

city.json

{
    "type": "FeatureCollection",
    "name": "city",
    "crs": {
        "type": "name",
        "properties": {
            "name": "urn:ogc:def:crs:OGC:1.3:CRS84"
        }
    },
    "features": [
        {
            "type": "Feature",
            "properties": {
                "municipio": "sapé"
                
            },
            "geometry": {
                "type": "Point",
                "coordinates": [
                    -41.821476100786512,
                    -8.282815195947387
                ]
            }
        },
        {
            "type": "Feature",
            "properties": {
                "municipio": "muito distante"
                
            },
            "geometry": {
                "type": "Point",
                "coordinates": [
                    -41.065329661162544,
                    -7.183034162673664
                ]
            }
        }
    ]
}

mapa.json

map geojson here

有多种选择,最简单的可能就是使用 SVG 路径绘制引脚形状。

路径生成器不会在这里帮助我们。相反,我们将直接使用投影,projection([longitude,latitude]) 将 return [x,y] 以像素为单位。这个投影的 x,y 坐标可以让我们将标记锚定到地图上的正确位置。您方便地使用的 geojson 有一个 属性 包含经度和纬度。为了简单起见,下面我只使用一个简单的非geojson数组。

下面我将每个城市绘制为具有适当翻译的 g 元素,这样我就可以放置与城市相关的文本、路径和圆形元素:

var svg = d3.select("body").append("svg").attr("width", 400);
var projection = d3.geoMercator();

var cityData = [{name:"Vancouver",longlat:[-123,49]},{name:"Anchorage",longlat:[-150,61]}];

var cities = svg.selectAll(null)
  .data(cityData)
  .enter()
  .append("g")
  .attr("transform", function(d) {
    return "translate("+projection(d.longlat)+")";
  })
  
// Two different paths for comparison:
cities.append("path")
  .attr("d", (d,i) => ["M0,0 Q0 -20 -40 -20 L-40 -40L40 -40L40 -20 Q 0 -20 0 0",
                       "M0,0 L-40 -20L-40 -40L40 -40L40 -20Z"][i] )
  .attr("fill","#ddd")
  .attr("stroke","black")
  .attr("stroke-width",1);

cities.append("text")
  .attr("dy", -25)
  .text( function(d) { return d.name; })
  .style("text-anchor","middle");
  
// optional circle
cities.append("circle")
  .attr("r", (d,i) => [2,6][i])
  .attr("fill", (d,i) => ["black","white"][i])
  .attr("stroke-width",(d,i) => [0,1][i])
  
circle { stroke:black; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

在这种情况下,路径是固定宽度的:恰好名称相对较好地填充了引脚。您可以确定图钉的适当宽度并使用它来创建路径。

如果您确实需要 div,则需要使用 SVG foreignObject 元素。您没有看到任何 div 呈现的原因是 SVG 在其命名空间中没有 div 元素,HTML 和 SVG 元素不可互换。 foreignObject 让我们在 SVG 中放置 HTML 个对象。 注意:IE 不支持 ForeignObjects.

这是一个在 foreignObject 中带有 div 的快速模型:

var svg = d3.select("body").append("svg").attr("width", 400);
var projection = d3.geoMercator();

var cityData = [{name:"Vancouver",longlat:[-123,49]},{name:"Anchorage",longlat:[-150,61]}];

var cities = svg.selectAll(null)
  .data(cityData)
  .enter()
  .append("g")
  .attr("transform", function(d) {
    return "translate("+projection(d.longlat)+")";
  })
  
var div = cities.append("foreignObject")
  .attr("x", -25)
  .attr("y", -35)
  .attr("width", 100)
  .attr("height", 100)
  .append("xhtml:div")
  .attr("class","box arrow-bottom")
  .text(d=>d.name);
  
cities.append("circle")
  .attr("r", 4)
  .attr("fill","orange");

  
  
/* css from https://codeconvey.com/css-message-box-with-arrow/ */

.box {
  height: auto;
  background-color: black;
  color: #fff;
  position: relative;
}

.box.arrow-bottom:after {
  content: "";
  position: absolute;
  left: 10px;
  bottom: -15px;
  border-top: 15px solid black;
  border-right: 15px solid transparent;
  border-left: 15px solid transparent;
  border-bottom: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

以上只是手动定位,如果有更好的css样式示例可以提供更精确的定位点。

第三种选择是使用绝对坐标将 divs 定位在地图 SVG 上,这需要考虑 SVG 在页面上的位置。它还使 zooming/panning 稍微复杂一些。