d3.js:将 SVG 地理路径转换为画布

Posted

技术标签:

【中文标题】d3.js:将 SVG 地理路径转换为画布【英文标题】:d3.js: convert SVG geo-path to canvas 【发布时间】:2021-04-10 09:00:40 【问题描述】:

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

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

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

我读到我应该使用画布来获得更好的性能,但是在阅读了一些教程之后,我仍然无法将我的 SVG 代码正确地重写为画布代码。任何有关如何在此处正确使用画布的帮助表示赞赏。

使用 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 文件包含这样的纬度/经度坐标:

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

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

谢谢! 迈克尔

【问题讨论】:

【参考方案1】:

我不确定您是否需要转换为画布。 20 000 转换 svg 节点应该很慢,但 500 应该是可管理的。为了比较,这里是 20 000 画布节点转换,对于不同策略的比较 this 可能会很有趣。

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

保留 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>

这也允许每个转换都受制于自己的持续时间/延迟,而无需等待所有其他转换完成后再重新开始。

画布

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

对于底层的地理特征,我们可以用画布相当容易地绘制它们,包括几个步骤:

 // 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, 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  ></canvas>

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

【讨论】:

完美,这完全解决了它!我现在用几千个元素运行它,它非常流畅。谢谢!我想我暂时会尝试坚持使用 SVG,因此无需在画布上深入了解太多细节。不过,我想知道.each 是否可以单独转换元素?现在这是一个重大的转变。我尝试在过渡时使用.delay((d, i) =&gt; i * 50)“惊人”以创建更多单独的动作,但它仍然是一个动作持续 4 秒然后重置。你能给我一个关于这个的提示吗?但也许这是一个不同的问题。谢谢!

以上是关于d3.js:将 SVG 地理路径转换为画布的主要内容,如果未能解决你的问题,请参考以下文章

用新的替换 d3.js 路径转换?

d3.js,将转换应用于数据?

如何将 SVG 文件转换为 HTML5 的画布?

D3.js:坐标轴

在将 svg 转换为 img 中使用 css

如何在不改变坐标的情况下转换(旋转)svg文本元素?