@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
|
@@ -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,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
|
+
}
|