@qfo/qfchart 0.5.0
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/LICENSE +222 -0
- package/README.md +271 -0
- package/dist/index.d.ts +465 -0
- package/dist/qfchart.min.browser.js +109 -0
- package/package.json +71 -0
- package/src/QFChart.ts +1198 -0
- package/src/Utils.ts +31 -0
- package/src/components/AbstractPlugin.ts +104 -0
- package/src/components/DrawingEditor.ts +248 -0
- package/src/components/GraphicBuilder.ts +263 -0
- package/src/components/Indicator.ts +104 -0
- package/src/components/LayoutManager.ts +459 -0
- package/src/components/PluginManager.ts +234 -0
- package/src/components/SeriesBuilder.ts +192 -0
- package/src/components/TooltipFormatter.ts +97 -0
- package/src/index.ts +6 -0
- package/src/plugins/FibonacciTool.ts +192 -0
- package/src/plugins/LineTool.ts +190 -0
- package/src/plugins/MeasureTool.ts +344 -0
- package/src/types.ts +160 -0
- package/src/utils/EventBus.ts +67 -0
|
@@ -0,0 +1,344 @@
|
|
|
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
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { EventBus } from './utils/EventBus';
|
|
2
|
+
|
|
3
|
+
export interface OHLCV {
|
|
4
|
+
time: number;
|
|
5
|
+
open: number;
|
|
6
|
+
high: number;
|
|
7
|
+
low: number;
|
|
8
|
+
close: number;
|
|
9
|
+
volume: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IndicatorPoint {
|
|
13
|
+
time: number;
|
|
14
|
+
value: number | null;
|
|
15
|
+
options?: {
|
|
16
|
+
color?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type IndicatorStyle = 'line' | 'columns' | 'histogram' | 'circles' | 'cross' | 'background';
|
|
21
|
+
|
|
22
|
+
export interface IndicatorOptions {
|
|
23
|
+
style: IndicatorStyle;
|
|
24
|
+
color: string;
|
|
25
|
+
linewidth?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface IndicatorPlot {
|
|
29
|
+
data: IndicatorPoint[];
|
|
30
|
+
options: IndicatorOptions;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A collection of plots that make up a single indicator (e.g. MACD has macd line, signal line, histogram)
|
|
34
|
+
export interface Indicator {
|
|
35
|
+
id: string;
|
|
36
|
+
plots: { [name: string]: IndicatorPlot };
|
|
37
|
+
paneIndex: number;
|
|
38
|
+
height?: number; // Desired height in percentage (e.g. 15 for 15%)
|
|
39
|
+
collapsed?: boolean;
|
|
40
|
+
titleColor?: string;
|
|
41
|
+
controls?: {
|
|
42
|
+
collapse?: boolean;
|
|
43
|
+
maximize?: boolean;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface QFChartOptions {
|
|
48
|
+
title?: string; // Title for the main chart (e.g. "BTC/USDT")
|
|
49
|
+
titleColor?: string;
|
|
50
|
+
backgroundColor?: string;
|
|
51
|
+
upColor?: string;
|
|
52
|
+
downColor?: string;
|
|
53
|
+
fontColor?: string;
|
|
54
|
+
fontFamily?: string;
|
|
55
|
+
padding?: number; // Defaults to 0.2
|
|
56
|
+
height?: string | number;
|
|
57
|
+
controls?: {
|
|
58
|
+
collapse?: boolean;
|
|
59
|
+
maximize?: boolean;
|
|
60
|
+
fullscreen?: boolean;
|
|
61
|
+
};
|
|
62
|
+
dataZoom?: {
|
|
63
|
+
visible?: boolean;
|
|
64
|
+
position?: 'top' | 'bottom';
|
|
65
|
+
height?: number; // height in %, default 6
|
|
66
|
+
start?: number; // 0-100, default 50
|
|
67
|
+
end?: number; // 0-100, default 100
|
|
68
|
+
};
|
|
69
|
+
databox?: {
|
|
70
|
+
position: 'floating' | 'left' | 'right';
|
|
71
|
+
};
|
|
72
|
+
layout?: {
|
|
73
|
+
mainPaneHeight: string; // e.g. "60%"
|
|
74
|
+
gap: number; // e.g. 5 (percent)
|
|
75
|
+
};
|
|
76
|
+
watermark?: boolean; // Default true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Plugin System Types
|
|
80
|
+
|
|
81
|
+
export interface Coordinate {
|
|
82
|
+
x: number;
|
|
83
|
+
y: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface DataCoordinate {
|
|
87
|
+
timeIndex: number;
|
|
88
|
+
value: number;
|
|
89
|
+
paneIndex?: number; // Optional pane index
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ChartContext {
|
|
93
|
+
// Core Access
|
|
94
|
+
getChart(): any; // echarts.ECharts instance
|
|
95
|
+
getMarketData(): OHLCV[];
|
|
96
|
+
getTimeToIndex(): Map<number, number>;
|
|
97
|
+
getOptions(): QFChartOptions;
|
|
98
|
+
|
|
99
|
+
// Event Bus
|
|
100
|
+
events: EventBus;
|
|
101
|
+
|
|
102
|
+
// Helpers
|
|
103
|
+
coordinateConversion: {
|
|
104
|
+
pixelToData: (point: Coordinate) => DataCoordinate | null;
|
|
105
|
+
dataToPixel: (point: DataCoordinate) => Coordinate | null;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Interaction Control
|
|
109
|
+
disableTools(): void; // To disable other active tools
|
|
110
|
+
|
|
111
|
+
// Zoom Control
|
|
112
|
+
setZoom(start: number, end: number): void;
|
|
113
|
+
|
|
114
|
+
// Drawing Management
|
|
115
|
+
addDrawing(drawing: DrawingElement): void;
|
|
116
|
+
removeDrawing(id: string): void;
|
|
117
|
+
getDrawing(id: string): DrawingElement | undefined;
|
|
118
|
+
updateDrawing(drawing: DrawingElement): void;
|
|
119
|
+
|
|
120
|
+
// Interaction Locking
|
|
121
|
+
lockChart(): void;
|
|
122
|
+
unlockChart(): void;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type DrawingType = 'line' | 'fibonacci';
|
|
126
|
+
|
|
127
|
+
export interface DrawingElement {
|
|
128
|
+
id: string;
|
|
129
|
+
type: DrawingType;
|
|
130
|
+
points: DataCoordinate[]; // [start, end]
|
|
131
|
+
paneIndex?: number; // Pane where this drawing belongs (default 0)
|
|
132
|
+
style?: {
|
|
133
|
+
color?: string;
|
|
134
|
+
lineWidth?: number;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface PluginConfig {
|
|
139
|
+
id: string;
|
|
140
|
+
name?: string;
|
|
141
|
+
icon?: string;
|
|
142
|
+
hotkey?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface Plugin {
|
|
146
|
+
id: string;
|
|
147
|
+
name?: string;
|
|
148
|
+
icon?: string;
|
|
149
|
+
|
|
150
|
+
init(context: ChartContext): void;
|
|
151
|
+
|
|
152
|
+
// Called when the tool button is clicked/activated
|
|
153
|
+
activate?(): void;
|
|
154
|
+
|
|
155
|
+
// Called when the tool is deactivated
|
|
156
|
+
deactivate?(): void;
|
|
157
|
+
|
|
158
|
+
// Cleanup when plugin is removed
|
|
159
|
+
destroy?(): void;
|
|
160
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type EventType =
|
|
2
|
+
| 'mouse:down'
|
|
3
|
+
| 'mouse:move'
|
|
4
|
+
| 'mouse:up'
|
|
5
|
+
| 'mouse:click'
|
|
6
|
+
| 'chart:resize'
|
|
7
|
+
| 'chart:dataZoom'
|
|
8
|
+
| 'chart:updated'
|
|
9
|
+
| 'plugin:activated'
|
|
10
|
+
| 'plugin:deactivated'
|
|
11
|
+
| 'drawing:hover'
|
|
12
|
+
| 'drawing:mouseout'
|
|
13
|
+
| 'drawing:mousedown'
|
|
14
|
+
| 'drawing:click'
|
|
15
|
+
| 'drawing:point:hover'
|
|
16
|
+
| 'drawing:point:mouseout'
|
|
17
|
+
| 'drawing:point:mousedown'
|
|
18
|
+
| 'drawing:point:click'
|
|
19
|
+
| 'drawing:selected'
|
|
20
|
+
| 'drawing:deselected'
|
|
21
|
+
| 'drawing:deleted';
|
|
22
|
+
|
|
23
|
+
export interface DrawingEventPayload {
|
|
24
|
+
id: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
pointIndex?: number;
|
|
27
|
+
event?: any;
|
|
28
|
+
x?: number;
|
|
29
|
+
y?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type EventHandler<T = any> = (payload: T) => void;
|
|
33
|
+
|
|
34
|
+
export class EventBus {
|
|
35
|
+
private handlers: Map<EventType, Set<EventHandler>> = new Map();
|
|
36
|
+
|
|
37
|
+
public on<T = any>(event: EventType, handler: EventHandler<T>): void {
|
|
38
|
+
if (!this.handlers.has(event)) {
|
|
39
|
+
this.handlers.set(event, new Set());
|
|
40
|
+
}
|
|
41
|
+
this.handlers.get(event)!.add(handler);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public off<T = any>(event: EventType, handler: EventHandler<T>): void {
|
|
45
|
+
const handlers = this.handlers.get(event);
|
|
46
|
+
if (handlers) {
|
|
47
|
+
handlers.delete(handler);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public emit<T = any>(event: EventType, payload?: T): void {
|
|
52
|
+
const handlers = this.handlers.get(event);
|
|
53
|
+
if (handlers) {
|
|
54
|
+
handlers.forEach((handler) => {
|
|
55
|
+
try {
|
|
56
|
+
handler(payload);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error(`Error in EventBus handler for ${event}:`, e);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public clear(): void {
|
|
65
|
+
this.handlers.clear();
|
|
66
|
+
}
|
|
67
|
+
}
|