@qfo/qfchart 0.7.3 → 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 +368 -14
- package/dist/qfchart.min.browser.js +34 -16
- package/dist/qfchart.min.es.js +34 -16
- package/package.json +1 -1
- package/src/QFChart.ts +460 -311
- 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 +284 -263
- package/src/components/LayoutManager.ts +72 -55
- package/src/components/SeriesBuilder.ts +110 -6
- package/src/components/TableCanvasRenderer.ts +467 -0
- package/src/components/TableOverlayRenderer.ts +38 -9
- package/src/components/TooltipFormatter.ts +97 -97
- package/src/components/renderers/BackgroundRenderer.ts +59 -47
- package/src/components/renderers/BoxRenderer.ts +113 -17
- package/src/components/renderers/FillRenderer.ts +118 -3
- package/src/components/renderers/LabelRenderer.ts +35 -9
- package/src/components/renderers/OHLCBarRenderer.ts +171 -161
- package/src/components/renderers/PolylineRenderer.ts +26 -19
- 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 +39 -4
- package/src/utils/ColorUtils.ts +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
const LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.272, 1.618, 2, 2.618];
|
|
4
|
+
const COLORS = [
|
|
5
|
+
'#787b86', '#f44336', '#ff9800', '#4caf50', '#2196f3',
|
|
6
|
+
'#00bcd4', '#787b86', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5',
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export class FibTrendExtensionDrawingRenderer implements DrawingRenderer {
|
|
10
|
+
type = 'fib_trend_extension';
|
|
11
|
+
|
|
12
|
+
render(ctx: DrawingRenderContext): any {
|
|
13
|
+
const { drawing, pixelPoints, isSelected, api } = ctx;
|
|
14
|
+
const color = drawing.style?.color || '#3b82f6';
|
|
15
|
+
if (pixelPoints.length < 3) return;
|
|
16
|
+
|
|
17
|
+
const [x1, y1] = pixelPoints[0]; // Trend start
|
|
18
|
+
const [x2, y2] = pixelPoints[1]; // Trend end
|
|
19
|
+
const [x3, y3] = pixelPoints[2]; // Retracement point
|
|
20
|
+
|
|
21
|
+
const pts = drawing.points;
|
|
22
|
+
const trendMove = pts[1].value - pts[0].value; // Signed price move
|
|
23
|
+
|
|
24
|
+
// Horizontal extent: from min(x1,x2,x3) to max(x1,x2,x3) + extra width
|
|
25
|
+
const minX = Math.min(x1, x2, x3);
|
|
26
|
+
const maxX = Math.max(x1, x2, x3);
|
|
27
|
+
const extraWidth = (maxX - minX) * 0.5;
|
|
28
|
+
const lineLeft = minX;
|
|
29
|
+
const lineRight = maxX + extraWidth;
|
|
30
|
+
|
|
31
|
+
const children: any[] = [];
|
|
32
|
+
|
|
33
|
+
// Compute all extension level Y positions
|
|
34
|
+
const levelData: { level: number; y: number; price: number; color: string }[] = [];
|
|
35
|
+
for (let i = 0; i < LEVELS.length; i++) {
|
|
36
|
+
const level = LEVELS[i];
|
|
37
|
+
const price = pts[2].value + trendMove * level;
|
|
38
|
+
// Convert price to pixel Y using the api
|
|
39
|
+
// We use the retracement point's x as reference for coord lookup
|
|
40
|
+
const pxCoord = api.coord([
|
|
41
|
+
pts[2].timeIndex + (ctx as any).drawing.points[2].timeIndex - pts[2].timeIndex,
|
|
42
|
+
price,
|
|
43
|
+
]);
|
|
44
|
+
// Actually, we can compute Y directly: the relationship between y3 and the
|
|
45
|
+
// trend pixel distance gives us the scale.
|
|
46
|
+
// trendMove maps to (y2 - y1) in pixels (inverted because Y axis is flipped)
|
|
47
|
+
// So for a given level: pixelY = y3 - (y2 - y1) * level
|
|
48
|
+
const py = y3 + (y2 - y1) * level;
|
|
49
|
+
|
|
50
|
+
levelData.push({ level, y: py, price, color: COLORS[i % COLORS.length] });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fill zones between adjacent levels
|
|
54
|
+
for (let i = 0; i < levelData.length - 1; i++) {
|
|
55
|
+
const curr = levelData[i];
|
|
56
|
+
const next = levelData[i + 1];
|
|
57
|
+
const rectY = Math.min(curr.y, next.y);
|
|
58
|
+
const rectH = Math.abs(next.y - curr.y);
|
|
59
|
+
|
|
60
|
+
children.push({
|
|
61
|
+
type: 'rect',
|
|
62
|
+
name: 'line',
|
|
63
|
+
shape: { x: lineLeft, y: rectY, width: lineRight - lineLeft, height: rectH },
|
|
64
|
+
style: { fill: next.color, opacity: 0.06 },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Level lines and labels
|
|
69
|
+
for (const ld of levelData) {
|
|
70
|
+
children.push({
|
|
71
|
+
type: 'line',
|
|
72
|
+
shape: { x1: lineLeft, y1: ld.y, x2: lineRight, y2: ld.y },
|
|
73
|
+
style: { stroke: ld.color, lineWidth: 1 },
|
|
74
|
+
silent: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
children.push({
|
|
78
|
+
type: 'text',
|
|
79
|
+
style: {
|
|
80
|
+
text: `${ld.level} (${ld.price.toFixed(2)})`,
|
|
81
|
+
x: lineRight + 4,
|
|
82
|
+
y: ld.y - 6,
|
|
83
|
+
fill: ld.color,
|
|
84
|
+
fontSize: 9,
|
|
85
|
+
},
|
|
86
|
+
silent: true,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Trend line (click1 → click2) dashed
|
|
91
|
+
children.push({
|
|
92
|
+
type: 'line',
|
|
93
|
+
name: 'line',
|
|
94
|
+
shape: { x1, y1, x2, y2 },
|
|
95
|
+
style: { stroke: '#2196f3', lineWidth: 1.5, lineDash: [5, 4] },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Retracement line (click2 → click3) dashed
|
|
99
|
+
children.push({
|
|
100
|
+
type: 'line',
|
|
101
|
+
name: 'line',
|
|
102
|
+
shape: { x1: x2, y1: y2, x2: x3, y2: y3 },
|
|
103
|
+
style: { stroke: '#ff9800', lineWidth: 1.5, lineDash: [5, 4] },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Control points
|
|
107
|
+
children.push({
|
|
108
|
+
type: 'circle', name: 'point-0',
|
|
109
|
+
shape: { cx: x1, cy: y1, r: 4 },
|
|
110
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
111
|
+
z: 100,
|
|
112
|
+
});
|
|
113
|
+
children.push({
|
|
114
|
+
type: 'circle', name: 'point-1',
|
|
115
|
+
shape: { cx: x2, cy: y2, r: 4 },
|
|
116
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
117
|
+
z: 100,
|
|
118
|
+
});
|
|
119
|
+
children.push({
|
|
120
|
+
type: 'circle', name: 'point-2',
|
|
121
|
+
shape: { cx: x3, cy: y3, r: 4 },
|
|
122
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
123
|
+
z: 100,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Vertex labels
|
|
127
|
+
const labels = ['1', '2', '3'];
|
|
128
|
+
const points = [pixelPoints[0], pixelPoints[1], pixelPoints[2]];
|
|
129
|
+
for (let i = 0; i < 3; i++) {
|
|
130
|
+
const [px, py] = points[i];
|
|
131
|
+
const isHigh = (i === 0 || py <= points[i - 1][1]) && (i === 2 || py <= points[i + 1]?.[1]);
|
|
132
|
+
children.push({
|
|
133
|
+
type: 'text',
|
|
134
|
+
style: { text: labels[i], x: px, y: isHigh ? py - 14 : py + 16, fill: '#e2e8f0', fontSize: 12, fontWeight: 'bold', align: 'center', verticalAlign: 'middle' },
|
|
135
|
+
silent: true,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { type: 'group', children };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as echarts from 'echarts';
|
|
2
|
+
import { AbstractPlugin } from '../../components/AbstractPlugin';
|
|
3
|
+
import { FibTrendExtensionDrawingRenderer } from './FibTrendExtensionDrawingRenderer';
|
|
4
|
+
|
|
5
|
+
const LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.272, 1.618, 2, 2.618];
|
|
6
|
+
const COLORS = [
|
|
7
|
+
'#787b86', '#f44336', '#ff9800', '#4caf50', '#2196f3',
|
|
8
|
+
'#00bcd4', '#787b86', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export class FibTrendExtensionTool extends AbstractPlugin {
|
|
12
|
+
private points: number[][] = [];
|
|
13
|
+
private state: 'idle' | 'drawing-trend' | 'drawing-retracement' | 'finished' = 'idle';
|
|
14
|
+
private graphicGroup: any = null;
|
|
15
|
+
|
|
16
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
17
|
+
super({
|
|
18
|
+
id: 'fib-trend-extension-tool',
|
|
19
|
+
name: options.name || 'Fib Trend Extension',
|
|
20
|
+
icon: options.icon || `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M120-80v-80h720v80H120Zm0-160v-80h720v80H120Zm0-160v-80h720v80H120Zm0-160v-80h720v80H120Zm0-160v-80h720v80H120Zm0-160v-80h720v80H120Z"/></svg>`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected onInit(): void {
|
|
25
|
+
this.context.registerDrawingRenderer(new FibTrendExtensionDrawingRenderer());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected onActivate(): void {
|
|
29
|
+
this.state = 'idle';
|
|
30
|
+
this.points = [];
|
|
31
|
+
this.context.getChart().getZr().setCursorStyle('crosshair');
|
|
32
|
+
this.bindEvents();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected onDeactivate(): void {
|
|
36
|
+
this.state = 'idle';
|
|
37
|
+
this.points = [];
|
|
38
|
+
this.removeGraphic();
|
|
39
|
+
this.unbindEvents();
|
|
40
|
+
this.context.getChart().getZr().setCursorStyle('default');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private bindEvents() {
|
|
44
|
+
const zr = this.context.getChart().getZr();
|
|
45
|
+
zr.on('click', this.onClick);
|
|
46
|
+
zr.on('mousemove', this.onMouseMove);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private unbindEvents() {
|
|
50
|
+
const zr = this.context.getChart().getZr();
|
|
51
|
+
zr.off('click', this.onClick);
|
|
52
|
+
zr.off('mousemove', this.onMouseMove);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private onClick = (params: any) => {
|
|
56
|
+
const pt = this.getPoint(params);
|
|
57
|
+
|
|
58
|
+
if (this.state === 'idle') {
|
|
59
|
+
this.state = 'drawing-trend';
|
|
60
|
+
this.points = [pt, [...pt]];
|
|
61
|
+
this.initGraphic();
|
|
62
|
+
this.updateGraphic();
|
|
63
|
+
} else if (this.state === 'drawing-trend') {
|
|
64
|
+
this.state = 'drawing-retracement';
|
|
65
|
+
this.points[1] = pt;
|
|
66
|
+
this.points.push([...pt]);
|
|
67
|
+
this.updateGraphic();
|
|
68
|
+
} else if (this.state === 'drawing-retracement') {
|
|
69
|
+
this.state = 'finished';
|
|
70
|
+
this.points[2] = pt;
|
|
71
|
+
this.updateGraphic();
|
|
72
|
+
this.saveDrawing();
|
|
73
|
+
this.removeGraphic();
|
|
74
|
+
this.context.disableTools();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
private onMouseMove = (params: any) => {
|
|
79
|
+
if (this.state === 'drawing-trend') {
|
|
80
|
+
this.points[1] = this.getPoint(params);
|
|
81
|
+
this.updateGraphic();
|
|
82
|
+
} else if (this.state === 'drawing-retracement') {
|
|
83
|
+
this.points[2] = this.getPoint(params);
|
|
84
|
+
this.updateGraphic();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
private initGraphic() {
|
|
89
|
+
this.graphicGroup = new echarts.graphic.Group();
|
|
90
|
+
this.context.getChart().getZr().add(this.graphicGroup);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private removeGraphic() {
|
|
94
|
+
if (this.graphicGroup) {
|
|
95
|
+
this.context.getChart().getZr().remove(this.graphicGroup);
|
|
96
|
+
this.graphicGroup = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private updateGraphic() {
|
|
101
|
+
if (!this.graphicGroup) return;
|
|
102
|
+
this.graphicGroup.removeAll();
|
|
103
|
+
|
|
104
|
+
const [x1, y1] = this.points[0];
|
|
105
|
+
const [x2, y2] = this.points[1];
|
|
106
|
+
|
|
107
|
+
// Trend line
|
|
108
|
+
this.graphicGroup.add(new echarts.graphic.Line({
|
|
109
|
+
shape: { x1, y1, x2, y2 },
|
|
110
|
+
style: { stroke: '#2196f3', lineWidth: 1.5, lineDash: [5, 4] },
|
|
111
|
+
silent: true,
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
if (this.points.length >= 3) {
|
|
115
|
+
const [x3, y3] = this.points[2];
|
|
116
|
+
|
|
117
|
+
// Retracement line
|
|
118
|
+
this.graphicGroup.add(new echarts.graphic.Line({
|
|
119
|
+
shape: { x1: x2, y1: y2, x2: x3, y2: y3 },
|
|
120
|
+
style: { stroke: '#ff9800', lineWidth: 1.5, lineDash: [5, 4] },
|
|
121
|
+
silent: true,
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
// Extension levels
|
|
125
|
+
const trendPixelDy = y2 - y1;
|
|
126
|
+
const minX = Math.min(x1, x2, x3);
|
|
127
|
+
const maxX = Math.max(x1, x2, x3);
|
|
128
|
+
const extraWidth = (maxX - minX) * 0.5;
|
|
129
|
+
const lineLeft = minX;
|
|
130
|
+
const lineRight = maxX + extraWidth;
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < LEVELS.length; i++) {
|
|
133
|
+
const level = LEVELS[i];
|
|
134
|
+
const ly = y3 + trendPixelDy * level;
|
|
135
|
+
const lColor = COLORS[i % COLORS.length];
|
|
136
|
+
|
|
137
|
+
this.graphicGroup.add(new echarts.graphic.Line({
|
|
138
|
+
shape: { x1: lineLeft, y1: ly, x2: lineRight, y2: ly },
|
|
139
|
+
style: { stroke: lColor, lineWidth: 1 },
|
|
140
|
+
silent: true,
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
this.graphicGroup.add(new echarts.graphic.Text({
|
|
144
|
+
style: { text: `${level}`, x: lineRight + 4, y: ly - 6, fill: lColor, fontSize: 9 },
|
|
145
|
+
silent: true,
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
// Fill between levels
|
|
149
|
+
if (i < LEVELS.length - 1) {
|
|
150
|
+
const nextLy = y3 + trendPixelDy * LEVELS[i + 1];
|
|
151
|
+
const rectY = Math.min(ly, nextLy);
|
|
152
|
+
const rectH = Math.abs(nextLy - ly);
|
|
153
|
+
this.graphicGroup.add(new echarts.graphic.Rect({
|
|
154
|
+
shape: { x: lineLeft, y: rectY, width: lineRight - lineLeft, height: rectH },
|
|
155
|
+
style: { fill: COLORS[(i + 1) % COLORS.length], opacity: 0.06 },
|
|
156
|
+
silent: true,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Point circles
|
|
163
|
+
for (const pt of this.points) {
|
|
164
|
+
this.graphicGroup.add(new echarts.graphic.Circle({
|
|
165
|
+
shape: { cx: pt[0], cy: pt[1], r: 4 },
|
|
166
|
+
style: { fill: '#fff', stroke: '#3b82f6', lineWidth: 1.5 },
|
|
167
|
+
z: 101,
|
|
168
|
+
silent: true,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private saveDrawing() {
|
|
174
|
+
const dataPoints = this.points.map((pt) =>
|
|
175
|
+
this.context.coordinateConversion.pixelToData({ x: pt[0], y: pt[1] }),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (dataPoints.every((p) => p !== null)) {
|
|
179
|
+
this.context.addDrawing({
|
|
180
|
+
id: `fib-ext-${Date.now()}`,
|
|
181
|
+
type: 'fib_trend_extension',
|
|
182
|
+
points: dataPoints as any[],
|
|
183
|
+
paneIndex: dataPoints[0]!.paneIndex || 0,
|
|
184
|
+
style: { color: '#3b82f6', lineWidth: 1 },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { DrawingRenderer, DrawingRenderContext } from '../../types';
|
|
2
|
+
|
|
3
|
+
const LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
|
|
4
|
+
const COLORS = ['#787b86', '#f44336', '#ff9800', '#4caf50', '#2196f3', '#00bcd4', '#787b86'];
|
|
5
|
+
|
|
6
|
+
export class FibonacciChannelDrawingRenderer implements DrawingRenderer {
|
|
7
|
+
type = 'fibonacci_channel';
|
|
8
|
+
|
|
9
|
+
render(ctx: DrawingRenderContext): any {
|
|
10
|
+
const { drawing, pixelPoints, isSelected } = ctx;
|
|
11
|
+
const [x1, y1] = pixelPoints[0];
|
|
12
|
+
const [x2, y2] = pixelPoints[1];
|
|
13
|
+
const [wx, wy] = pixelPoints[2];
|
|
14
|
+
const color = drawing.style?.color || '#3b82f6';
|
|
15
|
+
|
|
16
|
+
// Compute perpendicular offset from baseline to width point
|
|
17
|
+
const bdx = x2 - x1;
|
|
18
|
+
const bdy = y2 - y1;
|
|
19
|
+
const blen = Math.sqrt(bdx * bdx + bdy * bdy);
|
|
20
|
+
if (blen === 0) return;
|
|
21
|
+
|
|
22
|
+
// Normal vector (perpendicular to baseline)
|
|
23
|
+
const nx = -bdy / blen;
|
|
24
|
+
const ny = bdx / blen;
|
|
25
|
+
|
|
26
|
+
// Signed distance from baseline to width point along normal
|
|
27
|
+
const dist = (wx - x1) * nx + (wy - y1) * ny;
|
|
28
|
+
|
|
29
|
+
const children: any[] = [];
|
|
30
|
+
const levelCoords: { lx1: number; ly1: number; lx2: number; ly2: number }[] = [];
|
|
31
|
+
|
|
32
|
+
LEVELS.forEach((level, index) => {
|
|
33
|
+
const ox = nx * dist * level;
|
|
34
|
+
const oy = ny * dist * level;
|
|
35
|
+
|
|
36
|
+
const lx1 = x1 + ox;
|
|
37
|
+
const ly1 = y1 + oy;
|
|
38
|
+
const lx2 = x2 + ox;
|
|
39
|
+
const ly2 = y2 + oy;
|
|
40
|
+
|
|
41
|
+
levelCoords.push({ lx1, ly1, lx2, ly2 });
|
|
42
|
+
|
|
43
|
+
// Fill between this level and the next
|
|
44
|
+
if (index < LEVELS.length - 1) {
|
|
45
|
+
const nextLevel = LEVELS[index + 1];
|
|
46
|
+
const nox = nx * dist * nextLevel;
|
|
47
|
+
const noy = ny * dist * nextLevel;
|
|
48
|
+
|
|
49
|
+
children.push({
|
|
50
|
+
type: 'polygon',
|
|
51
|
+
name: 'line', // Enable dragging by clicking background
|
|
52
|
+
shape: {
|
|
53
|
+
points: [
|
|
54
|
+
[lx1, ly1],
|
|
55
|
+
[lx2, ly2],
|
|
56
|
+
[x2 + nox, y2 + noy],
|
|
57
|
+
[x1 + nox, y1 + noy],
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
style: {
|
|
61
|
+
fill: COLORS[(index + 1) % COLORS.length],
|
|
62
|
+
opacity: 0.1,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Level lines and labels on top of fills
|
|
69
|
+
levelCoords.forEach((coords, index) => {
|
|
70
|
+
const levelColor = COLORS[index % COLORS.length];
|
|
71
|
+
|
|
72
|
+
children.push({
|
|
73
|
+
type: 'line',
|
|
74
|
+
shape: { x1: coords.lx1, y1: coords.ly1, x2: coords.lx2, y2: coords.ly2 },
|
|
75
|
+
style: { stroke: levelColor, lineWidth: 1 },
|
|
76
|
+
silent: true,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
children.push({
|
|
80
|
+
type: 'text',
|
|
81
|
+
style: {
|
|
82
|
+
text: `${LEVELS[index]}`,
|
|
83
|
+
x: coords.lx2 + 5,
|
|
84
|
+
y: coords.ly2 - 5,
|
|
85
|
+
fill: levelColor,
|
|
86
|
+
fontSize: 10,
|
|
87
|
+
},
|
|
88
|
+
silent: true,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Baseline (dashed)
|
|
93
|
+
children.push({
|
|
94
|
+
type: 'line',
|
|
95
|
+
name: 'line',
|
|
96
|
+
shape: { x1, y1, x2, y2 },
|
|
97
|
+
style: { stroke: '#999', lineWidth: 1, lineDash: [4, 4] },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Control points
|
|
101
|
+
children.push({
|
|
102
|
+
type: 'circle',
|
|
103
|
+
name: 'point-0',
|
|
104
|
+
shape: { cx: x1, cy: y1, r: 4 },
|
|
105
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
106
|
+
z: 100,
|
|
107
|
+
});
|
|
108
|
+
children.push({
|
|
109
|
+
type: 'circle',
|
|
110
|
+
name: 'point-1',
|
|
111
|
+
shape: { cx: x2, cy: y2, r: 4 },
|
|
112
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
113
|
+
z: 100,
|
|
114
|
+
});
|
|
115
|
+
children.push({
|
|
116
|
+
type: 'circle',
|
|
117
|
+
name: 'point-2',
|
|
118
|
+
shape: { cx: wx, cy: wy, r: 4 },
|
|
119
|
+
style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
|
|
120
|
+
z: 100,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
type: 'group',
|
|
125
|
+
children,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import * as echarts from "echarts";
|
|
2
|
+
import { AbstractPlugin } from "../../components/AbstractPlugin";
|
|
3
|
+
import { FibonacciChannelDrawingRenderer } from "./FibonacciChannelDrawingRenderer";
|
|
4
|
+
|
|
5
|
+
export class FibonacciChannelTool extends AbstractPlugin {
|
|
6
|
+
private startPoint: number[] | null = null;
|
|
7
|
+
private endPoint: number[] | null = null;
|
|
8
|
+
private widthPoint: number[] | null = null;
|
|
9
|
+
private state: "idle" | "drawing-baseline" | "drawing-width" | "finished" =
|
|
10
|
+
"idle";
|
|
11
|
+
|
|
12
|
+
// Temporary ZRender elements
|
|
13
|
+
private graphicGroup: any = null;
|
|
14
|
+
|
|
15
|
+
// Fib levels config
|
|
16
|
+
private readonly levels = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
|
|
17
|
+
private readonly colors = [
|
|
18
|
+
"#787b86", // 0
|
|
19
|
+
"#f44336", // 0.236
|
|
20
|
+
"#ff9800", // 0.382
|
|
21
|
+
"#4caf50", // 0.5
|
|
22
|
+
"#2196f3", // 0.618
|
|
23
|
+
"#00bcd4", // 0.786
|
|
24
|
+
"#787b86", // 1
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
constructor(options: { name?: string; icon?: string } = {}) {
|
|
28
|
+
super({
|
|
29
|
+
id: "fibonacci-channel-tool",
|
|
30
|
+
name: options.name || "Fibonacci Channel",
|
|
31
|
+
icon:
|
|
32
|
+
options.icon ||
|
|
33
|
+
`<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M120-200v-80l80-80H120v-80h160l120-120H120v-80h360l120-120H120v-80h720v80H520l-120 120h440v80H320L200-440h640v80H280l-80 80h640v80H120Z"/></svg>`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected onInit(): void {
|
|
38
|
+
this.context.registerDrawingRenderer(new FibonacciChannelDrawingRenderer());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public onActivate(): void {
|
|
42
|
+
this.state = "idle";
|
|
43
|
+
this.startPoint = null;
|
|
44
|
+
this.endPoint = null;
|
|
45
|
+
this.widthPoint = null;
|
|
46
|
+
this.context.getChart().getZr().setCursorStyle("crosshair");
|
|
47
|
+
this.bindEvents();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public onDeactivate(): void {
|
|
51
|
+
this.state = "idle";
|
|
52
|
+
this.startPoint = null;
|
|
53
|
+
this.endPoint = null;
|
|
54
|
+
this.widthPoint = null;
|
|
55
|
+
this.removeGraphic();
|
|
56
|
+
this.unbindEvents();
|
|
57
|
+
this.context.getChart().getZr().setCursorStyle("default");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private bindEvents() {
|
|
61
|
+
const zr = this.context.getChart().getZr();
|
|
62
|
+
zr.on("click", this.onClick);
|
|
63
|
+
zr.on("mousemove", this.onMouseMove);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private unbindEvents() {
|
|
67
|
+
const zr = this.context.getChart().getZr();
|
|
68
|
+
zr.off("click", this.onClick);
|
|
69
|
+
zr.off("mousemove", this.onMouseMove);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private onClick = (params: any) => {
|
|
73
|
+
if (this.state === "idle") {
|
|
74
|
+
this.state = "drawing-baseline";
|
|
75
|
+
this.startPoint = this.getPoint(params);
|
|
76
|
+
this.endPoint = this.getPoint(params);
|
|
77
|
+
this.initGraphic();
|
|
78
|
+
this.updateGraphic();
|
|
79
|
+
} else if (this.state === "drawing-baseline") {
|
|
80
|
+
this.state = "drawing-width";
|
|
81
|
+
this.endPoint = this.getPoint(params);
|
|
82
|
+
this.widthPoint = this.getPoint(params);
|
|
83
|
+
this.updateGraphic();
|
|
84
|
+
} else if (this.state === "drawing-width") {
|
|
85
|
+
this.state = "finished";
|
|
86
|
+
this.widthPoint = this.getPoint(params);
|
|
87
|
+
this.updateGraphic();
|
|
88
|
+
this.saveDrawing();
|
|
89
|
+
|
|
90
|
+
this.removeGraphic();
|
|
91
|
+
this.context.disableTools();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
private onMouseMove = (params: any) => {
|
|
96
|
+
if (this.state === "drawing-baseline") {
|
|
97
|
+
this.endPoint = this.getPoint(params);
|
|
98
|
+
this.updateGraphic();
|
|
99
|
+
} else if (this.state === "drawing-width") {
|
|
100
|
+
this.widthPoint = this.getPoint(params);
|
|
101
|
+
this.updateGraphic();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
private initGraphic() {
|
|
106
|
+
this.graphicGroup = new echarts.graphic.Group();
|
|
107
|
+
this.context.getChart().getZr().add(this.graphicGroup);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private removeGraphic() {
|
|
111
|
+
if (this.graphicGroup) {
|
|
112
|
+
this.context.getChart().getZr().remove(this.graphicGroup);
|
|
113
|
+
this.graphicGroup = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private updateGraphic() {
|
|
118
|
+
if (!this.graphicGroup || !this.startPoint || !this.endPoint) return;
|
|
119
|
+
this.graphicGroup.removeAll();
|
|
120
|
+
|
|
121
|
+
const x1 = this.startPoint[0];
|
|
122
|
+
const y1 = this.startPoint[1];
|
|
123
|
+
const x2 = this.endPoint[0];
|
|
124
|
+
const y2 = this.endPoint[1];
|
|
125
|
+
|
|
126
|
+
// Baseline
|
|
127
|
+
this.graphicGroup.add(
|
|
128
|
+
new echarts.graphic.Line({
|
|
129
|
+
shape: { x1, y1, x2, y2 },
|
|
130
|
+
style: { stroke: "#787b86", lineWidth: 2 },
|
|
131
|
+
silent: true,
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// If we have a width point, draw the channel levels
|
|
136
|
+
if (this.widthPoint && this.state !== "drawing-baseline") {
|
|
137
|
+
const wp = this.widthPoint;
|
|
138
|
+
|
|
139
|
+
const dx = x2 - x1;
|
|
140
|
+
const dy = y2 - y1;
|
|
141
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
142
|
+
if (len === 0) return;
|
|
143
|
+
|
|
144
|
+
const nx = -dy / len;
|
|
145
|
+
const ny = dx / len;
|
|
146
|
+
|
|
147
|
+
const dist = (wp[0] - x1) * nx + (wp[1] - y1) * ny;
|
|
148
|
+
|
|
149
|
+
this.levels.forEach((level, index) => {
|
|
150
|
+
const offsetX = nx * dist * level;
|
|
151
|
+
const offsetY = ny * dist * level;
|
|
152
|
+
|
|
153
|
+
const lx1 = x1 + offsetX;
|
|
154
|
+
const ly1 = y1 + offsetY;
|
|
155
|
+
const lx2 = x2 + offsetX;
|
|
156
|
+
const ly2 = y2 + offsetY;
|
|
157
|
+
|
|
158
|
+
const color = this.colors[index % this.colors.length];
|
|
159
|
+
|
|
160
|
+
this.graphicGroup.add(
|
|
161
|
+
new echarts.graphic.Line({
|
|
162
|
+
shape: { x1: lx1, y1: ly1, x2: lx2, y2: ly2 },
|
|
163
|
+
style: { stroke: color, lineWidth: 1 },
|
|
164
|
+
silent: true,
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (index < this.levels.length - 1) {
|
|
169
|
+
const nextLevel = this.levels[index + 1];
|
|
170
|
+
const nOffsetX = nx * dist * nextLevel;
|
|
171
|
+
const nOffsetY = ny * dist * nextLevel;
|
|
172
|
+
|
|
173
|
+
const nx1 = x1 + nOffsetX;
|
|
174
|
+
const ny1 = y1 + nOffsetY;
|
|
175
|
+
const nx2 = x2 + nOffsetX;
|
|
176
|
+
const ny2 = y2 + nOffsetY;
|
|
177
|
+
|
|
178
|
+
this.graphicGroup.add(
|
|
179
|
+
new echarts.graphic.Polygon({
|
|
180
|
+
shape: {
|
|
181
|
+
points: [
|
|
182
|
+
[lx1, ly1],
|
|
183
|
+
[lx2, ly2],
|
|
184
|
+
[nx2, ny2],
|
|
185
|
+
[nx1, ny1],
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
style: {
|
|
189
|
+
fill: this.colors[(index + 1) % this.colors.length],
|
|
190
|
+
opacity: 0.1,
|
|
191
|
+
},
|
|
192
|
+
silent: true,
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private saveDrawing() {
|
|
201
|
+
if (!this.startPoint || !this.endPoint || !this.widthPoint) return;
|
|
202
|
+
|
|
203
|
+
const start = this.context.coordinateConversion.pixelToData({
|
|
204
|
+
x: this.startPoint[0],
|
|
205
|
+
y: this.startPoint[1],
|
|
206
|
+
});
|
|
207
|
+
const end = this.context.coordinateConversion.pixelToData({
|
|
208
|
+
x: this.endPoint[0],
|
|
209
|
+
y: this.endPoint[1],
|
|
210
|
+
});
|
|
211
|
+
const width = this.context.coordinateConversion.pixelToData({
|
|
212
|
+
x: this.widthPoint[0],
|
|
213
|
+
y: this.widthPoint[1],
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (start && end && width) {
|
|
217
|
+
const paneIndex = start.paneIndex || 0;
|
|
218
|
+
|
|
219
|
+
this.context.addDrawing({
|
|
220
|
+
id: `fib-channel-${Date.now()}`,
|
|
221
|
+
type: "fibonacci_channel",
|
|
222
|
+
points: [start, end, width],
|
|
223
|
+
paneIndex: paneIndex,
|
|
224
|
+
style: {
|
|
225
|
+
color: "#3b82f6",
|
|
226
|
+
lineWidth: 1,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|