首页 > 解决方案 > D3.js 跳跃缩放行为

问题描述

我正在尝试制作一个可缩放的热图,SO 上的社区提供了很大帮助,但是我今天整天都在试图修复一个小故障,而且我每次都碰壁。

问题是缩放看起来很跳跃,即绘图渲染得很好,但是当缩放事件被触发时,会发生某种转换,以突然的方式改变轴和缩放。下面的代码演示了这个问题。问题并不总是发生,它取决于热图尺寸和/或点数。

在 SO 上遇到同样问题的人的一些类似情况原来是缩放没有应用于正确的对象,但我认为我没有犯那个错误。提前谢谢了

<!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>Heatmap 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>
  <script src='heatmap.js' type='text/javascript'></script>
</head>

<body>
  <div id="chart">
    <svg width="550" height="1000"></svg>
  </div>
  <script>
    var dataset = [];
    for (let i = 1; i < 60; i++) { //360
      for (j = 1; j < 70; j++) { //75
        dataset.push({
          xKey: i,
          xLabel: "xMark " + i,
          yKey: j,
          yLabel: "yMark " + j,
          val: Math.random() * 25,

        })
      }
    };

    var svg = d3.select("#chart")
      .select("svg")

    var xLabels = [],
      yLabels = [];
    for (i = 0; i < dataset.length; i++) {
      if (i == 0) {
        xLabels.push(dataset[i].xLabel);
        var j = 0;
        while (dataset[j + 1].xLabel == dataset[j].xLabel) {
          yLabels.push(dataset[j].yLabel);
          j++;
        }
        yLabels.push(dataset[j].yLabel);
      } else {
        if (dataset[i - 1].xLabel == dataset[i].xLabel) {
          //do nothing
        } else {
          xLabels.push(dataset[i].xLabel);
        }
      }
    };



    var margin = {
      top: 0,
      right: 25,
      bottom: 40,
      left: 75
    };

    var width = +svg.attr("width") - margin.left - margin.right,
      height = +svg.attr("height") - margin.top - margin.bottom;

    var dotSpacing = 0,
      dotWidth = width / (2 * (xLabels.length + 1)),
      dotHeight = height / (2 * yLabels.length);

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


    var daysRange = d3.extent(dataset, function(d) {
        return d.xKey
      }),
      days = daysRange[1] - daysRange[0];

    var hoursRange = d3.extent(dataset, function(d) {
        return d.yKey
      }),
      hours = hoursRange[1] - hoursRange[0];

    var tRange = d3.extent(dataset, function(d) {
        return d.val
      }),
      tMin = tRange[0],
      tMax = tRange[1];

    //    var width = (dotWidth * 2 + dotSpacing) * days,
    //        height = (dotHeight * 2 + dotSpacing) * hours;
    //    var width = +svg.attr("width") - margin.left - margin.right,
    //        height = +svg.attr("height") - margin.top - margin.bottom;

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

    // the scale
    var scale = {
      x: d3.scaleLinear()
        .domain([-1, d3.max(dataset, d => d.xKey)])
        .range([-1, width]),
      y: d3.scaleLinear()
        .domain([0, d3.max(dataset, d => d.yKey)])
        .range([height, 0]),
      //.range([(dotHeight * 2 + dotSpacing) * hours, dotHeight * 2 + dotSpacing]),
    };

    var xBand = d3.scaleBand().domain(xLabels).range([0, width]),
      yBand = d3.scaleBand().domain(yLabels).range([height, 0]);

    var axis = {
      x: d3.axisBottom(scale.x).tickFormat((d, e) => xLabels[d]),
      y: d3.axisLeft(scale.y).tickFormat((d, e) => yLabels[d]),
    };


    function updateScales(data) {
      scale.x.domain([-1, d3.max(data, d => d.xKey)]),
        scale.y.domain([0, d3.max(data, d => d.yKey)])
    }

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



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

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

    // SVG canvas
    svg = d3.select("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 + dotHeight);


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

    heatDotsGroup.call(zoom);



    //Create X axis
    var renderXAxis = svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + scale.y(-1) + ")")
      .call(axis.x)

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


    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);
      d3.event.transform.k = Math.max(d3.event.transform.k, 1);
      //console.log(d3.event.transform)

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

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

    svg.call(renderPlot, dataset)

    function renderPlot(selection, dataset) {
      //updateScales(dataset);

      heatDotsGroup.selectAll("ellipse")
        .data(dataset)
        .enter()
        .append("ellipse")
        .attr("cx", function(d) {
          return scale.x(d.xKey) - xBand.bandwidth();
        })
        .attr("cy", function(d) {
          return scale.y(d.yKey) + yBand.bandwidth();
        })
        .attr("rx", dotWidth)
        .attr("ry", dotHeight)
        .attr("fill", function(d) {
          return colorScale(d.val);
        })
        .on("mouseover", function(d) {
          $("#tooltip").html("X: " + d.xKey + "<br/>Y: " + d.yKey + "<br/>Value: " + Math.round(d.val * 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);
        });


    }
  </script>
</body>

</html>

标签: javascriptd3.jszooming

解决方案


更改缩放比例Extend

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

调用整个svg 的缩放而不是 heatDotsGroup 因为这个节点接收转换,也不g是在具有图形转换的节点上svg(为了让事情有点模糊)

svg = d3.select("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 + ")");

// heatDotsGroup.call(zoom);

不要限制刻度k中的缩放比例。已经被保姆照顾了scaleExtent()

//   d3.event.transform.k = Math.max(d3.event.transform.k, 1);

d3.max()当您已经计算了这些值时,为什么还要计算所有d3.extent()这些值?


推荐阅读