Javascript / D3.js - 绘制大型数据集 - 提高 d3.js 绘制的 svg 图表中的缩放和平移速度

Posted

技术标签:

【中文标题】Javascript / D3.js - 绘制大型数据集 - 提高 d3.js 绘制的 svg 图表中的缩放和平移速度【英文标题】:Javascript / D3.js - draw large data set - improve the speed of zoom and pan in svg chart ploted by d3.js 【发布时间】:2020-10-18 20:05:25 【问题描述】:

编辑

刚刚找到了50 million points with d3.js.的帖子

与缩放和平移的交互缓慢是由于 svg 中的元素过多。关键是使用层次化细节层次,就像 image pyramid. 一样,限制 svg 中的最大元素。

原帖

我正在尝试从 csv/excel 文件中读取一些数据点并使用 d3.js 绘制它们。

数据集包含 100,000 行,每行包含一个时间戳和一个当时的值。

Time stamp, pressure
12/17/2019 12:00:00 AM, 600

我跟随this example绘制了缩放和平移的时间压力图。

没有问题,工作完美。

一个问题是,在处理大型数据集(例如 500,000 个数据点)时,与图表的交互缓慢。

500,000个数据点的图表显示了一个整体的形状,细节只有在放大时才会出现。

放大时,所有数据点都被重新绘制并被剪切路径剪切掉。速度还有提升空间吗?

更新代码

