Создание бесконечной горизонтальной оси с помощью d3 v4

Я использую 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, почему этот шаг терпит неудачу и что я могу сделать, чтобы исправить это.

Вот функция, которая рисует ось и подключает событие масштабирования:

  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 — это SVG, содержащий элемент группы scrollerContent. Это 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"
  }
];

Надеюсь, это показывает всю работу, необходимую для объяснения проблемы. Спасибо за любой совет или руководство.


person Alex Reynolds    schedule 03.12.2017    source источник
comment
пожалуйста, предоставьте axample рабочий код   -  person KEKUATAN    schedule 06.12.2017


Ответы (1)


Мне было трудно следовать вашему коду без полного воспроизводимого примера. Поэтому я закодировал простой пример того, что вы пытаетесь сделать. Возможно поможет:

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

person Mark    schedule 09.12.2017