如何解决d3.js:将 SVG 地理路径转换为画布
我是 D3 的新手,刚刚编写了一个小代码来在地图上显示点转换:
Static Image
我从大约 100 分的小样本开始,效果很好。
但最终,我想在此动画中显示大约 1000 个点:https://www.zeit.de/feature/pendeln-stau-arbeit-verkehr-wohnort-arbeitsweg-ballungsraeume
但是对于 D3 和浏览器来说,即使是 500 点似乎也太多了 - 动画不再重复。
我读到我应该使用 canvas 以获得更好的性能,但是在阅读了一些教程后,我仍然无法正确地将我的 SVG 代码重写为 canvas 代码。任何有关如何正确使用画布的帮助都表示赞赏。
使用SVG的代码:
Promise.all([
d3.json('http://opendata.ffe.de:3000/rpc/map_region_type?idregiontype=2&generalized=1'),d3.csv('../Daten/d3_input_sample_klein.csv')
]).then(([bl,centroids]) => {
var aProjection = d3.geoMercator()
.fitSize([800,600],bl);
var geoPath = d3.geoPath()
.projection(aProjection);
var svg = d3.select('body')
.append('svg')
.attr('width',1000)
.attr('height',1000);
// draw basemap
svg.selectAll('path')
.data(bl.features)
.enter()
.append('path')
.attr('d',geoPath)
.attr('class','bl');
// get max value for scaleLinear
var max = d3.max(centroids,function (d) {
return parseInt(d.value);
});
var radiusScale = d3.scaleLinear()
.domain([0,max])
.range([1,10]);
// create circles with radius based on "value"
function circleTransition() {
var circles = svg.selectAll('circle')
.data(centroids)
.enter()
.append('circle')
.style('fill','white')
.attr('r',function (d) {
return radiusScale(d.value);
});
repeat();
// transition circles from "start" to "target" and repeat
function repeat() {
circles
.attr('cx',(d) => aProjection([d.x_start,d.y_start])[0])
.attr('cy',d.y_start])[1])
.transition()
.duration(4000)
.attr('cx',(d) => aProjection([d.x_target,d.y_target])[0])
.attr('cy',d.y_target])[1])
.on('end',repeat);
};
};
circleTransition();
加载的 CSV 文件包含这样的纬度/经度坐标:
x_start | y_start | x_target | y_target | 价值 |
---|---|---|---|---|
9.11712 | 54.28097 | 8.77778 | 54.71323 | 122 |
9.79227 | 53.64759 | 9.60330 | 53.86844 | 87617 |
9.70219 | 53.58864 | 8.80382 | 54.80330 | 2740 |
是否有一种简单的方法可以将此代码转换为使用画布或以其他方式提高性能?
谢谢! 迈克尔
解决方法
我不确定您是否需要转换为画布。 20 000 转换 svg 节点应该很慢,但 500 应该是可以管理的。为了进行比较,这里有 20 000 画布节点转换,对于不同策略的比较,this 可能会很有趣。
我将为您的 SVG 代码提供优化,以及您可以如何使用画布进行优化。
保持 SVG
您没有有效地使用 d3.transition - 这可能是性能问题的原因。
d3.transition 的低效率在于您如何使用 transition.on("end"
,此方法在 每个 被转换的元素的转换结束时调用一个函数。由于您有 500 个元素正在转换,因此您在所有 500 个元素上调用此函数 500 次,实际上您试图在每个循环中对单个元素启动转换 250,000 次。转换的每次初始化都会调用投影函数 4 次,因此您在每个循环中投影了 100 万次,而您只需要这样做 2000 次(尽管这可以减少到 1000 次)。
相反,我们可以创建一个转换函数来转换单个特征,并在最后重新触发该转换函数,例如:
svg.selectAll("circle")
.each(repeat); // call repeat function on individual circles
function repeat() {
d3.select(this)
.attr('cx',(d) => aProjection([d.x_start,d.y_start])[0])
.attr('cy',d.y_start])[1])
.transition()
.duration(4000)
.attr('cx',(d) => aProjection([d.x_target,d.y_target])[0])
.attr('cy',d.y_target])[1])
.on('end',repeat);
};
这是一个稍微修改的例子:
var data = [{x:100,y:100},{x:200,y:100}];
var svg = d3.select("svg");
var circles = svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.style('fill','black')
.attr('r',10)
.attr("cx",d=>d.x)
.attr("cy",d=>d.y)
svg.selectAll("circle")
.each(repeat);
function repeat() {
d3.select(this)
.style("fill","orange")
.transition()
.duration(4000)
.style("fill","steelblue")
.on('end',repeat);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
这还允许每个过渡受制于其自己的持续时间/延迟,而无需等待所有其他过渡完成后再重新开始。
画布
Canvas 需要对您的代码进行相当彻底的修改,并且需要采用不同的 d3 方法。鼠标交互等功能需要非常不同的方法。我假设至少对下面的画布有基本的熟悉,因为问题是关于如何使用画布进行过渡,而不是如何使用画布。
对于潜在的地理特征,我们可以使用 canvas 很容易地绘制它们,包括几个步骤:
// set up canvas and context
var canvas = d3.select("canvas")
var context = canvas.node().getContext("2d");
// create the path
var path = d3.geoPath().projection(projection).context(context);
// draw a feature:
context.beginPath()
path(geojsonFeature)
context.stroke();
要进行过渡,有几个选项,我将在这里使用 d3.transition,我们可以在画布本身上调用它。但是我们需要使用补间函数来访问我们在过渡中的位置以及如何在过渡的每一帧中绘制圆圈。
transition.tween 用于在过渡过程中设置 html/svg 元素的某些属性。画布中的像素不是 html/svg 元素,也没有我们可以设置的属性/样式。但是我们可以在一些未使用的属性上使用 transition.tween 来访问其功能:
canvas.transition()
.tween("whatever",tweeningFunction);
tweeningFunction 返回一个插值器,该插值器采用单个参数 (t
),该参数表示转换的进度。 t
范围从 0 到 1。返回的内插器在整个过渡过程中被重复调用,因此我们可以使用它来定位画布圆圈:
function tween() {
// return interpolator:
return function(t) {
// clear the canvas once per frame:
context.clearRect(0,width,height);
// draw all points each frame:
data.forEach(function(d) {
// Start drawing a path:
context.beginPath();
// interpolate where point is based on t
var p = d3.interpolateObject(d.p0,d.p1)(t);
// draw that point:
context.arc(p.x,p.y,10,2*Math.PI);
context.fill();
})
return ""; // return a value to set the "whatever" attribute
}
作为一个工作示例:
var data = [
{ p0: {x:100,p1: {x:120,y:200} },{ p0: {x:150,p1: {x:400,y:150} },{ p0: {x:200,y:250},y:20} },];
var canvas = d3.select("canvas")
var context = canvas.node().getContext("2d");
var width = 500;
var height = 300;
canvas.call(repeat);
function repeat() {
d3.select(this).transition()
.tween("nothing",tween)
.duration(1000)
.on("end",repeat);
}
function tween() {
var interpolote = d3.interpoloate;
return function(t) {
// clear the canvas:
context.clearRect(0,2*Math.PI);
context.fill();
})
return "";
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<canvas width="500" height="300"></canvas>
我不知道你的其余代码,所以我不能说如何将它全部转换为画布,尽管这可能是几个单独的问题。如果您愿意,我可以进一步分解补间功能,但它确实假设对画布有基本的了解
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。