@qfo/qfchart 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +206 -1
- package/dist/qfchart.min.browser.js +16 -16
- package/dist/qfchart.min.es.js +16 -16
- package/package.json +1 -1
- package/src/QFChart.ts +11 -10
- package/src/components/LayoutManager.ts +51 -17
- package/src/index.ts +8 -0
- package/src/plugins/CrossLineTool/CrossLineDrawingRenderer.ts +49 -0
- package/src/plugins/CrossLineTool/CrossLineTool.ts +52 -0
- package/src/plugins/CrossLineTool/index.ts +2 -0
- package/src/plugins/ExtendedLineTool/ExtendedLineDrawingRenderer.ts +73 -0
- package/src/plugins/ExtendedLineTool/ExtendedLineTool.ts +173 -0
- package/src/plugins/ExtendedLineTool/index.ts +2 -0
- package/src/plugins/HorizontalLineTool/HorizontalLineDrawingRenderer.ts +54 -0
- package/src/plugins/HorizontalLineTool/HorizontalLineTool.ts +52 -0
- package/src/plugins/HorizontalLineTool/index.ts +2 -0
- package/src/plugins/HorizontalRayTool/HorizontalRayDrawingRenderer.ts +34 -0
- package/src/plugins/HorizontalRayTool/HorizontalRayTool.ts +52 -0
- package/src/plugins/HorizontalRayTool/index.ts +2 -0
- package/src/plugins/InfoLineTool/InfoLineDrawingRenderer.ts +72 -0
- package/src/plugins/InfoLineTool/InfoLineTool.ts +130 -0
- package/src/plugins/InfoLineTool/index.ts +2 -0
- package/src/plugins/LineTool/LineDrawingRenderer.ts +2 -2
- package/src/plugins/LineTool/LineTool.ts +5 -5
- package/src/plugins/RayTool/RayDrawingRenderer.ts +69 -0
- package/src/plugins/RayTool/RayTool.ts +162 -0
- package/src/plugins/RayTool/index.ts +2 -0
- package/src/plugins/TrendAngleTool/TrendAngleDrawingRenderer.ts +87 -0
- package/src/plugins/TrendAngleTool/TrendAngleTool.ts +176 -0
- package/src/plugins/TrendAngleTool/index.ts +2 -0
- package/src/plugins/VerticalLineTool/VerticalLineDrawingRenderer.ts +35 -0
- package/src/plugins/VerticalLineTool/VerticalLineTool.ts +52 -0
- package/src/plugins/VerticalLineTool/index.ts +2 -0
- package/src/types.ts +2 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AbstractPlugin } from '../../components/AbstractPlugin';
|
|
2
|
+
import { HorizontalRayDrawingRenderer } from './HorizontalRayDrawingRenderer';
|
|
3
|
+
|
|
4
|
+
export class HorizontalRayTool extends AbstractPlugin {
|
|
5
|
+
private zr!: any;
|
|
6
|
+
|
|
7
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
8
|
+
super({
|
|
9
|
+
id: 'horizontal-ray-tool',
|
|
10
|
+
name: options?.name || 'Horizontal Ray',
|
|
11
|
+
icon: options?.icon || `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="12" x2="22" y2="12"/><circle cx="4" cy="12" r="2" fill="currentColor"/></svg>`,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
protected onInit(): void {
|
|
16
|
+
this.zr = this.chart.getZr();
|
|
17
|
+
this.context.registerDrawingRenderer(new HorizontalRayDrawingRenderer());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected onActivate(): void {
|
|
21
|
+
this.chart.getZr().setCursorStyle('crosshair');
|
|
22
|
+
this.zr.on('click', this.onClick);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected onDeactivate(): void {
|
|
26
|
+
this.chart.getZr().setCursorStyle('default');
|
|
27
|
+
this.zr.off('click', this.onClick);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected onDestroy(): void {}
|
|
31
|
+
|
|
32
|
+
private onClick = (params: any) => {
|
|
33
|
+
const point = this.getPoint(params);
|
|
34
|
+
if (!point) return;
|
|
35
|
+
|
|
36
|
+
const data = this.context.coordinateConversion.pixelToData({
|
|
37
|
+
x: point[0], y: point[1],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (data) {
|
|
41
|
+
this.context.addDrawing({
|
|
42
|
+
id: `hray-${Date.now()}`,
|
|
43
|
+
type: 'horizontal-ray',
|
|
44
|
+
points: [data],
|
|
45
|
+
paneIndex: data.paneIndex || 0,
|
|
46
|
+
style: { color: '#d1d4dc', lineWidth: 1 },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.context.disableTools();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
export class InfoLineDrawingRenderer implements DrawingRenderer {
|
|
4
|
+
type = 'info-line';
|
|
5
|
+
|
|
6
|
+
render(ctx: DrawingRenderContext): any {
|
|
7
|
+
const { drawing, pixelPoints, isSelected } = ctx;
|
|
8
|
+
const [x1, y1] = pixelPoints[0];
|
|
9
|
+
const [x2, y2] = pixelPoints[1];
|
|
10
|
+
const color = drawing.style?.color || '#d1d4dc';
|
|
11
|
+
|
|
12
|
+
const p0 = drawing.points[0];
|
|
13
|
+
const p1 = drawing.points[1];
|
|
14
|
+
|
|
15
|
+
const priceChange = p1.value - p0.value;
|
|
16
|
+
const pctChange = p0.value !== 0 ? (priceChange / p0.value) * 100 : 0;
|
|
17
|
+
const bars = Math.abs(p1.timeIndex - p0.timeIndex);
|
|
18
|
+
|
|
19
|
+
const sign = priceChange >= 0 ? '+' : '';
|
|
20
|
+
const infoText = `${sign}${priceChange.toFixed(2)} (${sign}${pctChange.toFixed(2)}%) ${bars} bars`;
|
|
21
|
+
|
|
22
|
+
// Position info box at midpoint
|
|
23
|
+
const mx = (x1 + x2) / 2;
|
|
24
|
+
const my = (y1 + y2) / 2;
|
|
25
|
+
const isUp = priceChange >= 0;
|
|
26
|
+
const textColor = isUp ? '#26a69a' : '#ef5350';
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
type: 'group',
|
|
30
|
+
children: [
|
|
31
|
+
{
|
|
32
|
+
type: 'line',
|
|
33
|
+
name: 'line',
|
|
34
|
+
shape: { x1, y1, x2, y2 },
|
|
35
|
+
style: { stroke: color, lineWidth: drawing.style?.lineWidth || 1 },
|
|
36
|
+
},
|
|
37
|
+
// Info box background
|
|
38
|
+
{
|
|
39
|
+
type: 'rect',
|
|
40
|
+
shape: { x: mx - 2, y: my - 22, width: infoText.length * 6.5 + 12, height: 18, r: 3 },
|
|
41
|
+
style: { fill: '#1e293b', stroke: '#475569', lineWidth: 1, opacity: 0.9 },
|
|
42
|
+
z2: 10,
|
|
43
|
+
},
|
|
44
|
+
// Info text
|
|
45
|
+
{
|
|
46
|
+
type: 'text',
|
|
47
|
+
x: mx + 4,
|
|
48
|
+
y: my - 20,
|
|
49
|
+
style: {
|
|
50
|
+
text: infoText,
|
|
51
|
+
fill: textColor,
|
|
52
|
+
fontSize: 11,
|
|
53
|
+
fontFamily: 'monospace',
|
|
54
|
+
},
|
|
55
|
+
z2: 11,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'circle',
|
|
59
|
+
name: 'point-0',
|
|
60
|
+
shape: { cx: x1, cy: y1, r: 4 },
|
|
61
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: 'circle',
|
|
65
|
+
name: 'point-1',
|
|
66
|
+
shape: { cx: x2, cy: y2, r: 4 },
|
|
67
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { AbstractPlugin } from '../../components/AbstractPlugin';
|
|
2
|
+
import { InfoLineDrawingRenderer } from './InfoLineDrawingRenderer';
|
|
3
|
+
import * as echarts from 'echarts';
|
|
4
|
+
|
|
5
|
+
type PluginState = 'idle' | 'drawing' | 'finished';
|
|
6
|
+
|
|
7
|
+
export class InfoLineTool extends AbstractPlugin {
|
|
8
|
+
private zr!: any;
|
|
9
|
+
private state: PluginState = 'idle';
|
|
10
|
+
private startPoint: number[] | null = null;
|
|
11
|
+
private endPoint: number[] | null = null;
|
|
12
|
+
private group: any = null;
|
|
13
|
+
private line: any = null;
|
|
14
|
+
private startCircle: any = null;
|
|
15
|
+
private endCircle: any = null;
|
|
16
|
+
|
|
17
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
18
|
+
super({
|
|
19
|
+
id: 'info-line-tool',
|
|
20
|
+
name: options?.name || 'Info Line',
|
|
21
|
+
icon: options?.icon || `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="2" y1="22" x2="22" y2="2"/><rect x="12" y="8" width="8" height="5" rx="1" fill="none" stroke-width="1.5"/></svg>`,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected onInit(): void {
|
|
26
|
+
this.zr = this.chart.getZr();
|
|
27
|
+
this.context.registerDrawingRenderer(new InfoLineDrawingRenderer());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected onActivate(): void {
|
|
31
|
+
this.state = 'idle';
|
|
32
|
+
this.chart.getZr().setCursorStyle('crosshair');
|
|
33
|
+
this.zr.on('click', this.onClick);
|
|
34
|
+
this.zr.on('mousemove', this.onMouseMove);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected onDeactivate(): void {
|
|
38
|
+
this.state = 'idle';
|
|
39
|
+
this.chart.getZr().setCursorStyle('default');
|
|
40
|
+
this.zr.off('click', this.onClick);
|
|
41
|
+
this.zr.off('mousemove', this.onMouseMove);
|
|
42
|
+
this.removeGraphic();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected onDestroy(): void {
|
|
46
|
+
this.removeGraphic();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private onClick = (params: any) => {
|
|
50
|
+
if (this.state === 'idle') {
|
|
51
|
+
this.state = 'drawing';
|
|
52
|
+
this.startPoint = this.getPoint(params);
|
|
53
|
+
this.endPoint = this.getPoint(params);
|
|
54
|
+
this.initGraphic();
|
|
55
|
+
this.updateGraphic();
|
|
56
|
+
} else if (this.state === 'drawing') {
|
|
57
|
+
this.state = 'finished';
|
|
58
|
+
this.endPoint = this.getPoint(params);
|
|
59
|
+
this.updateGraphic();
|
|
60
|
+
|
|
61
|
+
if (this.startPoint && this.endPoint) {
|
|
62
|
+
const start = this.context.coordinateConversion.pixelToData({
|
|
63
|
+
x: this.startPoint[0], y: this.startPoint[1],
|
|
64
|
+
});
|
|
65
|
+
const end = this.context.coordinateConversion.pixelToData({
|
|
66
|
+
x: this.endPoint[0], y: this.endPoint[1],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (start && end) {
|
|
70
|
+
this.context.addDrawing({
|
|
71
|
+
id: `info-line-${Date.now()}`,
|
|
72
|
+
type: 'info-line',
|
|
73
|
+
points: [start, end],
|
|
74
|
+
paneIndex: start.paneIndex || 0,
|
|
75
|
+
style: { color: '#d1d4dc', lineWidth: 1 },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.removeGraphic();
|
|
81
|
+
this.context.disableTools();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
private onMouseMove = (params: any) => {
|
|
86
|
+
if (this.state !== 'drawing') return;
|
|
87
|
+
this.endPoint = this.getPoint(params);
|
|
88
|
+
this.updateGraphic();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
private initGraphic(): void {
|
|
92
|
+
if (this.group) return;
|
|
93
|
+
this.group = new echarts.graphic.Group();
|
|
94
|
+
this.line = new echarts.graphic.Line({
|
|
95
|
+
shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
96
|
+
style: { stroke: '#d1d4dc', lineWidth: 1 },
|
|
97
|
+
z: 100,
|
|
98
|
+
});
|
|
99
|
+
this.startCircle = new echarts.graphic.Circle({
|
|
100
|
+
shape: { cx: 0, cy: 0, r: 4 },
|
|
101
|
+
style: { fill: '#fff', stroke: '#d1d4dc', lineWidth: 1 },
|
|
102
|
+
z: 101,
|
|
103
|
+
});
|
|
104
|
+
this.endCircle = new echarts.graphic.Circle({
|
|
105
|
+
shape: { cx: 0, cy: 0, r: 4 },
|
|
106
|
+
style: { fill: '#fff', stroke: '#d1d4dc', lineWidth: 1 },
|
|
107
|
+
z: 101,
|
|
108
|
+
});
|
|
109
|
+
this.group.add(this.line);
|
|
110
|
+
this.group.add(this.startCircle);
|
|
111
|
+
this.group.add(this.endCircle);
|
|
112
|
+
this.zr.add(this.group);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private removeGraphic(): void {
|
|
116
|
+
if (this.group) {
|
|
117
|
+
this.zr.remove(this.group);
|
|
118
|
+
this.group = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private updateGraphic(): void {
|
|
123
|
+
if (!this.startPoint || !this.endPoint || !this.group) return;
|
|
124
|
+
const [x1, y1] = this.startPoint;
|
|
125
|
+
const [x2, y2] = this.endPoint;
|
|
126
|
+
this.line.setShape({ x1, y1, x2, y2 });
|
|
127
|
+
this.startCircle.setShape({ cx: x1, cy: y1 });
|
|
128
|
+
this.endCircle.setShape({ cx: x2, cy: y2 });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -7,7 +7,7 @@ export class LineDrawingRenderer implements DrawingRenderer {
|
|
|
7
7
|
const { drawing, pixelPoints, isSelected } = ctx;
|
|
8
8
|
const [x1, y1] = pixelPoints[0];
|
|
9
9
|
const [x2, y2] = pixelPoints[1];
|
|
10
|
-
const color = drawing.style?.color || '#
|
|
10
|
+
const color = drawing.style?.color || '#d1d4dc';
|
|
11
11
|
|
|
12
12
|
return {
|
|
13
13
|
type: 'group',
|
|
@@ -18,7 +18,7 @@ export class LineDrawingRenderer implements DrawingRenderer {
|
|
|
18
18
|
shape: { x1, y1, x2, y2 },
|
|
19
19
|
style: {
|
|
20
20
|
stroke: color,
|
|
21
|
-
lineWidth: drawing.style?.lineWidth ||
|
|
21
|
+
lineWidth: drawing.style?.lineWidth || 1,
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
24
|
{
|
|
@@ -90,8 +90,8 @@ export class LineTool extends AbstractPlugin {
|
|
|
90
90
|
points: [start, end],
|
|
91
91
|
paneIndex: paneIndex,
|
|
92
92
|
style: {
|
|
93
|
-
color: '#
|
|
94
|
-
lineWidth:
|
|
93
|
+
color: '#d1d4dc',
|
|
94
|
+
lineWidth: 1,
|
|
95
95
|
},
|
|
96
96
|
});
|
|
97
97
|
}
|
|
@@ -118,19 +118,19 @@ export class LineTool extends AbstractPlugin {
|
|
|
118
118
|
|
|
119
119
|
this.line = new echarts.graphic.Line({
|
|
120
120
|
shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
121
|
-
style: { stroke: '#
|
|
121
|
+
style: { stroke: '#d1d4dc', lineWidth: 1 },
|
|
122
122
|
z: 100,
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
this.startCircle = new echarts.graphic.Circle({
|
|
126
126
|
shape: { cx: 0, cy: 0, r: 4 },
|
|
127
|
-
style: { fill: '#fff', stroke: '#
|
|
127
|
+
style: { fill: '#fff', stroke: '#d1d4dc', lineWidth: 1 },
|
|
128
128
|
z: 101,
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
this.endCircle = new echarts.graphic.Circle({
|
|
132
132
|
shape: { cx: 0, cy: 0, r: 4 },
|
|
133
|
-
style: { fill: '#fff', stroke: '#
|
|
133
|
+
style: { fill: '#fff', stroke: '#d1d4dc', lineWidth: 1 },
|
|
134
134
|
z: 101,
|
|
135
135
|
});
|
|
136
136
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
export class RayDrawingRenderer implements DrawingRenderer {
|
|
4
|
+
type = 'ray';
|
|
5
|
+
|
|
6
|
+
render(ctx: DrawingRenderContext): any {
|
|
7
|
+
const { drawing, pixelPoints, isSelected, coordSys } = ctx;
|
|
8
|
+
const [x1, y1] = pixelPoints[0];
|
|
9
|
+
const [x2, y2] = pixelPoints[1];
|
|
10
|
+
const color = drawing.style?.color || '#d1d4dc';
|
|
11
|
+
|
|
12
|
+
// Extend the ray from p1 through p2 to the chart boundary
|
|
13
|
+
const [ex, ey] = this.extendToEdge(x1, y1, x2, y2, coordSys);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
type: 'group',
|
|
17
|
+
children: [
|
|
18
|
+
{
|
|
19
|
+
type: 'line',
|
|
20
|
+
name: 'line',
|
|
21
|
+
shape: { x1, y1, x2: ex, y2: ey },
|
|
22
|
+
style: {
|
|
23
|
+
stroke: color,
|
|
24
|
+
lineWidth: drawing.style?.lineWidth || 1,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: 'circle',
|
|
29
|
+
name: 'point-0',
|
|
30
|
+
shape: { cx: x1, cy: y1, r: 4 },
|
|
31
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'circle',
|
|
35
|
+
name: 'point-1',
|
|
36
|
+
shape: { cx: x2, cy: y2, r: 4 },
|
|
37
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private extendToEdge(
|
|
44
|
+
x1: number, y1: number, x2: number, y2: number,
|
|
45
|
+
cs: { x: number; y: number; width: number; height: number },
|
|
46
|
+
): [number, number] {
|
|
47
|
+
const dx = x2 - x1;
|
|
48
|
+
const dy = y2 - y1;
|
|
49
|
+
if (dx === 0 && dy === 0) return [x2, y2];
|
|
50
|
+
|
|
51
|
+
const left = cs.x;
|
|
52
|
+
const right = cs.x + cs.width;
|
|
53
|
+
const top = cs.y;
|
|
54
|
+
const bottom = cs.y + cs.height;
|
|
55
|
+
|
|
56
|
+
let tMax = Infinity;
|
|
57
|
+
if (dx !== 0) {
|
|
58
|
+
const tx = dx > 0 ? (right - x1) / dx : (left - x1) / dx;
|
|
59
|
+
if (tx > 0) tMax = Math.min(tMax, tx);
|
|
60
|
+
}
|
|
61
|
+
if (dy !== 0) {
|
|
62
|
+
const ty = dy > 0 ? (bottom - y1) / dy : (top - y1) / dy;
|
|
63
|
+
if (ty > 0) tMax = Math.min(tMax, ty);
|
|
64
|
+
}
|
|
65
|
+
if (!isFinite(tMax)) tMax = 1;
|
|
66
|
+
|
|
67
|
+
return [x1 + tMax * dx, y1 + tMax * dy];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { AbstractPlugin } from '../../components/AbstractPlugin';
|
|
2
|
+
import { RayDrawingRenderer } from './RayDrawingRenderer';
|
|
3
|
+
import * as echarts from 'echarts';
|
|
4
|
+
|
|
5
|
+
const COLOR = '#d1d4dc';
|
|
6
|
+
|
|
7
|
+
type PluginState = 'idle' | 'drawing' | 'finished';
|
|
8
|
+
|
|
9
|
+
export class RayTool extends AbstractPlugin {
|
|
10
|
+
private zr!: any;
|
|
11
|
+
private state: PluginState = 'idle';
|
|
12
|
+
private startPoint: number[] | null = null;
|
|
13
|
+
private endPoint: number[] | null = null;
|
|
14
|
+
private group: any = null;
|
|
15
|
+
private line: any = null;
|
|
16
|
+
private dashLine: any = null;
|
|
17
|
+
private startCircle: any = null;
|
|
18
|
+
private endCircle: any = null;
|
|
19
|
+
|
|
20
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
21
|
+
super({
|
|
22
|
+
id: 'ray-tool',
|
|
23
|
+
name: options?.name || 'Ray',
|
|
24
|
+
icon: options?.icon || `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="20" x2="21" y2="4"/><circle cx="21" cy="4" r="0" fill="currentColor"/><polyline points="16,4 21,4 21,9" stroke-width="1.5"/></svg>`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected onInit(): void {
|
|
29
|
+
this.zr = this.chart.getZr();
|
|
30
|
+
this.context.registerDrawingRenderer(new RayDrawingRenderer());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected onActivate(): void {
|
|
34
|
+
this.state = 'idle';
|
|
35
|
+
this.chart.getZr().setCursorStyle('crosshair');
|
|
36
|
+
this.zr.on('click', this.onClick);
|
|
37
|
+
this.zr.on('mousemove', this.onMouseMove);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected onDeactivate(): void {
|
|
41
|
+
this.state = 'idle';
|
|
42
|
+
this.chart.getZr().setCursorStyle('default');
|
|
43
|
+
this.zr.off('click', this.onClick);
|
|
44
|
+
this.zr.off('mousemove', this.onMouseMove);
|
|
45
|
+
this.removeGraphic();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected onDestroy(): void {
|
|
49
|
+
this.removeGraphic();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private onClick = (params: any) => {
|
|
53
|
+
if (this.state === 'idle') {
|
|
54
|
+
this.state = 'drawing';
|
|
55
|
+
this.startPoint = this.getPoint(params);
|
|
56
|
+
this.endPoint = this.getPoint(params);
|
|
57
|
+
this.initGraphic();
|
|
58
|
+
this.updateGraphic();
|
|
59
|
+
} else if (this.state === 'drawing') {
|
|
60
|
+
this.state = 'finished';
|
|
61
|
+
this.endPoint = this.getPoint(params);
|
|
62
|
+
|
|
63
|
+
if (this.startPoint && this.endPoint) {
|
|
64
|
+
const start = this.context.coordinateConversion.pixelToData({
|
|
65
|
+
x: this.startPoint[0], y: this.startPoint[1],
|
|
66
|
+
});
|
|
67
|
+
const end = this.context.coordinateConversion.pixelToData({
|
|
68
|
+
x: this.endPoint[0], y: this.endPoint[1],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (start && end) {
|
|
72
|
+
this.context.addDrawing({
|
|
73
|
+
id: `ray-${Date.now()}`,
|
|
74
|
+
type: 'ray',
|
|
75
|
+
points: [start, end],
|
|
76
|
+
paneIndex: start.paneIndex || 0,
|
|
77
|
+
style: { color: COLOR, lineWidth: 1 },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.removeGraphic();
|
|
83
|
+
this.context.disableTools();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
private onMouseMove = (params: any) => {
|
|
88
|
+
if (this.state !== 'drawing') return;
|
|
89
|
+
this.endPoint = this.getPoint(params);
|
|
90
|
+
this.updateGraphic();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
private initGraphic(): void {
|
|
94
|
+
if (this.group) return;
|
|
95
|
+
this.group = new echarts.graphic.Group();
|
|
96
|
+
this.line = new echarts.graphic.Line({
|
|
97
|
+
shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
98
|
+
style: { stroke: COLOR, lineWidth: 1 },
|
|
99
|
+
z: 100,
|
|
100
|
+
});
|
|
101
|
+
this.dashLine = new echarts.graphic.Line({
|
|
102
|
+
shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
103
|
+
style: { stroke: COLOR, lineWidth: 1, lineDash: [4, 4], opacity: 0.5 },
|
|
104
|
+
z: 99,
|
|
105
|
+
});
|
|
106
|
+
this.startCircle = new echarts.graphic.Circle({
|
|
107
|
+
shape: { cx: 0, cy: 0, r: 4 },
|
|
108
|
+
style: { fill: '#fff', stroke: COLOR, lineWidth: 1 },
|
|
109
|
+
z: 101,
|
|
110
|
+
});
|
|
111
|
+
this.endCircle = new echarts.graphic.Circle({
|
|
112
|
+
shape: { cx: 0, cy: 0, r: 4 },
|
|
113
|
+
style: { fill: '#fff', stroke: COLOR, lineWidth: 1 },
|
|
114
|
+
z: 101,
|
|
115
|
+
});
|
|
116
|
+
this.group.add(this.dashLine);
|
|
117
|
+
this.group.add(this.line);
|
|
118
|
+
this.group.add(this.startCircle);
|
|
119
|
+
this.group.add(this.endCircle);
|
|
120
|
+
this.zr.add(this.group);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private removeGraphic(): void {
|
|
124
|
+
if (this.group) {
|
|
125
|
+
this.zr.remove(this.group);
|
|
126
|
+
this.group = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private updateGraphic(): void {
|
|
131
|
+
if (!this.startPoint || !this.endPoint || !this.group) return;
|
|
132
|
+
const [x1, y1] = this.startPoint;
|
|
133
|
+
const [x2, y2] = this.endPoint;
|
|
134
|
+
this.line.setShape({ x1, y1, x2, y2 });
|
|
135
|
+
this.startCircle.setShape({ cx: x1, cy: y1 });
|
|
136
|
+
this.endCircle.setShape({ cx: x2, cy: y2 });
|
|
137
|
+
|
|
138
|
+
// Dashed extension from p2 to chart edge
|
|
139
|
+
const [ex, ey] = this.extendToEdge(x1, y1, x2, y2);
|
|
140
|
+
this.dashLine.setShape({ x1: x2, y1: y2, x2: ex, y2: ey });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private extendToEdge(x1: number, y1: number, x2: number, y2: number): [number, number] {
|
|
144
|
+
const dx = x2 - x1;
|
|
145
|
+
const dy = y2 - y1;
|
|
146
|
+
if (dx === 0 && dy === 0) return [x2, y2];
|
|
147
|
+
|
|
148
|
+
const w = this.chart.getWidth();
|
|
149
|
+
const h = this.chart.getHeight();
|
|
150
|
+
let tMax = Infinity;
|
|
151
|
+
if (dx !== 0) {
|
|
152
|
+
const tx = dx > 0 ? (w - x1) / dx : -x1 / dx;
|
|
153
|
+
if (tx > 0) tMax = Math.min(tMax, tx);
|
|
154
|
+
}
|
|
155
|
+
if (dy !== 0) {
|
|
156
|
+
const ty = dy > 0 ? (h - y1) / dy : -y1 / dy;
|
|
157
|
+
if (ty > 0) tMax = Math.min(tMax, ty);
|
|
158
|
+
}
|
|
159
|
+
if (!isFinite(tMax)) tMax = 1;
|
|
160
|
+
return [x1 + tMax * dx, y1 + tMax * dy];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
export class TrendAngleDrawingRenderer implements DrawingRenderer {
|
|
4
|
+
type = 'trend-angle';
|
|
5
|
+
|
|
6
|
+
render(ctx: DrawingRenderContext): any {
|
|
7
|
+
const { drawing, pixelPoints, isSelected } = ctx;
|
|
8
|
+
const [x1, y1] = pixelPoints[0];
|
|
9
|
+
const [x2, y2] = pixelPoints[1];
|
|
10
|
+
const color = drawing.style?.color || '#d1d4dc';
|
|
11
|
+
|
|
12
|
+
const dx = x2 - x1;
|
|
13
|
+
const dy = y2 - y1;
|
|
14
|
+
|
|
15
|
+
// Angle in degrees (screen Y is inverted, so negate dy for natural angle)
|
|
16
|
+
const angleRad = Math.atan2(-dy, dx);
|
|
17
|
+
const angleDeg = angleRad * (180 / Math.PI);
|
|
18
|
+
const displayAngle = angleDeg.toFixed(1);
|
|
19
|
+
|
|
20
|
+
// Arc radius
|
|
21
|
+
const arcR = Math.min(30, Math.sqrt(dx * dx + dy * dy) * 0.3);
|
|
22
|
+
|
|
23
|
+
// Horizontal reference line from p1 extending right
|
|
24
|
+
const hLineEndX = x1 + Math.max(Math.abs(dx), arcR + 20);
|
|
25
|
+
|
|
26
|
+
// Arc path: from 0 degrees (horizontal right) to the angle
|
|
27
|
+
// In screen coords, positive angle goes CCW (since Y is inverted)
|
|
28
|
+
const startAngle = 0;
|
|
29
|
+
const endAngle = -angleRad; // Convert back to screen angle
|
|
30
|
+
|
|
31
|
+
const children: any[] = [
|
|
32
|
+
// Main trend line
|
|
33
|
+
{
|
|
34
|
+
type: 'line',
|
|
35
|
+
name: 'line',
|
|
36
|
+
shape: { x1, y1, x2, y2 },
|
|
37
|
+
style: { stroke: color, lineWidth: drawing.style?.lineWidth || 1 },
|
|
38
|
+
},
|
|
39
|
+
// Horizontal reference line
|
|
40
|
+
{
|
|
41
|
+
type: 'line',
|
|
42
|
+
shape: { x1, y1, x2: hLineEndX, y2: y1 },
|
|
43
|
+
style: { stroke: color, lineWidth: 1, opacity: 0.4, lineDash: [4, 4] },
|
|
44
|
+
},
|
|
45
|
+
// Arc
|
|
46
|
+
{
|
|
47
|
+
type: 'arc',
|
|
48
|
+
shape: {
|
|
49
|
+
cx: x1,
|
|
50
|
+
cy: y1,
|
|
51
|
+
r: arcR,
|
|
52
|
+
startAngle: Math.min(startAngle, endAngle),
|
|
53
|
+
endAngle: Math.max(startAngle, endAngle),
|
|
54
|
+
},
|
|
55
|
+
style: { stroke: color, lineWidth: 1.5, fill: 'none' },
|
|
56
|
+
},
|
|
57
|
+
// Angle label
|
|
58
|
+
{
|
|
59
|
+
type: 'text',
|
|
60
|
+
x: x1 + arcR + 6,
|
|
61
|
+
y: y1 + (dy < 0 ? -14 : 2),
|
|
62
|
+
style: {
|
|
63
|
+
text: `${displayAngle}\u00B0`,
|
|
64
|
+
fill: color,
|
|
65
|
+
fontSize: 11,
|
|
66
|
+
fontFamily: 'sans-serif',
|
|
67
|
+
},
|
|
68
|
+
z2: 10,
|
|
69
|
+
},
|
|
70
|
+
// Control points
|
|
71
|
+
{
|
|
72
|
+
type: 'circle',
|
|
73
|
+
name: 'point-0',
|
|
74
|
+
shape: { cx: x1, cy: y1, r: 4 },
|
|
75
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: 'circle',
|
|
79
|
+
name: 'point-1',
|
|
80
|
+
shape: { cx: x2, cy: y2, r: 4 },
|
|
81
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
return { type: 'group', children };
|
|
86
|
+
}
|
|
87
|
+
}
|