@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.
Files changed (74) hide show
  1. package/dist/index.d.ts +524 -12
  2. package/dist/qfchart.min.browser.js +34 -18
  3. package/dist/qfchart.min.es.js +34 -18
  4. package/package.json +1 -1
  5. package/src/QFChart.ts +109 -272
  6. package/src/components/AbstractPlugin.ts +234 -104
  7. package/src/components/DrawingEditor.ts +297 -248
  8. package/src/components/DrawingRendererRegistry.ts +13 -0
  9. package/src/components/GraphicBuilder.ts +2 -2
  10. package/src/components/LayoutManager.ts +92 -52
  11. package/src/components/SeriesBuilder.ts +10 -10
  12. package/src/components/TooltipFormatter.ts +1 -1
  13. package/src/index.ts +25 -6
  14. package/src/plugins/ABCDPatternTool/ABCDPatternDrawingRenderer.ts +112 -0
  15. package/src/plugins/ABCDPatternTool/ABCDPatternTool.ts +136 -0
  16. package/src/plugins/ABCDPatternTool/index.ts +2 -0
  17. package/src/plugins/CrossLineTool/CrossLineDrawingRenderer.ts +49 -0
  18. package/src/plugins/CrossLineTool/CrossLineTool.ts +52 -0
  19. package/src/plugins/CrossLineTool/index.ts +2 -0
  20. package/src/plugins/CypherPatternTool/CypherPatternDrawingRenderer.ts +80 -0
  21. package/src/plugins/CypherPatternTool/CypherPatternTool.ts +84 -0
  22. package/src/plugins/CypherPatternTool/index.ts +2 -0
  23. package/src/plugins/ExtendedLineTool/ExtendedLineDrawingRenderer.ts +73 -0
  24. package/src/plugins/ExtendedLineTool/ExtendedLineTool.ts +173 -0
  25. package/src/plugins/ExtendedLineTool/index.ts +2 -0
  26. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanDrawingRenderer.ts +163 -0
  27. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanTool.ts +210 -0
  28. package/src/plugins/FibSpeedResistanceFanTool/index.ts +2 -0
  29. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionDrawingRenderer.ts +141 -0
  30. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionTool.ts +188 -0
  31. package/src/plugins/FibTrendExtensionTool/index.ts +2 -0
  32. package/src/plugins/FibonacciChannelTool/FibonacciChannelDrawingRenderer.ts +128 -0
  33. package/src/plugins/FibonacciChannelTool/FibonacciChannelTool.ts +231 -0
  34. package/src/plugins/FibonacciChannelTool/index.ts +2 -0
  35. package/src/plugins/FibonacciTool/FibonacciDrawingRenderer.ts +107 -0
  36. package/src/plugins/{FibonacciTool.ts → FibonacciTool/FibonacciTool.ts} +195 -192
  37. package/src/plugins/FibonacciTool/index.ts +2 -0
  38. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersDrawingRenderer.ts +95 -0
  39. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersTool.ts +97 -0
  40. package/src/plugins/HeadAndShouldersTool/index.ts +2 -0
  41. package/src/plugins/HorizontalLineTool/HorizontalLineDrawingRenderer.ts +54 -0
  42. package/src/plugins/HorizontalLineTool/HorizontalLineTool.ts +52 -0
  43. package/src/plugins/HorizontalLineTool/index.ts +2 -0
  44. package/src/plugins/HorizontalRayTool/HorizontalRayDrawingRenderer.ts +34 -0
  45. package/src/plugins/HorizontalRayTool/HorizontalRayTool.ts +52 -0
  46. package/src/plugins/HorizontalRayTool/index.ts +2 -0
  47. package/src/plugins/InfoLineTool/InfoLineDrawingRenderer.ts +72 -0
  48. package/src/plugins/InfoLineTool/InfoLineTool.ts +130 -0
  49. package/src/plugins/InfoLineTool/index.ts +2 -0
  50. package/src/plugins/LineTool/LineDrawingRenderer.ts +49 -0
  51. package/src/plugins/{LineTool.ts → LineTool/LineTool.ts} +161 -190
  52. package/src/plugins/LineTool/index.ts +2 -0
  53. package/src/plugins/{MeasureTool.ts → MeasureTool/MeasureTool.ts} +324 -344
  54. package/src/plugins/MeasureTool/index.ts +1 -0
  55. package/src/plugins/RayTool/RayDrawingRenderer.ts +69 -0
  56. package/src/plugins/RayTool/RayTool.ts +162 -0
  57. package/src/plugins/RayTool/index.ts +2 -0
  58. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternDrawingRenderer.ts +106 -0
  59. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternTool.ts +98 -0
  60. package/src/plugins/ThreeDrivesPatternTool/index.ts +2 -0
  61. package/src/plugins/ToolGroup.ts +211 -0
  62. package/src/plugins/TrendAngleTool/TrendAngleDrawingRenderer.ts +87 -0
  63. package/src/plugins/TrendAngleTool/TrendAngleTool.ts +176 -0
  64. package/src/plugins/TrendAngleTool/index.ts +2 -0
  65. package/src/plugins/TrianglePatternTool/TrianglePatternDrawingRenderer.ts +107 -0
  66. package/src/plugins/TrianglePatternTool/TrianglePatternTool.ts +98 -0
  67. package/src/plugins/TrianglePatternTool/index.ts +2 -0
  68. package/src/plugins/VerticalLineTool/VerticalLineDrawingRenderer.ts +35 -0
  69. package/src/plugins/VerticalLineTool/VerticalLineTool.ts +52 -0
  70. package/src/plugins/VerticalLineTool/index.ts +2 -0
  71. package/src/plugins/XABCDPatternTool/XABCDPatternDrawingRenderer.ts +178 -0
  72. package/src/plugins/XABCDPatternTool/XABCDPatternTool.ts +213 -0
  73. package/src/plugins/XABCDPatternTool/index.ts +2 -0
  74. 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,2 @@
1
+ export { TrendAngleTool } from './TrendAngleTool';
2
+ export { TrendAngleDrawingRenderer } from './TrendAngleDrawingRenderer';
@@ -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,2 @@
1
+ export { TrianglePatternTool } from './TrianglePatternTool';
2
+ export { TrianglePatternDrawingRenderer } from './TrianglePatternDrawingRenderer';
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { VerticalLineTool } from './VerticalLineTool';
2
+ export { VerticalLineDrawingRenderer } from './VerticalLineDrawingRenderer';