d3使用SVG轴和画布图表进行缩放和拖动

时间:2019-07-12 12:51:27

标签: d3.js svg canvas zoom drag

我有一个包含很多点的图表。这就是为什么我使用canvas画线的原因。对于x和y轴,我想使用SVG,因为它更清晰,并且使用canvas绘制文本并不是超级快。

这是代码(TypeScript)

import { min, max } from "d3-array";
import { scaleLinear, ScaleLinear } from "d3-scale";
import { select, event, Selection } from "d3-selection";
import { line, Line } from "d3-shape";
import { ZoomBehavior, zoom } from "d3-zoom";
import { axisBottom, Axis, axisLeft } from "d3-axis";

interface Margin {
  left: number;
  right: number;
  top: number;
  bottom: number;
}

interface Config {
  margin: Margin;
  target: HTMLCanvasElement;
  svg: SVGSVGElement;
}

export default class ScopeChart {
  private canvas: Selection<HTMLCanvasElement, unknown, null, undefined>;
  private svg: Selection<SVGGElement, unknown, null, undefined>;
  private xAxis: Axis<number>;
  private xAxisGroup: Selection<SVGGElement, unknown, null, undefined>;
  private yAxis: Axis<number>;
  private yAxisGroup: Selection<SVGGElement, unknown, null, undefined>;
  private context: CanvasRenderingContext2D;
  private raw: number[];
  private filtered: number[];
  private xScale: ScaleLinear<number, number>;
  private yScale: ScaleLinear<number, number>;
  private line: Line<number>;

  public constructor(config: Config) {
    this.raw = [];
    this.filtered = [];

    const behavior = zoom() as ZoomBehavior<SVGGElement, unknown>;

    const width = 640;
    const height = 480;

    const w = width - config.margin.left - config.margin.right;
    const h = height - config.margin.top - config.margin.bottom;

    this.canvas = select(config.target)
      .attr("width", w)
      .attr("height", h)
      .style(
        "transform",
        `translate(${config.margin.left}px, ${config.margin.top}px)`
      );

    this.svg = select(config.svg)
      .attr("width", width)
      .attr("height", height)
      .append("g")
      .attr(
        "transform",
        `translate(${config.margin.left}, ${config.margin.top})`
      );

    this.svg
      .append("rect")
      .attr("width", w)
      .attr("height", h)
      .style("fill", "none")
      .style("pointer-events", "all")
      .call(behavior);

    behavior
      // set min to 1 to prevent zooming out of data
      .scaleExtent([1, Infinity])
      // prevent dragging data out of view
      .translateExtent([[0, 0], [width, height]])
      .on("zoom", this.zoom);

    this.context = (this.canvas.node() as HTMLCanvasElement).getContext(
      "2d"
    ) as CanvasRenderingContext2D;

    this.xScale = scaleLinear().range([0, w]);

    this.xAxis = axisBottom(this.xScale) as Axis<number>;
    this.xAxisGroup = this.svg
      .append("g")
      .attr("class", "x axis")
      .attr("transform", `translate(0, ${h})`)
      .call(this.xAxis);

    this.yScale = scaleLinear().range([h, 0]);

    this.yAxis = axisLeft(this.yScale) as Axis<number>;
    this.yAxisGroup = this.svg
      .append("g")
      .attr("class", "y axis")
      .call(this.yAxis);

    this.line = line<number>()
      .x((_, i): number => this.xScale(i))
      .y((d): number => this.yScale(d))
      .context(this.context);
  }

  private drawRaw(context: CanvasRenderingContext2D): void {
    context.beginPath();
    this.line(this.raw);

    context.lineWidth = 1;
    context.strokeStyle = "steelblue";
    context.stroke();
  }

  private drawFiltered(context: CanvasRenderingContext2D): void {
    context.beginPath();
    this.line(this.filtered);

    context.lineWidth = 1;
    context.strokeStyle = "orange";
    context.stroke();
  }

  private clear(context: CanvasRenderingContext2D): void {
    const width = this.canvas.property("width");
    const height = this.canvas.property("height");

    context.clearRect(0, 0, width, height);
  }

