简体   繁体   English

D3:缓慢的可缩放热图

[英]D3: slow zoomable heatmap

I have this zoomable heatmap, which looks too slow when zooming-in or out. 我有这个可缩放的热图,在放大或缩小时看起来太慢了。 Is there anything to make it faster/smoother or it is just too many points and that is the best I can have. 是否有任何东西可以使它更快/更顺畅,或者只是太多点,这是我能拥有的最好的。 I was wondering if there is some trick to make it lighter for the browser please while keeping enhancements like tooltips. 我想知道是否有一些技巧可以让浏览器更轻松,同时保持工具提示等增强功能。 Or maybe my code handling the zoom feature is not great . 或者我处理缩放功能的代码也不是很好。

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

Thanks 谢谢

The solution is not to update all the dots for the zoom but to apply the zoom transform to the group containing the dots. 解决方案不是更新缩放的所有点,而是将缩放变换应用于包含点的组。 Clipping of the group needs to be done on an additional parent g heatDotsGroup . 需要在额外的父g heatDotsGroup上完成组的heatDotsGroup

The zoom scale of y is taken care of (set it fixed to 1) with a regex replace, limit translate in y by setting the transform.y to 0, and limit the translate of x based on the current scale. 使用正则表达式替换来处理y的缩放比例(将其固定为1),通过将transform.y设置为0来限制平移,并根据当前比例限制x的平移。

Allow a little translate past 0 to show the first dot complete when zoomed in. 允许稍微翻译过去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)"));
    }

Try Canvas 尝试画布

You have 27 000 nodes. 你有27 000个节点。 This is probably around the point where SVG performance drops off for most and Canvas starts to really shine. 这可能是SVG性能下降最多的时候,Canvas开始真正发光。 Sure, Canvas isn't stateful like SVG, its just pixels with no nice elements to mouse over in the DOM and tell you where and what they are. 当然,Canvas不像SVG那样有状态,它只是像素,没有很好的元素可以在DOM中鼠标悬停并告诉你它们在哪里和它们是什么。 But, there are ways to address this shortcoming so that we can retain speed and interactive abilities. 但是,有办法解决这个缺点,以便我们保持速度和互动能力。

For the initial rendering using your snippet, I have a average rendering time of ~440ms. 对于使用您的代码段的初始渲染,我的平均渲染时间约为440毫秒。 But, through the magic of canvas, I can render the same heat map with an average rendering time of ~103ms. 但是,通过画布的魔力,我可以渲染相同的热图,平均渲染时间约为103毫秒。 Those savings can be applied to things like zooming, animation etc. 这些节省可以应用于缩放,动画等。

For very small things like your ellipses there is a risk of aliasing issues that is harder to fix with canvas as opposed to SVG, though how each browser renders this will differ 对于像椭圆这样的非常小的东西,存在使用canvas而不是SVG更难修复别名问题的风险,尽管每个浏览器呈现的方式会有所不同

Design Implications 设计意义

With Canvas we can retain the enter/exit/update cycle as with SVG, but we also have the option of dropping it. 使用Canvas,我们可以像SVG一样保留进入/退出/更新周期,但我们也可以选择删除它。 At times the enter/exit/update cycle pairs extremely well with canvas: transitions, dynamic data, heirarcical data, etc. I have previously spent some time on some of the higher level differences between Canvas and SVG with regards to D3 here . 在次进入/退出/更新周期对非常好,用帆布:转换,动态数据,heirarcical数据等,我以前花了一些时间对某些与问候D3 Canvas和SVG之间的更高水平的差异在这里

For my answer here, we'll leave the enter cycle. 对于我在这里的回答,我们将离开进入周期。 When we want to update the visualization we just redraw everything based on the data array itself. 当我们想要更新可视化时,我们只是根据数据数组本身重绘所有内容。

Drawing the Heat Map 绘制热图

I'm using rectangles for the sake of brevity. 我为了简洁起见使用矩形。 Canvas's ellipse method isn't quite ready, but you can emulate it easily enough . Canvas的椭圆方法尚未准备好,但您可以轻松地模拟它

We need a function that draws the dataset. 我们需要一个绘制数据集的函数。 If you had x/y/color hard coded into the dataset we could use a very simple: 如果您将x / y /颜色硬编码到数据集中,我们可以使用非常简单的:

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

But we need to scale your values, calculate a color, and we should apply the zoom. 但我们需要缩放您的值,计算颜色,我们应该应用缩放。 I ended up with a relatively simple: 我最后得到了一个相对简单的:

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(); 
  })    
}

