代码之家  ›  专栏  ›  技术社区  ›  Aenaon

D3:慢速可缩放热图

  •  4
  • Aenaon  · 技术社区  · 7 年前

    我有这个可缩放的热图,放大或缩小时看起来太慢了。有什么可以让它更快/更平滑的吗,或者是它有太多的点,这是我所能拥有的最好的。我想知道是否有一些技巧,使它更轻的浏览器,请同时保持增强,如工具提示。或者我的代码处理缩放功能不是很好。

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

    谢谢

    3 回复  |  直到 7 年前
        1
  •  3
  •   rioV8    7 年前

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

    通过设置 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)"));
        }
    
        2
  •  3
  •   Andrew Reid    7 年前

    尝试画布

    你有27000个节点。这可能是SVG性能下降的主要原因,画布开始真正发光。当然,Canvas不像SVG那样有状态,它只是像素,没有好的元素可以在DOM中鼠标悬停,告诉您它们在哪里和什么地方。但是,有办法解决这个缺点,这样我们就可以保持速度和互动能力。

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

    对于像省略号这样的非常小的事情,存在混淆问题的风险,与SVG相比,canvas很难解决这些问题,尽管每个浏览器呈现这些问题的方式会有所不同

    设计含义

    使用画布,我们可以保持与SVG一样的输入/退出/更新周期,但我们也有选择放弃它。有时,输入/退出/更新周期对画布非常好:转换、动态数据、HiRracic数据等。我以前花费了一些时间来研究D3与画布和SVG之间的一些高级差异。 here .

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

    绘制热图

    我用矩形是为了简洁。Canvas的椭圆方法还没有完全准备好,但是您可以 emulate it easily enough .

    我们需要一个绘制数据集的函数。如果您在数据集中硬编码了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轴用于SVG。所以,我刚刚在画布元素的上方叠加了一个SVG,它绝对定位并在上面的SVG上禁用鼠标事件。

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

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

    我们可以使用以下几种方法获取像素位置并将其转换为特定的基准:

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

    第三个选项可能是最常见的选项之一,虽然前两个看起来很相似,但find方法在内部是不同的(voronoi neighbors vs quad tree)。在这种情况下,最后一种方法相当合适:我们有一个数据网格,我们可以反转鼠标坐标来获取行和列数据。基于您的代码片段,可能如下所示:

    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的边缘,原样),我没有修改这个来考虑椭圆/矩形的宽度/高度。这个比例问题与我没有修改的问题相差甚远。

    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>
        3
  •  2
  •   Xavier Guihot    7 年前

    以下综合建议的结果并不完美,但主观上稍好一些:

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

    我们可以尝试的第一件事是仅在zoom事件结束时激活zoom更改,以避免在单个zoom事件期间发生这些不确定的更新跳转。由于只进行一次计算,因此它有效地降低了所需的处理,并且消除了全局跳跃的不适:

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

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