D3: 缓慢可缩放的热力图

6

我有一个可缩放的热力图,但在缩放时看起来非常缓慢。是否有任何方法可以使其更快/更流畅,或者这只是因为点太多而已达到了最佳效果。我想知道是否有一些诀窍可以让浏览器更轻松地处理它,同时保持像工具提示这样的增强功能。或者也许我的处理缩放功能的代码不是很好。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <style>
        .axis text {
            font: 10px sans-serif;
        }

        .axis path,
        .axis line {
            fill: none;
            stroke: #000000;
        }

        .x.axis path {
            //display: none;
        }

        .chart rect {
            fill: steelblue;
        }

        .chart text {
            fill: white;
            font: 10px sans-serif;
            text-anchor: end;
        }
        
        #tooltip {
          position:absolute;
          background-color: #2B292E;
          color: white;
          font-family: sans-serif;
          font-size: 15px;
          pointer-events: none; /*dont trigger events on the tooltip*/
          padding: 15px 20px 10px 20px;
          text-align: center;
          opacity: 0;
          border-radius: 4px;
        }
    </style>
    <title>Bar Chart</title>

    <!-- Reference style.css -->
    <!--    <link rel="stylesheet" type="text/css" href="style.css">-->

    <!-- Reference minified version of D3 -->
    <script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
</head>

<body>
    <div id="chart" style="width: 700px; height: 500px"></div>
    <script>
        var dataset = [];
        for (let i = 1; i < 360; i++) {
            for (j = 1; j < 75; j++) {
                dataset.push({
                    day: i,
                    hour: j,
                    tOutC: Math.random() * 25,
                })
            }
        };


        var days = d3.max(dataset, function(d) {
                return d.day;
            }) -
            d3.min(dataset, function(d) {
                return d.day;
            });
        var hours = d3.max(dataset, function(d) {
                return d.hour;
            }) -
            d3.min(dataset, function(d) {
                return d.hour;
            });

        var tMin = d3.min(dataset, function(d) {
                return d.tOutC;
            }),
            tMax = d3.max(dataset, function(d) {
                return d.tOutC;
            });

        var dotWidth = 1,
            dotHeight = 3,
            dotSpacing = 0.5;

        var margin = {
                top: 0,
                right: 25,
                bottom: 40,
                left: 25
            },
            width = (dotWidth * 2 + dotSpacing) * days,
            height = (dotHeight * 2 + dotSpacing) * hours; 


        var colors = ['#2C7BB6', '#00A6CA','#00CCBC','#90EB9D','#FFFF8C','#F9D057','#F29E2E','#E76818','#D7191C'];

        var xScale = d3.scaleLinear()
            .domain(d3.extent(dataset, function(d){return d.day}))
            .range([0, width]);

        var yScale = d3.scaleLinear()
            .domain(d3.extent(dataset, function(d){return d.hour}))
            .range([(dotHeight * 2 + dotSpacing) * hours, dotHeight * 2 + dotSpacing]);

        var colorScale = d3.scaleQuantile()
            .domain([0, colors.length - 1, d3.max(dataset, function(d) {
                return d.tOutC;
            })])
            .range(colors);

        var xAxis = d3.axisBottom().scale(xScale);



        // Define Y axis
        var yAxis = d3.axisLeft().scale(yScale);


        var zoom = d3.zoom()
            .scaleExtent([dotWidth, dotHeight])
            .translateExtent([
                [80, 20],
                [width, height]
            ])
            .on("zoom", zoomed);

        var tooltip = d3.select("body").append("div")
        .attr("id", "tooltip")
        .style("opacity", 0);

        // SVG canvas
        var svg = d3.select("#chart")
            .append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .call(zoom)
            .append("g")
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

        // Clip path
        svg.append("clipPath")
            .attr("id", "clip")
            .append("rect")
            .attr("width", width)
            .attr("height", height);


        // Heatmap dots
        svg.append("g")
            .attr("clip-path", "url(#clip)")
            .selectAll("ellipse")
            .data(dataset)
            .enter()
            .append("ellipse")
            .attr("cx", function(d) {
                return xScale(d.day);
            })
            .attr("cy", function(d) {
                return yScale(d.hour);
            })
            .attr("rx", dotWidth)
            .attr("ry", dotHeight)
            .attr("fill", function(d) {
                return colorScale(d.tOutC);
            })
            .on("mouseover", function(d){
                $("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
                var xpos = d3.event.pageX +10;
                var ypos = d3.event.pageY +20;
                $("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
            }).on("mouseout", function(){
                $("#tooltip").animate({duration: 500}).css("opacity",0);
            }); 

        //Create X axis
        var renderXAxis = svg.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + yScale(0) + ")")
            .call(xAxis)

        //Create Y axis
        var renderYAxis = svg.append("g")
            .attr("class", "y axis")
            .call(yAxis);


        function zoomed() {
            // update: rescale x axis
            renderXAxis.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));

            update();
        }

        function update() {
            // update: cache rescaleX value
            var rescaleX = d3.event.transform.rescaleX(xScale);
            svg.selectAll("ellipse")
                .attr('clip-path', 'url(#clip)')
                // update: apply rescaleX value
                .attr("cx", function(d) {
                    return rescaleX(d.day);
                })
//                .attr("cy", function(d) {
//                    return yScale(d.hour);
//                })
                // update: apply rescaleX value
                .attr("rx", function(d) {
                    return (dotWidth * d3.event.transform.k);
                })
                .attr("fill", function(d) {
                    return colorScale(d.tOutC);
                });
        }
        

        
        
    </script>
