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

Ale*_*lds 5 svg d3.js

我正在使用d3 v4(4.12.0).

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

我想模拟一个"无限"或"无尽"的水平轴.

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

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

我的程序:

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

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

  2. 根据新的偏移值更新x轴刻度

  3. 使用更新的比例范围重新绘制轴标签(x_scale)

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

我的尝试直到第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);
    }
  }
Run Code Online (Sandbox Code Playgroud)

这是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>
    );
  }
Run Code Online (Sandbox Code Playgroud)

如图所示,scrollerContainerref是包含group元素的SVG scrollerContent.这scrollerContent是包含水平轴的内容.

平移或滚动x轴时,将应用变换scrollerContent.

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

import * as d3 from 'd3';
import { parseSvg } from "d3-interpolate/src/transform/parse";
Run Code Online (Sandbox Code Playgroud)

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

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"
  }
];
Run Code Online (Sandbox Code Playgroud)

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

Mar*_*ark 6

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

<!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>
Run Code Online (Sandbox Code Playgroud)