@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
@@ -1,344 +1,324 @@
1
- import { ChartContext, PluginConfig } from '../types';
2
- import { AbstractPlugin } from '../components/AbstractPlugin';
3
- import * as echarts from 'echarts';
4
-
5
- type PluginState = 'idle' | 'drawing' | 'finished';
6
-
7
- export class MeasureTool extends AbstractPlugin {
8
- private zr!: any;
9
-
10
- private state: PluginState = 'idle';
11
-
12
- private startPoint: number[] | null = null;
13
- private endPoint: number[] | null = null;
14
-
15
- // ZRender Elements
16
- private group: any = null;
17
- private rect: any = null; // Measurement Box
18
- private labelRect: any = null; // Label Background
19
- private labelText: any = null; // Label Text
20
- private lineV: any = null; // Vertical Arrow Line
21
- private lineH: any = null; // Horizontal Arrow Line
22
- private arrowStart: any = null; // Start Arrow
23
- private arrowEnd: any = null; // End Arrow
24
-
25
- constructor(options: { name?: string; icon?: string }) {
26
- super({
27
- id: 'measure',
28
- name: options?.name || 'Measure',
29
- icon:
30
- options?.icon ||
31
- `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M160-240q-33 0-56.5-23.5T80-320v-320q0-33 23.5-56.5T160-720h640q33 0 56.5 23.5T880-640v320q0 33-23.5 56.5T800-240H160Zm0-80h640v-320H680v160h-80v-160h-80v160h-80v-160h-80v160h-80v-160H160v320Zm120-160h80-80Zm160 0h80-80Zm160 0h80-80Zm-120 0Z"/></svg>`,
32
- });
33
- }
34
-
35
- protected onInit(): void {
36
- this.zr = this.chart.getZr();
37
- }
38
-
39
- protected onActivate(): void {
40
- this.state = 'idle';
41
- this.chart.getZr().setCursorStyle('crosshair');
42
-
43
- // We can use this.on() to register listeners that will be cleaned up automatically on destroy
44
- // BUT we need manual control over add/remove for deactivate() logic.
45
- // AbstractPlugin.on() is for lifecycle-bound listeners.
46
- // Here we toggle listeners based on tool state.
47
- // So we'll use ZRender direct listeners as before, but maybe cleaner.
48
-
49
- this.zr.on('click', this.onClick);
50
- this.zr.on('mousemove', this.onMouseMove);
51
-
52
- // We can still use the Event Bus for internal communication if needed
53
- }
54
-
55
- protected onDeactivate(): void {
56
- this.state = 'idle';
57
- this.chart.getZr().setCursorStyle('default');
58
-
59
- this.zr.off('click', this.onClick);
60
- this.zr.off('mousemove', this.onMouseMove);
61
-
62
- // Clean up clear listeners if any
63
- this.disableClearListeners();
64
-
65
- // @ts-ignore - state type comparison
66
- if (this.state === 'drawing') {
67
- this.removeGraphic();
68
- }
69
- }
70
-
71
- protected onDestroy(): void {
72
- this.removeGraphic();
73
- }
74
-
75
- // --- Interaction Handlers ---
76
-
77
- private onMouseDown = () => {
78
- if (this.state === 'finished') {
79
- this.removeGraphic();
80
- }
81
- };
82
-
83
- private onChartInteraction = () => {
84
- if (this.group) {
85
- this.removeGraphic();
86
- }
87
- };
88
-
89
- private onClick = (params: any) => {
90
- if (this.state === 'idle') {
91
- this.state = 'drawing';
92
- this.startPoint = [params.offsetX, params.offsetY];
93
- this.endPoint = [params.offsetX, params.offsetY];
94
- this.initGraphic();
95
- this.updateGraphic();
96
- } else if (this.state === 'drawing') {
97
- this.state = 'finished';
98
- this.endPoint = [params.offsetX, params.offsetY];
99
- this.updateGraphic();
100
- this.context.disableTools();
101
-
102
- // Enable listeners to clear the graphic on interaction
103
- this.enableClearListeners();
104
- }
105
- };
106
-
107
- private enableClearListeners(): void {
108
- const clickHandler = () => {
109
- this.removeGraphic();
110
- };
111
- setTimeout(() => {
112
- this.zr.on('click', clickHandler);
113
- }, 10);
114
-
115
- this.zr.on('mousedown', this.onMouseDown);
116
- this.context.events.on('chart:dataZoom', this.onChartInteraction);
117
-
118
- this.clearHandlers = {
119
- click: clickHandler,
120
- mousedown: this.onMouseDown,
121
- dataZoom: this.onChartInteraction,
122
- };
123
- }
124
-
125
- private clearHandlers: any = {};
126
-
127
- private disableClearListeners(): void {
128
- if (this.clearHandlers.click) this.zr.off('click', this.clearHandlers.click);
129
- if (this.clearHandlers.mousedown) this.zr.off('mousedown', this.clearHandlers.mousedown);
130
- if (this.clearHandlers.dataZoom) {
131
- this.context.events.off('chart:dataZoom', this.clearHandlers.dataZoom);
132
- }
133
- this.clearHandlers = {};
134
- }
135
-
136
- private onMouseMove = (params: any) => {
137
- if (this.state !== 'drawing') return;
138
- this.endPoint = [params.offsetX, params.offsetY];
139
- this.updateGraphic();
140
- };
141
-
142
- // --- Graphics ---
143
-
144
- private initGraphic(): void {
145
- if (this.group) return;
146
-
147
- this.group = new echarts.graphic.Group();
148
-
149
- // 1. Rectangle (Box)
150
- this.rect = new echarts.graphic.Rect({
151
- shape: { x: 0, y: 0, width: 0, height: 0 },
152
- style: { fill: 'rgba(0,0,0,0)', stroke: 'transparent', lineWidth: 0 },
153
- z: 100,
154
- });
155
-
156
- // 2. Lines (Arrows)
157
- this.lineV = new echarts.graphic.Line({
158
- shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
159
- style: { stroke: '#fff', lineWidth: 1, lineDash: [4, 4] },
160
- z: 101,
161
- });
162
- this.lineH = new echarts.graphic.Line({
163
- shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
164
- style: { stroke: '#fff', lineWidth: 1, lineDash: [4, 4] },
165
- z: 101,
166
- });
167
-
168
- // Arrows
169
- this.arrowStart = new echarts.graphic.Polygon({
170
- shape: {
171
- points: [
172
- [0, 0],
173
- [-5, 10],
174
- [5, 10],
175
- ],
176
- },
177
- style: { fill: '#fff' },
178
- z: 102,
179
- });
180
- this.arrowEnd = new echarts.graphic.Polygon({
181
- shape: {
182
- points: [
183
- [0, 0],
184
- [-5, -10],
185
- [5, -10],
186
- ],
187
- },
188
- style: { fill: '#fff' },
189
- z: 102,
190
- });
191
-
192
- // 3. Label
193
- this.labelRect = new echarts.graphic.Rect({
194
- shape: { x: 0, y: 0, width: 0, height: 0, r: 4 },
195
- style: {
196
- fill: 'transparent',
197
- stroke: 'transparent',
198
- lineWidth: 0,
199
- shadowBlur: 5,
200
- shadowColor: 'rgba(0,0,0,0.3)',
201
- },
202
- z: 102,
203
- });
204
-
205
- this.labelText = new echarts.graphic.Text({
206
- style: {
207
- x: 0,
208
- y: 0,
209
- text: '',
210
- fill: '#fff',
211
- font: '12px sans-serif',
212
- align: 'center',
213
- verticalAlign: 'middle',
214
- },
215
- z: 103,
216
- });
217
-
218
- this.group.add(this.rect);
219
- this.group.add(this.lineV);
220
- this.group.add(this.lineH);
221
- this.group.add(this.arrowStart);
222
- this.group.add(this.arrowEnd);
223
- this.group.add(this.labelRect);
224
- this.group.add(this.labelText);
225
-
226
- this.zr.add(this.group);
227
- }
228
-
229
- private removeGraphic(): void {
230
- if (this.group) {
231
- this.zr.remove(this.group);
232
- this.group = null;
233
- // ... clear refs
234
- this.disableClearListeners();
235
- }
236
- }
237
-
238
- private updateGraphic(): void {
239
- if (!this.startPoint || !this.endPoint || !this.group) return;
240
-
241
- const [x1, y1] = this.startPoint;
242
- const [x2, y2] = this.endPoint;
243
-
244
- // Use Context Helper
245
- const p1 = this.context.coordinateConversion.pixelToData({ x: x1, y: y1 });
246
- const p2 = this.context.coordinateConversion.pixelToData({ x: x2, y: y2 });
247
-
248
- if (!p1 || !p2) return;
249
-
250
- const idx1 = Math.round(p1.timeIndex);
251
- const idx2 = Math.round(p2.timeIndex);
252
- const val1 = p1.value;
253
- const val2 = p2.value;
254
-
255
- const bars = idx2 - idx1;
256
- const priceDiff = val2 - val1;
257
- const priceChangePercent = (priceDiff / val1) * 100;
258
- const isUp = priceDiff >= 0;
259
-
260
- const color = isUp ? 'rgba(33, 150, 243, 0.2)' : 'rgba(236, 0, 0, 0.2)';
261
- const strokeColor = isUp ? '#2196F3' : '#ec0000';
262
-
263
- // --- Visuals ---
264
- this.rect.setShape({
265
- x: Math.min(x1, x2),
266
- y: Math.min(y1, y2),
267
- width: Math.abs(x2 - x1),
268
- height: Math.abs(y2 - y1),
269
- });
270
- this.rect.setStyle({ fill: color });
271
-
272
- const midX = (x1 + x2) / 2;
273
- const midY = (y1 + y2) / 2;
274
-
275
- this.lineV.setShape({ x1: midX, y1: y1, x2: midX, y2: y2 });
276
- this.lineV.setStyle({ stroke: strokeColor });
277
-
278
- this.lineH.setShape({ x1: x1, y1: midY, x2: x2, y2: midY });
279
- this.lineH.setStyle({ stroke: strokeColor });
280
-
281
- // Arrows
282
- const topY = Math.min(y1, y2);
283
- const bottomY = Math.max(y1, y2);
284
-
285
- this.arrowStart.setStyle({ fill: 'none' });
286
- this.arrowEnd.setStyle({ fill: 'none' });
287
-
288
- if (isUp) {
289
- this.arrowStart.setShape({
290
- points: [
291
- [midX, topY],
292
- [midX - 4, topY + 6],
293
- [midX + 4, topY + 6],
294
- ],
295
- });
296
- this.arrowStart.setStyle({ fill: strokeColor });
297
- } else {
298
- this.arrowEnd.setShape({
299
- points: [
300
- [midX, bottomY],
301
- [midX - 4, bottomY - 6],
302
- [midX + 4, bottomY - 6],
303
- ],
304
- });
305
- this.arrowEnd.setStyle({ fill: strokeColor });
306
- }
307
-
308
- // Label
309
- const textContent = [`${priceDiff.toFixed(2)} (${priceChangePercent.toFixed(2)}%)`, `${bars} bars, ${(bars * 0).toFixed(0)}d`].join('\n');
310
-
311
- const labelW = 140;
312
- const labelH = 40;
313
- const rectBottomY = Math.max(y1, y2);
314
- const rectTopY = Math.min(y1, y2);
315
- const rectCenterX = (x1 + x2) / 2;
316
-
317
- let labelX = rectCenterX - labelW / 2;
318
- let labelY = rectBottomY + 10;
319
-
320
- const canvasHeight = this.chart.getHeight();
321
- if (labelY + labelH > canvasHeight) {
322
- labelY = rectTopY - labelH - 10;
323
- }
324
-
325
- this.labelRect.setShape({
326
- x: labelX,
327
- y: labelY,
328
- width: labelW,
329
- height: labelH,
330
- });
331
- this.labelRect.setStyle({
332
- fill: '#1e293b',
333
- stroke: strokeColor,
334
- lineWidth: 1,
335
- });
336
-
337
- this.labelText.setStyle({
338
- x: labelX + labelW / 2,
339
- y: labelY + labelH / 2,
340
- text: textContent,
341
- fill: '#fff',
342
- });
343
- }
344
- }
1
+ import { AbstractPlugin } from '../../components/AbstractPlugin';
2
+ import * as echarts from 'echarts';
3
+
4
+ type PluginState = 'idle' | 'drawing' | 'finished';
5
+
6
+ export class MeasureTool extends AbstractPlugin {
7
+ private zr!: any;
8
+
9
+ private state: PluginState = 'idle';
10
+
11
+ private startPoint: number[] | null = null;
12
+ private endPoint: number[] | null = null;
13
+
14
+ // ZRender Elements
15
+ private group: any = null;
16
+ private rect: any = null;
17
+ private labelRect: any = null;
18
+ private labelText: any = null;
19
+ private lineV: any = null;
20
+ private lineH: any = null;
21
+ private arrowStart: any = null;
22
+ private arrowEnd: any = null;
23
+
24
+ constructor(options: { name?: string; icon?: string } = {}) {
25
+ super({
26
+ id: 'measure',
27
+ name: options?.name || 'Measure',
28
+ icon:
29
+ options?.icon ||
30
+ `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M160-240q-33 0-56.5-23.5T80-320v-320q0-33 23.5-56.5T160-720h640q33 0 56.5 23.5T880-640v320q0 33-23.5 56.5T800-240H160Zm0-80h640v-320H680v160h-80v-160h-80v160h-80v-160h-80v160h-80v-160H160v320Zm120-160h80-80Zm160 0h80-80Zm160 0h80-80Zm-120 0Z"/></svg>`,
31
+ });
32
+ }
33
+
34
+ protected onInit(): void {
35
+ this.zr = this.chart.getZr();
36
+ }
37
+
38
+ protected onActivate(): void {
39
+ this.state = 'idle';
40
+ this.chart.getZr().setCursorStyle('crosshair');
41
+
42
+ this.zr.on('click', this.onClick);
43
+ this.zr.on('mousemove', this.onMouseMove);
44
+ }
45
+
46
+ protected onDeactivate(): void {
47
+ this.state = 'idle';
48
+ this.chart.getZr().setCursorStyle('default');
49
+
50
+ this.zr.off('click', this.onClick);
51
+ this.zr.off('mousemove', this.onMouseMove);
52
+
53
+ this.disableClearListeners();
54
+
55
+ // @ts-ignore - state type comparison
56
+ if (this.state === 'drawing') {
57
+ this.removeGraphic();
58
+ }
59
+ }
60
+
61
+ protected onDestroy(): void {
62
+ this.removeGraphic();
63
+ }
64
+
65
+ // --- Interaction Handlers ---
66
+
67
+ private onMouseDown = () => {
68
+ if (this.state === 'finished') {
69
+ this.removeGraphic();
70
+ }
71
+ };
72
+
73
+ private onChartInteraction = () => {
74
+ if (this.group) {
75
+ this.removeGraphic();
76
+ }
77
+ };
78
+
79
+ private onClick = (params: any) => {
80
+ if (this.state === 'idle') {
81
+ this.state = 'drawing';
82
+ this.startPoint = this.getPoint(params);
83
+ this.endPoint = this.getPoint(params);
84
+ this.initGraphic();
85
+ this.updateGraphic();
86
+ } else if (this.state === 'drawing') {
87
+ this.state = 'finished';
88
+ this.endPoint = this.getPoint(params);
89
+ this.updateGraphic();
90
+ this.context.disableTools();
91
+
92
+ this.enableClearListeners();
93
+ }
94
+ };
95
+
96
+ private enableClearListeners(): void {
97
+ const clickHandler = () => {
98
+ this.removeGraphic();
99
+ };
100
+ setTimeout(() => {
101
+ this.zr.on('click', clickHandler);
102
+ }, 10);
103
+
104
+ this.zr.on('mousedown', this.onMouseDown);
105
+ this.context.events.on('chart:dataZoom', this.onChartInteraction);
106
+
107
+ this.clearHandlers = {
108
+ click: clickHandler,
109
+ mousedown: this.onMouseDown,
110
+ dataZoom: this.onChartInteraction,
111
+ };
112
+ }
113
+
114
+ private clearHandlers: any = {};
115
+
116
+ private disableClearListeners(): void {
117
+ if (this.clearHandlers.click) this.zr.off('click', this.clearHandlers.click);
118
+ if (this.clearHandlers.mousedown) this.zr.off('mousedown', this.clearHandlers.mousedown);
119
+ if (this.clearHandlers.dataZoom) {
120
+ this.context.events.off('chart:dataZoom', this.clearHandlers.dataZoom);
121
+ }
122
+ this.clearHandlers = {};
123
+ }
124
+
125
+ private onMouseMove = (params: any) => {
126
+ if (this.state !== 'drawing') return;
127
+ this.endPoint = this.getPoint(params);
128
+ this.updateGraphic();
129
+ };
130
+
131
+ // --- Graphics ---
132
+
133
+ private initGraphic(): void {
134
+ if (this.group) return;
135
+
136
+ this.group = new echarts.graphic.Group();
137
+
138
+ this.rect = new echarts.graphic.Rect({
139
+ shape: { x: 0, y: 0, width: 0, height: 0 },
140
+ style: { fill: 'rgba(0,0,0,0)', stroke: 'transparent', lineWidth: 0 },
141
+ z: 100,
142
+ });
143
+
144
+ this.lineV = new echarts.graphic.Line({
145
+ shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
146
+ style: { stroke: '#fff', lineWidth: 1, lineDash: [4, 4] },
147
+ z: 101,
148
+ });
149
+ this.lineH = new echarts.graphic.Line({
150
+ shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
151
+ style: { stroke: '#fff', lineWidth: 1, lineDash: [4, 4] },
152
+ z: 101,
153
+ });
154
+
155
+ this.arrowStart = new echarts.graphic.Polygon({
156
+ shape: {
157
+ points: [
158
+ [0, 0],
159
+ [-5, 10],
160
+ [5, 10],
161
+ ],
162
+ },
163
+ style: { fill: '#fff' },
164
+ z: 102,
165
+ });
166
+ this.arrowEnd = new echarts.graphic.Polygon({
167
+ shape: {
168
+ points: [
169
+ [0, 0],
170
+ [-5, -10],
171
+ [5, -10],
172
+ ],
173
+ },
174
+ style: { fill: '#fff' },
175
+ z: 102,
176
+ });
177
+
178
+ this.labelRect = new echarts.graphic.Rect({
179
+ shape: { x: 0, y: 0, width: 0, height: 0, r: 4 },
180
+ style: {
181
+ fill: 'transparent',
182
+ stroke: 'transparent',
183
+ lineWidth: 0,
184
+ shadowBlur: 5,
185
+ shadowColor: 'rgba(0,0,0,0.3)',
186
+ },
187
+ z: 102,
188
+ });
189
+
190
+ this.labelText = new echarts.graphic.Text({
191
+ style: {
192
+ x: 0,
193
+ y: 0,
194
+ text: '',
195
+ fill: '#fff',
196
+ font: '12px sans-serif',
197
+ align: 'center',
198
+ verticalAlign: 'middle',
199
+ },
200
+ z: 103,
201
+ });
202
+
203
+ this.group.add(this.rect);
204
+ this.group.add(this.lineV);
205
+ this.group.add(this.lineH);
206
+ this.group.add(this.arrowStart);
207
+ this.group.add(this.arrowEnd);
208
+ this.group.add(this.labelRect);
209
+ this.group.add(this.labelText);
210
+
211
+ this.zr.add(this.group);
212
+ }
213
+
214
+ private removeGraphic(): void {
215
+ if (this.group) {
216
+ this.zr.remove(this.group);
217
+ this.group = null;
218
+ this.disableClearListeners();
219
+ }
220
+ }
221
+
222
+ private updateGraphic(): void {
223
+ if (!this.startPoint || !this.endPoint || !this.group) return;
224
+
225
+ const [x1, y1] = this.startPoint;
226
+ const [x2, y2] = this.endPoint;
227
+
228
+ const p1 = this.context.coordinateConversion.pixelToData({ x: x1, y: y1 });
229
+ const p2 = this.context.coordinateConversion.pixelToData({ x: x2, y: y2 });
230
+
231
+ if (!p1 || !p2) return;
232
+
233
+ const idx1 = Math.round(p1.timeIndex);
234
+ const idx2 = Math.round(p2.timeIndex);
235
+ const val1 = p1.value;
236
+ const val2 = p2.value;
237
+
238
+ const bars = idx2 - idx1;
239
+ const priceDiff = val2 - val1;
240
+ const priceChangePercent = (priceDiff / val1) * 100;
241
+ const isUp = priceDiff >= 0;
242
+
243
+ const color = isUp ? 'rgba(33, 150, 243, 0.2)' : 'rgba(236, 0, 0, 0.2)';
244
+ const strokeColor = isUp ? '#2196F3' : '#ec0000';
245
+
246
+ this.rect.setShape({
247
+ x: Math.min(x1, x2),
248
+ y: Math.min(y1, y2),
249
+ width: Math.abs(x2 - x1),
250
+ height: Math.abs(y2 - y1),
251
+ });
252
+ this.rect.setStyle({ fill: color });
253
+
254
+ const midX = (x1 + x2) / 2;
255
+ const midY = (y1 + y2) / 2;
256
+
257
+ this.lineV.setShape({ x1: midX, y1: y1, x2: midX, y2: y2 });
258
+ this.lineV.setStyle({ stroke: strokeColor });
259
+
260
+ this.lineH.setShape({ x1: x1, y1: midY, x2: x2, y2: midY });
261
+ this.lineH.setStyle({ stroke: strokeColor });
262
+
263
+ const topY = Math.min(y1, y2);
264
+ const bottomY = Math.max(y1, y2);
265
+
266
+ this.arrowStart.setStyle({ fill: 'none' });
267
+ this.arrowEnd.setStyle({ fill: 'none' });
268
+
269
+ if (isUp) {
270
+ this.arrowStart.setShape({
271
+ points: [
272
+ [midX, topY],
273
+ [midX - 4, topY + 6],
274
+ [midX + 4, topY + 6],
275
+ ],
276
+ });
277
+ this.arrowStart.setStyle({ fill: strokeColor });
278
+ } else {
279
+ this.arrowEnd.setShape({
280
+ points: [
281
+ [midX, bottomY],
282
+ [midX - 4, bottomY - 6],
283
+ [midX + 4, bottomY - 6],
284
+ ],
285
+ });
286
+ this.arrowEnd.setStyle({ fill: strokeColor });
287
+ }
288
+
289
+ const textContent = [`${priceDiff.toFixed(2)} (${priceChangePercent.toFixed(2)}%)`, `${bars} bars`].join('\n');
290
+
291
+ const labelW = 140;
292
+ const labelH = 40;
293
+ const rectBottomY = Math.max(y1, y2);
294
+ const rectTopY = Math.min(y1, y2);
295
+ const rectCenterX = (x1 + x2) / 2;
296
+
297
+ let labelX = rectCenterX - labelW / 2;
298
+ let labelY = rectBottomY + 10;
299
+
300
+ const canvasHeight = this.chart.getHeight();
301
+ if (labelY + labelH > canvasHeight) {
302
+ labelY = rectTopY - labelH - 10;
303
+ }
304
+
305
+ this.labelRect.setShape({
306
+ x: labelX,
307
+ y: labelY,
308
+ width: labelW,
309
+ height: labelH,
310
+ });
311
+ this.labelRect.setStyle({
312
+ fill: '#1e293b',
313
+ stroke: strokeColor,
314
+ lineWidth: 1,
315
+ });
316
+
317
+ this.labelText.setStyle({
318
+ x: labelX + labelW / 2,
319
+ y: labelY + labelH / 2,
320
+ text: textContent,
321
+ fill: '#fff',
322
+ });
323
+ }
324
+ }