</body>

</html>

谢谢


也许这里的答案会有用 https://dev59.com/-2Ml5IYBdhLWcg3woYPa 当每次缩放时,浏览器都会重新绘制DOM元素,这显然需要一些时间。也许你可以考虑使用指定的缩放级别和+/-按钮,而不是滚动缩放。 - Nevelina A.
4个回答

5

尝试使用Canvas

你有27,000个节点。这可能是大多数人的SVG性能下降的点,而Canvas开始真正发挥作用的点。当然,Canvas不像SVG那样具有状态,它只是没有漂亮的元素在DOM中鼠标悬停告诉您它们在哪里和是什么的像素。但是,有办法解决这个缺点,以便我们保留速度和交互能力。

使用您的片段进行初始渲染,我平均渲染时间约为440毫秒。但是,通过Canvas的魔力,我可以用平均渲染时间约为103毫秒来渲染相同的热图。这些节省可以应用于缩放、动画等方面。

对于像椭圆这样非常小的东西,存在一些易于使用SVG修复而难以使用Canvas修复的走样问题,尽管每个浏览器呈现方式都不同

设计影响

使用Canvas,我们可以保留与SVG相同的输入/输出/更新循环,但我们也可以选择放弃它。有时,输入/输出/更新循环与canvas非常配合:过渡、动态数据、层次数据等等。我之前花了一些时间在关于D3中Canvas和SVG之间的高级差异方面 这里

对于我的答案,我们将保留输入循环。当我们想要更新可视化时,我们只需根据数据数组重新绘制所有内容。

绘制热图

出于简洁起见,我使用矩形。Canvas的椭圆方法还不太成熟,但您可以轻松地模拟它

我们需要一个绘制数据集的函数。如果你已经将x/y/color硬编码到数据集中,我们可以使用非常简单的:

function drawNodes()
  dataset.forEach(function(d) {
    ctx.beginPath();
    ctx.rect(d.x,d.y,width,height);
    ctx.fillStyle = d.color;
    ctx.fill(); 
  })    
}

但我们需要按比例缩放您的数值,计算颜色,并应用缩放。最终我得到了一个相对简单的方案:

function drawNodes()
  var k = d3.event ? d3.event.transform.k : 1;
  var dw = dotWidth * k;
  ctx.clearRect(0,0,width,height);      // erase what's there
  dataset.forEach(function(d) {
    var x = xScale(d.day);
    var y = yScale(d.hour);
    var fill = colorScale(d.tOutC);
    ctx.beginPath();
    ctx.rect(x,y,dw,dotHeight);
    ctx.fillStyle = fill;
    ctx.strokeStyle = fill;
    ctx.stroke();
    ctx.fill(); 
  })    
}

这可以用于最初绘制节点(当d3.event未定义时),或在缩放/平移事件上(之后每次调用此函数)。

轴怎么办?

d3-axis适用于SVG。因此,我只是在Canvas元素上方叠加了一个SVG,将它们都绝对定位,并禁用了覆盖SVG上的鼠标事件。

