@qfo/qfchart 0.8.0 → 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 +524 -12
- package/dist/qfchart.min.browser.js +34 -18
- package/dist/qfchart.min.es.js +34 -18
- package/package.json +1 -1
- package/src/QFChart.ts +109 -272
- package/src/components/AbstractPlugin.ts +234 -104
- package/src/components/DrawingEditor.ts +297 -248
- package/src/components/DrawingRendererRegistry.ts +13 -0
- package/src/components/GraphicBuilder.ts +2 -2
- package/src/components/LayoutManager.ts +92 -52
- package/src/components/SeriesBuilder.ts +10 -10
- package/src/components/TooltipFormatter.ts +1 -1
- package/src/index.ts +25 -6
- package/src/plugins/ABCDPatternTool/ABCDPatternDrawingRenderer.ts +112 -0
- package/src/plugins/ABCDPatternTool/ABCDPatternTool.ts +136 -0
- package/src/plugins/ABCDPatternTool/index.ts +2 -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/CypherPatternTool/CypherPatternDrawingRenderer.ts +80 -0
- package/src/plugins/CypherPatternTool/CypherPatternTool.ts +84 -0
- package/src/plugins/CypherPatternTool/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/FibSpeedResistanceFanTool/FibSpeedResistanceFanDrawingRenderer.ts +163 -0
- package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanTool.ts +210 -0
- package/src/plugins/FibSpeedResistanceFanTool/index.ts +2 -0
- package/src/plugins/FibTrendExtensionTool/FibTrendExtensionDrawingRenderer.ts +141 -0
- package/src/plugins/FibTrendExtensionTool/FibTrendExtensionTool.ts +188 -0
- package/src/plugins/FibTrendExtensionTool/index.ts +2 -0
- package/src/plugins/FibonacciChannelTool/FibonacciChannelDrawingRenderer.ts +128 -0
- package/src/plugins/FibonacciChannelTool/FibonacciChannelTool.ts +231 -0
- package/src/plugins/FibonacciChannelTool/index.ts +2 -0
- package/src/plugins/FibonacciTool/FibonacciDrawingRenderer.ts +107 -0
- package/src/plugins/{FibonacciTool.ts → FibonacciTool/FibonacciTool.ts} +195 -192
- package/src/plugins/FibonacciTool/index.ts +2 -0
- package/src/plugins/HeadAndShouldersTool/HeadAndShouldersDrawingRenderer.ts +95 -0
- package/src/plugins/HeadAndShouldersTool/HeadAndShouldersTool.ts +97 -0
- package/src/plugins/HeadAndShouldersTool/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 +49 -0
- package/src/plugins/{LineTool.ts → LineTool/LineTool.ts} +161 -190
- package/src/plugins/LineTool/index.ts +2 -0
- package/src/plugins/{MeasureTool.ts → MeasureTool/MeasureTool.ts} +324 -344
- package/src/plugins/MeasureTool/index.ts +1 -0
- 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/ThreeDrivesPatternTool/ThreeDrivesPatternDrawingRenderer.ts +106 -0
- package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternTool.ts +98 -0
- package/src/plugins/ThreeDrivesPatternTool/index.ts +2 -0
- package/src/plugins/ToolGroup.ts +211 -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/TrianglePatternTool/TrianglePatternDrawingRenderer.ts +107 -0
- package/src/plugins/TrianglePatternTool/TrianglePatternTool.ts +98 -0
- package/src/plugins/TrianglePatternTool/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/plugins/XABCDPatternTool/XABCDPatternDrawingRenderer.ts +178 -0
- package/src/plugins/XABCDPatternTool/XABCDPatternTool.ts +213 -0
- package/src/plugins/XABCDPatternTool/index.ts +2 -0
- package/src/types.ts +39 -11
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { AbstractPlugin } from '../../components/AbstractPlugin';
|
|
2
|
+
import { TrendAngleDrawingRenderer } from './TrendAngleDrawingRenderer';
|
|
3
|
+
import * as echarts from 'echarts';
|
|
4
|
+
|
|
5
|
+
const COLOR = '#d1d4dc';
|
|
6
|
+
|
|
7
|
+
type PluginState = 'idle' | 'drawing' | 'finished';
|
|
8
|
+
|
|
9
|
+
export class TrendAngleTool 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 hRefLine: any = null;
|
|
17
|
+
private arc: any = null;
|
|
18
|
+
private angleText: any = null;
|
|
19
|
+
private startCircle: any = null;
|
|
20
|
+
private endCircle: any = null;
|
|
21
|
+
|
|
22
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
23
|
+
super({
|
|
24
|
+
id: 'trend-angle-tool',
|
|
25
|
+
name: options?.name || 'Trend Angle',
|
|
26
|
+
icon: options?.icon || `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="20" x2="21" y2="6"/><line x1="3" y1="20" x2="14" y2="20" opacity="0.4"/><path d="M8 20 A5 5 0 0 1 7 16" stroke-width="1.5"/></svg>`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected onInit(): void {
|
|
31
|
+
this.zr = this.chart.getZr();
|
|
32
|
+
this.context.registerDrawingRenderer(new TrendAngleDrawingRenderer());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected onActivate(): void {
|
|
36
|
+
this.state = 'idle';
|
|
37
|
+
this.chart.getZr().setCursorStyle('crosshair');
|
|
38
|
+
this.zr.on('click', this.onClick);
|
|
39
|
+
this.zr.on('mousemove', this.onMouseMove);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected onDeactivate(): void {
|
|
43
|
+
this.state = 'idle';
|
|
44
|
+
this.chart.getZr().setCursorStyle('default');
|
|
45
|
+
this.zr.off('click', this.onClick);
|
|
46
|
+
this.zr.off('mousemove', this.onMouseMove);
|
|
47
|
+
this.removeGraphic();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected onDestroy(): void {
|
|
51
|
+
this.removeGraphic();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private onClick = (params: any) => {
|
|
55
|
+
if (this.state === 'idle') {
|
|
56
|
+
this.state = 'drawing';
|
|
57
|
+
this.startPoint = this.getPoint(params);
|
|
58
|
+
this.endPoint = this.getPoint(params);
|
|
59
|
+
this.initGraphic();
|
|
60
|
+
this.updateGraphic();
|
|
61
|
+
} else if (this.state === 'drawing') {
|
|
62
|
+
this.state = 'finished';
|
|
63
|
+
this.endPoint = this.getPoint(params);
|
|
64
|
+
|
|
65
|
+
if (this.startPoint && this.endPoint) {
|
|
66
|
+
const start = this.context.coordinateConversion.pixelToData({
|
|
67
|
+
x: this.startPoint[0], y: this.startPoint[1],
|
|
68
|
+
});
|
|
69
|
+
const end = this.context.coordinateConversion.pixelToData({
|
|
70
|
+
x: this.endPoint[0], y: this.endPoint[1],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (start && end) {
|
|
74
|
+
this.context.addDrawing({
|
|
75
|
+
id: `trend-angle-${Date.now()}`,
|
|
76
|
+
type: 'trend-angle',
|
|
77
|
+
points: [start, end],
|
|
78
|
+
paneIndex: start.paneIndex || 0,
|
|
79
|
+
style: { color: COLOR, lineWidth: 1 },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.removeGraphic();
|
|
85
|
+
this.context.disableTools();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
private onMouseMove = (params: any) => {
|
|
90
|
+
if (this.state !== 'drawing') return;
|
|
91
|
+
this.endPoint = this.getPoint(params);
|
|
92
|
+
this.updateGraphic();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
private initGraphic(): void {
|
|
96
|
+
if (this.group) return;
|
|
97
|
+
this.group = new echarts.graphic.Group();
|
|
98
|
+
this.line = new echarts.graphic.Line({
|
|
99
|
+
shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
100
|
+
style: { stroke: COLOR, lineWidth: 1 },
|
|
101
|
+
z: 100,
|
|
102
|
+
});
|
|
103
|
+
this.hRefLine = new echarts.graphic.Line({
|
|
104
|
+
shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
105
|
+
style: { stroke: COLOR, lineWidth: 1, lineDash: [4, 4], opacity: 0.4 },
|
|
106
|
+
z: 99,
|
|
107
|
+
});
|
|
108
|
+
this.arc = new echarts.graphic.Arc({
|
|
109
|
+
shape: { cx: 0, cy: 0, r: 25, startAngle: 0, endAngle: 0 },
|
|
110
|
+
style: { stroke: COLOR, lineWidth: 1, fill: 'none' },
|
|
111
|
+
z: 99,
|
|
112
|
+
});
|
|
113
|
+
this.angleText = new echarts.graphic.Text({
|
|
114
|
+
style: { text: '', fill: COLOR, fontSize: 11, fontFamily: 'sans-serif' },
|
|
115
|
+
z: 101,
|
|
116
|
+
});
|
|
117
|
+
this.startCircle = new echarts.graphic.Circle({
|
|
118
|
+
shape: { cx: 0, cy: 0, r: 4 },
|
|
119
|
+
style: { fill: '#fff', stroke: COLOR, lineWidth: 1 },
|
|
120
|
+
z: 101,
|
|
121
|
+
});
|
|
122
|
+
this.endCircle = new echarts.graphic.Circle({
|
|
123
|
+
shape: { cx: 0, cy: 0, r: 4 },
|
|
124
|
+
style: { fill: '#fff', stroke: COLOR, lineWidth: 1 },
|
|
125
|
+
z: 101,
|
|
126
|
+
});
|
|
127
|
+
this.group.add(this.hRefLine);
|
|
128
|
+
this.group.add(this.arc);
|
|
129
|
+
this.group.add(this.line);
|
|
130
|
+
this.group.add(this.angleText);
|
|
131
|
+
this.group.add(this.startCircle);
|
|
132
|
+
this.group.add(this.endCircle);
|
|
133
|
+
this.zr.add(this.group);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private removeGraphic(): void {
|
|
137
|
+
if (this.group) {
|
|
138
|
+
this.zr.remove(this.group);
|
|
139
|
+
this.group = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private updateGraphic(): void {
|
|
144
|
+
if (!this.startPoint || !this.endPoint || !this.group) return;
|
|
145
|
+
const [x1, y1] = this.startPoint;
|
|
146
|
+
const [x2, y2] = this.endPoint;
|
|
147
|
+
|
|
148
|
+
this.line.setShape({ x1, y1, x2, y2 });
|
|
149
|
+
this.startCircle.setShape({ cx: x1, cy: y1 });
|
|
150
|
+
this.endCircle.setShape({ cx: x2, cy: y2 });
|
|
151
|
+
|
|
152
|
+
const dx = x2 - x1;
|
|
153
|
+
const dy = y2 - y1;
|
|
154
|
+
|
|
155
|
+
// Horizontal reference from p1
|
|
156
|
+
const hLen = Math.max(Math.abs(dx), 40);
|
|
157
|
+
this.hRefLine.setShape({ x1, y1, x2: x1 + hLen, y2: y1 });
|
|
158
|
+
|
|
159
|
+
// Angle (negate dy for natural angle since screen Y is inverted)
|
|
160
|
+
const angleRad = Math.atan2(-dy, dx);
|
|
161
|
+
const angleDeg = angleRad * (180 / Math.PI);
|
|
162
|
+
const arcR = Math.min(25, Math.sqrt(dx * dx + dy * dy) * 0.3);
|
|
163
|
+
|
|
164
|
+
// Arc from 0 (horizontal) to the line angle
|
|
165
|
+
const screenAngle = Math.atan2(dy, dx); // screen-space angle
|
|
166
|
+
const arcStart = Math.min(0, screenAngle);
|
|
167
|
+
const arcEnd = Math.max(0, screenAngle);
|
|
168
|
+
this.arc.setShape({ cx: x1, cy: y1, r: arcR, startAngle: arcStart, endAngle: arcEnd });
|
|
169
|
+
|
|
170
|
+
// Angle label
|
|
171
|
+
this.angleText.setStyle({ text: `${angleDeg.toFixed(1)}\u00B0` });
|
|
172
|
+
this.angleText.x = x1 + arcR + 6;
|
|
173
|
+
this.angleText.y = y1 + (dy < 0 ? -14 : 2);
|
|
174
|
+
this.angleText.markRedraw();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
const LABELS = ['1', '2', '3', '4', '5'];
|
|
4
|
+
|
|
5
|
+
export class TrianglePatternDrawingRenderer implements DrawingRenderer {
|
|
6
|
+
type = 'triangle_pattern';
|
|
7
|
+
|
|
8
|
+
render(ctx: DrawingRenderContext): any {
|
|
9
|
+
const { drawing, pixelPoints, isSelected } = ctx;
|
|
10
|
+
const color = drawing.style?.color || '#3b82f6';
|
|
11
|
+
if (pixelPoints.length < 2) return;
|
|
12
|
+
|
|
13
|
+
const children: any[] = [];
|
|
14
|
+
|
|
15
|
+
// Fill the triangle polygon with all points
|
|
16
|
+
if (pixelPoints.length >= 3) {
|
|
17
|
+
children.push({
|
|
18
|
+
type: 'polygon',
|
|
19
|
+
name: 'line',
|
|
20
|
+
shape: { points: pixelPoints.map(([x, y]) => [x, y]) },
|
|
21
|
+
style: { fill: 'rgba(156, 39, 176, 0.06)' },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Upper trendline: connect odd-indexed points (0, 2, 4) — highs
|
|
26
|
+
const upperPts = pixelPoints.filter((_, i) => i % 2 === 0);
|
|
27
|
+
if (upperPts.length >= 2) {
|
|
28
|
+
for (let i = 0; i < upperPts.length - 1; i++) {
|
|
29
|
+
children.push({
|
|
30
|
+
type: 'line', name: 'line',
|
|
31
|
+
shape: { x1: upperPts[i][0], y1: upperPts[i][1], x2: upperPts[i + 1][0], y2: upperPts[i + 1][1] },
|
|
32
|
+
style: { stroke: '#f44336', lineWidth: 2 },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Extend upper trendline
|
|
36
|
+
if (upperPts.length >= 2) {
|
|
37
|
+
const last = upperPts[upperPts.length - 1];
|
|
38
|
+
const prev = upperPts[upperPts.length - 2];
|
|
39
|
+
const dx = last[0] - prev[0];
|
|
40
|
+
const dy = last[1] - prev[1];
|
|
41
|
+
if (dx !== 0) {
|
|
42
|
+
const extendX = last[0] + dx * 0.5;
|
|
43
|
+
const extendY = last[1] + dy * 0.5;
|
|
44
|
+
children.push({
|
|
45
|
+
type: 'line',
|
|
46
|
+
shape: { x1: last[0], y1: last[1], x2: extendX, y2: extendY },
|
|
47
|
+
style: { stroke: '#f44336', lineWidth: 1, lineDash: [4, 4] },
|
|
48
|
+
silent: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Lower trendline: connect even-indexed points (1, 3) — lows
|
|
55
|
+
const lowerPts = pixelPoints.filter((_, i) => i % 2 === 1);
|
|
56
|
+
if (lowerPts.length >= 2) {
|
|
57
|
+
for (let i = 0; i < lowerPts.length - 1; i++) {
|
|
58
|
+
children.push({
|
|
59
|
+
type: 'line', name: 'line',
|
|
60
|
+
shape: { x1: lowerPts[i][0], y1: lowerPts[i][1], x2: lowerPts[i + 1][0], y2: lowerPts[i + 1][1] },
|
|
61
|
+
style: { stroke: '#4caf50', lineWidth: 2 },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Extend lower trendline
|
|
65
|
+
if (lowerPts.length >= 2) {
|
|
66
|
+
const last = lowerPts[lowerPts.length - 1];
|
|
67
|
+
const prev = lowerPts[lowerPts.length - 2];
|
|
68
|
+
const dx = last[0] - prev[0];
|
|
69
|
+
const dy = last[1] - prev[1];
|
|
70
|
+
if (dx !== 0) {
|
|
71
|
+
const extendX = last[0] + dx * 0.5;
|
|
72
|
+
const extendY = last[1] + dy * 0.5;
|
|
73
|
+
children.push({
|
|
74
|
+
type: 'line',
|
|
75
|
+
shape: { x1: last[0], y1: last[1], x2: extendX, y2: extendY },
|
|
76
|
+
style: { stroke: '#4caf50', lineWidth: 1, lineDash: [4, 4] },
|
|
77
|
+
silent: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Zigzag connecting all points
|
|
84
|
+
for (let i = 0; i < pixelPoints.length - 1; i++) {
|
|
85
|
+
children.push({
|
|
86
|
+
type: 'line',
|
|
87
|
+
shape: { x1: pixelPoints[i][0], y1: pixelPoints[i][1], x2: pixelPoints[i + 1][0], y2: pixelPoints[i + 1][1] },
|
|
88
|
+
style: { stroke: '#9c27b0', lineWidth: 1, lineDash: [2, 2] },
|
|
89
|
+
silent: true,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Labels
|
|
94
|
+
for (let i = 0; i < pixelPoints.length && i < LABELS.length; i++) {
|
|
95
|
+
const [px, py] = pixelPoints[i];
|
|
96
|
+
const isHigh = i % 2 === 0;
|
|
97
|
+
children.push({ type: 'text', style: { text: LABELS[i], x: px, y: isHigh ? py - 14 : py + 16, fill: '#e2e8f0', fontSize: 12, fontWeight: 'bold', align: 'center', verticalAlign: 'middle' }, silent: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Control points
|
|
101
|
+
for (let i = 0; i < pixelPoints.length; i++) {
|
|
102
|
+
children.push({ type: 'circle', name: `point-${i}`, shape: { cx: pixelPoints[i][0], cy: pixelPoints[i][1], r: 4 }, style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 }, z: 100 });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { type: 'group', children };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as echarts from 'echarts';
|
|
2
|
+
import { AbstractPlugin } from '../../components/AbstractPlugin';
|
|
3
|
+
import { TrianglePatternDrawingRenderer } from './TrianglePatternDrawingRenderer';
|
|
4
|
+
|
|
5
|
+
const LABELS = ['1', '2', '3', '4', '5'];
|
|
6
|
+
const TOTAL_POINTS = 5;
|
|
7
|
+
|
|
8
|
+
export class TrianglePatternTool extends AbstractPlugin {
|
|
9
|
+
private points: number[][] = [];
|
|
10
|
+
private state: 'idle' | 'drawing' | 'finished' = 'idle';
|
|
11
|
+
private graphicGroup: any = null;
|
|
12
|
+
|
|
13
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
14
|
+
super({
|
|
15
|
+
id: 'triangle-pattern-tool',
|
|
16
|
+
name: options.name || 'Triangle Pattern',
|
|
17
|
+
icon: options.icon || `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#e3e3e3" stroke-width="1.5"><path d="M2,4 L22,4 L12,20 Z"/></svg>`,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected onInit(): void { this.context.registerDrawingRenderer(new TrianglePatternDrawingRenderer()); }
|
|
22
|
+
|
|
23
|
+
protected onActivate(): void {
|
|
24
|
+
this.state = 'idle'; this.points = [];
|
|
25
|
+
this.context.getChart().getZr().setCursorStyle('crosshair');
|
|
26
|
+
const zr = this.context.getChart().getZr();
|
|
27
|
+
zr.on('click', this.onClick); zr.on('mousemove', this.onMouseMove);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected onDeactivate(): void {
|
|
31
|
+
this.state = 'idle'; this.points = []; this.removeGraphic();
|
|
32
|
+
const zr = this.context.getChart().getZr();
|
|
33
|
+
zr.off('click', this.onClick); zr.off('mousemove', this.onMouseMove);
|
|
34
|
+
zr.setCursorStyle('default');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private onClick = (params: any) => {
|
|
38
|
+
const pt = this.getPoint(params);
|
|
39
|
+
if (this.state === 'idle') {
|
|
40
|
+
this.state = 'drawing'; this.points = [pt, [...pt]]; this.initGraphic(); this.updateGraphic();
|
|
41
|
+
} else if (this.state === 'drawing') {
|
|
42
|
+
this.points[this.points.length - 1] = pt;
|
|
43
|
+
if (this.points.length >= TOTAL_POINTS) {
|
|
44
|
+
this.state = 'finished'; this.updateGraphic(); this.saveDrawing(); this.removeGraphic(); this.context.disableTools();
|
|
45
|
+
} else { this.points.push([...pt]); this.updateGraphic(); }
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
private onMouseMove = (params: any) => {
|
|
50
|
+
if (this.state !== 'drawing' || this.points.length < 2) return;
|
|
51
|
+
this.points[this.points.length - 1] = this.getPoint(params); this.updateGraphic();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
private initGraphic() { this.graphicGroup = new echarts.graphic.Group(); this.context.getChart().getZr().add(this.graphicGroup); }
|
|
55
|
+
private removeGraphic() { if (this.graphicGroup) { this.context.getChart().getZr().remove(this.graphicGroup); this.graphicGroup = null; } }
|
|
56
|
+
|
|
57
|
+
private updateGraphic() {
|
|
58
|
+
if (!this.graphicGroup) return;
|
|
59
|
+
this.graphicGroup.removeAll();
|
|
60
|
+
const pts = this.points;
|
|
61
|
+
|
|
62
|
+
if (pts.length >= 3) this.graphicGroup.add(new echarts.graphic.Polygon({ shape: { points: pts }, style: { fill: 'rgba(156,39,176,0.06)' }, silent: true }));
|
|
63
|
+
|
|
64
|
+
// Zigzag
|
|
65
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
66
|
+
this.graphicGroup.add(new echarts.graphic.Line({ shape: { x1: pts[i][0], y1: pts[i][1], x2: pts[i + 1][0], y2: pts[i + 1][1] }, style: { stroke: '#9c27b0', lineWidth: 2 }, silent: true }));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Upper trendline (even indices)
|
|
70
|
+
const upper = pts.filter((_, i) => i % 2 === 0);
|
|
71
|
+
if (upper.length >= 2) {
|
|
72
|
+
for (let i = 0; i < upper.length - 1; i++) {
|
|
73
|
+
this.graphicGroup.add(new echarts.graphic.Line({ shape: { x1: upper[i][0], y1: upper[i][1], x2: upper[i + 1][0], y2: upper[i + 1][1] }, style: { stroke: '#f44336', lineWidth: 1, lineDash: [4, 4] }, silent: true }));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Lower trendline (odd indices)
|
|
77
|
+
const lower = pts.filter((_, i) => i % 2 === 1);
|
|
78
|
+
if (lower.length >= 2) {
|
|
79
|
+
for (let i = 0; i < lower.length - 1; i++) {
|
|
80
|
+
this.graphicGroup.add(new echarts.graphic.Line({ shape: { x1: lower[i][0], y1: lower[i][1], x2: lower[i + 1][0], y2: lower[i + 1][1] }, style: { stroke: '#4caf50', lineWidth: 1, lineDash: [4, 4] }, silent: true }));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < pts.length && i < LABELS.length; i++) {
|
|
85
|
+
const [px, py] = pts[i];
|
|
86
|
+
const isHigh = i % 2 === 0;
|
|
87
|
+
this.graphicGroup.add(new echarts.graphic.Text({ style: { text: LABELS[i], x: px, y: isHigh ? py - 14 : py + 16, fill: '#e2e8f0', fontSize: 12, fontWeight: 'bold', align: 'center', verticalAlign: 'middle' }, silent: true }));
|
|
88
|
+
this.graphicGroup.add(new echarts.graphic.Circle({ shape: { cx: px, cy: py, r: 4 }, style: { fill: '#fff', stroke: '#3b82f6', lineWidth: 1.5 }, z: 101, silent: true }));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private saveDrawing() {
|
|
93
|
+
const dataPoints = this.points.map((pt) => this.context.coordinateConversion.pixelToData({ x: pt[0], y: pt[1] }));
|
|
94
|
+
if (dataPoints.every((p) => p !== null)) {
|
|
95
|
+
this.context.addDrawing({ id: `triangle-${Date.now()}`, type: 'triangle_pattern', points: dataPoints as any[], paneIndex: dataPoints[0]!.paneIndex || 0, style: { color: '#3b82f6', lineWidth: 2 } });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
export class VerticalLineDrawingRenderer implements DrawingRenderer {
|
|
4
|
+
type = 'vertical-line';
|
|
5
|
+
|
|
6
|
+
render(ctx: DrawingRenderContext): any {
|
|
7
|
+
const { drawing, pixelPoints, isSelected, coordSys } = ctx;
|
|
8
|
+
const [px, py] = pixelPoints[0];
|
|
9
|
+
const color = drawing.style?.color || '#d1d4dc';
|
|
10
|
+
|
|
11
|
+
const top = coordSys.y;
|
|
12
|
+
const bottom = coordSys.y + coordSys.height;
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
type: 'group',
|
|
16
|
+
children: [
|
|
17
|
+
{
|
|
18
|
+
type: 'line',
|
|
19
|
+
name: 'line',
|
|
20
|
+
shape: { x1: px, y1: top, x2: px, y2: bottom },
|
|
21
|
+
style: {
|
|
22
|
+
stroke: color,
|
|
23
|
+
lineWidth: drawing.style?.lineWidth || 1,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'circle',
|
|
28
|
+
name: 'point-0',
|
|
29
|
+
shape: { cx: px, cy: py, r: 4 },
|
|
30
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AbstractPlugin } from '../../components/AbstractPlugin';
|
|
2
|
+
import { VerticalLineDrawingRenderer } from './VerticalLineDrawingRenderer';
|
|
3
|
+
|
|
4
|
+
export class VerticalLineTool extends AbstractPlugin {
|
|
5
|
+
private zr!: any;
|
|
6
|
+
|
|
7
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
8
|
+
super({
|
|
9
|
+
id: 'vertical-line-tool',
|
|
10
|
+
name: options?.name || 'Vertical Line',
|
|
11
|
+
icon: options?.icon || `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="2" x2="12" y2="22"/><circle cx="12" 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 VerticalLineDrawingRenderer());
|
|
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: `vline-${Date.now()}`,
|
|
43
|
+
type: 'vertical-line',
|
|
44
|
+
points: [data],
|
|
45
|
+
paneIndex: data.paneIndex || 0,
|
|
46
|
+
style: { color: '#d1d4dc', lineWidth: 1 },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.context.disableTools();
|
|
51
|
+
};
|
|
52
|
+
}
|