@qfo/qfchart 0.8.0 → 0.8.1
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 +319 -12
- package/dist/qfchart.min.browser.js +32 -16
- package/dist/qfchart.min.es.js +32 -16
- package/package.json +1 -1
- package/src/QFChart.ts +98 -262
- 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 +41 -35
- package/src/components/SeriesBuilder.ts +10 -10
- package/src/components/TooltipFormatter.ts +1 -1
- package/src/index.ts +17 -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/CypherPatternTool/CypherPatternDrawingRenderer.ts +80 -0
- package/src/plugins/CypherPatternTool/CypherPatternTool.ts +84 -0
- package/src/plugins/CypherPatternTool/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/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/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/TrianglePatternTool/TrianglePatternDrawingRenderer.ts +107 -0
- package/src/plugins/TrianglePatternTool/TrianglePatternTool.ts +98 -0
- package/src/plugins/TrianglePatternTool/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 +37 -11
|
@@ -1,192 +1,195 @@
|
|
|
1
|
-
import * as echarts from "echarts";
|
|
2
|
-
import { AbstractPlugin } from "
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
private
|
|
7
|
-
private
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
private readonly
|
|
15
|
-
|
|
16
|
-
"#
|
|
17
|
-
"#
|
|
18
|
-
"#
|
|
19
|
-
"#
|
|
20
|
-
"#
|
|
21
|
-
"#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
zr.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.
|
|
72
|
-
this.endPoint =
|
|
73
|
-
this.
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
this.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
1
|
+
import * as echarts from "echarts";
|
|
2
|
+
import { AbstractPlugin } from "../../components/AbstractPlugin";
|
|
3
|
+
import { FibonacciDrawingRenderer } from "./FibonacciDrawingRenderer";
|
|
4
|
+
|
|
5
|
+
export class FibonacciTool extends AbstractPlugin {
|
|
6
|
+
private startPoint: number[] | null = null;
|
|
7
|
+
private endPoint: number[] | null = null;
|
|
8
|
+
private state: "idle" | "drawing" | "finished" = "idle";
|
|
9
|
+
|
|
10
|
+
// Temporary ZRender elements
|
|
11
|
+
private graphicGroup: any = null;
|
|
12
|
+
|
|
13
|
+
// Fib levels config
|
|
14
|
+
private readonly levels = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
|
|
15
|
+
private readonly colors = [
|
|
16
|
+
"#787b86", // 0
|
|
17
|
+
"#f44336", // 0.236
|
|
18
|
+
"#ff9800", // 0.382
|
|
19
|
+
"#4caf50", // 0.5
|
|
20
|
+
"#2196f3", // 0.618
|
|
21
|
+
"#00bcd4", // 0.786
|
|
22
|
+
"#787b86", // 1
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
26
|
+
super({
|
|
27
|
+
id: "fibonacci-tool",
|
|
28
|
+
name: options.name || "Fibonacci Retracement",
|
|
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="M120-80v-80h720v80H120Zm0-240v-80h720v80H120Zm0-240v-80h720v80H120Zm0-240v-80h720v80H120Z"/></svg>`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected onInit(): void {
|
|
36
|
+
this.context.registerDrawingRenderer(new FibonacciDrawingRenderer());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public onActivate(): void {
|
|
40
|
+
this.state = "idle";
|
|
41
|
+
this.startPoint = null;
|
|
42
|
+
this.endPoint = null;
|
|
43
|
+
this.context.getChart().getZr().setCursorStyle("crosshair");
|
|
44
|
+
this.bindEvents();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public onDeactivate(): void {
|
|
48
|
+
this.state = "idle";
|
|
49
|
+
this.startPoint = null;
|
|
50
|
+
this.endPoint = null;
|
|
51
|
+
this.removeGraphic();
|
|
52
|
+
this.unbindEvents();
|
|
53
|
+
this.context.getChart().getZr().setCursorStyle("default");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private bindEvents() {
|
|
57
|
+
const zr = this.context.getChart().getZr();
|
|
58
|
+
zr.on("click", this.onClick);
|
|
59
|
+
zr.on("mousemove", this.onMouseMove);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private unbindEvents() {
|
|
63
|
+
const zr = this.context.getChart().getZr();
|
|
64
|
+
zr.off("click", this.onClick);
|
|
65
|
+
zr.off("mousemove", this.onMouseMove);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private onClick = (params: any) => {
|
|
69
|
+
if (this.state === "idle") {
|
|
70
|
+
this.state = "drawing";
|
|
71
|
+
this.startPoint = this.getPoint(params);
|
|
72
|
+
this.endPoint = this.getPoint(params);
|
|
73
|
+
this.initGraphic();
|
|
74
|
+
this.updateGraphic();
|
|
75
|
+
} else if (this.state === "drawing") {
|
|
76
|
+
this.state = "finished";
|
|
77
|
+
this.endPoint = this.getPoint(params);
|
|
78
|
+
this.updateGraphic();
|
|
79
|
+
this.saveDrawing();
|
|
80
|
+
|
|
81
|
+
// Cleanup local graphic and deactivate
|
|
82
|
+
this.removeGraphic();
|
|
83
|
+
this.context.disableTools();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
private onMouseMove = (params: any) => {
|
|
88
|
+
if (this.state === "drawing") {
|
|
89
|
+
this.endPoint = this.getPoint(params);
|
|
90
|
+
this.updateGraphic();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
private initGraphic() {
|
|
95
|
+
this.graphicGroup = new echarts.graphic.Group();
|
|
96
|
+
this.context.getChart().getZr().add(this.graphicGroup);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private removeGraphic() {
|
|
100
|
+
if (this.graphicGroup) {
|
|
101
|
+
this.context.getChart().getZr().remove(this.graphicGroup);
|
|
102
|
+
this.graphicGroup = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private updateGraphic() {
|
|
107
|
+
if (!this.graphicGroup || !this.startPoint || !this.endPoint) return;
|
|
108
|
+
this.graphicGroup.removeAll();
|
|
109
|
+
|
|
110
|
+
const x1 = this.startPoint[0];
|
|
111
|
+
const y1 = this.startPoint[1];
|
|
112
|
+
const x2 = this.endPoint[0];
|
|
113
|
+
const y2 = this.endPoint[1];
|
|
114
|
+
|
|
115
|
+
// Diagonal trend line
|
|
116
|
+
const trendLine = new echarts.graphic.Line({
|
|
117
|
+
shape: { x1, y1, x2, y2 },
|
|
118
|
+
style: {
|
|
119
|
+
stroke: "#999",
|
|
120
|
+
lineWidth: 1,
|
|
121
|
+
lineDash: [4, 4],
|
|
122
|
+
},
|
|
123
|
+
silent: true,
|
|
124
|
+
});
|
|
125
|
+
this.graphicGroup.add(trendLine);
|
|
126
|
+
|
|
127
|
+
// Levels
|
|
128
|
+
const startX = Math.min(x1, x2);
|
|
129
|
+
const endX = Math.max(x1, x2);
|
|
130
|
+
const width = endX - startX;
|
|
131
|
+
|
|
132
|
+
const diffY = y2 - y1;
|
|
133
|
+
|
|
134
|
+
this.levels.forEach((level, index) => {
|
|
135
|
+
const levelY = y2 - diffY * level;
|
|
136
|
+
|
|
137
|
+
const color = this.colors[index % this.colors.length];
|
|
138
|
+
|
|
139
|
+
const line = new echarts.graphic.Line({
|
|
140
|
+
shape: { x1: startX, y1: levelY, x2: endX, y2: levelY },
|
|
141
|
+
style: {
|
|
142
|
+
stroke: color,
|
|
143
|
+
lineWidth: 1,
|
|
144
|
+
},
|
|
145
|
+
silent: true,
|
|
146
|
+
});
|
|
147
|
+
this.graphicGroup.add(line);
|
|
148
|
+
|
|
149
|
+
if (index < this.levels.length - 1) {
|
|
150
|
+
const nextLevel = this.levels[index + 1];
|
|
151
|
+
const nextY = y2 - diffY * nextLevel;
|
|
152
|
+
const rectH = Math.abs(nextY - levelY);
|
|
153
|
+
const rectY = Math.min(levelY, nextY);
|
|
154
|
+
|
|
155
|
+
const rect = new echarts.graphic.Rect({
|
|
156
|
+
shape: { x: startX, y: rectY, width, height: rectH },
|
|
157
|
+
style: {
|
|
158
|
+
fill: this.colors[(index + 1) % this.colors.length],
|
|
159
|
+
opacity: 0.1,
|
|
160
|
+
},
|
|
161
|
+
silent: true,
|
|
162
|
+
});
|
|
163
|
+
this.graphicGroup.add(rect);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private saveDrawing() {
|
|
169
|
+
if (!this.startPoint || !this.endPoint) return;
|
|
170
|
+
|
|
171
|
+
const start = this.context.coordinateConversion.pixelToData({
|
|
172
|
+
x: this.startPoint[0],
|
|
173
|
+
y: this.startPoint[1],
|
|
174
|
+
});
|
|
175
|
+
const end = this.context.coordinateConversion.pixelToData({
|
|
176
|
+
x: this.endPoint[0],
|
|
177
|
+
y: this.endPoint[1],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (start && end) {
|
|
181
|
+
const paneIndex = start.paneIndex || 0;
|
|
182
|
+
|
|
183
|
+
this.context.addDrawing({
|
|
184
|
+
id: `fib-${Date.now()}`,
|
|
185
|
+
type: "fibonacci",
|
|
186
|
+
points: [start, end],
|
|
187
|
+
paneIndex: paneIndex,
|
|
188
|
+
style: {
|
|
189
|
+
color: "#3b82f6",
|
|
190
|
+
lineWidth: 1,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
// Points: 0=left base, 1=left shoulder, 2=neckline left, 3=head, 4=neckline right, 5=right shoulder, 6=right base
|
|
4
|
+
const LABELS = ['', 'LS', '', 'H', '', 'RS', ''];
|
|
5
|
+
|
|
6
|
+
export class HeadAndShouldersDrawingRenderer implements DrawingRenderer {
|
|
7
|
+
type = 'head_and_shoulders';
|
|
8
|
+
|
|
9
|
+
render(ctx: DrawingRenderContext): any {
|
|
10
|
+
const { drawing, pixelPoints, isSelected } = ctx;
|
|
11
|
+
const color = drawing.style?.color || '#3b82f6';
|
|
12
|
+
if (pixelPoints.length < 2) return;
|
|
13
|
+
|
|
14
|
+
const children: any[] = [];
|
|
15
|
+
|
|
16
|
+
// Fill left shoulder region (points 0,1,2)
|
|
17
|
+
if (pixelPoints.length >= 3) {
|
|
18
|
+
children.push({ type: 'polygon', name: 'line', shape: { points: pixelPoints.slice(0, 3).map(([x, y]) => [x, y]) }, style: { fill: 'rgba(33, 150, 243, 0.06)' } });
|
|
19
|
+
}
|
|
20
|
+
// Fill head region (points 2,3,4)
|
|
21
|
+
if (pixelPoints.length >= 5) {
|
|
22
|
+
children.push({ type: 'polygon', name: 'line', shape: { points: pixelPoints.slice(2, 5).map(([x, y]) => [x, y]) }, style: { fill: 'rgba(244, 67, 54, 0.08)' } });
|
|
23
|
+
}
|
|
24
|
+
// Fill right shoulder region (points 4,5,6)
|
|
25
|
+
if (pixelPoints.length >= 7) {
|
|
26
|
+
children.push({ type: 'polygon', name: 'line', shape: { points: pixelPoints.slice(4, 7).map(([x, y]) => [x, y]) }, style: { fill: 'rgba(33, 150, 243, 0.06)' } });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Zigzag through all points
|
|
30
|
+
for (let i = 0; i < pixelPoints.length - 1; i++) {
|
|
31
|
+
const [x1, y1] = pixelPoints[i];
|
|
32
|
+
const [x2, y2] = pixelPoints[i + 1];
|
|
33
|
+
children.push({
|
|
34
|
+
type: 'line', name: 'line',
|
|
35
|
+
shape: { x1, y1, x2, y2 },
|
|
36
|
+
style: { stroke: '#2196f3', lineWidth: drawing.style?.lineWidth || 2 },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Neckline: connect neckline-left (2) and neckline-right (4), extended
|
|
41
|
+
if (pixelPoints.length >= 5) {
|
|
42
|
+
const [nx1, ny1] = pixelPoints[2];
|
|
43
|
+
const [nx2, ny2] = pixelPoints[4];
|
|
44
|
+
const dx = nx2 - nx1;
|
|
45
|
+
const dy = ny2 - ny1;
|
|
46
|
+
|
|
47
|
+
// Extended neckline (0.3 beyond each side)
|
|
48
|
+
const extL = 0.3;
|
|
49
|
+
const extR = 0.3;
|
|
50
|
+
const exlx = nx1 - dx * extL;
|
|
51
|
+
const exly = ny1 - dy * extL;
|
|
52
|
+
const exrx = nx2 + dx * extR;
|
|
53
|
+
const exry = ny2 + dy * extR;
|
|
54
|
+
|
|
55
|
+
children.push({
|
|
56
|
+
type: 'line',
|
|
57
|
+
shape: { x1: exlx, y1: exly, x2: exrx, y2: exry },
|
|
58
|
+
style: { stroke: '#ff9800', lineWidth: 2, lineDash: [6, 4] },
|
|
59
|
+
silent: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Neckline label
|
|
63
|
+
children.push({
|
|
64
|
+
type: 'text',
|
|
65
|
+
style: { text: 'Neckline', x: (nx1 + nx2) / 2, y: (ny1 + ny2) / 2 + 14, fill: '#ff9800', fontSize: 10, align: 'center' },
|
|
66
|
+
silent: true,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Labels
|
|
71
|
+
for (let i = 0; i < pixelPoints.length && i < LABELS.length; i++) {
|
|
72
|
+
if (!LABELS[i]) continue;
|
|
73
|
+
const [px, py] = pixelPoints[i];
|
|
74
|
+
// Shoulders and head are peaks (above neighbors)
|
|
75
|
+
const isHigh = (i === 0 || py <= pixelPoints[i - 1][1]) && (i === pixelPoints.length - 1 || py <= pixelPoints[i + 1]?.[1]);
|
|
76
|
+
children.push({
|
|
77
|
+
type: 'text',
|
|
78
|
+
style: { text: LABELS[i], x: px, y: isHigh ? py - 14 : py + 16, fill: '#e2e8f0', fontSize: 12, fontWeight: 'bold', align: 'center', verticalAlign: 'middle' },
|
|
79
|
+
silent: true,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Control points
|
|
84
|
+
for (let i = 0; i < pixelPoints.length; i++) {
|
|
85
|
+
children.push({
|
|
86
|
+
type: 'circle', name: `point-${i}`,
|
|
87
|
+
shape: { cx: pixelPoints[i][0], cy: pixelPoints[i][1], r: 4 },
|
|
88
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
89
|
+
z: 100,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { type: 'group', children };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as echarts from 'echarts';
|
|
2
|
+
import { AbstractPlugin } from '../../components/AbstractPlugin';
|
|
3
|
+
import { HeadAndShouldersDrawingRenderer } from './HeadAndShouldersDrawingRenderer';
|
|
4
|
+
|
|
5
|
+
const LABELS = ['', 'LS', '', 'H', '', 'RS', ''];
|
|
6
|
+
const TOTAL_POINTS = 7;
|
|
7
|
+
|
|
8
|
+
export class HeadAndShouldersTool 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: 'head-and-shoulders-tool',
|
|
16
|
+
name: options.name || 'Head & Shoulders',
|
|
17
|
+
icon: options.icon || `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#e3e3e3" stroke-width="1.5"><polyline points="1,18 4,10 7,14 12,3 17,14 20,10 23,18"/></svg>`,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected onInit(): void { this.context.registerDrawingRenderer(new HeadAndShouldersDrawingRenderer()); }
|
|
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
|
+
// Fills
|
|
63
|
+
if (pts.length >= 3) this.graphicGroup.add(new echarts.graphic.Polygon({ shape: { points: pts.slice(0, 3) }, style: { fill: 'rgba(33,150,243,0.06)' }, silent: true }));
|
|
64
|
+
if (pts.length >= 5) this.graphicGroup.add(new echarts.graphic.Polygon({ shape: { points: pts.slice(2, 5) }, style: { fill: 'rgba(244,67,54,0.08)' }, silent: true }));
|
|
65
|
+
if (pts.length >= 7) this.graphicGroup.add(new echarts.graphic.Polygon({ shape: { points: pts.slice(4, 7) }, style: { fill: 'rgba(33,150,243,0.06)' }, silent: true }));
|
|
66
|
+
|
|
67
|
+
// Zigzag
|
|
68
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
69
|
+
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: '#2196f3', lineWidth: 2 }, silent: true }));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Neckline
|
|
73
|
+
if (pts.length >= 5) {
|
|
74
|
+
const [nx1, ny1] = pts[2];
|
|
75
|
+
const [nx2, ny2] = pts[4];
|
|
76
|
+
const dx = nx2 - nx1; const dy = ny2 - ny1;
|
|
77
|
+
this.graphicGroup.add(new echarts.graphic.Line({ shape: { x1: nx1 - dx * 0.3, y1: ny1 - dy * 0.3, x2: nx2 + dx * 0.3, y2: ny2 + dy * 0.3 }, style: { stroke: '#ff9800', lineWidth: 2, lineDash: [6, 4] }, silent: true }));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Labels & circles
|
|
81
|
+
for (let i = 0; i < pts.length && i < LABELS.length; i++) {
|
|
82
|
+
const [px, py] = pts[i];
|
|
83
|
+
const isHigh = (i === 0 || py <= pts[i - 1][1]) && (i === pts.length - 1 || py <= pts[i + 1]?.[1]);
|
|
84
|
+
if (LABELS[i]) {
|
|
85
|
+
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 }));
|
|
86
|
+
}
|
|
87
|
+
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 }));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private saveDrawing() {
|
|
92
|
+
const dataPoints = this.points.map((pt) => this.context.coordinateConversion.pixelToData({ x: pt[0], y: pt[1] }));
|
|
93
|
+
if (dataPoints.every((p) => p !== null)) {
|
|
94
|
+
this.context.addDrawing({ id: `hs-${Date.now()}`, type: 'head_and_shoulders', points: dataPoints as any[], paneIndex: dataPoints[0]!.paneIndex || 0, style: { color: '#3b82f6', lineWidth: 2 } });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
export class LineDrawingRenderer implements DrawingRenderer {
|
|
4
|
+
type = 'line';
|
|
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 || '#3b82f6';
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
type: 'group',
|
|
14
|
+
children: [
|
|
15
|
+
{
|
|
16
|
+
type: 'line',
|
|
17
|
+
name: 'line',
|
|
18
|
+
shape: { x1, y1, x2, y2 },
|
|
19
|
+
style: {
|
|
20
|
+
stroke: color,
|
|
21
|
+
lineWidth: drawing.style?.lineWidth || 2,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: 'circle',
|
|
26
|
+
name: 'point-0',
|
|
27
|
+
shape: { cx: x1, cy: y1, r: 4 },
|
|
28
|
+
style: {
|
|
29
|
+
fill: '#fff',
|
|
30
|
+
stroke: color,
|
|
31
|
+
lineWidth: 1,
|
|
32
|
+
opacity: isSelected ? 1 : 0,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'circle',
|
|
37
|
+
name: 'point-1',
|
|
38
|
+
shape: { cx: x2, cy: y2, r: 4 },
|
|
39
|
+
style: {
|
|
40
|
+
fill: '#fff',
|
|
41
|
+
stroke: color,
|
|
42
|
+
lineWidth: 1,
|
|
43
|
+
opacity: isSelected ? 1 : 0,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|