function draw(res)
        //clear the current content in the div
      document.getElementById("spectrum-fig").innerhtml = '';    

      var fullwidth = d3.select('#spectrum-fig').node().getBoundingClientRect().width;
      fullwidth = fullwidth < 500? 500:fullwidth;
      var fullheight = 500;
      var resLevelOne = getWindowed(res, 1);
      var resLevelTwo = getWindowed(res, 2);

      var designMax= getMaxPressureKPa();
      var resMax = getPsiTopTen(res);
      const SMYSKPa = getSMYSPressureKPa();
      const avePsi = getAvePsi(res);

      var psiRange = d3.max(res, d=>d.psi) - d3.min(res, d=>d.psi);
      var resSmallChart = getWindowed(res, 2);//
      //filtSpectrum(res, 0.05*psiRange); //0.05 magic numbers
      //var resSmallChart = res;
      //margin for focus chart, margin for small chart
      var margin = left:50, right: 50, top: 30, bottom:170,
          margin2 = left:50, right: 50, top: 360, bottom:30, 
          width = fullwidth - margin.left - margin.right,
          height = fullheight - margin.top - margin.bottom,
          height2 = fullheight - margin2.top-margin2.bottom;
      
      //x, y, for big chart, x2, y2 for small chart
      var x = d3.scaleTime().domain(d3.extent(res, d => d.Time)).range([0, width]),
          x2 = d3.scaleTime().domain(d3.extent(res, d => d.Time)).range([0, width]),
          y  = d3.scaleLinear().domain([0, SMYSKPa]).range([height, 0]),
          y2 = d3.scaleLinear().domain([0, SMYSKPa]).range([height2, 0]);

      //clear the content in Spectrum-fig div before drawring
      //avoid multiple drawings;
      var xAxis =d3.axisBottom(x).tickFormat(d3.timeFormat("%m-%d")),
          xAxis2 = d3.axisBottom(x2).tickFormat(d3.timeFormat("%b")),
          yAxis = d3.axisLeft(y);

      var brush = d3.brushX()                   // Add the brush feature using the d3.brush function
          .extent( [ [0,0], [width,height2] ] )  // initialise the brush area: start at 0,0 and finishes at width,height: it means I select the whole graph area
          .on("brush end", brushed);               // trigger the brushed function 
        
      var zoom = d3.zoom()
          .scaleExtent([1, 100]) //defined the scale extend 
          .translateExtent([[0, 0], [width, height]])
          .extent([[0, 0], [width, height]])
          .on("zoom", zoomed); //at the zoom end trigger zoomed function
      
          
      //line for big chart line
      var line = d3.line()
                .x(function(d)  return x(d.Time) )
                .y(function(d)  return y(d.psi) );

      //line2 for small chart line
      var line2 = d3.line()
                  .x(function(d)  return x2(d.Time) )
                  .y(function(d)  return y2(d.psi) );

      var svg = d3.select("#spectrum-fig")
                  .append("svg")
                  .attr("width", fullwidth)
                  .attr("height", fullheight);
      
      svg.append("defs").append("clipPath")
            .attr("id", "clip")
        .append("rect")
            .attr("width", width)
            .attr("height", height);

      var focus = svg.append("g")
            .attr("class", "focus")
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
              
      var context = svg.append("g")
            .attr("class", "context")
            .attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
      
      focus.append("g")
      .attr("class", "axis axis--x")
      .attr("transform", "translate (0," + height +")")
      .call(xAxis);

      focus.append("g")
      .attr("class", "axis axis--y")
      .call(yAxis);

      focus.append("g")
      .attr("transform", "translate (" +  width + ", 0)")
      .call(d3.axisRight(y).tickFormat('').tickSize(0));

      focus.append("g")
      .attr("transform", "translate (0, 0)")
      .call(d3.axisTop(x).tickFormat('').tickSize(0));

      // Add the line
      focus.insert("path")
        //.datum(res)
        .attr("class", "line")  // I add the class line to be able to modify this line later on.
        .attr("fill", "none")
        .attr('clip-path', 'url(#clip)')
        .attr("stroke", "steelblue")
        .attr("stroke-width", 1.5)
        .attr("d", line(resLevelTwo));

      context.insert("path")
        //.datum(resSmallChart)
        .attr("class", "line")
        .attr("stroke", "steelblue")
        .attr("stroke-width", 1.5)
        .attr("fill", "none")
        .attr("d", line2(resSmallChart));
  
      context.append("g")
        .attr("class", "axis axis--x")
        .attr("transform", "translate(0," + height2 + ")")
        .call(xAxis2);
  
      context.append("g")
        .attr("class", "brush")
        .call(brush)
        .call(brush.move, x.range());
  
      svg.append("rect")
        .attr("class", "zoom")
        .attr('fill', 'none')
        .attr('cursor', 'move')
        .attr('pointer-events', 'all')
        .attr("width", width)
        .attr("height", height)
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
        .call(zoom);

      function getWindowed(arr, level) 
        var windowed = new Array();
        var arrLength = arr.length;
        var windowSize =Math.pow(16, level); //set the window size
        for(let i = 0; i * windowSize < arrLength; i++ ) //each to be the window size
          let startIndex = i * windowSize;
          let endIndex = (i+1) * windowSize;
          endIndex = endIndex >= arrLength ? arrLength-1 : endIndex;
          let localExtreme = findLocalExtreme(arr.slice(startIndex, endIndex));
          if (localExtreme.Max.Time.getTime() === localExtreme.Min.Time.getTime()) //anything include = need getTime
            windowed.push(localExtreme.Max)
          else if(localExtreme.Max.Time  < localExtreme.Min.Time)
            windowed.push(localExtreme.Max);
            windowed.push(localExtreme.Min);
          else
            windowed.push(localExtreme.Min);
            windowed.push(localExtreme.Max);
          
        
        let firstElement = ...arr[0];
        let lastElement = ...arr[arr.length-1];
        if(firstElement.Time.getTime() != windowed[0].Time.getTime()) //insert to the position zero
          windowed.unshift(firstElement);
        
        if(lastElement.Time.getTime() != windowed[windowed.length-1].Time.getTime())
          windowed.push(lastElement);
        //insert to the end last member;
        return windowed;
      

      function findLocalExtreme(slicedArr)
        if(slicedArr === undefined || slicedArr.length == 0)
          throw 'error: no array members';
         
        let slicedArrLength = slicedArr.length;
        let tempMax = ...slicedArr[0];
        let tempMin = ...slicedArr[0];
        if(slicedArrLength === 1)
          return 
            Max: tempMax,
            Min: tempMin
          
        
        for (let i = 1; i < slicedArrLength; i++)
          if (slicedArr[i].psi > tempMax.psi)
            tempMax = ...slicedArr[i];
          
          if (slicedArr[i].psi < tempMin.psi)
            tempMin = ...slicedArr[i];
          
        
        return 
          Max: tempMax,
          Min: tempMin
        
      

      function getDataToDraw(timeRange) //timeRange [0,1] , [startTime, endTime]
        const bisect = d3.bisector(d => d.Time).left;
        const startIndex = bisect(res, timeRange[0]);
        const endIndex = bisect(res, timeRange[1]);
        const numberInOriginal = endIndex-startIndex+1;
        const windowSize =16;
        const maxNumber = 8000;
        let level = Math.ceil(Math.log(numberInOriginal/maxNumber ) / Math.log(windowSize));
        if(level <=0 ) level =0;
        console.log(endIndex, startIndex, endIndex-startIndex+1, level);
        if(level === 0)
          return res.slice(startIndex, endIndex);
        if(level === 1)
          let start_i = bisect(resLevelOne, timeRange[0]);
          let end_i =bisect(resLevelOne, timeRange[1]);
          return resLevelOne.slice(start_i, end_i);
        else  //if level 2 or higher, never happen
          let start_i = bisect(resLevelTwo, timeRange[0]);
          let end_i =bisect(resLevelTwo, timeRange[1]);
          return resLevelTwo.slice(start_i, end_i);
        
      

      function brushed() 
          if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
          var s = d3.event.selection || x2.range();
          x.domain(s.map(x2.invert, x2));
          focus.select(".line").attr("d", line(getDataToDraw(x.domain())));
          focus.select(".axis--x").call(xAxis);
          svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
              .scale(width / (s[1] - s[0]))
              .translate(-s[0], 0));
      
        
      function zoomed() 
          if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
          var t = d3.event.transform;
          //console.log(t);
          x.domain(t.rescaleX(x2).domain());
          focus.select(".line").attr("d", line(getDataToDraw(t.rescaleX(x2).domain())));
          focus.select(".axis--x").call(xAxis);
          context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
      