This can be used to initially draw the nodes (when d3.event isn't defined), or on zoom/pan events (after which this function is called each time). 这可以用于初始绘制节点(当未定义d3.event时),或用于缩放/平移事件(之后每次调用此函数)。

What about the axes? 轴怎么样?

d3-axis is intended for SVG. d3轴用于SVG。 So, I've just superimposed an SVG overtop of a Canvas element positioning both absolutely and disabling mouse events on the overlying SVG. 所以,我刚刚叠加了一个Canvas元素的SVG顶层,在上层SVG中定位绝对和禁用鼠标事件。

Speaking of axes, I only have one drawing function (no difference between update/initial drawing), so I use a reference x scale and a rendering x scale from the get go, rather than creating a disposable rescaled x scale in the update function 说到轴,我只有一个绘图功能(更新/初始绘图之间没有区别),所以我使用参考x刻度和get get的渲染x刻度,而不是在更新函数中创建一次性重新缩放的x刻度

Now I Have a Canvas, How Do I Interact With It? 现在我有一个画布,我该如何与它交互?

There are a few methods we could use take a pixel position and convert it to a specific datum: 我们可以使用一些像素位置并将其转换为特定数据的方法:

  • Use a Voronoi diagram (using the .find method to locate a datum) 使用Voronoi图(使用.find方法定位基准)
  • Use a Force layout (also using the .find method to locate a datum) 使用Force布局(也使用.find方法定位基准)
  • Use a hidden Canvas (using pixel color to indicate datum index) 使用隐藏的画布(使用像素颜色表示基准索引)
  • Use a scale's invert function (when data is gridded) 使用比例的反转功能(当数据被网格化时)

The third option may be one of the most common, and while the first two look similar the find methods do differ internally (voronoi neighbors vs quad tree). 第三个选项可能是最常见的选项之一,虽然前两个选项看起来相似,但查找方法在内部确实不同(voronoi邻居vs四叉树)。 The last method is fairly appropriate in this case: we have a grid of data and we can invert the mouse coordinate to get row and column data. 在这种情况下,最后一种方法非常合适:我们有一个数据网格,我们可以反转鼠标坐标来获取行和列数据。 Based on your snippet that might look like: 根据您的代码段看起来像:

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

*I've used mousemove since mouseover will trigger once when moving over the canvas, we need to continuously update, if we wanted to hide the tooltip, we could just check to see if the pixel selected is white: *我使用了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 */ }

Example

I've explicitly mentioned most of the changes above, but I've made some additional changes below. 我已经明确提到了上面的大部分更改,但我在下面做了一些额外的更改。 First, I need to select the canvas, position it, get the context, etc. I also have swapped rects for ellipses, so the positioning is a bit different (but you have other positioning issues to from using a linear scale (the ellipse centroids can fall on the edge of the svg as is), I've not modified this to account for the width/height of the ellipses/rects. This scale issue was far enough from the question that I didn't modify it. 首先,我需要选择画布,定位它,获取上下文等等。我也交换了椭圆形,所以定位有点不同(但是你使用线性刻度(椭圆形质心)还有其他定位问题我可以按原样落在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> 

The result of all following combined suggestions is not perfect, but it is subjectively slightly better: 以下所有组合建议的结果并不完美,但主观上略好一些:

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

  • Using on("end", zoomed) instead of on("zoom", zoomed) : 使用on("end", zoomed)而不是on("zoom", zoomed)

First thing we can try is to activate the zoom change only at the end of the zoom event in order not to have these non deterministic updates jumps during a single zoom event. 我们可以尝试的第一件事是仅在缩放事件结束时激活缩放更改,以便在单个缩放事件期间不会跳过这些非确定性更新。 It has for effect to lower the required processing as only one computation happens, and it removes the global jump discomfort: 它可以降低所需的处理效果,因为只进行了一次计算,它消除了全局跳跃的不适:

var zoom = d3.zoom()
  .scaleExtent([dotWidth, dotHeight])
  .translateExtent([ [80, 20], [width, height] ])
  .on("end", zoomed); // instead of .on("zoom", zoomed);
  • Remove updates of things which remains the same during the zoom: 删除缩放期间保持相同的更新:

We can also remove from the nodes update things which stay the same such as the color of a circle which during the zoom remains the same anyway .attr("fill", function(d) { return colorScale(d.tOutC); }); 我们还可以从节点中删除保持相同的内容,例如在缩放期间保持相同的圆的颜色.attr("fill", function(d) { return colorScale(d.tOutC); }); and .attr('clip-path', 'url(#clip)') . .attr('clip-path', 'url(#clip)')

  • Computing only once things used several times: 只计算一次使用过几次:

The new circle radius after the zoom can only be computed once instead of 27K times as it's the same for all circles: 缩放后的新圆半径只能计算一次而不是27K次,因为它对所有圆都是相同的:

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

.attr("rx", scaledRadius)

Same for x positions, we can compute it once per possible x value (360 times) and store it in an array to access them in constant time instead of computing it 27K times: 对于x位置,我们可以按照每个可能的x值(360次)计算一次,并将其存储在一个数组中,以便在恒定时间内访问它们,而不是计算它27K次:

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

.attr("cx", d => scaledCxes[d.day])
  • Last obvious option would be to reduce the number of nodes since it's the root of the issue! 最后一个显而易见的选择是减少节点数量,因为它是问题的根源!

  • If the zoom extent would have been bigger, I would have also suggested filtering nodes not visible anymore. 如果缩放范围会更大,我还建议过滤节点不再可见。

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

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