@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.
Files changed (59) hide show
  1. package/dist/index.d.ts +368 -14
  2. package/dist/qfchart.min.browser.js +34 -16
  3. package/dist/qfchart.min.es.js +34 -16
  4. package/package.json +1 -1
  5. package/src/QFChart.ts +460 -311
  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 +284 -263
  10. package/src/components/LayoutManager.ts +72 -55
  11. package/src/components/SeriesBuilder.ts +110 -6
  12. package/src/components/TableCanvasRenderer.ts +467 -0
  13. package/src/components/TableOverlayRenderer.ts +38 -9
  14. package/src/components/TooltipFormatter.ts +97 -97
  15. package/src/components/renderers/BackgroundRenderer.ts +59 -47
  16. package/src/components/renderers/BoxRenderer.ts +113 -17
  17. package/src/components/renderers/FillRenderer.ts +118 -3
  18. package/src/components/renderers/LabelRenderer.ts +35 -9
  19. package/src/components/renderers/OHLCBarRenderer.ts +171 -161
  20. package/src/components/renderers/PolylineRenderer.ts +26 -19
  21. package/src/index.ts +17 -6
  22. package/src/plugins/ABCDPatternTool/ABCDPatternDrawingRenderer.ts +112 -0
  23. package/src/plugins/ABCDPatternTool/ABCDPatternTool.ts +136 -0
  24. package/src/plugins/ABCDPatternTool/index.ts +2 -0
  25. package/src/plugins/CypherPatternTool/CypherPatternDrawingRenderer.ts +80 -0
  26. package/src/plugins/CypherPatternTool/CypherPatternTool.ts +84 -0
  27. package/src/plugins/CypherPatternTool/index.ts +2 -0
  28. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanDrawingRenderer.ts +163 -0
  29. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanTool.ts +210 -0
  30. package/src/plugins/FibSpeedResistanceFanTool/index.ts +2 -0
  31. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionDrawingRenderer.ts +141 -0
  32. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionTool.ts +188 -0
  33. package/src/plugins/FibTrendExtensionTool/index.ts +2 -0
  34. package/src/plugins/FibonacciChannelTool/FibonacciChannelDrawingRenderer.ts +128 -0
  35. package/src/plugins/FibonacciChannelTool/FibonacciChannelTool.ts +231 -0
  36. package/src/plugins/FibonacciChannelTool/index.ts +2 -0
  37. package/src/plugins/FibonacciTool/FibonacciDrawingRenderer.ts +107 -0
  38. package/src/plugins/{FibonacciTool.ts → FibonacciTool/FibonacciTool.ts} +195 -192
  39. package/src/plugins/FibonacciTool/index.ts +2 -0
  40. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersDrawingRenderer.ts +95 -0
  41. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersTool.ts +97 -0
  42. package/src/plugins/HeadAndShouldersTool/index.ts +2 -0
  43. package/src/plugins/LineTool/LineDrawingRenderer.ts +49 -0
  44. package/src/plugins/{LineTool.ts → LineTool/LineTool.ts} +161 -190
  45. package/src/plugins/LineTool/index.ts +2 -0
  46. package/src/plugins/{MeasureTool.ts → MeasureTool/MeasureTool.ts} +324 -344
  47. package/src/plugins/MeasureTool/index.ts +1 -0
  48. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternDrawingRenderer.ts +106 -0
  49. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternTool.ts +98 -0
  50. package/src/plugins/ThreeDrivesPatternTool/index.ts +2 -0
  51. package/src/plugins/ToolGroup.ts +211 -0
  52. package/src/plugins/TrianglePatternTool/TrianglePatternDrawingRenderer.ts +107 -0
  53. package/src/plugins/TrianglePatternTool/TrianglePatternTool.ts +98 -0
  54. package/src/plugins/TrianglePatternTool/index.ts +2 -0
  55. package/src/plugins/XABCDPatternTool/XABCDPatternDrawingRenderer.ts +178 -0
  56. package/src/plugins/XABCDPatternTool/XABCDPatternTool.ts +213 -0
  57. package/src/plugins/XABCDPatternTool/index.ts +2 -0
  58. package/src/types.ts +39 -4
  59. package/src/utils/ColorUtils.ts +1 -1