【问题讨论】:

【参考方案1】:

这是我的想法。

重新绘制似乎是必须的,因为当你放大点时你怎么能期望有相同的位置?

但是,您可以控制某些重绘频率。例如,人们使用debounce 在任何事件期间将触发次数减少到 50 毫秒以下(尤其是平移)。 Debounce 是一个通用的解决方案,您可以查看lodash library 以获得一些实现。

  .on("zoom", debounced(zoomed)) // lower the chance if you get 5 calls under 500ms

此外,如果涉及任何动画,您可以将动画推迟到缩放(或平移)的最后阶段,这类似于去抖动概念。或者只是简单地禁用动画。

注意:React 确实支持另一种称为并发的模式,它默认不启用,目前还没有。但是它的作用是,假设每个绘图被一个小组件捕获,并且它花费 1ms 进行渲染,那么在它渲染了 16 个组件之后,它认为它在此渲染中花费了太多时间,并将响应返回给浏览器处理其他事情,例如。用户输入等。这样您就可以开始滚动页面或移动鼠标。在下一个周期中,它可以拾取接下来的 16 个组件。假设您有 1000 个组件,它需要几个周期才能完成所有渲染。如果你在中间再次放大,它将跳过前 16 个组件并重新移动到新的渲染。希望你能明白。它可能会帮助您解决最新的 React 18 问题。

【讨论】:

【参考方案2】:

参考帖子plotting 50 million points with d3.js.

与缩放和平移的交互缓慢是由于 svg 中的元素过多。关键是使用层次化细节层次,限制svg中的最大元素。

【讨论】:

以上是关于Javascript / D3.js - 绘制大型数据集 - 提高 d3.js 绘制的 svg 图表中的缩放和平移速度的主要内容,如果未能解决你的问题,请参考以下文章

d3.select 不抓取正确的标签

使用D3.js绘制地图并打点

D3.js 绘制geojson不正确

使用 d3.js 绘制赛道

使用 d3.js 和 TypeScript 绘制饼图时出现编译错误

d3.js地图图表绘制矩形