繁体   English   中英

D3:缓慢的可缩放热图

[英]D3: slow zoomable heatmap

我有这个可缩放的热图,在放大或缩小时看起来太慢了。 是否有任何东西可以使它更快/更顺畅,或者只是太多点,这是我能拥有的最好的。 我想知道是否有一些技巧可以让浏览器更轻松,同时保持工具提示等增强功能。 或者我处理缩放功能的代码也不是很好。

 <!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> 

谢谢

解决方案不是更新缩放的所有点,而是将缩放变换应用于包含点的组。 需要在额外的父g heatDotsGroup上完成组的heatDotsGroup

使用正则表达式替换来处理y的缩放比例(将其固定为1),通过将transform.y设置为0来限制平移,并根据当前比例限制x的平移。

允许稍微翻译过去0以显示放大时的第一个点完成。

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

    // Heatmap dots
    var heatDotsGroup = svg.append("g")
        .attr("clip-path", "url(#clip)")
        .append("g");

    heatDotsGroup.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);
        }); 

    function zoomed() {
        d3.event.transform.y = 0;
        d3.event.transform.x = Math.min(d3.event.transform.x, 5);
        d3.event.transform.x = Math.max(d3.event.transform.x, (1-d3.event.transform.k) * width );

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

        heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
    }

尝试画布

你有27 000个节点。 这可能是SVG性能下降最多的时候,Canvas开始真正发光。 当然,Canvas不像SVG那样有状态,它只是像素,没有很好的元素可以在DOM中鼠标悬停并告诉你它们在哪里和它们是什么。 但是,有办法解决这个缺点,以便我们保持速度和互动能力。

对于使用您的代码段的初始渲染,我的平均渲染时间约为440毫秒。 但是,通过画布的魔力,我可以渲染相同的热图,平均渲染时间约为103毫秒。 这些节省可以应用于缩放,动画等。

对于像椭圆这样的非常小的东西,存在使用canvas而不是SVG更难修复别名问题的风险,尽管每个浏览器呈现的方式会有所不同

设计意义

使用Canvas,我们可以像SVG一样保留进入/退出/更新周期,但我们也可以选择删除它。 在次进入/退出/更新周期对非常好,用帆布:转换,动态数据,heirarcical数据等,我以前花了一些时间对某些与问候D3 Canvas和SVG之间的更高水平的差异在这里

对于我在这里的回答,我们将离开进入周期。 当我们想要更新可视化时,我们只是根据数据数组本身重绘所有内容。

绘制热图

我为了简洁起见使用矩形。 Canvas的椭圆方法尚未准备好,但您可以轻松地模拟它

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

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轴用于SVG。 所以,我刚刚叠加了一个Canvas元素的SVG顶层,在上层SVG中定位绝对和禁用鼠标事件。

说到轴,我只有一个绘图功能(更新/初始绘图之间没有区别),所以我使用参考x刻度和get get的渲染x刻度,而不是在更新函数中创建一次性重新缩放的x刻度

现在我有一个画布,我该如何与它交互?

我们可以使用一些像素位置并将其转换为特定数据的方法:

  • 使用Voronoi图(使用.find方法定位基准)
  • 使用Force布局(也使用.find方法定位基准)
  • 使用隐藏的画布(使用像素颜色表示基准索引)
  • 使用比例的反转功能(当数据被网格化时)

第三个选项可能是最常见的选项之一,虽然前两个选项看起来相似,但查找方法在内部确实不同(voronoi邻居vs四叉树)。 在这种情况下,最后一种方法非常合适:我们有一个数据网格,我们可以反转鼠标坐标来获取行和列数据。 根据您的代码段看起来像:

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将在画布上移动时触发一次,我们需要不断更新,如果我们想要隐藏工具提示,我们可以检查选择的像素是否为白色:

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的边缘上,我没有修改它来考虑椭圆/ rects的宽度/高度。这个比例问题远远不是我没有修改它的问题。

 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> 

以下所有组合建议的结果并不完美,但主观上略好一些:

 <!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])
  • 最后一个显而易见的选择是减少节点数量,因为它是问题的根源!

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM