d3.js:将 SVG 地理路径转换为 ​​canvas

d3.js: convert SVG geo-path to canvas

我是 D3 的新手,刚写了一小段代码来在地图上显示点转换: Static Image
我从大约 100 点的小样本开始,效果很好。

但最终,我想在这里显示大约 1000 个点,如动画所示:https://www.zeit.de/feature/pendeln-stau-arbeit-verkehr-wohnort-arbeitsweg-ballungsraeume

但对于 D3 和浏览器来说,即使 500 点似乎也无法处理 - 动画不再重复。

我读到我应该使用 canvas 以获得更好的性能,但是在阅读了一些教程之后我仍然无法将我的 SVG 代码正确地重写为 canvas 代码。在此感谢有关如何正确使用 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) => aProjection([d.x_start, d.y_start])[1])
                    .transition()
                    .duration(4000)
                    .attr('cx', (d) => aProjection([d.x_target, d.y_target])[0])
                    .attr('cy', (d) => aProjection([d.x_target, d.y_target])[1])
                    .on('end', repeat);
            };
        };
        circleTransition();

加载的 CSV 文件包含 lat/lon 坐标,如下所示:

x_start y_start x_target y_target value
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

有没有一种简单的方法可以将此代码转换为使用 canvas 或以其他方式提高性能?

谢谢! 迈克尔

我不确定您是否需要转换为 canvas。 20 000 transitioning svg nodes should be slow, but 500 should be manageable. For comparision, here's 20 000 canvas nodes transitioning, and for comparision of different strategies this 可能很有趣。

我将为您的 SVG 代码提供优化,以及如何使用 canvas。

保持 SVG

您没有有效地使用 d3.transition - 这可能是性能问题的原因。

d3.transition 中的低效率在于您如何使用 transition.on("end",此方法在 每个 元素的转换结束时调用一个函数.由于您有 500 个元素正在转换,因此您对所有 500 个元素调用此函数 500 次,实际上您试图在每个循环中对单个元素启动转换 250,000 次。转换的每次初始化都会调用投影函数 4 次,因此您每个周期投影点一百万次,而您只需要这样做 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) => aProjection([d.x_start, d.y_start])[1])
                .transition()
                .duration(4000)
                .attr('cx', (d) => aProjection([d.x_target, d.y_target])[0])
                .attr('cy', (d) => aProjection([d.x_target, 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>

这还允许每个转换都受制于它自己的 duration/delay,而无需等待所有其他转换完成后再开始。

Canvas

Canvas 将需要对您的代码进行相当完整的修改,并且需要对 d3 采取不同的方法。鼠标交互等功能需要非常 种不同的方法。我假设至少对下面的 canvas 有基本的了解,因为问题是关于如何使用 canvas 进行转换,而不是如何使用 canvas.

对于基础地理特征,我们可以使用 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,我们可以在 canvas 本身上调用它。但是我们需要使用补间函数来访问我们在过渡中的位置以及如何在过渡的每一帧绘制圆圈。

transition.tween 用于在过渡过程中设置 html/svg 元素的某些 属性。 canvas 中的像素不是 html/svg 元素,也没有我们可以设置的 attributes/styles。但是我们可以在一些未使用的属性上使用 transition.tween 来访问其功能:

 canvas.transition()
  .tween("whatever",tweeningFunction);

tweeningFunction returns 一个采用单个参数 (t) 的插值器,该参数表示过渡的进度。 t 范围从 0 到 1。返回的插值器在整个转换过程中被重复调用,所以我们可以用它来定位 canvas 圆圈:

 function tween() {
   // return interpolator:
   return function(t) {

       // clear the canvas once per frame:
       context.clearRect(0, 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,0,2*Math.PI);
           context.fill();
       }) 
       return ""; // return a value to set the "whatever" attribute
 }

作为一个工作示例:

var data = [
{ p0: {x:100,y:100}, p1: {x:120,y:200} },
{ p0: {x:150,y:100}, p1: {x:400,y:150} },
{ p0: {x:200,y:250}, p1: {x:120,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, 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,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>

我不知道你的其余代码,所以我不能说如何将它们全部转换为 canvas,尽管这可能是几个单独的问题。如果你愿意,我可以进一步分解补间函数,但它确实假设对 canvas

有基本的理解