说到轴,我只有一个绘图函数(更新/初始绘图没有区别),因此我从一开始就使用参考x比例尺和渲染x比例尺,而不是在更新函数中创建一次性重新缩放的x比例尺

现在我有一个Canvas,如何与它交互?

我们可以使用几种方法将像素位置转换为特定数据:

  • 使用泰森多边形图(使用.find方法查找数据)
  • 使用力导向布局(也使用.find方法查找数据)
  • 使用隐藏的Canvas(使用像素颜色指示数据索引)
  • 使用比例尺的invert函数(当数据被网格化时)

第三个选项可能是最常见的选项,虽然前两个看起来相似,但查找方法在内部确实有所不同(泰森多边形邻居与四叉树)。最后一种方法在这种情况下相当合适:我们有一个数据网格,我们可以反转鼠标坐标以获取行和列数据。基于你的片段,可能看起来像:

function mousemove() {
  var xy = d3.mouse(this);
  var x = Math.round(xScale.invert(xy[0]));
  var y = Math.round(yScale.invert(xy[1]));
  // For rounding on canvas edges:
  if(x > xScaleRef.domain()[1]) x = xScaleRef.domain()[1];
  if(x < xScaleRef.domain()[0]) x = xScaleRef.domain()[0];
  if(y > yScale.domain()[1]) y = yScale.domain()[1];
  if(y < yScale.domain()[0]) y = yScale.domain()[0];

  var index = --x*74 + y-1;  // minus ones for non zero indexed x,y values.
  var d = dataset[index];
  console.log(x,y,index,d)

  $("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
  var xpos = d3.event.pageX +10;
  var ypos = d3.event.pageY +20;
  $("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
}

我使用mousemove而不是mouseover,因为当鼠标移动到画布上时,mouseover只会触发一次,我们需要不断更新。如果我们想要隐藏工具提示,我们可以检查所选像素是否为白色:

var p = ctx.getImageData(xy[0], xy[1], 1, 1).data; // pixel data:
if (!p[0] && !p[1] && !p[2])   {  /* show tooltip */ }
else {  /* hide tooltip */ }

示例

我已经明确提到了大部分的更改,但是我在下面进行了一些额外的更改。首先,我需要选择画布,定位它,获取上下文等。我还将矩形替换为椭圆形,因此位置略有不同(但是由于使用线性比例尺会产生其他定位问题(椭圆形的质心可能会落在svg的边缘上),我没有修改这个来考虑椭圆形/矩形的宽度/高度。这个比例尺问题与问题相差太远,我没有对其进行修改。

var dataset = [];
  for (let i = 1; i < 360; i++) {
    for (j = 1; j < 75; j++) {
      dataset.push({
        day: i,
        hour: j,
        tOutC: Math.random() * 25,
      })
    }
};


var days = d3.max(dataset, function(d) { return d.day; }) - d3.min(dataset, function(d) { return d.day; });
var hours = d3.max(dataset, function(d) { return d.hour; }) - d3.min(dataset, function(d) { return d.hour; });
var tMin = d3.min(dataset, function(d) { return d.tOutC; }), tMax = d3.max(dataset, function(d) { return d.tOutC; });

var dotWidth = 1,
  dotHeight = 3,
  dotSpacing = 0.5;

var margin = { top: 20, right: 25, bottom: 40, left: 25 },
    width = (dotWidth * 2 + dotSpacing) * days,
    height = (dotHeight * 2 + dotSpacing) * hours; 

var tooltip = d3.select("body").append("div")
  .attr("id", "tooltip")
  .style("opacity", 0);

var colors = ['#2C7BB6', '#00A6CA','#00CCBC','#90EB9D','#FFFF8C','#F9D057','#F29E2E','#E76818','#D7191C'];

var xScale = d3.scaleLinear()
  .domain(d3.extent(dataset, function(d){return d.day}))
  .range([0, width]);
   
var xScaleRef = xScale.copy();

var yScale = d3.scaleLinear()
  .domain(d3.extent(dataset, function(d){return d.hour}))
  .range([height,0]);

var colorScale = d3.scaleQuantile()
  .domain([0, colors.length - 1, d3.max(dataset, function(d) { return d.tOutC; })])
  .range(colors);

var xAxis = d3.axisBottom().scale(xScale);
var yAxis = d3.axisLeft().scale(yScale);

var zoom = d3.zoom()
  .scaleExtent([dotWidth, dotHeight])
  .translateExtent([
    [0,0],
    [width, height]
])
.on("zoom", zoomed);

var tooltip = d3.select("body").append("div")
  .attr("id", "tooltip")
  .style("opacity", 0);

// SVG & Canvas:
var canvas = d3.select("#chart")
  .append("canvas")
  .attr("width", width)
  .attr("height", height)
  .style("left", margin.left + "px")
  .style("top", margin.top + "px")
  .style("position","absolute")
  .on("mousemove", mousemove)
  .on("mouseout", mouseout);
   
var svg = d3.select("#chart")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform","translate("+[margin.left,margin.top]+")");
   
var ctx = canvas.node().getContext("2d");

canvas.call(zoom);

// Initial Draw:
drawNodes(dataset);

//Create Axes:
var renderXAxis = svg.append("g")
  .attr("class", "x axis")
  .attr("transform", "translate(0," + yScale(0) + ")")
  .call(xAxis)

var renderYAxis = svg.append("g")
  .attr("class", "y axis")
  .call(yAxis);

// Handle Zoom:
function zoomed() {
  // rescale the x Axis:
  xScale = d3.event.transform.rescaleX(xScaleRef);  // Use Reference Scale.
  // Redraw the x Axis:
  renderXAxis.call(xAxis.scale(xScale));
  // Clear and redraw the nodes:

  drawNodes();
}
// Draw nodes:  
function drawNodes() {
  var k = d3.event ? d3.event.transform.k : 1;
  var dw = dotWidth * k;
  ctx.clearRect(0,0,width,height);  
  
  dataset.forEach(function(d) {
    var x = xScale(d.day);
    var y = yScale(d.hour);
    var fill = colorScale(d.tOutC);
    ctx.beginPath();
    ctx.rect(x,y,dw,dotHeight);
    ctx.fillStyle = fill;
    ctx.strokeStyle = fill;
    ctx.stroke();
    ctx.fill(); 
  }) 
}

// Mouse movement:
function mousemove() {
  var xy = d3.mouse(this);
  var x = Math.round(xScale.invert(xy[0]));
  var y = Math.round(yScale.invert(xy[1]));
  
  if(x > xScaleRef.domain()[1]) x = xScaleRef.domain()[1];
  if(x < xScaleRef.domain()[0]) x = xScaleRef.domain()[0];
  if(y > yScale.domain()[1]) y = yScale.domain()[1];
  if(y < yScale.domain()[0]) y = yScale.domain()[0];
  
  var index = --x*74 + y-1;  // minus ones for non zero indexed x,y values.
  var d = dataset[index];
 
  $("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
  var xpos = d3.event.pageX +10;
  var ypos = d3.event.pageY +20;
  $("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
}
function mouseout() {
   $("#tooltip").animate({duration: 500}).css("opacity",0);
};
.axis text {
            font: 10px sans-serif;
        }

        .axis path,
        .axis line {
            fill: none;
            stroke: #000000;
        }

        .x.axis path {
            //display: none;
        }

        .chart rect {
            fill: steelblue;
        }

        .chart text {
            fill: white;
            font: 10px sans-serif;
            text-anchor: end;
        }
        
        #tooltip {
          position:absolute;
          background-color: #2B292E;
          color: white;
          font-family: sans-serif;
          font-size: 15px;
          pointer-events: none; /*dont trigger events on the tooltip*/
          padding: 15px 20px 10px 20px;
          text-align: center;
          opacity: 0;
          border-radius: 4px;
        }
  svg {
   position: absolute;
   top: 0;
   left:0;
   pointer-events: none;
  }
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
<div id="chart" style="width: 700px; height: 500px"></div>


嗨,安德鲁!非常感谢您的帮助,它确实运行得非常好。您和rioV8上面的方法在视觉输出方面看起来非常接近。关于省略号没有问题,这不是什么大问题。感谢您发布它并对输入/更新/退出模式发表评论,因为下一个在我的列表中的任务是热图将随着新数据集的即时更新。您的帖子在我标记rioV8的评论/代码为已接受答案后几秒钟就出现了,再次感谢! - Aenaon
@Aenaon,没问题,根据更新,这使得它非常容易,因为你只需要更新数组并绘制 - 除非你想用一些技巧(例如转换)退出/进入/更新节点,否则在使用canvas进行此类任务时不是必需的。如果要为进入/退出/更新转换动画27,000个节点,则SVG和Canvas之间的差异将更加明显 - Andrew Reid
好的,太棒了。我会在周末处理更新模式,最好能有一个过渡效果,但如果对画布来说太过繁琐,没关系,我会跳过它的。再次感谢! - Aenaon
@Aenaon,如果打算在canvas中使用转换效果,则保留输入/更新/退出循环会使事情变得更容易(因为我们可以使用d3-transition与未呈现的元素-请参见上面引用的答案中的演示)。在最后一条评论中,我不清楚哪个表现更好,27000次转换在canvas上比svg要好得多,您的SVG帧速率将变慢(我知道一个很好的比较,我会看看是否可以重新创建它以进行视觉比较)。 - Andrew Reid
@Aenaon 这里是19200个同时转换的性能比较:SVG - Canvas。我没有得到我想要的例子,所以我做了这个基本的例子 - 不确定许可问题(它甚至有帧速率计数器)。两者应该是类似的。在两者之间应该有明显的性能差异。Firefox可能会有轻微的锯齿问题,Chrome在这个例子中的Canvas应该没问题,我还没有检查其他浏览器。 - Andrew Reid
更新一下,加入一些更有趣的东西(仅改变颜色并不太令人兴奋),动作可能更容易评判流畅性,链接保持不变。 - Andrew Reid

4
解决方案不是更新缩放比例尺下的所有点,而是将缩放转换应用于包含点的组。需要在另一个父级

太棒了!这个真的很好用,非常感谢rioV8!你能再解释一下你代码中的最后一行吗(那个正则表达式的那一行)。我知道你在注释中提到了它,但我觉得我还是没懂。再次感谢。 - Aenaon
如果将 d3.event.transform 转换为字符串,您会得到 X 和 Y 的统一缩放比例 scale(2.4567)。这不是我们想要的。正则表达式替换将其转换为 scale(2.4567, 1),Y 方向的缩放比例为 1。整个技巧利用了 SVG 擅长的:可缩放矢量图形。 - rioV8
啊,好的,是的!我现在明白你的意思了。我也可以问一下关于缩放函数中前三行吗?那些事件转换的部分。我认为它们设置了一些上限来转换x轴上的事件,但我也对你代码中math.max函数的第二个参数感到困惑。再次感谢! - Aenaon
第一行:不允许对y进行翻译。 第二行:不要在x上进行正向翻译(只需将5看作第一个点)。 第三行:基于当前比例尺的限制,对x进行翻译,以避免右侧出现白色区域。请参考d3文档中有关变换方程的说明。或者使用开发人员工具查看没有这些行时的变换情况。 - rioV8

2
所有以下建议的结合并不能完美地解决问题,但主观上会稍微好一些:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <style>
    .axis text {
      font: 10px sans-serif;
    }

    .axis path,
    .axis line {
      fill: none;
      stroke: #000000;
    }

    .x.axis path {
    //display: none;
    }

    .chart rect {
      fill: steelblue;
    }

    .chart text {
      fill: white;
      font: 10px sans-serif;
      text-anchor: end;
    }

    #tooltip {
      position:absolute;
      background-color: #2B292E;
      color: white;
      font-family: sans-serif;
      font-size: 15px;
      pointer-events: none; /*dont trigger events on the tooltip*/
      padding: 15px 20px 10px 20px;
      text-align: center;
      opacity: 0;
      border-radius: 4px;
    }
  </style>
  <title>Bar Chart</title>

  <!-- Reference style.css -->
  <!--    <link rel="stylesheet" type="text/css" href="style.css">-->

  <!-- Reference minified version of D3 -->
  <script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
  <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
</head>

<body>
<div id="chart" style="width: 700px; height: 500px"></div>
<script>
  var dataset = [];
  for (let i = 1; i < 360; i++) {
    for (j = 1; j < 75; j++) {
      dataset.push({
        day: i,
        hour: j,
        tOutC: Math.random() * 25,
      })
    }
  };


  var days = d3.max(dataset, function(d) {
      return d.day;
    }) -
    d3.min(dataset, function(d) {
      return d.day;
    });
  var hours = d3.max(dataset, function(d) {
      return d.hour;
    }) -
    d3.min(dataset, function(d) {
      return d.hour;
    });

  var tMin = d3.min(dataset, function(d) {
      return d.tOutC;
    }),
    tMax = d3.max(dataset, function(d) {
      return d.tOutC;
    });

  var dotWidth = 1,
    dotHeight = 3,
    dotSpacing = 0.5;

  var margin = {
      top: 0,
      right: 25,
      bottom: 40,
      left: 25
    },
    width = (dotWidth * 2 + dotSpacing) * days,
    height = (dotHeight * 2 + dotSpacing) * hours;


  var colors = ['#2C7BB6', '#00A6CA','#00CCBC','#90EB9D','#FFFF8C','#F9D057','#F29E2E','#E76818','#D7191C'];

  var xScale = d3.scaleLinear()
  .domain(d3.extent(dataset, function(d){return d.day}))
  .range([0, width]);

  var yScale = d3.scaleLinear()
  .domain(d3.extent(dataset, function(d){return d.hour}))
  .range([(dotHeight * 2 + dotSpacing) * hours, dotHeight * 2 + dotSpacing]);

  var colorScale = d3.scaleQuantile()
  .domain([0, colors.length - 1, d3.max(dataset, function(d) {
    return d.tOutC;
  })])
  .range(colors);

  var xAxis = d3.axisBottom().scale(xScale);



  // Define Y axis
  var yAxis = d3.axisLeft().scale(yScale);


  var zoom = d3.zoom()
  .scaleExtent([dotWidth, dotHeight])
  .translateExtent([
    [80, 20],
    [width, height]
  ])
  // .on("zoom", zoomed);
  .on("end", zoomed);

  var tooltip = d3.select("body").append("div")
  .attr("id", "tooltip")
  .style("opacity", 0);

  // SVG canvas
  var svg = d3.select("#chart")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .call(zoom)
  .append("g")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  // Clip path
  svg.append("clipPath")
  .attr("id", "clip")
  .append("rect")
  .attr("width", width)
  .attr("height", height);


  // Heatmap dots
  svg.append("g")
  .attr("clip-path", "url(#clip)")
  .selectAll("ellipse")
  .data(dataset)
  .enter()
  .append("ellipse")
  .attr("cx", function(d) {
    return xScale(d.day);
  })
  .attr("cy", function(d) {
    return yScale(d.hour);
  })
  .attr("rx", dotWidth)
  .attr("ry", dotHeight)
  .attr("fill", function(d) {
    return colorScale(d.tOutC);
  })
  .on("mouseover", function(d){
    $("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
    var xpos = d3.event.pageX +10;
    var ypos = d3.event.pageY +20;
    $("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
  }).on("mouseout", function(){
    $("#tooltip").animate({duration: 500}).css("opacity",0);
  });

  //Create X axis
  var renderXAxis = svg.append("g")
  .attr("class", "x axis")
  .attr("transform", "translate(0," + yScale(0) + ")")
  .call(xAxis)

  //Create Y axis
  var renderYAxis = svg.append("g")
  .attr("class", "y axis")
  .call(yAxis);


  function zoomed() {
    // update: rescale x axis
    renderXAxis.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));

    update();
  }

  function update() {

    // update: cache rescaleX value
    var rescaleX = d3.event.transform.rescaleX(xScale);

    var scaledRadius = dotWidth * d3.event.transform.k;

    var scaledCxes = [...Array(360).keys()].map(i => rescaleX(i));

    svg.selectAll("ellipse")
    // .attr('clip-path', 'url(#clip)')
    // update: apply rescaleX value
    .attr("cx", d => scaledCxes[d.day])
    //                .attr("cy", function(d) {
    //                    return yScale(d.hour);
    //                })
    // update: apply rescaleX value
    .attr("rx", scaledRadius)
    // .attr("fill", function(d) {
    //   return colorScale(d.tOutC);
    // });
  }




</script>
</body>

</html>

  • 使用on("end", zoomed)替代on("zoom", zoomed)

我们可以尝试的第一件事是仅在缩放事件结束时激活缩放更改,以避免在单个缩放事件期间出现这些不确定性更新跳跃。它会降低所需的处理量,因为只有一个计算会发生,并且它会消除全局跳跃不适感:

var zoom = d3.zoom()
  .scaleExtent([dotWidth, dotHeight])
  .translateExtent([ [80, 20], [width, height] ])
  .on("end", zoomed); // instead of .on("zoom", zoomed);
  • 删除缩放过程中保持不变的更新内容:

我们还可以从节点更新中删除那些在缩放过程中保持不变的内容,例如圆的颜色,因为它在缩放时仍然保持不变:.attr("fill", function(d) { return colorScale(d.tOutC); });.attr('clip-path', 'url(#clip)')

  • 只计算多次使用的内容一次:

缩放后的新圆半径只需计算一次,而不是像之前一样计算27K次,因为所有圆的半径都相同:

var scaledRadius = dotWidth * d3.event.transform.k;

.attr("rx", scaledRadius)

对于X轴位置,我们可以针对每个可能的X值(360次)计算一次,并将其存储在数组中,以便在访问时以恒定时间访问它们,而不是计算27K次:

var scaledCxes = [...Array(360).keys()].map(i => rescaleX(i));

.attr("cx", d => scaledCxes[d.day])
  • 显而易见的最后一个选项是减少节点数量,因为这是问题的根源!

  • 如果缩放范围更大,我还建议过滤不再可见的节点。


谢谢Xavier。你的评论非常有道理。 - Aenaon

0
请查看 LightningChart JS 热图 - 非商业使用免费。
以下是最佳性能热图网络图表的性能比较 https://github.com/Arction/javascript-charts-performance-comparison-heatmaps 正如您在那里所看到的,我们正在讨论可视化数十亿数据点范围内的热图,用户交互仍然运作良好。

// Source https://www.arction.com/lightningchart-js-interactive-examples/edit/lcjs-example-0800-heatmapGrid.html

/*
 * LightningChartJS example that showcases a simple XY line series.
 */

// Extract required parts from LightningChartJS.
const { lightningChart, PalettedFill, LUT, ColorRGBA, emptyLine, Themes } =
  lcjs;

const { createWaterDropDataGenerator } = xydata;

// Specify the resolution used for the heatmap.
const resolutionX = 1000;
const resolutionY = 1000;

// Create a XY Chart.
const chart = lightningChart()
  .ChartXY({
    // theme: Themes.darkGold
  })
  .setTitle(
    `Heatmap Grid Series ${resolutionX}x${resolutionY} (${(
      (resolutionX * resolutionY) /
      1000000
    ).toFixed(1)} million data points)`
  )
  .setPadding({ right: 40 });

// Create LUT and FillStyle
const palette = new LUT({
  units: "intensity",
  steps: [
    { value: 0, color: ColorRGBA(255, 255, 0) },
    { value: 30, color: ColorRGBA(255, 204, 0) },
    { value: 45, color: ColorRGBA(255, 128, 0) },
    { value: 60, color: ColorRGBA(255, 0, 0) },
  ],
  interpolate: false,
});

// Generate heatmap data.
createWaterDropDataGenerator()
  .setRows(resolutionX)
  .setColumns(resolutionY)
  .generate()
  .then((data) => {
    // Add a Heatmap to the Chart.
    const heatmap = chart
      .addHeatmapGridSeries({
        columns: resolutionX,
        rows: resolutionY,
        start: { x: 0, y: 0 },
        end: { x: resolutionX, y: resolutionY },
        dataOrder: "columns",
      })
      // Color Heatmap using previously created color look up table.
      .setFillStyle(new PalettedFill({ lut: palette }))
      .setWireframeStyle(emptyLine)
      .invalidateIntensityValues(data)
      .setMouseInteractions(false);

    // Add LegendBox.
    const legend = chart.addLegendBox()
      // Dispose example UI elements automatically if they take too much space. This is to avoid bad UI on mobile / etc. devices.
      .setAutoDispose({
        type: 'max-height',
        maxHeight: 0.70,
    })
      .add(chart)
  });
<script src="http://unpkg.com/@arction/lcjs@3.1.0/dist/lcjs.iife.js"></script>
<script src="http://unpkg.com/@arction/xydata@1.4.0/dist/xydata.iife.js"></script>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接