@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.
Files changed (50) hide show
  1. package/dist/index.d.ts +319 -12
  2. package/dist/qfchart.min.browser.js +32 -16
  3. package/dist/qfchart.min.es.js +32 -16
  4. package/package.json +1 -1
  5. package/src/QFChart.ts +98 -262
  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 +2 -2
  10. package/src/components/LayoutManager.ts +41 -35
  11. package/src/components/SeriesBuilder.ts +10 -10
  12. package/src/components/TooltipFormatter.ts +1 -1
  13. package/src/index.ts +17 -6
  14. package/src/plugins/ABCDPatternTool/ABCDPatternDrawingRenderer.ts +112 -0
  15. package/src/plugins/ABCDPatternTool/ABCDPatternTool.ts +136 -0
  16. package/src/plugins/ABCDPatternTool/index.ts +2 -0
  17. package/src/plugins/CypherPatternTool/CypherPatternDrawingRenderer.ts +80 -0
  18. package/src/plugins/CypherPatternTool/CypherPatternTool.ts +84 -0
  19. package/src/plugins/CypherPatternTool/index.ts +2 -0
  20. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanDrawingRenderer.ts +163 -0
  21. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanTool.ts +210 -0
  22. package/src/plugins/FibSpeedResistanceFanTool/index.ts +2 -0
  23. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionDrawingRenderer.ts +141 -0
  24. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionTool.ts +188 -0
  25. package/src/plugins/FibTrendExtensionTool/index.ts +2 -0
  26. package/src/plugins/FibonacciChannelTool/FibonacciChannelDrawingRenderer.ts +128 -0
  27. package/src/plugins/FibonacciChannelTool/FibonacciChannelTool.ts +231 -0
  28. package/src/plugins/FibonacciChannelTool/index.ts +2 -0
  29. package/src/plugins/FibonacciTool/FibonacciDrawingRenderer.ts +107 -0
  30. package/src/plugins/{FibonacciTool.ts → FibonacciTool/FibonacciTool.ts} +195 -192
  31. package/src/plugins/FibonacciTool/index.ts +2 -0
  32. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersDrawingRenderer.ts +95 -0
  33. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersTool.ts +97 -0
  34. package/src/plugins/HeadAndShouldersTool/index.ts +2 -0
  35. package/src/plugins/LineTool/LineDrawingRenderer.ts +49 -0
  36. package/src/plugins/{LineTool.ts → LineTool/LineTool.ts} +161 -190
  37. package/src/plugins/LineTool/index.ts +2 -0
  38. package/src/plugins/{MeasureTool.ts → MeasureTool/MeasureTool.ts} +324 -344
  39. package/src/plugins/MeasureTool/index.ts +1 -0
  40. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternDrawingRenderer.ts +106 -0
  41. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternTool.ts +98 -0
  42. package/src/plugins/ThreeDrivesPatternTool/index.ts +2 -0
  43. package/src/plugins/ToolGroup.ts +211 -0
  44. package/src/plugins/TrianglePatternTool/TrianglePatternDrawingRenderer.ts +107 -0
  45. package/src/plugins/TrianglePatternTool/TrianglePatternTool.ts +98 -0
  46. package/src/plugins/TrianglePatternTool/index.ts +2 -0
  47. package/src/plugins/XABCDPatternTool/XABCDPatternDrawingRenderer.ts +178 -0
  48. package/src/plugins/XABCDPatternTool/XABCDPatternTool.ts +213 -0
  49. package/src/plugins/XABCDPatternTool/index.ts +2 -0
  50. package/src/types.ts +37 -11
