我正在开发一个使用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;
所有的帮助都很感谢,我是D3的新手,所以可能有一些我不知道如何做的事情,帮助引导我朝着正确的方向前进,谢谢!
发布于 2020-06-09 22:11:06
我提供了两种通用的D3解决方案;对React的适应应该相当简单。通过保持它的一般性,答案和片段要清晰得多,尽管答案仍然很长.
坐标
在处理地理投影和缩放时,我们处理的是几个坐标系:
地理坐标(地球上的三维坐标,以degrees)
svg
变换属性应用于投影坐标的平移和缩放)测量)
可以通过投影函数使用适当的转换和缩放,这样就不需要SVG/画布转换。例如,如果需要进一步放大地图,则将D3投影比例尺或SVG/画布比例尺加倍具有相同的效果。因此,我们可以有效地消除对第三坐标系的考虑,通过设置投影参数,不需要设置SVG/画布变换。
如果我们想避免使用SVG/画布转换,我们将进行语义缩放:我们将根据更新的投影重新绘制所有数据。这个让我们用投影来做所有的工作。
如果我们想要操作SVG/画布转换来实现缩放,我们将进行几何缩放。这只需要对特性的初始绘图进行投影,然后使用SVG/画布转换来移动已经绘制的特性,并根据需要调整它们的大小。
两者各有优缺点。我不会在这里和他们说话,但我会告诉他们这两者是如何实现的。
语义缩放
对于您的用例,这可能是更容易实现的解决方案。
我们可以使用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);
以上将允许用户通过摇摄或缩放鼠标与功能交互(通过投影)。变焦跟踪累积变焦、平移和缩放。我们将使用它的值设置类似SVG转换的值,以设置投影参数,然后用更新的投影重新绘制所有功能。
但是,我们需要设置缩放的初始值。变焦的初始比例尺是1,对于d3,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));
翻译表示视图端口的中心。D3墨卡托投影的尺度通常是一个经度弧度分布在多少像素上。Mercator的默认中心是0,0,所以上面的中心是地图并将其缩放到viewport。
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));
}
注意,这确实设置了两次投影参数:一次使用fitSize,一次在变焦事件函数中,这是可以避免的,但是性能命中应该是不存在的,需要绘制特性。
下面是正在工作的三个代码块(最后一个代码块略有调整):
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];
}
}
path {
stroke: #ccc;
stroke-width: 1px;
fill: #333;
}
<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>
几何缩放
这可能会造成更多的混乱,因为它很容易混淆语义和几何,但应该同样容易实现。
投影将使用一次:最初绘制特征。使用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);
同样,上面的内容将允许用户通过摇摄或缩放鼠标来与功能交互。
我们不需要设置初始缩放状态,只需在设置初始投影参数之后绘制我们想要的功能:
let projection = d3.geoMercator()
.translate([width/2,height/2])
.scale(width/Math.PI/2);
let path = d3.geoPath(projection);
features.attr("d", path);
而且,要以编程方式缩放到特定国家或功能,我们可以使用:
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));
}
这里我使用t0
跟踪初始投影、翻译和缩放。我们想知道初始投影中坐标值的相对变化,然后用selection.call(zoom.transform,...
将这个相对变化应用到缩放中。
下面是正在工作的这三个代码块(稍加调整--例如计算缩放时的笔画宽度):
var width = 480;
var height = 480;
var t0 = {k:width/2/Math.PI,x:width/2,y:height/2};
var svg = d3.select("svg");
var g = svg.append("g");
var projection = d3.geoMercator().translate([t0.x,t0.y]).scale(t0.k);
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 = g.selectAll("path")
.data(countries)
.enter()
.append("path")
.attr("d", path)
.style("stroke-width",1);
// Let the zoom take care of modifying the projection:
let zoom = d3.zoom()
.on("zoom", function() {
g.attr("transform", d3.event.transform); // apply current transform to a parent holding our features
features.style("stroke-width", 1/d3.event.transform.k); // update stroke width.
})
svg.call(zoom);
// Let us click on a country:
features.on("click", function(d) {
projection.fitSize([width,height],d);
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));
})
// 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));
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));
})
});
// Helper function:
function getCountrybyID(id,countries) {
for(var i = 0; i < countries.length; i++) {
if(id == countries[i].id) return countries[i];
}
}
path {
stroke: #ccc;
fill: #333;
}
<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>
混合进场
最好避免这种情况。
https://stackoverflow.com/questions/62228556
复制