d3 синхронізація 2 окремих режимів збільшення


11

У мене є така діаграма d3 / d3fc

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

Діаграма має поведінку масштабування для основної області та окрему поведінку масштабування для осі у. Вісь y може бути перетягнута для зміни шкали.

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

Очевидно, що поведінка з двома зумами має відключення і потребує синхронізації, але я ламаю мозок, намагаючись це виправити.

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 дозволяє лише симетричне масштабування. Це те, що широко обговорювалося , і я вважаю, що Майк Босток вирішує це в наступному випуску.

Отже, щоб подолати проблему, вам потрібно скористатися поведінкою із кількома масштабами. Я створив діаграму, яка містить три, по одній для кожної осі та одна для ділянки ділянки. Для масштабування осей застосовується поведінка зумів 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


Колін, велике спасибі за те, що ти ще раз пішов. Я відкрив ваш кодек і помітив, що він не працює так, як очікувалося. У моєму кодені перетягування осі Y повторно масштабує графік (це бажана поведінка). У вашому кодені перетягування осі просто перемикає діаграму.
парламент

1
Мені вдалося створити таку, яка дозволяє перетягувати як y-масштаб, так і площу ділянки codepen.io/colineberhardt-the-bashful/pen/mdeJyrK - але масштабування ділянки ділянки є досить складною проблемою
ColinE

5

З вашим кодом є кілька проблем, таку, яку легко вирішити, та одну, яка не є ...

По-перше, зум d3 працює, зберігаючи перетворення на вибраних елементах DOM - це можна побачити через __zoomвластивість. Коли користувач взаємодіє з елементом DOM, це перетворення оновлюється і події випромінюються. Тому, якщо вам доводиться різними способами масштабування, обидва з яких керують панорамою / збільшенням одного елемента, вам потрібно тримати ці перетворення синхронізованими.

Ви можете скопіювати перетворення так:

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

Однак це також спричинить звільнення подій масштабування від цільової поведінки.

Альтернативою є скопіювати безпосередньо у властивість "сховано" перетворення:

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

Однак існує більша проблема з тим, що ви намагаєтесь досягти. Перетворення d3-зуму зберігається у вигляді 3 компонентів матриці перетворення:

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

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


Дякую Колін, я хотів би, щоб щось із цього було для мене сенсом, але я не розумію, яке рішення. Я додам 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
]

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.lows. Або навіть обидва xExtentі yExtentуникати використання матриці перетворення взагалі.


Дякую. На жаль, поведінка масштабування тут справді заплутана. Масштаб відчувається дуже ривком, а в деяких випадках навіть кілька разів повертає напрямок (використовуючи трекпад Macbook). Поведінка масштабування повинна залишатися такою ж, як і оригінальна ручка.
парламент

Я оновив перо з декількома вдосконаленнями, зокрема з реверсним збільшенням.
adelriosantiago

1
Я збираюся нагородити вас щедротою за ваші зусилля, і я розпочинаю другу суму, бо мені ще потрібна відповідь з кращим збільшенням / панорами. На жаль, цей метод, заснований на змінах дельти, все ще хиткий і не гладкий при збільшенні, а також при панорамуванні занадто швидко. Я підозрюю, що ви можете, ймовірно, нескінченно коригувати фактор і все одно не отримувати плавної поведінки. Я шукаю відповідь, яка не змінює масштабування / панорамування оригінальної ручки.
парламент

1
Я також налаштував оригінальну ручку для збільшення лише по осі X. Це заслужена поведінка, і я зараз розумію, що це може ускладнити відповідь.
парламент
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.