[英]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邻居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.