@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
@@ -15,9 +15,9 @@ export interface PaneConfiguration {
15
15
  }
16
16
 
17
17
  export interface PaneBoundary {
18
- yPercent: number; // Y position in %, center of the gap between panes
19
- aboveId: string | 'main'; // pane above (main chart or indicator id)
20
- belowId: string; // indicator id below
18
+ yPercent: number; // Y position in %, center of the gap between panes
19
+ aboveId: string | 'main'; // pane above (main chart or indicator id)
20
+ belowId: string; // indicator id below
21
21
  }
22
22
 
23
23
  export interface LayoutResult {
@@ -40,7 +40,7 @@ export class LayoutManager {
40
40
  isMainCollapsed: boolean = false,
41
41
  maximizedPaneId: string | null = null,
42
42
  marketData?: import('../types').OHLCV[],
43
- mainHeightOverride?: number
43
+ mainHeightOverride?: number,
44
44
  ): LayoutResult & { overlayYAxisMap: Map<string, number>; separatePaneYAxisOffset: number } {
45
45
  // Calculate pixelToPercent early for maximized logic
46
46
  let pixelToPercent = 0;
@@ -52,11 +52,11 @@ export class LayoutManager {
52
52
  const yAxisPaddingPercent = options.yAxisPadding !== undefined ? options.yAxisPadding : 5;
53
53
 
54
54
  // Grid styling options
55
- const gridShow = options.grid?.show === true; // default false
55
+ const gridShow = options.grid?.show === true; // default false
56
56
  const gridLineColor = options.grid?.lineColor ?? '#334155';
57
57
  const gridLineOpacity = options.grid?.lineOpacity ?? 0.5;
58
58
  const gridBorderColor = options.grid?.borderColor ?? '#334155';
59
- const gridBorderShow = options.grid?.borderShow === true; // default false
59
+ const gridBorderShow = options.grid?.borderShow === true; // default false
60
60
 
61
61
  // Layout margin options
62
62
  const layoutLeft = options.layout?.left ?? '10%';
@@ -195,9 +195,10 @@ export class LayoutManager {
195
195
  if (options.yAxisLabelFormatter) {
196
196
  return options.yAxisLabelFormatter(value);
197
197
  }
198
- const decimals = options.yAxisDecimalPlaces !== undefined
199
- ? options.yAxisDecimalPlaces
200
- : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
198
+ const decimals =
199
+ options.yAxisDecimalPlaces !== undefined
200
+ ? options.yAxisDecimalPlaces
201
+ : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
201
202
  return AxisUtils.formatValue(value, decimals);
202
203
  },
203
204
  },
@@ -407,9 +408,8 @@ export class LayoutManager {
407
408
  if (options.yAxisLabelFormatter) {
408
409
  return options.yAxisLabelFormatter(value);
409
410
  }
410
- const decimals = options.yAxisDecimalPlaces !== undefined
411
- ? options.yAxisDecimalPlaces
412
- : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
411
+ const decimals =
412
+ options.yAxisDecimalPlaces !== undefined ? options.yAxisDecimalPlaces : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
413
413
  return AxisUtils.formatValue(value, decimals);
414
414
  },
415
415
  },
@@ -483,9 +483,8 @@ export class LayoutManager {
483
483
  if (options.yAxisLabelFormatter) {
484
484
  return options.yAxisLabelFormatter(value);
485
485
  }
486
- const decimals = options.yAxisDecimalPlaces !== undefined
487
- ? options.yAxisDecimalPlaces
488
- : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
486
+ const decimals =
487
+ options.yAxisDecimalPlaces !== undefined ? options.yAxisDecimalPlaces : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
489
488
  return AxisUtils.formatValue(value, decimals);
490
489
  },
491
490
  },
@@ -521,7 +520,11 @@ export class LayoutManager {
521
520
 
522
521
  // Check if this is a shape with price-relative positioning
523
522
  const isShapeWithPriceLocation =
524
- plot.options.style === 'shape' && (plot.options.location === 'abovebar' || plot.options.location === 'AboveBar' || plot.options.location === 'belowbar' || plot.options.location === 'BelowBar');
523
+ plot.options.style === 'shape' &&
524
+ (plot.options.location === 'abovebar' ||
525
+ plot.options.location === 'AboveBar' ||
526
+ plot.options.location === 'belowbar' ||
527
+ plot.options.location === 'BelowBar');
525
528
 
526
529
  if (visualOnlyStyles.includes(plot.options.style)) {
527
530
  // Assign these to a separate Y-axis so they don't affect price scale
@@ -582,7 +585,7 @@ export class LayoutManager {
582
585
  // Create Y-axes for incompatible plots
583
586
  // nextYAxisIndex already incremented in the loop above, so we know how many axes we need
584
587
  const numOverlayAxes = overlayYAxisMap.size > 0 ? nextYAxisIndex - 1 : 0;
585
-
588
+
586
589
  // Track which overlay axes are for visual-only plots (background, barcolor, etc.)
587
590
  const visualOnlyAxes = new Set<number>();
588
591
  overlayYAxisMap.forEach((yAxisIdx, plotKey) => {
@@ -596,11 +599,11 @@ export class LayoutManager {
596
599
  });
597
600
  });
598
601
  });
