@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,192 @@
|
|
|
1
|
+
import * as echarts from "echarts";
|
|
2
|
+
import { AbstractPlugin } from "../components/AbstractPlugin";
|
|
3
|
+
|
|
4
|
+
export class FibonacciTool extends AbstractPlugin {
|
|
5
|
+
private startPoint: number[] | null = null;
|
|
6
|
+
private endPoint: number[] | null = null;
|
|
7
|
+
private state: "idle" | "drawing" | "finished" = "idle";
|
|
8
|
+
|
|
9
|
+
// Temporary ZRender elements
|
|
10
|
+
private graphicGroup: any = null;
|
|
11
|
+
|
|
12
|
+
// Fib levels config
|
|
13
|
+
private readonly levels = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
|
|
14
|
+
private readonly colors = [
|
|
15
|
+
"#787b86", // 0
|
|
16
|
+
"#f44336", // 0.236
|
|
17
|
+
"#ff9800", // 0.382
|
|
18
|
+
"#4caf50", // 0.5
|
|
19
|
+
"#2196f3", // 0.618
|
|
20
|
+
"#00bcd4", // 0.786
|
|
21
|
+
"#787b86", // 1
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
25
|
+
super({
|
|
26
|
+
id: "fibonacci-tool",
|
|
27
|
+
name: options.name || "Fibonacci Retracement",
|
|
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="M120-80v-80h720v80H120Zm0-240v-80h720v80H120Zm0-240v-80h720v80H120Zm0-240v-80h720v80H120Z"/></svg>`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public onActivate(): void {
|
|
35
|
+
this.state = "idle";
|
|
36
|
+
this.startPoint = null;
|
|
37
|
+
this.endPoint = null;
|
|
38
|
+
this.context.getChart().getZr().setCursorStyle("crosshair");
|
|
39
|
+
this.bindEvents();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public onDeactivate(): void {
|
|
43
|
+
this.state = "idle";
|
|
44
|
+
this.startPoint = null;
|
|
45
|
+
this.endPoint = null;
|
|
46
|
+
this.removeGraphic();
|
|
47
|
+
this.unbindEvents();
|
|
48
|
+
this.context.getChart().getZr().setCursorStyle("default");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private bindEvents() {
|
|
52
|
+
const zr = this.context.getChart().getZr();
|
|
53
|
+
zr.on("click", this.onClick);
|
|
54
|
+
zr.on("mousemove", this.onMouseMove);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private unbindEvents() {
|
|
58
|
+
const zr = this.context.getChart().getZr();
|
|
59
|
+
zr.off("click", this.onClick);
|
|
60
|
+
zr.off("mousemove", this.onMouseMove);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private onClick = (params: any) => {
|
|
64
|
+
if (this.state === "idle") {
|
|
65
|
+
this.state = "drawing";
|
|
66
|
+
this.startPoint = [params.offsetX, params.offsetY];
|
|
67
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
68
|
+
this.initGraphic();
|
|
69
|
+
this.updateGraphic();
|
|
70
|
+
} else if (this.state === "drawing") {
|
|
71
|
+
this.state = "finished";
|
|
72
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
73
|
+
this.updateGraphic();
|
|
74
|
+
this.saveDrawing();
|
|
75
|
+
|
|
76
|
+
// Cleanup local graphic and deactivate
|
|
77
|
+
this.removeGraphic();
|
|
78
|
+
this.context.disableTools();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
private onMouseMove = (params: any) => {
|
|
83
|
+
if (this.state === "drawing") {
|
|
84
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
85
|
+
this.updateGraphic();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
private initGraphic() {
|
|
90
|
+
this.graphicGroup = new echarts.graphic.Group();
|
|
91
|
+
this.context.getChart().getZr().add(this.graphicGroup);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private removeGraphic() {
|
|
95
|
+
if (this.graphicGroup) {
|
|
96
|
+
this.context.getChart().getZr().remove(this.graphicGroup);
|
|
97
|
+
this.graphicGroup = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private updateGraphic() {
|
|
102
|
+
if (!this.graphicGroup || !this.startPoint || !this.endPoint) return;
|
|
103
|
+
this.graphicGroup.removeAll();
|
|
104
|
+
|
|
105
|
+
const x1 = this.startPoint[0];
|
|
106
|
+
const y1 = this.startPoint[1];
|
|
107
|
+
const x2 = this.endPoint[0];
|
|
108
|
+
const y2 = this.endPoint[1];
|
|
109
|
+
|
|
110
|
+
// Diagonal trend line
|
|
111
|
+
const trendLine = new echarts.graphic.Line({
|
|
112
|
+
shape: { x1, y1, x2, y2 },
|
|
113
|
+
style: {
|
|
114
|
+
stroke: "#999",
|
|
115
|
+
lineWidth: 1,
|
|
116
|
+
lineDash: [4, 4],
|
|
117
|
+
},
|
|
118
|
+
silent: true,
|
|
119
|
+
});
|
|
120
|
+
this.graphicGroup.add(trendLine);
|
|
121
|
+
|
|
122
|
+
// Levels
|
|
123
|
+
const startX = Math.min(x1, x2);
|
|
124
|
+
const endX = Math.max(x1, x2);
|
|
125
|
+
const width = endX - startX;
|
|
126
|
+
|
|
127
|
+
// Y range
|
|
128
|
+
const diffY = y2 - y1; // Pixel difference
|
|
129
|
+
|
|
130
|
+
this.levels.forEach((level, index) => {
|
|
131
|
+
const levelY = y2 - diffY * level;
|
|
132
|
+
|
|
133
|
+
const color = this.colors[index % this.colors.length];
|
|
134
|
+
|
|
135
|
+
// Line
|
|
136
|
+
const line = new echarts.graphic.Line({
|
|
137
|
+
shape: { x1: startX, y1: levelY, x2: endX, y2: levelY },
|
|
138
|
+
style: {
|
|
139
|
+
stroke: color,
|
|
140
|
+
lineWidth: 1,
|
|
141
|
+
},
|
|
142
|
+
silent: true,
|
|
143
|
+
});
|
|
144
|
+
this.graphicGroup.add(line);
|
|
145
|
+
|
|
146
|
+
if (index < this.levels.length - 1) {
|
|
147
|
+
const nextLevel = this.levels[index + 1];
|
|
148
|
+
const nextY = y2 - diffY * nextLevel;
|
|
149
|
+
const rectH = Math.abs(nextY - levelY);
|
|
150
|
+
const rectY = Math.min(levelY, nextY);
|
|
151
|
+
|
|
152
|
+
const rect = new echarts.graphic.Rect({
|
|
153
|
+
shape: { x: startX, y: rectY, width, height: rectH },
|
|
154
|
+
style: {
|
|
155
|
+
fill: this.colors[(index + 1) % this.colors.length], // Use next level's color
|
|
156
|
+
opacity: 0.1,
|
|
157
|
+
},
|
|
158
|
+
silent: true,
|
|
159
|
+
});
|
|
160
|
+
this.graphicGroup.add(rect);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private saveDrawing() {
|
|
166
|
+
if (!this.startPoint || !this.endPoint) return;
|
|
167
|
+
|
|
168
|
+
const start = this.context.coordinateConversion.pixelToData({
|
|
169
|
+
x: this.startPoint[0],
|
|
170
|
+
y: this.startPoint[1],
|
|
171
|
+
});
|
|
172
|
+
const end = this.context.coordinateConversion.pixelToData({
|
|
173
|
+
x: this.endPoint[0],
|
|
174
|
+
y: this.endPoint[1],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (start && end) {
|
|
178
|
+
const paneIndex = start.paneIndex || 0;
|
|
179
|
+
|
|
180
|
+
this.context.addDrawing({
|
|
181
|
+
id: `fib-${Date.now()}`,
|
|
182
|
+
type: "fibonacci",
|
|
183
|
+
points: [start, end],
|
|
184
|
+
paneIndex: paneIndex,
|
|
185
|
+
style: {
|
|
186
|
+
color: "#3b82f6", // Default color, though individual lines use specific colors
|
|
187
|
+
lineWidth: 1,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { AbstractPlugin } from '../components/AbstractPlugin';
|
|
2
|
+
import * as echarts from 'echarts';
|
|
3
|
+
|
|
4
|
+
type PluginState = 'idle' | 'drawing' | 'finished';
|
|
5
|
+
|
|
6
|
+
export class LineTool extends AbstractPlugin {
|
|
7
|
+
private zr!: any;
|
|
8
|
+
private state: PluginState = 'idle';
|
|
9
|
+
private startPoint: number[] | null = null;
|
|
10
|
+
private endPoint: number[] | null = null;
|
|
11
|
+
|
|
12
|
+
// ZRender Elements
|
|
13
|
+
private group: any = null;
|
|
14
|
+
private line: any = null;
|
|
15
|
+
private startCircle: any = null;
|
|
16
|
+
private endCircle: any = null;
|
|
17
|
+
|
|
18
|
+
constructor(options: { name?: string; icon?: string }) {
|
|
19
|
+
super({
|
|
20
|
+
id: 'trend-line',
|
|
21
|
+
name: options?.name || 'Trend Line',
|
|
22
|
+
icon:
|
|
23
|
+
options?.icon ||
|
|
24
|
+
`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="2" y1="22" x2="22" y2="2" /></svg>`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected onInit(): void {
|
|
29
|
+
this.zr = this.chart.getZr();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected onActivate(): void {
|
|
33
|
+
this.state = 'idle';
|
|
34
|
+
this.chart.getZr().setCursorStyle('crosshair');
|
|
35
|
+
|
|
36
|
+
this.zr.on('click', this.onClick);
|
|
37
|
+
this.zr.on('mousemove', this.onMouseMove);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected onDeactivate(): void {
|
|
41
|
+
this.state = 'idle';
|
|
42
|
+
this.chart.getZr().setCursorStyle('default');
|
|
43
|
+
|
|
44
|
+
this.zr.off('click', this.onClick);
|
|
45
|
+
this.zr.off('mousemove', this.onMouseMove);
|
|
46
|
+
|
|
47
|
+
// Clean up clear listeners
|
|
48
|
+
this.disableClearListeners();
|
|
49
|
+
|
|
50
|
+
// @ts-ignore - state type comparison
|
|
51
|
+
if (this.state === 'drawing') {
|
|
52
|
+
this.removeGraphic();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected onDestroy(): void {
|
|
57
|
+
this.removeGraphic();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- Interaction Handlers ---
|
|
61
|
+
|
|
62
|
+
private onMouseDown = () => {
|
|
63
|
+
// No longer needed
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
private onChartInteraction = () => {
|
|
67
|
+
// No longer needed
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
private onClick = (params: any) => {
|
|
71
|
+
if (this.state === 'idle') {
|
|
72
|
+
this.state = 'drawing';
|
|
73
|
+
this.startPoint = [params.offsetX, params.offsetY];
|
|
74
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
75
|
+
this.initGraphic();
|
|
76
|
+
this.updateGraphic();
|
|
77
|
+
} else if (this.state === 'drawing') {
|
|
78
|
+
this.state = 'finished';
|
|
79
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
80
|
+
this.updateGraphic();
|
|
81
|
+
|
|
82
|
+
// Convert to native chart drawing
|
|
83
|
+
if (this.startPoint && this.endPoint) {
|
|
84
|
+
const start = this.context.coordinateConversion.pixelToData({
|
|
85
|
+
x: this.startPoint[0],
|
|
86
|
+
y: this.startPoint[1],
|
|
87
|
+
});
|
|
88
|
+
const end = this.context.coordinateConversion.pixelToData({
|
|
89
|
+
x: this.endPoint[0],
|
|
90
|
+
y: this.endPoint[1],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (start && end) {
|
|
94
|
+
// Use the pane index from the start point (assume drawing starts and ends in same pane or use start pane)
|
|
95
|
+
const paneIndex = start.paneIndex || 0;
|
|
96
|
+
|
|
97
|
+
this.context.addDrawing({
|
|
98
|
+
id: `line-${Date.now()}`,
|
|
99
|
+
type: 'line',
|
|
100
|
+
points: [start, end],
|
|
101
|
+
paneIndex: paneIndex,
|
|
102
|
+
style: {
|
|
103
|
+
color: '#3b82f6',
|
|
104
|
+
lineWidth: 2,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Cleanup local ZRender graphic as it's now part of the chart series
|
|
111
|
+
this.removeGraphic();
|
|
112
|
+
this.context.disableTools();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
private saveDataCoordinates() {
|
|
117
|
+
// No longer needed
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private updateGraphicFromData() {
|
|
121
|
+
// No longer needed
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private enableClearListeners(): void {
|
|
125
|
+
// No longer needed
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private clearHandlers: any = {};
|
|
129
|
+
|
|
130
|
+
private disableClearListeners(): void {
|
|
131
|
+
// No longer needed
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private onMouseMove = (params: any) => {
|
|
135
|
+
if (this.state !== 'drawing') return;
|
|
136
|
+
this.endPoint = [params.offsetX, params.offsetY];
|
|
137
|
+
this.updateGraphic();
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// --- Graphics ---
|
|
141
|
+
|
|
142
|
+
private initGraphic(): void {
|
|
143
|
+
if (this.group) return;
|
|
144
|
+
|
|
145
|
+
this.group = new echarts.graphic.Group();
|
|
146
|
+
|
|
147
|
+
this.line = new echarts.graphic.Line({
|
|
148
|
+
shape: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
149
|
+
style: { stroke: '#3b82f6', lineWidth: 2 },
|
|
150
|
+
z: 100,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.startCircle = new echarts.graphic.Circle({
|
|
154
|
+
shape: { cx: 0, cy: 0, r: 4 },
|
|
155
|
+
style: { fill: '#fff', stroke: '#3b82f6', lineWidth: 1 },
|
|
156
|
+
z: 101,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
this.endCircle = new echarts.graphic.Circle({
|
|
160
|
+
shape: { cx: 0, cy: 0, r: 4 },
|
|
161
|
+
style: { fill: '#fff', stroke: '#3b82f6', lineWidth: 1 },
|
|
162
|
+
z: 101,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
this.group.add(this.line);
|
|
166
|
+
this.group.add(this.startCircle);
|
|
167
|
+
this.group.add(this.endCircle);
|
|
168
|
+
|
|
169
|
+
this.zr.add(this.group);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private removeGraphic(): void {
|
|
173
|
+
if (this.group) {
|
|
174
|
+
this.zr.remove(this.group);
|
|
175
|
+
this.group = null;
|
|
176
|
+
this.disableClearListeners();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private updateGraphic(): void {
|
|
181
|
+
if (!this.startPoint || !this.endPoint || !this.group) return;
|
|
182
|
+
|
|
183
|
+
const [x1, y1] = this.startPoint;
|
|
184
|
+
const [x2, y2] = this.endPoint;
|
|
185
|
+
|
|
186
|
+
this.line.setShape({ x1, y1, x2, y2 });
|
|
187
|
+
this.startCircle.setShape({ cx: x1, cy: y1 });
|
|
188
|
+
this.endCircle.setShape({ cx: x2, cy: y2 });
|
|
189
|
+
}
|
|
190
|
+
}
|