d3 синхронизирует 2 отдельных режима увеличения - PullRequest
10 голосов
/ 07 апреля 2020

У меня есть следующий d3 / d3f c диаграмма

https://codepen.io/parliament718/pen/BaNQPXx

Диаграмма имеет поведение масштабирования для основной области и отдельное поведение масштабирования для оси у. Ось Y можно перетащить для изменения масштаба.

Проблема, с которой я сталкиваюсь, заключается в том, что после перетаскивания оси Y для изменения масштаба, а затем последующего панорамирования диаграммы на диаграмме возникает «скачок». .

Очевидно, что 2 режима увеличения имеют разъединение и должны быть синхронизированы, но я ломаю голову, пытаясь это исправить.

const mainZoom = zoom()
    .on('zoom', () => {
       xScale.domain(t.rescaleX(x2).domain());
       yScale.domain(t.rescaleY(y2).domain());
    });

const yAxisZoom = zoom()
    .on('zoom', () => {
        const t = event.transform;
        yScale.domain(t.rescaleY(y2).domain());
        render();
    });

const yAxisDrag = drag()
    .on('drag', (args) => {
        const factor = Math.pow(2, -event.dy * 0.01);
        plotArea.call(yAxisZoom.scaleBy, factor);
    });

Желаемое поведение для масштабирования, панорамирования, и / или масштабирование оси, чтобы всегда применять преобразование, где бы ни было завершено предыдущее действие, без каких-либо «скачков».

Ответы [ 3 ]

10 голосов
/ 10 апреля 2020

ОК, так что у меня было еще go - как уже упоминалось в моем предыдущем ответе , самая большая проблема, которую вам нужно преодолеть, заключается в том, что d3-zoom допускает только симметричное масштабирование. Это то, что широко обсуждается , и я считаю, что Майк Босток решает эту проблему в следующем выпуске.

Итак, чтобы преодолеть эту проблему, вам нужно использовать несколько масштабирования , Я создал диаграмму с тремя, по одному для каждой оси и по одному для области графика. Режимы масштабирования X & Y используются для масштабирования осей. Всякий раз, когда событие масштабирования вызывается режимами масштабирования X & Y, их значения перевода копируются в область графика. Аналогично, когда происходит перевод в области графика, компоненты x & y копируются в соответствующие поведения оси.

Масштабирование в области графика немного сложнее, так как нам нужно поддерживать соотношение сторон. Чтобы добиться этого, я сохраняю предыдущее преобразование масштаба и использую дельту шкалы, чтобы определить подходящий масштаб для применения к режимам масштабирования X & Y.

Для удобства я обернул все это в компонент диаграммы:


const interactiveChart = (xScale, yScale) => {
  const zoom = d3.zoom();
  const xZoom = d3.zoom();
  const yZoom = d3.zoom();

  const chart = fc.chartCartesian(xScale, yScale).decorate(sel => {
    const plotAreaNode = sel.select(".plot-area").node();
    const xAxisNode = sel.select(".x-axis").node();
    const yAxisNode = sel.select(".y-axis").node();

    const applyTransform = () => {
      // apply the zoom transform from the x-scale
      xScale.domain(
        d3
          .zoomTransform(xAxisNode)
          .rescaleX(xScaleOriginal)
          .domain()
      );
      // apply the zoom transform from the y-scale
      yScale.domain(
        d3
          .zoomTransform(yAxisNode)
          .rescaleY(yScaleOriginal)
          .domain()
      );
      sel.node().requestRedraw();
    };

    zoom.on("zoom", () => {
      // compute how much the user has zoomed since the last event
      const factor = (plotAreaNode.__zoom.k - plotAreaNode.__zoomOld.k) / plotAreaNode.__zoomOld.k;
      plotAreaNode.__zoomOld = plotAreaNode.__zoom;

      // apply scale to the x & y axis, maintaining their aspect ratio
      xAxisNode.__zoom.k = xAxisNode.__zoom.k * (1 + factor);
      yAxisNode.__zoom.k = yAxisNode.__zoom.k * (1 + factor);

      // apply transform
      xAxisNode.__zoom.x = d3.zoomTransform(plotAreaNode).x;
      yAxisNode.__zoom.y = d3.zoomTransform(plotAreaNode).y;

      applyTransform();
    });

    xZoom.on("zoom", () => {
      plotAreaNode.__zoom.x = d3.zoomTransform(xAxisNode).x;
      applyTransform();
    });

    yZoom.on("zoom", () => {
      plotAreaNode.__zoom.y = d3.zoomTransform(yAxisNode).y;
      applyTransform();
    });

    sel
      .enter()
      .select(".plot-area")
      .on("measure.range", () => {
        xScaleOriginal.range([0, d3.event.detail.width]);
        yScaleOriginal.range([d3.event.detail.height, 0]);
      })
      .call(zoom);

    plotAreaNode.__zoomOld = plotAreaNode.__zoom;

    // cannot use enter selection as this pulls data through
    sel.selectAll(".y-axis").call(yZoom);
    sel.selectAll(".x-axis").call(xZoom);

    decorate(sel);
  });

  let xScaleOriginal = xScale.copy(),
    yScaleOriginal = yScale.copy();

  let decorate = () => {};

  const instance = selection => chart(selection);

  // property setters not show 

  return instance;
};