599
-
602
+
600
603
  for (let i = 0; i < numOverlayAxes; i++) {
601
604
  const yAxisIndex = i + 1; // Y-axis indices start at 1 for overlays
602
605
  const isVisualOnly = visualOnlyAxes.has(yAxisIndex);
603
-
606
+
604
607
  yAxis.push({
605
608
  position: 'left',
606
609
  scale: !isVisualOnly, // Disable scaling for visual-only plots
@@ -636,9 +639,10 @@ export class LayoutManager {
636
639
  if (options.yAxisLabelFormatter) {
637
640
  return options.yAxisLabelFormatter(value);
638
641
  }
639
- const decimals = options.yAxisDecimalPlaces !== undefined
640
- ? options.yAxisDecimalPlaces
641
- : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
642
+ const decimals =
643
+ options.yAxisDecimalPlaces !== undefined
644
+ ? options.yAxisDecimalPlaces
645
+ : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
642
646
  return AxisUtils.formatValue(value, decimals);
643
647
  },
644
648
  },
@@ -648,19 +652,21 @@ export class LayoutManager {
648
652
 
649
653
  // --- Generate DataZoom ---
650
654
  const dataZoom: any[] = [];
651
- if (dzVisible) {
652
- // Add 'inside' zoom (pan/drag) only if zoomOnTouch is enabled (default true)
653
- const zoomOnTouch = options.dataZoom?.zoomOnTouch ?? true;
654
- if (zoomOnTouch) {
655
- dataZoom.push({
656
- type: 'inside',
657
- xAxisIndex: allXAxisIndices,
658
- start: dzStart,
659
- end: dzEnd,
660
- filterMode: 'weakFilter',
661
- });
662
- }
655
+ const zoomOnTouch = options.dataZoom?.zoomOnTouch ?? true;
656
+ const pannable = options.dataZoom?.pannable ?? true;
657
+
658
+ // 'inside' zoom provides pan/drag — enabled independently of slider visibility
659
+ if (zoomOnTouch && pannable) {
660
+ dataZoom.push({
661
+ type: 'inside',
662
+ xAxisIndex: allXAxisIndices,
663
+ start: dzStart,
664
+ end: dzEnd,
665
+ filterMode: 'weakFilter',
666
+ });
667
+ }
663
668
 
669
+ if (dzVisible) {
664
670
  if (dzPosition === 'top') {
665
671
  dataZoom.push({
666
672
  type: 'slider',
@@ -708,7 +714,7 @@ export class LayoutManager {
708
714
  private static calculateMaximized(
709
715
  containerHeight: number,
710
716
  options: QFChartOptions,
711
- targetPaneIndex: number // 0 for main, 1+ for indicators
717
+ targetPaneIndex: number, // 0 for main, 1+ for indicators
712
718
  ): LayoutResult {
713
719
  return {
714
720
  grid: [],
@@ -35,9 +35,7 @@ export class SeriesBuilder {
35
35
  if (lineStyleType.startsWith('linestyle_')) {
36
36
  lineStyleType = lineStyleType.replace('linestyle_', '') as any;
37
37
  }
38
- const decimals = options.yAxisDecimalPlaces !== undefined
39
- ? options.yAxisDecimalPlaces
40
- : AxisUtils.autoDetectDecimals(marketData);
38
+ const decimals = options.yAxisDecimalPlaces !== undefined ? options.yAxisDecimalPlaces : AxisUtils.autoDetectDecimals(marketData);
41
39
 
42
40
  markLine = {
43
41
  symbol: ['none', 'none'],
@@ -77,7 +75,8 @@ export class SeriesBuilder {
77
75
 
78
76
  return {
79
77
  type: 'candlestick',
80
- name: options.title || 'Market',
78
+ id: '__candlestick__',
79
+ name: options.title,
81
80
  data: data,
82
81
  itemStyle: {
83
82
  color: upColor,
@@ -100,7 +99,7 @@ export class SeriesBuilder {
100
99
  dataIndexOffset: number = 0,
101
100
  candlestickData?: OHLCV[], // Add candlestick data to access High/Low for positioning
102
101
  overlayYAxisMap?: Map<string, number>, // Map of overlay indicator IDs to their Y-axis indices
103
- separatePaneYAxisOffset: number = 1 // Offset for separate pane Y-axes (accounts for overlay axes)
102
+ separatePaneYAxisOffset: number = 1, // Offset for separate pane Y-axes (accounts for overlay axes)
104
103
  ): { series: any[]; barColors: (string | null)[] } {
105
104
  const series: any[] = [];
106
105
  const barColors: (string | null)[] = new Array(totalDataLength).fill(null);
@@ -220,7 +219,7 @@ export class SeriesBuilder {
220
219
  }
221
220
 
222
221
  dataArray[offsetIndex] = value;
223
- colorArray[offsetIndex] = isNaColor ? null : (pointColor || plot.options.color || SeriesBuilder.DEFAULT_COLOR);
222
+ colorArray[offsetIndex] = isNaColor ? null : pointColor || plot.options.color || SeriesBuilder.DEFAULT_COLOR;
224
223
  optionsArray[offsetIndex] = point.options || {};
225
224
  }
226
225
  }
@@ -281,8 +280,9 @@ export class SeriesBuilder {
281
280
 
282
281
  if (plot1Data && plot2Data) {
283
282
  // Parse per-bar colors
284
- const { color: defaultColor, opacity: defaultOpacity } =
285
- ColorUtils.parseColor(plot.options.color || 'rgba(128, 128, 128, 0.2)');
283
+ const { color: defaultColor, opacity: defaultOpacity } = ColorUtils.parseColor(
284
+ plot.options.color || 'rgba(128, 128, 128, 0.2)',
285
+ );
286
286
  const hasPerBarColor = optionsArray.some((o: any) => o && o.color !== undefined);
287
287
 
288
288
  const fillBarColors: { color: string; opacity: number }[] = [];
@@ -359,7 +359,7 @@ export class SeriesBuilder {
359
359
  xAxisIndex,
360
360
  yAxisIndex,
361
361
  totalDataLength,
362
- entries
362
+ entries,
363
363
  );
364
364
  if (batchedConfig) {
365
365
  series.push(batchedConfig);
@@ -371,7 +371,7 @@ export class SeriesBuilder {
371
371
  xAxisIndex,
372
372
  yAxisIndex,
373
373
  totalDataLength,
374
- entries
374
+ entries,
375
375
  );
376
376
  if (batchedConfig) {
377
377
  series.push(batchedConfig);
@@ -4,7 +4,7 @@ export class TooltipFormatter {
4
4
  public static format(params: any[], options: QFChartOptions): string {
5
5
  if (!params || params.length === 0) return "";
6
6
 
7
- const marketName = options.title || "Market";
7
+ const marketName = options.title || "";
8
8
  const upColor = options.upColor || "#00da3c";
9
9
  const downColor = options.downColor || "#ec0000";
10
10
  const fontFamily = options.fontFamily || "sans-serif";
package/src/index.ts CHANGED
@@ -1,6 +1,17 @@
1
- export * from "./types";
2
- export * from "./QFChart";
3
- export * from "./plugins/MeasureTool";
4
- export * from "./plugins/LineTool";
5
- export * from "./plugins/FibonacciTool";
6
- export * from "./components/AbstractPlugin";
1
+ export * from "./types";
2
+ export * from "./QFChart";
3
+ export * from "./plugins/MeasureTool";
4
+ export * from "./plugins/LineTool";
5
+ export * from "./plugins/FibonacciTool";
6
+ export * from "./plugins/FibonacciChannelTool";
7
+ export * from "./plugins/FibSpeedResistanceFanTool";
8
+ export * from "./plugins/FibTrendExtensionTool";
9
+ export * from "./plugins/XABCDPatternTool";
10
+ export * from "./plugins/ABCDPatternTool";
11
+ export * from "./plugins/CypherPatternTool";
12
+ export * from "./plugins/HeadAndShouldersTool";
13
+ export * from "./plugins/TrianglePatternTool";
14
+ export * from "./plugins/ThreeDrivesPatternTool";
15
+ export * from "./plugins/ToolGroup";
16
+ export * from "./components/AbstractPlugin";
17
+ export * from "./components/DrawingRendererRegistry";
@@ -0,0 +1,112 @@
1
+ import { DrawingRenderer, DrawingRenderContext } from '../../types';
2
+
3
+ const LABELS = ['A', 'B', 'C', 'D'];
4
+ const LEG_COLORS = ['#2196f3', '#ff9800', '#4caf50'];
5
+
6
+ export class ABCDPatternDrawingRenderer implements DrawingRenderer {
7
+ type = 'abcd_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 triangle ABC
17
+ if (pixelPoints.length >= 3) {
18
+ children.push({
19
+ type: 'polygon',
20
+ name: 'line',
21
+ shape: { points: pixelPoints.slice(0, 3).map(([x, y]) => [x, y]) },
22
+ style: { fill: 'rgba(33, 150, 243, 0.08)' },
23
+ });
24
+ }
25
+ // Fill triangle BCD
26
+ if (pixelPoints.length >= 4) {
27
+ children.push({
28
+ type: 'polygon',
29
+ name: 'line',
30
+ shape: { points: pixelPoints.slice(1, 4).map(([x, y]) => [x, y]) },
31
+ style: { fill: 'rgba(244, 67, 54, 0.08)' },
32
+ });
33
+ }
34
+
35
+ // Leg lines
36
+ for (let i = 0; i < pixelPoints.length - 1; i++) {
37
+ const [x1, y1] = pixelPoints[i];
38
+ const [x2, y2] = pixelPoints[i + 1];
39
+ children.push({
40
+ type: 'line',
41
+ name: 'line',
42
+ shape: { x1, y1, x2, y2 },
43
+ style: { stroke: LEG_COLORS[i % LEG_COLORS.length], lineWidth: drawing.style?.lineWidth || 2 },
44
+ });
45
+ }
46
+
47
+ // Dashed connector A→C
48
+ if (pixelPoints.length >= 3) {
49
+ children.push({
50
+ type: 'line',
51
+ shape: { x1: pixelPoints[0][0], y1: pixelPoints[0][1], x2: pixelPoints[2][0], y2: pixelPoints[2][1] },
52
+ style: { stroke: '#555', lineWidth: 1, lineDash: [4, 4] },
53
+ silent: true,
54
+ });
55
+ }
56
+ // Dashed connector B→D
57
+ if (pixelPoints.length >= 4) {
58
+ children.push({
59
+ type: 'line',
60
+ shape: { x1: pixelPoints[1][0], y1: pixelPoints[1][1], x2: pixelPoints[3][0], y2: pixelPoints[3][1] },
61
+ style: { stroke: '#555', lineWidth: 1, lineDash: [4, 4] },
62
+ silent: true,
63
+ });
64
+ }
65
+
66
+ // Fibonacci ratios
67
+ if (drawing.points.length >= 3) {
68
+ const ab = Math.abs(drawing.points[1].value - drawing.points[0].value);
69
+ const bc = Math.abs(drawing.points[2].value - drawing.points[1].value);
70
+ if (ab !== 0) {
71
+ const ratio = (bc / ab).toFixed(3);
72
+ const mx = (pixelPoints[1][0] + pixelPoints[2][0]) / 2;
73
+ const my = (pixelPoints[1][1] + pixelPoints[2][1]) / 2;
74
+ children.push({ type: 'text', style: { text: ratio, x: mx + 8, y: my, fill: '#ff9800', fontSize: 10 }, silent: true });
75
+ }
76
+ }
77
+ if (drawing.points.length >= 4) {
78
+ const bc = Math.abs(drawing.points[2].value - drawing.points[1].value);
79
+ const cd = Math.abs(drawing.points[3].value - drawing.points[2].value);
80
+ if (bc !== 0) {
81
+ const ratio = (cd / bc).toFixed(3);
82
+ const mx = (pixelPoints[2][0] + pixelPoints[3][0]) / 2;
83
+ const my = (pixelPoints[2][1] + pixelPoints[3][1]) / 2;
84
+ children.push({ type: 'text', style: { text: ratio, x: mx + 8, y: my, fill: '#4caf50', fontSize: 10 }, silent: true });
85
+ }
86
+ }
87
+
88
+ // Vertex labels
89
+ for (let i = 0; i < pixelPoints.length && i < LABELS.length; i++) {
90
+ const [px, py] = pixelPoints[i];
91
+ const isHigh = (i === 0 || py <= pixelPoints[i - 1][1]) && (i === pixelPoints.length - 1 || py <= pixelPoints[i + 1]?.[1]);
92
+ children.push({
93
+ type: 'text',
94
+ style: { text: LABELS[i], x: px, y: isHigh ? py - 14 : py + 16, fill: '#e2e8f0', fontSize: 12, fontWeight: 'bold', align: 'center', verticalAlign: 'middle' },
95
+ silent: true,
96
+ });
97
+ }
98
+
99
+ // Control points
100
+ for (let i = 0; i < pixelPoints.length; i++) {
101
+ children.push({
102
+ type: 'circle',
103
+ name: `point-${i}`,
104
+ shape: { cx: pixelPoints[i][0], cy: pixelPoints[i][1], r: 4 },
105
+ style: { fill: '#fff', stroke: color, lineWidth: 1, opacity: isSelected ? 1 : 0 },
106
+ z: 100,
107
+ });
108
+ }
109
+
110
+ return { type: 'group', children };
111
+ }
112
+ }
@@ -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
+ }