@@ -0,0 +1 @@
1
+ export { MeasureTool } from './MeasureTool';
@@ -0,0 +1,106 @@
1
+ import { DrawingRenderer, DrawingRenderContext } from '../../types';
2
+
3
+ // Points: 0=start, 1=drive1, 2=correction1, 3=drive2, 4=correction2, 5=drive3, 6=end
4
+ const LABELS = ['0', 'D1', 'C1', 'D2', 'C2', 'D3', ''];
5
+ const LEG_COLORS = ['#2196f3', '#ff9800', '#4caf50', '#f44336', '#00bcd4', '#e91e63'];
6
+
7
+ export class ThreeDrivesPatternDrawingRenderer implements DrawingRenderer {
8
+ type = 'three_drives_pattern';
9
+
10
+ render(ctx: DrawingRenderContext): any {
11
+ const { drawing, pixelPoints, isSelected } = ctx;
12
+ const color = drawing.style?.color || '#3b82f6';
13
+ if (pixelPoints.length < 2) return;
14
+
15
+ const children: any[] = [];
16
+
17
+ // Fill drive regions
18
+ // Drive 1 zone (0,1,2)
19
+ if (pixelPoints.length >= 3) {
20
+ 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)' } });
21
+ }
22
+ // Drive 2 zone (2,3,4)
23
+ if (pixelPoints.length >= 5) {
24
+ children.push({ type: 'polygon', name: 'line', shape: { points: pixelPoints.slice(2, 5).map(([x, y]) => [x, y]) }, style: { fill: 'rgba(76, 175, 80, 0.06)' } });
25
+ }
26
+ // Drive 3 zone (4,5,6)
27
+ if (pixelPoints.length >= 7) {
28
+ children.push({ type: 'polygon', name: 'line', shape: { points: pixelPoints.slice(4, 7).map(([x, y]) => [x, y]) }, style: { fill: 'rgba(0, 188, 212, 0.06)' } });
29
+ }
30
+
31
+ // Zigzag legs
32
+ for (let i = 0; i < pixelPoints.length - 1; i++) {
33
+ const [x1, y1] = pixelPoints[i];
34
+ const [x2, y2] = pixelPoints[i + 1];
35
+ children.push({
36
+ type: 'line', name: 'line',
37
+ shape: { x1, y1, x2, y2 },
38
+ style: { stroke: LEG_COLORS[i % LEG_COLORS.length], lineWidth: drawing.style?.lineWidth || 2 },
39
+ });
40
+ }
41
+
42
+ // Dashed lines connecting drives (1→3, 3→5) and corrections (2→4)
43
+ const connectors: [number, number][] = [[1, 3], [3, 5], [2, 4]];
44
+ for (const [from, to] of connectors) {
45
+ if (from < pixelPoints.length && to < pixelPoints.length) {
46
+ children.push({
47
+ type: 'line',
48
+ shape: { x1: pixelPoints[from][0], y1: pixelPoints[from][1], x2: pixelPoints[to][0], y2: pixelPoints[to][1] },
49
+ style: { stroke: '#555', lineWidth: 1, lineDash: [4, 4] },
50
+ silent: true,
51
+ });
52
+ }
53
+ }
54
+
55
+ // Ratios between drives
56
+ const pts = drawing.points;
57
+ // Drive2/Drive1
58
+ if (pts.length >= 4) {
59
+ const d1 = Math.abs(pts[1].value - pts[0].value);
60
+ const d2 = Math.abs(pts[3].value - pts[2].value);
61
+ if (d1 !== 0) {
62
+ const r = (d2 / d1).toFixed(3);
63
+ const mx = (pixelPoints[2][0] + pixelPoints[3][0]) / 2;
64
+ const my = (pixelPoints[2][1] + pixelPoints[3][1]) / 2;
65
+ children.push({ type: 'text', style: { text: `D2/D1: ${r}`, x: mx + 10, y: my, fill: '#4caf50', fontSize: 9 }, silent: true });
66
+ }
67
+ }
68
+ // Drive3/Drive2
69
+ if (pts.length >= 6) {
70
+ const d2 = Math.abs(pts[3].value - pts[2].value);
71
+ const d3 = Math.abs(pts[5].value - pts[4].value);
72
+ if (d2 !== 0) {
73
+ const r = (d3 / d2).toFixed(3);
74
+ const mx = (pixelPoints[4][0] + pixelPoints[5][0]) / 2;
75
+ const my = (pixelPoints[4][1] + pixelPoints[5][1]) / 2;
76
+ children.push({ type: 'text', style: { text: `D3/D2: ${r}`, x: mx + 10, y: my, fill: '#00bcd4', fontSize: 9 }, silent: true });
77
+ }
78
+ }
79
+ // Correction1/Drive1
80
+ if (pts.length >= 3) {
81
+ const d1 = Math.abs(pts[1].value - pts[0].value);
82
+ const c1 = Math.abs(pts[2].value - pts[1].value);
83
+ if (d1 !== 0) {
84
+ const r = (c1 / d1).toFixed(3);
85
+ const mx = (pixelPoints[1][0] + pixelPoints[2][0]) / 2;
86
+ const my = (pixelPoints[1][1] + pixelPoints[2][1]) / 2;
87
+ children.push({ type: 'text', style: { text: r, x: mx + 8, y: my, fill: '#ff9800', fontSize: 10 }, silent: true });
88
+ }
89
+ }
90
+
91
+ // Labels
92
+ for (let i = 0; i < pixelPoints.length && i < LABELS.length; i++) {
93
+ if (!LABELS[i]) continue;
94
+ const [px, py] = pixelPoints[i];
95
+ const isHigh = (i === 0 || py <= pixelPoints[i - 1][1]) && (i === pixelPoints.length - 1 || py <= pixelPoints[i + 1]?.[1]);
96
+ children.push({ type: 'text', style: { text: LABELS[i], x: px, y: isHigh ? py - 14 : py + 16, fill: '#e2e8f0', fontSize: 11, fontWeight: 'bold', align: 'center', verticalAlign: 'middle' }, silent: true });
97
+ }
98
+
99
+ // Control points
100
+ for (let i = 0; i < pixelPoints.length; i++) {
101
+ 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 });
102
+ }
103
+
104
+ return { type: 'group', children };
105
+ }
106
+ }
@@ -0,0 +1,98 @@
1
+ import * as echarts from 'echarts';
2
+ import { AbstractPlugin } from '../../components/AbstractPlugin';
3
+ import { ThreeDrivesPatternDrawingRenderer } from './ThreeDrivesPatternDrawingRenderer';
4
+
5
+ const LABELS = ['0', 'D1', 'C1', 'D2', 'C2', 'D3', ''];
6
+ const LEG_COLORS = ['#2196f3', '#ff9800', '#4caf50', '#f44336', '#00bcd4', '#e91e63'];
7
+ const TOTAL_POINTS = 7;
8
+
9
+ export class ThreeDrivesPatternTool 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: 'three-drives-pattern-tool',
17
+ name: options.name || 'Three Drives',
18
+ icon: options.icon || `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#e3e3e3" stroke-width="1.5"><polyline points="1,20 4,8 7,14 11,5 15,12 19,2 23,10"/></svg>`,
19
+ });
20
+ }
21
+
22
+ protected onInit(): void { this.context.registerDrawingRenderer(new ThreeDrivesPatternDrawingRenderer()); }
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
+ // Fills
64
+ 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 }));
65
+ if (pts.length >= 5) this.graphicGroup.add(new echarts.graphic.Polygon({ shape: { points: pts.slice(2, 5) }, style: { fill: 'rgba(76,175,80,0.06)' }, silent: true }));
66
+ if (pts.length >= 7) this.graphicGroup.add(new echarts.graphic.Polygon({ shape: { points: pts.slice(4, 7) }, style: { fill: 'rgba(0,188,212,0.06)' }, silent: true }));
67
+
68
+ // Zigzag
69
+ for (let i = 0; i < pts.length - 1; i++) {
70
+ 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 }));
71
+ }
72
+
73
+ // Dashed connectors
74
+ const conn: [number, number][] = [[1, 3], [3, 5], [2, 4]];
75
+ for (const [f, t] of conn) {
76
+ if (f < pts.length && t < pts.length) {
77
+ this.graphicGroup.add(new echarts.graphic.Line({ shape: { x1: pts[f][0], y1: pts[f][1], x2: pts[t][0], y2: pts[t][1] }, style: { stroke: '#555', lineWidth: 1, lineDash: [4, 4] }, silent: true }));
78
+ }
79
+ }
80
+
81
+ // Labels & circles
82
+ for (let i = 0; i < pts.length && i < LABELS.length; i++) {
83
+ const [px, py] = pts[i];
84
+ const isHigh = (i === 0 || py <= pts[i - 1][1]) && (i === pts.length - 1 || py <= pts[i + 1]?.[1]);
85
+ if (LABELS[i]) {
86
+ this.graphicGroup.add(new echarts.graphic.Text({ style: { text: LABELS[i], x: px, y: isHigh ? py - 14 : py + 16, fill: '#e2e8f0', fontSize: 11, fontWeight: 'bold', align: 'center', verticalAlign: 'middle' }, silent: true }));
87
+ }
88
+ 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 }));
89
+ }
90
+ }
91
+
92
+ private saveDrawing() {
93
+ const dataPoints = this.points.map((pt) => this.context.coordinateConversion.pixelToData({ x: pt[0], y: pt[1] }));
94
+ if (dataPoints.every((p) => p !== null)) {
95
+ this.context.addDrawing({ id: `3drives-${Date.now()}`, type: 'three_drives_pattern', points: dataPoints as any[], paneIndex: dataPoints[0]!.paneIndex || 0, style: { color: '#3b82f6', lineWidth: 2 } });
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,2 @@
1
+ export { ThreeDrivesPatternTool } from './ThreeDrivesPatternTool';
2
+ export { ThreeDrivesPatternDrawingRenderer } from './ThreeDrivesPatternDrawingRenderer';
@@ -0,0 +1,211 @@
1
+ import { ChartContext, Plugin, PluginConfig } from '../types';
2
+ import { AbstractPlugin } from '../components/AbstractPlugin';
3
+
4
+ export interface ToolGroupConfig extends Omit<PluginConfig, 'id'> {
5
+ id?: string;
6
+ name: string;
7
+ icon?: string;
8
+ }
9
+
10
+ export class ToolGroup extends AbstractPlugin {
11
+ private plugins: Plugin[] = [];
12
+ private activeSubPlugin: Plugin | null = null;
13
+ private menuElement: HTMLElement | null = null;
14
+ private buttonElement: HTMLElement | null = null;
15
+ private originalIcon: string = '';
16
+ private arrowSvg: string = '';
17
+
18
+ constructor(config: ToolGroupConfig) {
19
+ // Create a small right-facing chevron arrow to indicate a dropdown menu
20
+ const arrowSvg = `<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="position: absolute; right: -4px; top: 50%; transform: translateY(-50%); opacity: 0.6;"><polyline points="9 18 15 12 9 6"></polyline></svg>`;
21
+
22
+ let enhancedIcon = '';
23
+ if (config.icon) {
24
+ enhancedIcon = `<div style="position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
25
+ <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
26
+ ${config.icon}
27
+ </div>
28
+ ${arrowSvg}
29
+ </div>`;
30
+ } else {
31
+ enhancedIcon = `<div style="position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
32
+ <span>${config.name.substring(0, 2).toUpperCase()}</span>
33
+ ${arrowSvg}
34
+ </div>`;
35
+ }
36
+
37
+ super({
38
+ id: config.id || `group-${config.name.toLowerCase().replace(/\s+/g, '-')}`,
39
+ name: config.name,
40
+ icon: enhancedIcon
41
+ });
42
+
43
+ this.originalIcon = enhancedIcon;
44
+ this.arrowSvg = arrowSvg;
45
+ }
46
+
47
+ public add(plugin: Plugin): void {
48
+ this.plugins.push(plugin);
49
+ }
50
+
51
+ protected onInit(): void {
52
+ this.plugins.forEach(p => p.init(this.context));
53
+ }
54
+
55
+ protected onActivate(): void {
56
+ this.showMenu();
57
+ }
58
+
59
+ protected onDeactivate(): void {
60
+ this.hideMenu();
61
+ if (this.activeSubPlugin) {
62
+ this.activeSubPlugin.deactivate?.();
63
+ this.activeSubPlugin = null;
64
+ }
65
+
66
+ // Restore original icon
67
+ if (this.buttonElement) {
68
+ this.buttonElement.innerHTML = this.originalIcon;
69
+ }
70
+ }
71
+
72
+ protected onDestroy(): void {
73
+ this.hideMenu();
74
+ this.plugins.forEach(p => p.destroy?.());
75
+ }
76
+
77
+ private showMenu(): void {
78
+ this.buttonElement = document.getElementById(`qfchart-plugin-btn-${this.id}`);
79
+ if (!this.buttonElement) return;
80
+
81
+ if (this.menuElement) {
82
+ this.hideMenu();
83
+ }
84
+
85
+ this.menuElement = document.createElement('div');
86
+ Object.assign(this.menuElement.style, {
87
+ position: 'fixed',
88
+ backgroundColor: '#1e293b',
89
+ border: '1px solid #334155',
90
+ borderRadius: '6px',
91
+ padding: '4px',
92
+ display: 'flex',
93
+ flexDirection: 'column',
94
+ gap: '2px',
95
+ zIndex: '10000',
96
+ boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.3)',
97
+ minWidth: '150px'
98
+ });
99
+
100
+ this.plugins.forEach(plugin => {
101
+ const item = document.createElement('div');
102
+ Object.assign(item.style, {
103
+ display: 'flex',
104
+ alignItems: 'center',
105
+ padding: '8px 12px',
106
+ cursor: 'pointer',
107
+ color: '#cbd5e1',
108
+ borderRadius: '4px',
109
+ fontSize: '13px',
110
+ fontFamily: this.context.getOptions().fontFamily || 'sans-serif',
111
+ transition: 'background-color 0.2s'
112
+ });
113
+
114
+ item.addEventListener('mouseenter', () => {
115
+ item.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
116
+ });
117
+ item.addEventListener('mouseleave', () => {
118
+ item.style.backgroundColor = 'transparent';
119
+ });
120
+
121
+ if (plugin.icon) {
122
+ const iconContainer = document.createElement('div');
123
+ iconContainer.innerHTML = plugin.icon;
124
+ Object.assign(iconContainer.style, {
125
+ width: '20px',
126
+ height: '20px',
127
+ marginRight: '10px',
128
+ display: 'flex',
129
+ alignItems: 'center',
130
+ justifyContent: 'center'
131
+ });
132
+ const svg = iconContainer.querySelector('svg');
133
+ if (svg) {
134
+ svg.style.width = '100%';
135
+ svg.style.height = '100%';
136
+ }
137
+ item.appendChild(iconContainer);
138
+ }
139
+
140
+ const nameSpan = document.createElement('span');
141
+ nameSpan.textContent = plugin.name || plugin.id;
142
+ item.appendChild(nameSpan);
143
+
144
+ item.addEventListener('click', (e) => {
145
+ e.stopPropagation();
146
+ this.activateSubPlugin(plugin);
147
+ });
148
+
149
+ this.menuElement!.appendChild(item);
150
+ });
151
+
152
+ document.body.appendChild(this.menuElement);
153
+
154
+ const rect = this.buttonElement.getBoundingClientRect();
155
+ this.menuElement.style.top = `${rect.top}px`;
156
+ this.menuElement.style.left = `${rect.right + 5}px`;
157
+
158
+ // Delay attaching the outside click listener so it doesn't fire on the current click
159
+ setTimeout(() => {
160
+ document.addEventListener('click', this.handleOutsideClick);
161
+ }, 0);
162
+ }
163
+
164
+ private hideMenu(): void {
165
+ if (this.menuElement && this.menuElement.parentNode) {
166
+ this.menuElement.parentNode.removeChild(this.menuElement);
167
+ }
168
+ this.menuElement = null;
169
+ document.removeEventListener('click', this.handleOutsideClick);
170
+ }
171
+
172
+ private handleOutsideClick = (e: MouseEvent): void => {
173
+ if (this.menuElement && !this.menuElement.contains(e.target as Node)) {
174
+ this.hideMenu();
175
+ if (!this.activeSubPlugin) {
176
+ // If clicked outside and no sub-plugin is active, deactivate the group
177
+ this.buttonElement?.click();
178
+ }
179
+ }
180
+ };
181
+
182
+ private activateSubPlugin(plugin: Plugin): void {
183
+ this.hideMenu();
184
+
185
+ if (this.activeSubPlugin) {
186
+ this.activeSubPlugin.deactivate?.();
187
+ }
188
+
189
+ this.activeSubPlugin = plugin;
190
+ this.activeSubPlugin.activate?.();
191
+
192
+ // Update the group's button icon to match the active plugin
193
+ if (this.buttonElement) {
194
+ let subIcon = '';
195
+ if (plugin.icon) {
196
+ subIcon = `<div style="position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
197
+ <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
198
+ ${plugin.icon}
199
+ </div>
200
+ ${this.arrowSvg}
201
+ </div>`;
202
+ } else {
203
+ subIcon = `<div style="position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
204
+ <span>${(plugin.name || plugin.id).substring(0, 2).toUpperCase()}</span>
205
+ ${this.arrowSvg}
206
+ </div>`;
207
+ }
208
+ this.buttonElement.innerHTML = subIcon;
209
+ }
210
+ }
211
+ }
@@ -0,0 +1,107 @@
1
+ import { DrawingRenderer, DrawingRenderContext } from '../../types';
2
+
3
+ const LABELS = ['1', '2', '3', '4', '5'];
4
+
5
+ export class TrianglePatternDrawingRenderer implements DrawingRenderer {
6
+ type = 'triangle_pattern';
7
+
8
+ render(ctx: DrawingRenderContext): any {
9
+ const { drawing, pixelPoints, isSelected } = ctx;
10
+ const color = drawing.style?.color || '#3b82f6';
11
+ if (pixelPoints.length < 2) return;
12
+
13
+ const children: any[] = [];
14
+
15
+ // Fill the triangle polygon with all points
16
+ if (pixelPoints.length >= 3) {
17
+ children.push({
18
+ type: 'polygon',
19
+ name: 'line',
20
+ shape: { points: pixelPoints.map(([x, y]) => [x, y]) },
21
+ style: { fill: 'rgba(156, 39, 176, 0.06)' },
22
+ });
23
+ }
24
+
25
+ // Upper trendline: connect odd-indexed points (0, 2, 4) — highs
26
+ const upperPts = pixelPoints.filter((_, i) => i % 2 === 0);
27
+ if (upperPts.length >= 2) {
28
+ for (let i = 0; i < upperPts.length - 1; i++) {
29
+ children.push({
30
+ type: 'line', name: 'line',
31
+ shape: { x1: upperPts[i][0], y1: upperPts[i][1], x2: upperPts[i + 1][0], y2: upperPts[i + 1][1] },
32
+ style: { stroke: '#f44336', lineWidth: 2 },
33
+ });
34
+ }
35
+ // Extend upper trendline
36
+ if (upperPts.length >= 2) {
37
+ const last = upperPts[upperPts.length - 1];
38
+ const prev = upperPts[upperPts.length - 2];
39
+ const dx = last[0] - prev[0];
40
+ const dy = last[1] - prev[1];
41
+ if (dx !== 0) {
42
+ const extendX = last[0] + dx * 0.5;
43
+ const extendY = last[1] + dy * 0.5;
44
+ children.push({
45
+ type: 'line',
46
+ shape: { x1: last[0], y1: last[1], x2: extendX, y2: extendY },
47
+ style: { stroke: '#f44336', lineWidth: 1, lineDash: [4, 4] },
48
+ silent: true,
49
+ });
50
+ }
51
+ }
52
+ }
53
+
54
+ // Lower trendline: connect even-indexed points (1, 3) — lows
55
+ const lowerPts = pixelPoints.filter((_, i) => i % 2 === 1);
56
+ if (lowerPts.length >= 2) {
57
+ for (let i = 0; i < lowerPts.length - 1; i++) {
58
+ children.push({
59
+ type: 'line', name: 'line',
60
+ shape: { x1: lowerPts[i][0], y1: lowerPts[i][1], x2: lowerPts[i + 1][0], y2: lowerPts[i + 1][1] },
61
+ style: { stroke: '#4caf50', lineWidth: 2 },
62
+ });
63
+ }
64
+ // Extend lower trendline
65
+ if (lowerPts.length >= 2) {
66
+ const last = lowerPts[lowerPts.length - 1];
67
+ const prev = lowerPts[lowerPts.length - 2];
68
+ const dx = last[0] - prev[0];
69
+ const dy = last[1] - prev[1];
70
+ if (dx !== 0) {
71
+ const extendX = last[0] + dx * 0.5;
72
+ const extendY = last[1] + dy * 0.5;
73
+ children.push({
74
+ type: 'line',
75
+ shape: { x1: last[0], y1: last[1], x2: extendX, y2: extendY },
76
+ style: { stroke: '#4caf50', lineWidth: 1, lineDash: [4, 4] },
77
+ silent: true,
78
+ });
79
+ }
80
+ }
81
+ }
82
+
83
+ // Zigzag connecting all points
84
+ for (let i = 0; i < pixelPoints.length - 1; i++) {
85
+ children.push({
86
+ type: 'line',
87
+ shape: { x1: pixelPoints[i][0], y1: pixelPoints[i][1], x2: pixelPoints[i + 1][0], y2: pixelPoints[i + 1][1] },
88
+ style: { stroke: '#9c27b0', lineWidth: 1, lineDash: [2, 2] },
89
+ silent: true,
90
+ });
91
+ }
92
+
93
+ // Labels
94
+ for (let i = 0; i < pixelPoints.length && i < LABELS.length; i++) {
95
+ const [px, py] = pixelPoints[i];
96
+ const isHigh = i % 2 === 0;
97
+ 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 });
98
+ }
99
+
100
+ // Control points
101
+ for (let i = 0; i < pixelPoints.length; i++) {
102
+ 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 });
103
+ }
104
+
105
+ return { type: 'group', children };
106
+ }
107
+ }
@@ -0,0 +1,98 @@
1
+ import * as echarts from 'echarts';
2
+ import { AbstractPlugin } from '../../components/AbstractPlugin';
3
+ import { TrianglePatternDrawingRenderer } from './TrianglePatternDrawingRenderer';
4
+
5
+ const LABELS = ['1', '2', '3', '4', '5'];
6
+ const TOTAL_POINTS = 5;
7
+
8
+ export class TrianglePatternTool 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: 'triangle-pattern-tool',
16
+ name: options.name || 'Triangle Pattern',
17
+ icon: options.icon || `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#e3e3e3" stroke-width="1.5"><path d="M2,4 L22,4 L12,20 Z"/></svg>`,
18
+ });
19
+ }
20
+
21
+ protected onInit(): void { this.context.registerDrawingRenderer(new TrianglePatternDrawingRenderer()); }
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
+ if (pts.length >= 3) this.graphicGroup.add(new echarts.graphic.Polygon({ shape: { points: pts }, style: { fill: 'rgba(156,39,176,0.06)' }, silent: true }));
63
+
64
+ // Zigzag
65
+ for (let i = 0; i < pts.length - 1; i++) {
66
+ 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: '#9c27b0', lineWidth: 2 }, silent: true }));
67
+ }
68
+
69
+ // Upper trendline (even indices)
70
+ const upper = pts.filter((_, i) => i % 2 === 0);
71
+ if (upper.length >= 2) {
72
+ for (let i = 0; i < upper.length - 1; i++) {
73
+ this.graphicGroup.add(new echarts.graphic.Line({ shape: { x1: upper[i][0], y1: upper[i][1], x2: upper[i + 1][0], y2: upper[i + 1][1] }, style: { stroke: '#f44336', lineWidth: 1, lineDash: [4, 4] }, silent: true }));
74
+ }
75
+ }
76
+ // Lower trendline (odd indices)
77
+ const lower = pts.filter((_, i) => i % 2 === 1);
78
+ if (lower.length >= 2) {
79
+ for (let i = 0; i < lower.length - 1; i++) {
80
+ this.graphicGroup.add(new echarts.graphic.Line({ shape: { x1: lower[i][0], y1: lower[i][1], x2: lower[i + 1][0], y2: lower[i + 1][1] }, style: { stroke: '#4caf50', lineWidth: 1, lineDash: [4, 4] }, silent: true }));
81
+ }
82
+ }
83
+
84
+ for (let i = 0; i < pts.length && i < LABELS.length; i++) {
85
+ const [px, py] = pts[i];
86
+ const isHigh = i % 2 === 0;
87
+ 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 }));
88
+ 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 }));
89
+ }
90
+ }
91
+
92
+ private saveDrawing() {
93
+ const dataPoints = this.points.map((pt) => this.context.coordinateConversion.pixelToData({ x: pt[0], y: pt[1] }));
94
+ if (dataPoints.every((p) => p !== null)) {
95
+ this.context.addDrawing({ id: `triangle-${Date.now()}`, type: 'triangle_pattern', points: dataPoints as any[], paneIndex: dataPoints[0]!.paneIndex || 0, style: { color: '#3b82f6', lineWidth: 2 } });
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,2 @@
1
+ export { TrianglePatternTool } from './TrianglePatternTool';
2
+ export { TrianglePatternDrawingRenderer } from './TrianglePatternDrawingRenderer';