d3 синхронизирует 2 отдельных режима увеличения

11

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

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

Диаграмма имеет поведение масштабирования для основной области и отдельное поведение масштабирования для оси Y. Ось 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);
    });

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

парламент
источник

Ответы:

10

Итак, я попробовал еще раз - как уже упоминалось в моем предыдущем ответе , самая большая проблема, которую вам нужно преодолеть, заключается в том, что 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

Coline
источник
Колин, большое спасибо за то, что дал еще один шанс. Я открыл ваш кодекс и заметил, что он не работает, как ожидалось. В моем коде ручки перетаскивание оси Y изменяет масштаб графика (это желаемое поведение). В вашем коде ручки перетаскивание оси просто перемещает диаграмму.
парламент
1
Мне удалось создать такой, который позволяет перетаскивать как масштаб по оси Y, так и область графика codepen.io/colineberhardt-the-bashful/pen/mdeJyrK - но масштабирование области графика является довольно сложной задачей
ColinE
5

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

Во-первых, d3-zoom работает, сохраняя преобразование на выбранных элементах DOM - вы можете увидеть это через __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, не может быть точно представлен этим преобразованием и повторно применен к области графика.

Coline
источник
Спасибо, Колин, я хочу, чтобы все это имело смысл для меня, но я не понимаю, каково решение. Я добавлю 500 к этому вопросу как можно скорее, и, надеюсь, кто-то может помочь исправить мой код.
парламент
Не беспокойтесь, не легко решить проблему, но щедрость может привлечь некоторые предложения помощи.
ColinE
2

Это предстоящая особенность новая , как уже отмечалось @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
]

Примечание: обратите внимание, что есть 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чтобы вообще не использовать матрицу преобразования.

adelriosantiago
источник
Спасибо. К сожалению, поведение зума здесь действительно испорчено. Зум ощущается очень резким, а в некоторых случаях даже несколько раз меняет направление (с помощью трекпада MacBook). Поведение при масштабировании должно оставаться таким же, как и у оригинального пера.
парламент
Я обновил ручку с помощью нескольких улучшений, в частности, реверсивного зума.
adelriosantiago
1
Я собираюсь назначить вам награду за ваши усилия, и я начинаю вторую награду, потому что мне все еще нужен ответ с лучшим масштабированием / панорамированием. К сожалению, этот метод, основанный на дельта-изменениях, по-прежнему прерывистый и не плавный при масштабировании и при панорамировании слишком быстро. Я подозреваю, что вы, вероятно, можете бесконечно корректировать фактор и все же не получить плавного поведения. Я ищу ответ, который не меняет масштабирование / панорамирование исходного пера.
парламент
1
Я также настроил оригинальное перо, чтобы масштабировать только вдоль оси X. Это заслуженное поведение, и теперь я понимаю, что это может усложнить ответ.
парламент