我有一个包含很多点的图表。这就是为什么我使用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
中。
有什么想法吗?谢谢!
答案 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);
在上面,width
和height
是指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。