d3 同步 2 个单独的缩放行为

par*_*ent 25 d3.js d3fc

我有以下 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);
    });
Run Code Online (Sandbox Code Playgroud)

所需的行为是缩放、平移和/或重新缩放轴,以始终从上一个操作完成的位置应用变换,而不会出现任何“跳跃”。

Col*_*inE 16

好的,所以我又做了一次 - 正如我之前的回答中提到的,您需要克服的最大问题是 d3-zoom 只允许对称缩放。这是一个已经被广泛讨论的问题,我相信 Mike Bostock 会在下一个版本中解决这个问题。

因此,为了克服这个问题,您需要使用多个缩放行为。我创建了一个图表,其中包含三个,每个轴一个,绘图区域一个。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;
};
Run Code Online (Sandbox Code Playgroud)

这是带有工作示例的笔:

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


Col*_*inE 7

你的代码有几个问题,一个很容易解决,一个不是......

首先,d3-zoom 通过在选定的 DOM 元素上存储变换来工作 - 您可以通过__zoom属性看到这一点。当用户与 DOM 元素交互时,此转换会更新并发出事件。因此,如果您有不同的缩放行为,这两者都控制单个元素的平移/缩放,则需要保持这些变换同步。

您可以按如下方式复制转换:

selection.call(zoom.transform, d3.event.transform);
Run Code Online (Sandbox Code Playgroud)

但是,这也会导致缩放事件也从目标行为中触发。

另一种方法是直接复制到“隐藏”转换属性:

selection.node().__zoom = d3.event.transform;
Run Code Online (Sandbox Code Playgroud)

但是,您要实现的目标存在更大的问题。d3-zoom 变换存储为变换矩阵的 3 个分量:

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

因此,缩放只能表示对称缩放和平移。您应用于 x 轴的非对称缩放无法通过此变换忠实地表示并重新应用于绘图区域。


ade*_*ago 6

正如@ColinE 已经指出的那样,这是一项即将推出的功能。原始代码始终执行与变换矩阵不同步的“时间缩放”。

最好的解决方法是调整xExtent范围,使图表相信两侧有额外的蜡烛。这可以通过在侧面添加衬垫来实现。的accessors,而不是,

[d => d.date]
Run Code Online (Sandbox Code Playgroud)

变成,

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

旁注:请注意,有一个pad函数应该这样做,但由于某种原因它只能工作一次并且永远不会再次更新,这就是为什么它被添加为accessors.

旁注 2:addDays为了简单起见,作为原型添加的函数(不是最好的做法)。

现在缩放事件修改了我们的 X 缩放因子xZoom

zoomFactor = Math.sign(d3.event.sourceEvent.wheelDelta) * -5;
if (zoomFactor) xZoom += zoomFactor;
Run Code Online (Sandbox Code Playgroud)

直接从 读取差值很重要wheelDelta。这就是不受支持的功能所在:我们无法读取,t.x因为即使您拖动 Y 轴它也会改变。

最后重新计算 chart.xDomain(xExtent(data.series));以便新范围可用。

在此处查看没有跳转的工作演示:https : //codepen.io/adelriosantiago/pen/QWjwRXa?editors=0011

修正:缩放反转,改进了触控板上的行为。

从技术上讲,您还可以yExtent通过添加额外的d.highand 来进行调整d.low。甚至都xExtentyExtent避免使用在所有的变换矩阵。