Вот ручка с рабочим примером:

https://codepen.io/colineberhardt-the-bashful/pen/qBOEEGJ

5 голосов
/ 07 апреля 2020

Есть несколько проблем с вашим кодом, один из которых легко решить, а другой нет ...

Во-первых, d3-zoom работает путем сохранения преобразования в выбранном элементе DOM. (s) - вы можете увидеть это через свойство __zoom. Когда пользователь взаимодействует с элементом DOM, это преобразование обновляется и события генерируются. Следовательно, если вам необходимо использовать разные способы масштабирования, которые управляют панорамированием / масштабированием одного элемента, вам необходимо синхронизировать эти преобразования.

Вы можете скопировать преобразование следующим образом:

selection.call(zoom.transform, d3.event.transform);

Однако это также приведет к тому, что события масштабирования также будут запускаться из целевого поведения.

Альтернативой является копирование непосредственно в свойство преобразования stashed:

selection.node().__zoom = d3.event.transform;

Однако есть большая проблема с тем, чего вы пытаетесь достичь. Преобразование d3-zoom сохраняется как 3 компонента матрицы преобразования:

https://github.com/d3/d3-zoom#zoomTransform

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

2 голосов
/ 12 апреля 2020

Это предстоящая функция , как уже отмечалось @ColinE. Исходный код всегда выполняет «временное масштабирование», которое не синхронизируется с матрицей преобразования.

Лучший обходной путь - настроить диапазон xExtent так, чтобы график полагал, что на стороны. Это может быть достигнуто путем добавления колодок по бокам. accessors вместо

[d => d.date]

становится

[
  () => new Date(taken[0].date.addDays(-xZoom)), // Left pad
  d => d.date,
  () => new Date(taken[taken.length - 1].date.addDays(xZoom)) // Right pad
]

Sidenote: Обратите внимание, что есть функция pad, которую должна делать это , но по какой-то причине он работает только один раз и никогда не обновляется, поэтому он добавляется как accessors.

Sidenote 2: Функция addDays добавлена ​​в качестве прототипа ( не самое лучшее, что нужно сделать) просто для простоты.

Теперь событие масштабирования изменяет наш коэффициент увеличения X, xZoom,

zoomFactor = Math.sign(d3.event.sourceEvent.wheelDelta) * -5;
if (zoomFactor) xZoom += zoomFactor;

Важно читать дифференциал непосредственно с wheelDelta. Вот где неподдерживаемая функция: мы не можем прочитать из t.x, поскольку она изменится, даже если вы перетащите ось Y.

Наконец, пересчитайте chart.xDomain(xExtent(data.series));, чтобы был доступен новый экстент.

Смотрите рабочую демонстрацию без скачка здесь: https://codepen.io/adelriosantiago/pen/QWjwRXa?editors=0011

Исправлено: изменение масштаба, улучшенное поведение на трекпаде.

Технически вы также можете настроить yExtent, добавив дополнительные d.high и d.low. Или даже оба xExtent и yExtent, чтобы вообще не использовать матрицу преобразования.

...