  public render(raw: number[], filtered: number[]): void {
    this.raw = raw;
    this.filtered = filtered;

    this.xScale.domain([0, raw.length - 1]);
    this.yScale.domain([min(raw) as number, max(raw) as number]);

    this.clear(this.context);
    this.drawRaw(this.context);
    this.drawFiltered(this.context);

    this.xAxisGroup.call(this.xAxis);
    this.yAxisGroup.call(this.yAxis);
  }

  public zoom = (): void => {
    const newXScale = event.transform.rescaleX(this.xScale);
    const newYScale = event.transform.rescaleY(this.yScale);

    this.line.x((_, i): number => newXScale(i));
    this.line.y((d): number => newYScale(d));

    this.clear(this.context);
    this.drawRaw(this.context);
    this.drawFiltered(this.context);

    this.xAxisGroup.call(this.xAxis.scale(newXScale));
    this.yAxisGroup.call(this.yAxis.scale(newYScale));
  };
}

这是现场示例

https://codesandbox.io/s/1pprq

问题是translateExtent。在放大到可用数据(即x轴上的[0, 20000]和y轴上的[-1.2, 1.2])时,我想限制拖动。

我目前可以以某种方式进一步放大。放大并一直拖动到底部时,可以看到效果。您将看到最小值与x轴之间的间隙。

我认为这与使用canvassvg有关。 svgcanvas之上,而ZoomBehaviorsvg上。缩放不正确地转换为canvas

我想将svg放在最前面,因为我以后需要更多的接口元素,这些元素必须添加到svg中。

有什么想法吗?谢谢!

1 个答案:

答案 0 :(得分:1)

如果我正确理解了这个问题:

您遇到的问题是您的翻译范围不正确

behavior
  // set min to 1 to prevent zooming out of data
  .scaleExtent([1, Infinity])
  // prevent dragging data out of view
  .translateExtent([[0, 0], [width, height]])
  .on("zoom", this.zoom);

在上面,widthheight是指SVG的宽度和高度,而不是画布。另外,通常不会显式指定缩放范围,但如果未使用zoom.extent()指定缩放范围,则缩放范围默认为调用它的容器的尺寸。

如果平移范围的大小与缩放范围(默认情况下为容器(SVG)的范围)相等,则可以缩放和平移该容器的坐标空间内的任何位置,但不能超出其坐标范围。因此,当缩放比例为1时,我们无法平移任何地方,就像我们定义的那样平移超出平移范围。

注意:从逻辑上讲,这意味着翻译范围必须包含且不小于缩放范围。

但是,在这种情况下,如果我们放大,则可以平移并保持在平移范围内。

我们可以看到,如果放大,则无法平移超过预期的限制。这是因为画布的顶部在y==0,这是翻译范围的边界。

如您所注意,如果放大,则可以平移超出预期的限制。画布的底部是h,小于翻译范围限制的height,因此当我们放大时,随着h之间的差距,我们可以越来越往下平移每次缩放时height都会增加(如上所述,k==1时无法平移)。

我们可以尝试更改翻译范围以反映画布的边界。但是,由于画布小于SVG,因此无法使用,因为平移范围小于缩放范围。如上文所述,并由Mike here指出:“问题是您指定的translateExtent小于缩放范围。因此无法满足所请求的约束。”

但是,我们可以修改translateExtent和缩放范围:

behavior
  // set min to 1 to prevent zooming out of data
  .scaleExtent([1, Infinity])
  // set the zoom extent to the canvas size:
  .extent([[0,0],[w,h]])
  // prevent dragging data out of view
  .translateExtent([[0, 0], [w, h]])
  .on("zoom", this.zoom);

上面的代码创建了一个缩放行为,将画布限制在其原始范围内-如果我们在画布上调用缩放并且希望无法平移画布,我们将提供相同的参数(除非我们可以依靠默认缩放范围以提供适当的值,而不是手动指定缩放范围。

这是更新的sandbox