使用d3 v4创建无尽的水平轴

时间:2017-12-03 12:48:21

标签: d3.js svg

我正在使用d3 v4(4.12.0)。

我有一个SVG容器,我正在绘制一个简单的水平轴(x轴,线性刻度),它响应用鼠标平移。

我想模拟一个“无限”或“无尽”的水平轴。

通过这个,我的意思是我只想加载和渲染一个非常大的数据集的一小部分,并且只绘制足够的轴来显示这个大集合中的一小部分元素。

假设我的水平轴显示来自更大对象数组的10个数据点。我持有offset参数,该参数从0开始,以显示该数组的前十个点。

我的程序:

当我将轴向左滚动到足以显示第11个及后续数据点时,我接着:

  1. 更新offset参数以反映我翻译的单位数

  2. 根据新的偏移值

  3. 更新x轴刻度
  4. 使用更新的比例范围(x_scale)重绘轴标签

  5. 将包含轴的组元素转换为表示轴上一个单位的像素数(scroller_element_width

  6. 我的尝试直到第3步。此过程似乎在步骤4失败,因为轴的最终转换从未发生过。

    整个轴向左移动,并且它有新标签,但它不会随着那些更新的标签向右移动 - 它基本上会脱离页面。

    我想问d3专家,为什么这一步失败了,我可以做些什么来解决这个问题。

    这是绘制轴并挂钩zoom事件的函数:

      renderScroller() {
        console.log("renderScroller called");
        if ((this.state.scrollerWidth == 0) || (this.state.scrollerHeight == 0)) return;
    
        const self = this;
        const scroller = this.scrollerContainer;
        const scroller_content = this.scrollerContent;
        const scroller_width = this.state.scrollerWidth;
        const scroller_height = this.state.scrollerHeight; 
    
        var offset = 0,
            limit = 10,
            current_index = 10;
    
        var min_translate_x = 0,
            max_translate_x;
    
        var scroller_data = Constants.test_data.slice(offset, limit);
    
        var x_extent = d3.extent(scroller_data, function(d) { return d.window; });
        var y_extent = [0, d3.max(scroller_data, function(d) { return d.total; })];
    
        var x_scale = d3.scaleLinear();
        var y_scale = d3.scaleLinear();
    
        var x_axis_call = d3.axisTop();
    
        x_scale.domain(x_extent).range([0, scroller_width]);
        y_scale.domain(y_extent).range([scroller_height, 0]);
    
        x_axis_call.scale(x_scale);
    
        d3.select(scroller_content)
          .append("g")
          .attr("class", "x axis")
          .attr("transform", "translate(" + [0, scroller_height] + ")")
          .call(x_axis_call);
    
        var scroller_element_width = parseFloat(scroller_width / (x_scale.domain()[1] - x_scale.domain()[0]));
    
        var pan = d3.zoom()
          .on("zoom", function () { 
    
            var t = parseSvg(d3.select(scroller_content).attr("transform"));
            var x_offset = parseFloat((t.translateX + d3.event.transform.x) / scroller_element_width);
    
            //
            // lock scale and prevent y-axis pan
            //
            d3.event.transform.y = 0;
            if (d3.event.transform.k == 1) {
              d3.event.transform.x = (x_offset > 0) ? 0 : d3.event.transform.x;
            }
            else {
              d3.event.transform.k = 1;
              d3.event.transform.x = t.translateX;
            }
            d3.select(scroller_content).attr("transform", d3.event.transform);
    
            t = parseSvg(d3.select(scroller_content).attr("transform"));
            x_offset = parseFloat(t.translateX / scroller_element_width);
    
            var test_offset = Math.abs(parseInt(x_offset));
    
            if (test_offset != offset) {
              scroller_data = updateScrollerData(test_offset);
              x_extent = d3.extent(scroller_data, function(d) { return d.window; });
              y_extent = [0, d3.max(scroller_data, function(d) { return d.total; })];
              x_scale.domain(x_extent).range([0, scroller_width]);
              y_scale.domain(y_extent).range([scroller_height, 0]);
              x_axis_call.scale(x_scale);
    
              //
              // update axis labels
              //
              d3.select(scroller_content)
                .selectAll(".x.axis")
                .call(x_axis_call);
    
              //
              // shift the axis backwards to simulate an endless horizontal axis
              //  
              var pre_shift = parseSvg(d3.select(scroller_content).attr("transform"));
              console.log("pre_shift", pre_shift.translateX);
              console.log("scroller_element_width", scroller_element_width);
              var expected_post_shift = pre_shift.translateX + scroller_element_width;
              console.log("(expected) post_shift", expected_post_shift);
    
              d3.zoom().translateBy(d3.select(scroller_content), expected_post_shift, 0);
    
              //               
              // observed and expected translate values do not match!
              // 
              var post_shift = parseSvg(d3.select(scroller_content).attr("transform"));
              console.log("(observed) post_shift", post_shift.translateX);
            }
    
          });
    
        d3.select(scroller).call(pan);
    
        max_translate_x = this.state.scrollerWidth - x_scale(x_extent[1]);
        d3.zoom().translateBy(d3.select(scroller), max_translate_x, 0);
    
        // fetch test data
        function updateScrollerData(updated_offset) {
          offset = updated_offset;
          return Constants.test_data.slice(updated_offset - 1, updated_offset + limit - 1);
        }
      }
    

    这是React组件中的一个函数。 React的东西不是那么相关,但这里是该组件的render()函数,用于显示父SVG和子组元素:

      render() {
        return (
          <svg 
            className="scroller" 
            ref={(scroller) => { this.scrollerContainer = scroller; }} 
            width={this.state.scrollerWidth} 
            height={this.state.scrollerHeight}>
            <g 
              className="scroller-content"
              ref={(scrollerContent) => { this.scrollerContent = scrollerContent; }} 
            />
          </svg>
        );
      }
    

    如图所示,scrollerContainer ref是包含组元素scrollerContent的SVG。此scrollerContent包含水平轴。

    平移或滚动x轴时,转换将应用于scrollerContent

    要获取转换参数,我使用parseSvg中的d3-interpolate辅助方法,即通过ES6:

    import * as d3 from 'd3';
    import { parseSvg } from "d3-interpolate/src/transform/parse";
    

    为了完整性,这里有一小段测试数据:

    export const test_data = [
      {
        "total": 29.86,
        "signal": [
          4.842,
          1.608,
          1.837,
          3.052,
          1.677,
          0.8041,
          3.09,
          1.813,
          2.106,
          2.38,
          1.773,
          0.8128,
          2.047,
          1.658,
          0.3588
        ],
        "window": 0,
        "chr": "chr1"
      },
      {
        "total": 35.67,
        "signal": [
          0.6111,
          1.995,
          0.5715,
          2.51,
          3.318,
          1.523,
          3.94,
          2.743,
          4.445,
          0.759,
          4.938,
          2.61,
          3.379,
          1.27,
          1.057
        ],
        "window": 1,
        "chr": "chr1"
      },
      {
        "total": 39.14,
        "signal": [
          0.0589,
          0.1608,
          2.426,
          4.673,
          3.511,
          3.912,
          2.809,
          4.197,
          4.648,
          2.069,
          2.84,
          3.878,
          0.2681,
          3.622,
          0.06911
        ],
        "window": 2,
        "chr": "chr1"
      },
      {
        "total": 37.45,
        "signal": [
          2.688,
          1.235,
          2.358,
          1.994,
          1.541,
          1.189,
          0.8078,
          4.872,
          2.287,
          4.266,
          2.24,
          3.349,
          3.519,
          1.896,
          3.21
        ],
        "window": 3,
        "chr": "chr1"
      },
      {
        "total": 47.17,
        "signal": [
          3.338,
          3.613,
          3.872,
          1.166,
          1.828,
          4.24,
          1.476,
          4.025,
          4.144,
          4.922,
          2.183,
          2.701,
          3.825,
          4.346,
          1.494
        ],
        "window": 4,
        "chr": "chr1"
      },
      {
        "total": 41.7,
        "signal": [
          0.2787,
          1.74,
          0.7557,
          4.236,
          2.865,
          4.542,
          4.113,
          1.265,
          4.826,
          3.731,
          4.931,
          2.392,
          2.014,
          0.6566,
          3.352
        ],
        "window": 5,
        "chr": "chr1"
      },
      {
        "total": 31.43,
        "signal": [
          3.025,
          4.399,
          1.001,
          4.859,
          0.9173,
          2.851,
          2.916,
          1.821,
          1.228,
          1.646,
          0.1008,
          2.09,
          2.502,
          0.1476,
          1.924
        ],
        "window": 6,
        "chr": "chr1"
      },
      {
        "total": 38.23,
        "signal": [
          1.123,
          1.972,
          0.5079,
          4.808,
          0.5669,
          4.647,
          2.598,
          1.874,
          0.8699,
          4.876,
          3.981,
          1.503,
          4.683,
          2.853,
          1.366
        ],
        "window": 7,
        "chr": "chr1"
      },
      {
        "total": 44.2,
        "signal": [
          3.895,
          0.7457,
          2.208,
          1.837,
          3.219,
          3.98,
          3.494,
          4.225,
          3.117,
          3.162,
          3.171,
          2.449,
          0.1419,
          3.745,
          4.807
        ],
        "window": 8,
        "chr": "chr1"
      },
      {
        "total": 36.33,
        "signal": [
          0.3164,
          2.753,
          4.094,
          2.237,
          4.748,
          2.483,
          1.541,
          4.113,
          0.1874,
          3.71,
          1.313,
          0.221,
          2.736,
          1.208,
          4.671
        ],
        "window": 9,
        "chr": "chr1"
      },
      {
        "total": 43.05,
        "signal": [
          1.924,
          0.4136,
          3.057,
          4.686,
          1.263,
          0.1333,
          0.8786,
          4.715,
          4.845,
          4.282,
          2.112,
          4.597,
          3.822,
          1.322,
          4.999
        ],
        "window": 10,
        "chr": "chr1"
      },
      {
        "total": 31.28,
        "signal": [
          4.216,
          0.6655,
          2.078,
          1.235,
          0.5526,
          1.556,
          1.005,
          3.196,
          1.907,
          4.932,
          0.006601,
          1.269,
          3.964,
          4.608,
          0.09109
        ],
        "window": 11,
        "chr": "chr1"
      },
      {
        "total": 48.3,
        "signal": [
          4.469,
          1.138,
          3.958,
          2.801,
          3.404,
          4.988,
          2.649,
          3.818,
          3.284,
          0.9281,
          3.982,
          0.496,
          4.28,
          3.258,
          4.845
        ],
        "window": 12,
        "chr": "chr1"
      },
      {
        "total": 42.1,
        "signal": [
          1.087,
          3.127,
          0.493,
          3.276,
          4.195,
          1.561,
          2.638,
          4.897,
          3.675,
          4.937,
          0.05847,
          4.272,
          2.33,
          1.776,
          3.776
        ],
        "window": 13,
        "chr": "chr1"
      },
      {
        "total": 40.1,
        "signal": [
          1.275,
          4.574,
          2.805,
          1.646,
          0.8759,
          4.948,
          3.637,
          3.227,
          2.259,
          2.983,
          2.905,
          4.134,
          3.133,
          0.08384,
          1.617
        ],
        "window": 14,
        "chr": "chr1"
      },
      {
        "total": 50.31,
        "signal": [
          2.228,
          0.7037,
          4.977,
          1.143,
          2.506,
          4.348,
          4.344,
          3.998,
          4.213,
          2.745,
          4.374,
          3.411,
          4.504,
          4.417,
          2.396
        ],
        "window": 15,
        "chr": "chr1"
      },
      {
        "total": 34.7,
        "signal": [
          2.729,
          3.891,
          3.873,
          2.973,
          0.1487,
          1.573,
          1.781,
          2.788,
          2.191,
          2.912,
          1.355,
          2.582,
          2.374,
          3.164,
          0.3641
        ],
        "window": 16,
        "chr": "chr1"
      },
      {
        "total": 32.89,
        "signal": [
          3.619,
          2.119,
          1.854,
          4.083,
          0.9916,
          0.5065,
          0.8343,
          4.835,
          1.723,
          3.926,
          2.675,
          2.281,
          0.1531,
          2.239,
          1.049
        ],
        "window": 17,
        "chr": "chr1"
      },
      {
        "total": 38.94,
        "signal": [
          1.976,
          1.587,
          3.808,
          0.1173,
          3.823,
          4.349,
          3.652,
          1.308,
          3.434,
          3.855,
          1.622,
          0.2916,
          2.382,
          3.091,
          3.647
        ],
        "window": 18,
        "chr": "chr1"
      },
      {
        "total": 34.18,
        "signal": [
          0.339,
          3.695,
          3.108,
          3.267,
          0.08282,
          3.53,
          2.316,
          1.11,
          4.504,
          4.111,
          0.007636,
          0.5581,
          2.985,
          1.707,
          2.857
        ],
        "window": 19,
        "chr": "chr1"
      },
      {
        "total": 29.62,
        "signal": [
          2.695,
          0.8477,
          4.417,
          3.012,
          2.454,
          2.686,
          0.6529,
          0.2275,
          1.052,
          0.2092,
          2.968,
          3.268,
          0.7144,
          0.4441,
          3.973
        ],
        "window": 20,
        "chr": "chr1"
      }
    ];
    

    希望这能说明解释问题所需的所有工作。感谢您的任何建议或指导。

1 个答案:

答案 0 :(得分:4)

如果没有完整的可重复示例,我发现您的代码难以理解。所以我编写了一个简单的例子来说明你要做的事情。也许它会有所帮助:

&#13;
&#13;
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
  <style>
    .axis path {
      display: none;
    }
    
    .axis line {
      stroke-opacity: 0.3;
      shape-rendering: crispEdges;
    }
    
    .view {
      fill: url(#gradient);
      stroke: #000;
    }
    
    button {
      position: absolute;
      top: 20px;
      left: 20px;
    }
  </style>
</head>

<body>
  <svg width="500" height="500"></svg>
  <script src="//d3js.org/d3.v4.min.js"></script>
  <script>
  
    // 10,000 random data points
    var data = d3.range(1, 10000).map(function(d) {
      return {
        i: d,
        x: Math.random() < 0.5 ? Math.random() * 1000 : Math.random() * -1000,
        y: Math.random() < 0.5 ? Math.random() * 1000 : Math.random() * -1000,
      }
    });

    var svg = d3.select("svg"),
      margin = {
        top: 10,
        right: 10,
        bottom: 10,
        left: 10
      },
      width = +svg.attr("width") - margin.left - margin.right,
      height = +svg.attr("height") - margin.top - margin.bottom,
      g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // large "endless" zoom
    var zoom = d3.zoom()
      .scaleExtent([-1e100, 1e100])
      .translateExtent([
        [-1e100, -1e100],
        [1e100, 1e100]
      ])
      .on("zoom", zoomed);

    var x = d3.scaleLinear()
      .domain([-100, 100])
      .range([0, width]);

    var y = d3.scaleLinear()
      .domain([-100, 100])
      .range([height, 0]);

    var xAxis = d3.axisBottom(x)
      .ticks((width + 2) / (height + 2) * 10)
      .tickSize(-height);

    var yAxis = d3.axisRight(y)
      .ticks(10)
      .tickSize(width)
      .tickPadding(8 - width);

    var gX = svg.append("g")
      .attr("transform", "translate(0," + height + ")")
      .attr("class", "axis axis--x")
      .call(xAxis);

    var gY = svg.append("g")
      .attr("class", "axis axis--y")
      .call(yAxis);

    svg.call(zoom);

    // plot our data initially
    updateData(x, y);

    function zoomed() {
      var t = d3.event.transform,
        sx = t.rescaleX(x), //<-- rescale the scales
        sy = t.rescaleY(x);

      // swap out axis
      gX.call(xAxis.scale(sx));
      gY.call(yAxis.scale(sy));

      updateData(sx, sy)
    }

    // classic enter, update, exit pattern
    function updateData(sx, sy) {

      // filter are data to those points in range
      var f = data.filter(function(d) {
        return (
          d.x > sx.domain()[0] &&
          d.x < sx.domain()[1] &&
          d.y > sy.domain()[0] &&
          d.y < sy.domain()[1]
        )
      });

      var s = g.selectAll(".point")
        .data(f, function(d) {
          return d.i;
        });

      // remove those out of range
      s.exit().remove();

      // add the new ones in range
      s = s.enter()
        .append('circle')
        .attr('class', 'point')
        .attr('r', 10)
        .style('fill', 'steelblue')
        .merge(s);

      // update all in range
      s.attr('cx', function(d) {
          return sx(d.x);
        })
        .attr('cy', function(d) {
          return sy(d.y);
        });
    }
  </script>
</body>

</html>
&#13;
&#13;
&#13;