d3 使用 SVG 轴和 canvas 图表缩放和拖动
d3 zoom and drag with SVG axes and canvas chart
我有一张包含很多点的图表。这就是为什么我使用 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 轴之间存在间隙。
我认为这与使用canvas
和svg
有关。 svg
在 canvas
之上,ZoomBehavior
在 svg
之上。不知何故,缩放未正确转换为 canvas
.
我想将 svg
放在最前面,因为我需要更多的界面元素,这些元素会添加到 svg
。
有什么想法吗?谢谢!
如果我理解正确:
您 运行 遇到的问题是您的翻译范围不正确
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);
在上面,width
和height
指的是SVG的宽度和高度,而不是canvas。此外,通常不会明确指定缩放范围,但如果未使用 zoom.extent()
指定缩放范围,则缩放范围默认为调用它的容器的尺寸。
如果您的平移范围与您的缩放范围大小相等 - 默认情况下容器(SVG)的范围 - 它是,您可以在该容器的坐标 space 内的任何地方缩放和平移,但不要超出它的坐标。因此,当缩放比例为 1 时,我们无法平移任何地方,因为我们会平移超出平移范围的定义。
注意:这在逻辑上意味着平移范围必须包含且不小于缩放范围。
但是,在这种情况下,如果我们放大,我们可以平移并保持在平移范围内。
我们可以看到,如果您放大,则无法向上平移超出预期限制。这是因为 canvas 的顶部在 y==0
,这是翻译范围的边界。
正如您所注意到的,如果您放大,您可以向下平移超出预期的限制。 canvas 的底部是 h
,它小于平移范围限制 height
,因此当我们放大时,我们可以随着 h
和 height
每次缩放都会增加(如上所述,当 k==1
时无法平移)。
我们可以尝试更改翻译范围以反映 canvas 的范围。但是,由于 canvas 小于 SVG,因此这将不起作用,因为平移范围将小于缩放范围。如上所述,Mike 指出 here:"The problem is that the translateExtent you’ve specified is smaller than the zoom extent. So there’s no way to satisfy the requested constraint."
我们可以修改 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);
上面创建了一个缩放行为,将 canvas 限制在其原始范围内 - 如果我们在 canvas 上调用缩放并且不希望能够提供相同的参数平移到它之外(除非我们可以依靠默认缩放范围来提供适当的值,而不是手动指定缩放范围)。
这是更新后的 sandbox。
我有一张包含很多点的图表。这就是为什么我使用 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 轴之间存在间隙。
我认为这与使用canvas
和svg
有关。 svg
在 canvas
之上,ZoomBehavior
在 svg
之上。不知何故,缩放未正确转换为 canvas
.
我想将 svg
放在最前面,因为我需要更多的界面元素,这些元素会添加到 svg
。
有什么想法吗?谢谢!
如果我理解正确:
您 运行 遇到的问题是您的翻译范围不正确
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);
在上面,width
和height
指的是SVG的宽度和高度,而不是canvas。此外,通常不会明确指定缩放范围,但如果未使用 zoom.extent()
指定缩放范围,则缩放范围默认为调用它的容器的尺寸。
如果您的平移范围与您的缩放范围大小相等 - 默认情况下容器(SVG)的范围 - 它是,您可以在该容器的坐标 space 内的任何地方缩放和平移,但不要超出它的坐标。因此,当缩放比例为 1 时,我们无法平移任何地方,因为我们会平移超出平移范围的定义。
注意:这在逻辑上意味着平移范围必须包含且不小于缩放范围。
但是,在这种情况下,如果我们放大,我们可以平移并保持在平移范围内。
我们可以看到,如果您放大,则无法向上平移超出预期限制。这是因为 canvas 的顶部在 y==0
,这是翻译范围的边界。
正如您所注意到的,如果您放大,您可以向下平移超出预期的限制。 canvas 的底部是 h
,它小于平移范围限制 height
,因此当我们放大时,我们可以随着 h
和 height
每次缩放都会增加(如上所述,当 k==1
时无法平移)。
我们可以尝试更改翻译范围以反映 canvas 的范围。但是,由于 canvas 小于 SVG,因此这将不起作用,因为平移范围将小于缩放范围。如上所述,Mike 指出 here:"The problem is that the translateExtent you’ve specified is smaller than the zoom extent. So there’s no way to satisfy the requested constraint."
我们可以修改 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);
上面创建了一个缩放行为,将 canvas 限制在其原始范围内 - 如果我们在 canvas 上调用缩放并且不希望能够提供相同的参数平移到它之外(除非我们可以依靠默认缩放范围来提供适当的值,而不是手动指定缩放范围)。
这是更新后的 sandbox。