@@ -0,0 +1,136 @@
1
+ import * as echarts from 'echarts';
2
+ import { AbstractPlugin } from '../../components/AbstractPlugin';
3
+ import { ABCDPatternDrawingRenderer } from './ABCDPatternDrawingRenderer';
4
+
5
+ const LABELS = ['A', 'B', 'C', 'D'];
6
+ const LEG_COLORS = ['#2196f3', '#ff9800', '#4caf50'];
7
+ const TOTAL_POINTS = 4;
8
+
9
+ export class ABCDPatternTool extends AbstractPlugin {
10
+ private points: number[][] = [];
11
+ private state: 'idle' | 'drawing' | 'finished' = 'idle';
12
+ private graphicGroup: any = null;
13
+
14
+ constructor(options: { name?: string; icon?: string } = {}) {
15
+ super({
16
+ id: 'abcd-pattern-tool',
17
+ name: options.name || 'ABCD Pattern',
18
+ icon: options.icon || `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#e3e3e3" stroke-width="1.5"><polyline points="3,18 8,5 15,15 21,3"/><circle cx="3" cy="18" r="1.5" fill="#e3e3e3"/><circle cx="8" cy="5" r="1.5" fill="#e3e3e3"/><circle cx="15" cy="15" r="1.5" fill="#e3e3e3"/><circle cx="21" cy="3" r="1.5" fill="#e3e3e3"/></svg>`,
19
+ });
20
+ }
21
+
22
+ protected onInit(): void {
23
+ this.context.registerDrawingRenderer(new ABCDPatternDrawingRenderer());
24
+ }
25
+
26
+ protected onActivate(): void {
27
+ this.state = 'idle';
28
+ this.points = [];
29
+ this.context.getChart().getZr().setCursorStyle('crosshair');
30
+ const zr = this.context.getChart().getZr();
31
+ zr.on('click', this.onClick);
32
+ zr.on('mousemove', this.onMouseMove);
33
+ }
34
+
35
+ protected onDeactivate(): void {
36
+ this.state = 'idle';
37
+ this.points = [];
38
+ this.removeGraphic();
39
+ const zr = this.context.getChart().getZr();
40
+ zr.off('click', this.onClick);
41
+ zr.off('mousemove', this.onMouseMove);
42
+ this.context.getChart().getZr().setCursorStyle('default');
43
+ }
44
+
45
+ private onClick = (params: any) => {
46
+ const pt = this.getPoint(params);
47
+ if (this.state === 'idle') {
48
+ this.state = 'drawing';
49
+ this.points = [pt, [...pt]];
50
+ this.initGraphic();
51
+ this.updateGraphic();
52
+ } else if (this.state === 'drawing') {
53
+ this.points[this.points.length - 1] = pt;
54
+ if (this.points.length >= TOTAL_POINTS) {
55
+ this.state = 'finished';
56
+ this.updateGraphic();
57
+ this.saveDrawing();
58
+ this.removeGraphic();
59
+ this.context.disableTools();
60
+ } else {
61
+ this.points.push([...pt]);
62
+ this.updateGraphic();
63
+ }
64
+ }
65
+ };
66
+
67
+ private onMouseMove = (params: any) => {
68
+ if (this.state !== 'drawing' || this.points.length < 2) return;
69
+ this.points[this.points.length - 1] = this.getPoint(params);
70
+ this.updateGraphic();
71
+ };
72
+
73
+ private initGraphic() {
74
+ this.graphicGroup = new echarts.graphic.Group();
75
+ this.context.getChart().getZr().add(this.graphicGroup);
76
+ }
77
+
78
+ private removeGraphic() {
79
+ if (this.graphicGroup) {
80
+ this.context.getChart().getZr().remove(this.graphicGroup);
81
+ this.graphicGroup = null;
82
+ }
83
+ }
84
+
85
+ private updateGraphic() {
86
+ if (!this.graphicGroup) return;
87
+ this.graphicGroup.removeAll();
88
+ const pts = this.points;
89
+
90
+ // Fills
91
+ if (pts.length >= 3) {
92
+ this.graphicGroup.add(new echarts.graphic.Polygon({ shape: { points: pts.slice(0, 3) }, style: { fill: 'rgba(33,150,243,0.08)' }, silent: true }));
93
+ }
94
+ if (pts.length >= 4) {
95
+ this.graphicGroup.add(new echarts.graphic.Polygon({ shape: { points: pts.slice(1, 4) }, style: { fill: 'rgba(244,67,54,0.08)' }, silent: true }));
96
+ }
97
+
98
+ // Legs
99
+ for (let i = 0; i < pts.length - 1; i++) {
100
+ this.graphicGroup.add(new echarts.graphic.Line({
101
+ shape: { x1: pts[i][0], y1: pts[i][1], x2: pts[i + 1][0], y2: pts[i + 1][1] },
102
+ style: { stroke: LEG_COLORS[i % LEG_COLORS.length], lineWidth: 2 },
103
+ silent: true,
104
+ }));
105
+ }
106
+
107
+ // Dashed connectors
108
+ if (pts.length >= 3) {
109
+ this.graphicGroup.add(new echarts.graphic.Line({ shape: { x1: pts[0][0], y1: pts[0][1], x2: pts[2][0], y2: pts[2][1] }, style: { stroke: '#555', lineWidth: 1, lineDash: [4, 4] }, silent: true }));
110
+ }
111
+ if (pts.length >= 4) {
112
+ this.graphicGroup.add(new echarts.graphic.Line({ shape: { x1: pts[1][0], y1: pts[1][1], x2: pts[3][0], y2: pts[3][1] }, style: { stroke: '#555', lineWidth: 1, lineDash: [4, 4] }, silent: true }));
113
+ }
114
+
115
+ // Labels & circles
116
+ for (let i = 0; i < pts.length && i < LABELS.length; i++) {
117
+ const [px, py] = pts[i];
118
+ const isHigh = (i === 0 || py <= pts[i - 1][1]) && (i === pts.length - 1 || py <= pts[i + 1]?.[1]);
119
+ 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 }));
120
+ 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 }));
121
+ }
122
+ }
123
+
124
+ private saveDrawing() {
125
+ const dataPoints = this.points.map((pt) => this.context.coordinateConversion.pixelToData({ x: pt[0], y: pt[1] }));
126
+ if (dataPoints.every((p) => p !== null)) {
127
+ this.context.addDrawing({
128
+ id: `abcd-${Date.now()}`,
129
+ type: 'abcd_pattern',
130
+ points: dataPoints as any[],
131
+ paneIndex: dataPoints[0]!.paneIndex || 0,
132
+ style: { color: '#3b82f6', lineWidth: 2 },
133
+ });
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,2 @@
1
+ export { ABCDPatternTool } from './ABCDPatternTool';
2
+ export { ABCDPatternDrawingRenderer } from './ABCDPatternDrawingRenderer';
@@ -0,0 +1,80 @@
1
+ import { DrawingRenderer, DrawingRenderContext } from '../../types';
2
+
3
+ const LABELS = ['X', 'A', 'B', 'C', 'D'];
4
+ const LEG_COLORS = ['#00bcd4', '#e91e63', '#8bc34a', '#ff5722'];
5
+
6
+ export class CypherPatternDrawingRenderer implements DrawingRenderer {
7
+ type = 'cypher_pattern';
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 triangles XAB and BCD
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(0, 188, 212, 0.08)' } });
19
+ }
20
+ if (pixelPoints.length >= 5) {
21
+ children.push({ type: 'polygon', name: 'line', shape: { points: pixelPoints.slice(2, 5).map(([x, y]) => [x, y]) }, style: { fill: 'rgba(233, 30, 99, 0.08)' } });
22
+ }
23
+
24
+ // Legs
25
+ for (let i = 0; i < pixelPoints.length - 1; i++) {
26
+ const [x1, y1] = pixelPoints[i];
27
+ const [x2, y2] = pixelPoints[i + 1];
28
+ children.push({ type: 'line', name: 'line', shape: { x1, y1, x2, y2 }, style: { stroke: LEG_COLORS[i % LEG_COLORS.length], lineWidth: drawing.style?.lineWidth || 2 } });
29
+ }
30
+
31
+ // Dashed connectors X→B, X→C, A→D
32
+ const connectors: [number, number][] = [[0, 2], [0, 3], [1, 4]];
33
+ for (const [from, to] of connectors) {
34
+ if (from < pixelPoints.length && to < pixelPoints.length) {
35
+ children.push({ type: 'line', shape: { x1: pixelPoints[from][0], y1: pixelPoints[from][1], x2: pixelPoints[to][0], y2: pixelPoints[to][1] }, style: { stroke: '#555', lineWidth: 1, lineDash: [4, 4] }, silent: true });
36
+ }
37
+ }
38
+
39
+ // Ratios
40
+ const pts = drawing.points;
41
+ if (pts.length >= 3) {
42
+ const xa = Math.abs(pts[1].value - pts[0].value);
43
+ const ab = Math.abs(pts[2].value - pts[1].value);
44
+ if (xa !== 0) {
45
+ const r = (ab / xa).toFixed(3);
46
+ children.push({ type: 'text', style: { text: r, x: (pixelPoints[1][0] + pixelPoints[2][0]) / 2 + 8, y: (pixelPoints[1][1] + pixelPoints[2][1]) / 2, fill: '#e91e63', fontSize: 10 }, silent: true });
47
+ }
48
+ }
49
+ if (pts.length >= 4) {
50
+ const xa = Math.abs(pts[1].value - pts[0].value);
51
+ const xc = Math.abs(pts[3].value - pts[0].value);
52
+ if (xa !== 0) {
53
+ const r = (xc / xa).toFixed(3);
54
+ children.push({ type: 'text', style: { text: `XC/XA: ${r}`, x: (pixelPoints[0][0] + pixelPoints[3][0]) / 2 + 8, y: (pixelPoints[0][1] + pixelPoints[3][1]) / 2, fill: '#8bc34a', fontSize: 10 }, silent: true });
55
+ }
56
+ }
57
+ if (pts.length >= 5) {
58
+ const xc = Math.abs(pts[3].value - pts[0].value);
59
+ const cd = Math.abs(pts[4].value - pts[3].value);
60
+ if (xc !== 0) {
61
+ const r = (cd / xc).toFixed(3);
62
+ children.push({ type: 'text', style: { text: r, x: (pixelPoints[3][0] + pixelPoints[4][0]) / 2 + 8, y: (pixelPoints[3][1] + pixelPoints[4][1]) / 2, fill: '#ff5722', fontSize: 10 }, silent: true });
63
+ }
64
+ }
65
+
66
+ // Labels
67
+ for (let i = 0; i < pixelPoints.length && i < LABELS.length; i++) {
68
+ const [px, py] = pixelPoints[i];
69
+ const isHigh = (i === 0 || py <= pixelPoints[i - 1][1]) && (i === pixelPoints.length - 1 || py <= pixelPoints[i + 1]?.[1]);
70
+ children.push({ type: '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 });
71
+ }
72
+
73
+ // Control points
74
+ for (let i = 0; i < pixelPoints.length; i++) {
75
+ children.push({ type: 'circle', name: `point-${i}`, shape: { cx: pixelPoints[i][0], cy: pixelPoints[i][1], r: 4 }, style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 }, z: 100 });
76
+ }
77
+
78
+ return { type: 'group', children };
79
+ }
80
+ }
@@ -0,0 +1,84 @@
1
+ import * as echarts from 'echarts';
2
+ import { AbstractPlugin } from '../../components/AbstractPlugin';
3
+ import { CypherPatternDrawingRenderer } from './CypherPatternDrawingRenderer';
4
+
5
+ const LABELS = ['X', 'A', 'B', 'C', 'D'];
6
+ const LEG_COLORS = ['#00bcd4', '#e91e63', '#8bc34a', '#ff5722'];
7
+ const TOTAL_POINTS = 5;
8
+
9
+ export class CypherPatternTool extends AbstractPlugin {
10
+ private points: number[][] = [];
11
+ private state: 'idle' | 'drawing' | 'finished' = 'idle';
12
+ private graphicGroup: any = null;
13
+
14
+ constructor(options: { name?: string; icon?: string } = {}) {
15
+ super({
16
+ id: 'cypher-pattern-tool',
17
+ name: options.name || 'Cypher Pattern',
18
+ icon: options.icon || `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#e3e3e3" stroke-width="1.5"><polyline points="2,16 7,4 11,12 17,2 22,14"/><circle cx="2" cy="16" r="1.5" fill="#e3e3e3"/><circle cx="7" cy="4" r="1.5" fill="#e3e3e3"/><circle cx="11" cy="12" r="1.5" fill="#e3e3e3"/><circle cx="17" cy="2" r="1.5" fill="#e3e3e3"/><circle cx="22" cy="14" r="1.5" fill="#e3e3e3"/></svg>`,
19
+ });
20
+ }
21
+
22
+ protected onInit(): void { this.context.registerDrawingRenderer(new CypherPatternDrawingRenderer()); }
23
+
24
+ protected onActivate(): void {
25
+ this.state = 'idle'; this.points = [];
26
+ this.context.getChart().getZr().setCursorStyle('crosshair');
27
+ const zr = this.context.getChart().getZr();
28
+ zr.on('click', this.onClick); zr.on('mousemove', this.onMouseMove);
29
+ }
30
+
31
+ protected onDeactivate(): void {
32
+ this.state = 'idle'; this.points = []; this.removeGraphic();
33
+ const zr = this.context.getChart().getZr();
34
+ zr.off('click', this.onClick); zr.off('mousemove', this.onMouseMove);
35
+ zr.setCursorStyle('default');
36
+ }
37
+
38
+ private onClick = (params: any) => {
39
+ const pt = this.getPoint(params);
40
+ if (this.state === 'idle') {
41
+ this.state = 'drawing'; this.points = [pt, [...pt]]; this.initGraphic(); this.updateGraphic();
42
+ } else if (this.state === 'drawing') {
43
+ this.points[this.points.length - 1] = pt;
44
+ if (this.points.length >= TOTAL_POINTS) {
45
+ this.state = 'finished'; this.updateGraphic(); this.saveDrawing(); this.removeGraphic(); this.context.disableTools();
46
+ } else { this.points.push([...pt]); this.updateGraphic(); }
47
+ }
48
+ };
49
+
50
+ private onMouseMove = (params: any) => {
51
+ if (this.state !== 'drawing' || this.points.length < 2) return;
52
+ this.points[this.points.length - 1] = this.getPoint(params); this.updateGraphic();
53
+ };
54
+
55
+ private initGraphic() { this.graphicGroup = new echarts.graphic.Group(); this.context.getChart().getZr().add(this.graphicGroup); }
56
+ private removeGraphic() { if (this.graphicGroup) { this.context.getChart().getZr().remove(this.graphicGroup); this.graphicGroup = null; } }
57
+
58
+ private updateGraphic() {
59
+ if (!this.graphicGroup) return;
60
+ this.graphicGroup.removeAll();
61
+ const pts = this.points;
62
+
63
+ if (pts.length >= 3) this.graphicGroup.add(new echarts.graphic.Polygon({ shape: { points: pts.slice(0, 3) }, style: { fill: 'rgba(0,188,212,0.08)' }, silent: true }));
64
+ if (pts.length >= 5) this.graphicGroup.add(new echarts.graphic.Polygon({ shape: { points: pts.slice(2, 5) }, style: { fill: 'rgba(233,30,99,0.08)' }, silent: true }));
65
+
66
+ for (let i = 0; i < pts.length - 1; i++) {
67
+ 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: LEG_COLORS[i % LEG_COLORS.length], lineWidth: 2 }, silent: true }));
68
+ }
69
+
70
+ for (let i = 0; i < pts.length && i < LABELS.length; i++) {
71
+ const [px, py] = pts[i];
72
+ const isHigh = (i === 0 || py <= pts[i - 1][1]) && (i === pts.length - 1 || py <= pts[i + 1]?.[1]);
73
+ 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 }));
74
+ 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 }));
75
+ }
76
+ }
77
+
78
+ private saveDrawing() {
79
+ const dataPoints = this.points.map((pt) => this.context.coordinateConversion.pixelToData({ x: pt[0], y: pt[1] }));
80
+ if (dataPoints.every((p) => p !== null)) {
81
+ this.context.addDrawing({ id: `cypher-${Date.now()}`, type: 'cypher_pattern', points: dataPoints as any[], paneIndex: dataPoints[0]!.paneIndex || 0, style: { color: '#3b82f6', lineWidth: 2 } });
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,2 @@
1
+ export { CypherPatternTool } from './CypherPatternTool';
2
+ export { CypherPatternDrawingRenderer } from './CypherPatternDrawingRenderer';
@@ -0,0 +1,163 @@
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 FibSpeedResistanceFanDrawingRenderer implements DrawingRenderer {
7
+ type = 'fib_speed_resistance_fan';
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 color = drawing.style?.color || '#3b82f6';
14
+
15
+ const dx = x2 - x1;
16
+ const dy = y2 - y1;
17
+
18
+ const children: any[] = [];
19
+
20
+ // Compute fan ray endpoints for each level
21
+ // Price rays: start → (x1 + dx, y1 + dy * level)
22
+ // Time rays: start → (x1 + dx * level, y1 + dy)
23
+ const priceRays: [number, number][] = [];
24
+ const timeRays: [number, number][] = [];
25
+
26
+ for (const level of LEVELS) {
27
+ priceRays.push([x1 + dx, y1 + dy * level]);
28
+ timeRays.push([x1 + dx * level, y1 + dy]);
29
+ }
30
+
31
+ // Fill zones between adjacent price rays
32
+ for (let i = 0; i < priceRays.length - 1; i++) {
33
+ children.push({
34
+ type: 'polygon',
35
+ name: 'line',
36
+ shape: {
37
+ points: [
38
+ [x1, y1],
39
+ priceRays[i],
40
+ priceRays[i + 1],
41
+ ],
42
+ },
43
+ style: {
44
+ fill: COLORS[(i + 1) % COLORS.length],
45
+ opacity: 0.06,
46
+ },
47
+ });
48
+ }
49
+
50
+ // Fill zones between adjacent time rays
51
+ for (let i = 0; i < timeRays.length - 1; i++) {
52
+ children.push({
53
+ type: 'polygon',
54
+ name: 'line',
55
+ shape: {
56
+ points: [
57
+ [x1, y1],
58
+ timeRays[i],
59
+ timeRays[i + 1],
60
+ ],
61
+ },
62
+ style: {
63
+ fill: COLORS[(i + 1) % COLORS.length],
64
+ opacity: 0.06,
65
+ },
66
+ });
67
+ }
68
+
69
+ // Draw price ray lines
70
+ LEVELS.forEach((level, index) => {
71
+ const [ex, ey] = priceRays[index];
72
+ const levelColor = COLORS[index % COLORS.length];
73
+
74
+ children.push({
75
+ type: 'line',
76
+ shape: { x1, y1, x2: ex, y2: ey },
77
+ style: { stroke: levelColor, lineWidth: 1 },
78
+ silent: true,
79
+ });
80
+
81
+ children.push({
82
+ type: 'text',
83
+ style: {
84
+ text: `${level}`,
85
+ x: ex + 3,
86
+ y: ey - 2,
87
+ fill: levelColor,
88
+ fontSize: 9,
89
+ },
90
+ silent: true,
91
+ });
92
+ });
93
+
94
+ // Draw time ray lines
95
+ LEVELS.forEach((level, index) => {
96
+ const [ex, ey] = timeRays[index];
97
+ const levelColor = COLORS[index % COLORS.length];
98
+
99
+ children.push({
100
+ type: 'line',
101
+ shape: { x1, y1, x2: ex, y2: ey },
102
+ style: { stroke: levelColor, lineWidth: 1 },
103
+ silent: true,
104
+ });
105
+
106
+ // Label on the bottom/right end
107
+ children.push({
108
+ type: 'text',
109
+ style: {
110
+ text: `${level}`,
111
+ x: ex - 2,
112
+ y: ey + 8,
113
+ fill: levelColor,
114
+ fontSize: 9,
115
+ },
116
+ silent: true,
117
+ });
118
+ });
119
+
120
+ // Bounding box edges (dashed)
121
+ children.push({
122
+ type: 'line',
123
+ name: 'line',
124
+ shape: { x1: x2, y1, x2, y2 },
125
+ style: { stroke: '#555', lineWidth: 1, lineDash: [3, 3] },
126
+ });
127
+ children.push({
128
+ type: 'line',
129
+ name: 'line',
130
+ shape: { x1, y1: y2, x2, y2 },
131
+ style: { stroke: '#555', lineWidth: 1, lineDash: [3, 3] },
132
+ });
133
+
134
+ // Diagonal (start to end)
135
+ children.push({
136
+ type: 'line',
137
+ name: 'line',
138
+ shape: { x1, y1, x2, y2 },
139
+ style: { stroke: '#999', lineWidth: 1, lineDash: [4, 4] },
140
+ });
141
+
142
+ // Control points
143
+ children.push({
144
+ type: 'circle',
145
+ name: 'point-0',
146
+ shape: { cx: x1, cy: y1, r: 4 },
147
+ style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
148
+ z: 100,
149
+ });
150
+ children.push({
151
+ type: 'circle',
152
+ name: 'point-1',
153
+ shape: { cx: x2, cy: y2, r: 4 },
154
+ style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
155
+ z: 100,
156
+ });
157
+
158
+ return {
159
+ type: 'group',
160
+ children,
161
+ };
162
+ }
163
+ }
@@ -0,0 +1,210 @@
1
+ import * as echarts from 'echarts';
2
+ import { AbstractPlugin } from '../../components/AbstractPlugin';
3
+ import { FibSpeedResistanceFanDrawingRenderer } from './FibSpeedResistanceFanDrawingRenderer';
4
+
5
+ const LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
6
+ const COLORS = ['#787b86', '#f44336', '#ff9800', '#4caf50', '#2196f3', '#00bcd4', '#787b86'];
7
+
8
+ export class FibSpeedResistanceFanTool extends AbstractPlugin {
9
+ private startPoint: number[] | null = null;
10
+ private endPoint: number[] | null = null;
11
+ private state: 'idle' | 'drawing' | 'finished' = 'idle';
12
+
13
+ // Temporary ZRender elements
14
+ private graphicGroup: any = null;
15
+
16
+ constructor(options: { name?: string; icon?: string } = {}) {
17
+ super({
18
+ id: 'fib-speed-resistance-fan-tool',
19
+ name: options.name || 'Fib Speed Resistance Fan',
20
+ icon:
21
+ options.icon ||
22
+ `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#e3e3e3"><path d="M2 21L22 3M2 21l20-6M2 21l20-9M2 21l20-12M2 21l20-15M2 21l6-18M2 21l9-18M2 21l12-18M2 21l15-18" stroke="#e3e3e3" stroke-width="1" fill="none"/></svg>`,
23
+ });
24
+ }
25
+
26
+ protected onInit(): void {
27
+ this.context.registerDrawingRenderer(new FibSpeedResistanceFanDrawingRenderer());
28
+ }
29
+
30
+ protected onActivate(): void {
31
+ this.state = 'idle';
32
+ this.startPoint = null;
33
+ this.endPoint = null;
34
+ this.context.getChart().getZr().setCursorStyle('crosshair');
35
+ this.bindEvents();
36
+ }
37
+
38
+ protected onDeactivate(): void {
39
+ this.state = 'idle';
40
+ this.startPoint = null;
41
+ this.endPoint = null;
42
+ this.removeGraphic();
43
+ this.unbindEvents();
44
+ this.context.getChart().getZr().setCursorStyle('default');
45
+ }
46
+
47
+ private bindEvents() {
48
+ const zr = this.context.getChart().getZr();
49
+ zr.on('click', this.onClick);
50
+ zr.on('mousemove', this.onMouseMove);
51
+ }
52
+
53
+ private unbindEvents() {
54
+ const zr = this.context.getChart().getZr();
55
+ zr.off('click', this.onClick);
56
+ zr.off('mousemove', this.onMouseMove);
57
+ }
58
+
59
+ private onClick = (params: any) => {
60
+ if (this.state === 'idle') {
61
+ this.state = 'drawing';
62
+ this.startPoint = this.getPoint(params);
63
+ this.endPoint = this.getPoint(params);
64
+ this.initGraphic();
65
+ this.updateGraphic();
66
+ } else if (this.state === 'drawing') {
67
+ this.state = 'finished';
68
+ this.endPoint = this.getPoint(params);
69
+ this.updateGraphic();
70
+ this.saveDrawing();
71
+
72
+ this.removeGraphic();
73
+ this.context.disableTools();
74
+ }
75
+ };
76
+
77
+ private onMouseMove = (params: any) => {
78
+ if (this.state === 'drawing') {
79
+ this.endPoint = this.getPoint(params);
80
+ this.updateGraphic();
81
+ }
82
+ };
83
+
84
+ private initGraphic() {
85
+ this.graphicGroup = new echarts.graphic.Group();
86
+ this.context.getChart().getZr().add(this.graphicGroup);
87
+ }
88
+
89
+ private removeGraphic() {
90
+ if (this.graphicGroup) {
91
+ this.context.getChart().getZr().remove(this.graphicGroup);
92
+ this.graphicGroup = null;
93
+ }
94
+ }
95
+
96
+ private updateGraphic() {
97
+ if (!this.graphicGroup || !this.startPoint || !this.endPoint) return;
98
+ this.graphicGroup.removeAll();
99
+
100
+ const x1 = this.startPoint[0];
101
+ const y1 = this.startPoint[1];
102
+ const x2 = this.endPoint[0];
103
+ const y2 = this.endPoint[1];
104
+
105
+ const dx = x2 - x1;
106
+ const dy = y2 - y1;
107
+
108
+ // Price rays and time rays
109
+ LEVELS.forEach((level, index) => {
110
+ const color = COLORS[index % COLORS.length];
111
+
112
+ // Price ray: start → (x1 + dx, y1 + dy * level)
113
+ this.graphicGroup.add(
114
+ new echarts.graphic.Line({
115
+ shape: { x1, y1, x2: x1 + dx, y2: y1 + dy * level },
116
+ style: { stroke: color, lineWidth: 1 },
117
+ silent: true,
118
+ }),
119
+ );
120
+
121
+ // Time ray: start → (x1 + dx * level, y1 + dy)
122
+ this.graphicGroup.add(
123
+ new echarts.graphic.Line({
124
+ shape: { x1, y1, x2: x1 + dx * level, y2: y1 + dy },
125
+ style: { stroke: color, lineWidth: 1 },
126
+ silent: true,
127
+ }),
128
+ );
129
+ });
130
+
131
+ // Fill between adjacent price rays
132
+ for (let i = 0; i < LEVELS.length - 1; i++) {
133
+ const pr1: [number, number] = [x1 + dx, y1 + dy * LEVELS[i]];
134
+ const pr2: [number, number] = [x1 + dx, y1 + dy * LEVELS[i + 1]];
135
+
136
+ this.graphicGroup.add(
137
+ new echarts.graphic.Polygon({
138
+ shape: { points: [[x1, y1], pr1, pr2] },
139
+ style: { fill: COLORS[(i + 1) % COLORS.length], opacity: 0.06 },
140
+ silent: true,
141
+ }),
142
+ );
143
+ }
144
+
145
+ // Fill between adjacent time rays
146
+ for (let i = 0; i < LEVELS.length - 1; i++) {
147
+ const tr1: [number, number] = [x1 + dx * LEVELS[i], y1 + dy];
148
+ const tr2: [number, number] = [x1 + dx * LEVELS[i + 1], y1 + dy];
149
+
150
+ this.graphicGroup.add(
151
+ new echarts.graphic.Polygon({
152
+ shape: { points: [[x1, y1], tr1, tr2] },
153
+ style: { fill: COLORS[(i + 1) % COLORS.length], opacity: 0.06 },
154
+ silent: true,
155
+ }),
156
+ );
157
+ }
158
+
159
+ // Bounding box edges
160
+ this.graphicGroup.add(
161
+ new echarts.graphic.Line({
162
+ shape: { x1: x2, y1, x2, y2 },
163
+ style: { stroke: '#555', lineWidth: 1, lineDash: [3, 3] },
164
+ silent: true,
165
+ }),
166
+ );
167
+ this.graphicGroup.add(
168
+ new echarts.graphic.Line({
169
+ shape: { x1, y1: y2, x2, y2 },
170
+ style: { stroke: '#555', lineWidth: 1, lineDash: [3, 3] },
171
+ silent: true,
172
+ }),
173
+ );
174
+
175
+ // Diagonal
176
+ this.graphicGroup.add(
177
+ new echarts.graphic.Line({
178
+ shape: { x1, y1, x2, y2 },
179
+ style: { stroke: '#999', lineWidth: 1, lineDash: [4, 4] },
180
+ silent: true,
181
+ }),
182
+ );
183
+ }
184
+
185
+ private saveDrawing() {
186
+ if (!this.startPoint || !this.endPoint) return;
187
+
188
+ const start = this.context.coordinateConversion.pixelToData({
189
+ x: this.startPoint[0],
190
+ y: this.startPoint[1],
191
+ });
192
+ const end = this.context.coordinateConversion.pixelToData({
193
+ x: this.endPoint[0],
194
+ y: this.endPoint[1],
195
+ });
196
+
197
+ if (start && end) {
198
+ this.context.addDrawing({
199
+ id: `fib-fan-${Date.now()}`,
200
+ type: 'fib_speed_resistance_fan',
201
+ points: [start, end],
202
+ paneIndex: start.paneIndex || 0,
203
+ style: {
204
+ color: '#3b82f6',
205
+ lineWidth: 1,
206
+ },
207
+ });
208
+ }
209
+ }
210
+ }
@@ -0,0 +1,2 @@
1
+ export { FibSpeedResistanceFanTool } from './FibSpeedResistanceFanTool';
2
+ export { FibSpeedResistanceFanDrawingRenderer } from './FibSpeedResistanceFanDrawingRenderer';