@qfo/qfchart 0.8.0 → 0.8.2

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 (74) hide show
  1. package/dist/index.d.ts +524 -12
  2. package/dist/qfchart.min.browser.js +34 -18
  3. package/dist/qfchart.min.es.js +34 -18
  4. package/package.json +1 -1
  5. package/src/QFChart.ts +109 -272
  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 +92 -52
  11. package/src/components/SeriesBuilder.ts +10 -10
  12. package/src/components/TooltipFormatter.ts +1 -1
  13. package/src/index.ts +25 -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/CrossLineTool/CrossLineDrawingRenderer.ts +49 -0
  18. package/src/plugins/CrossLineTool/CrossLineTool.ts +52 -0
  19. package/src/plugins/CrossLineTool/index.ts +2 -0
  20. package/src/plugins/CypherPatternTool/CypherPatternDrawingRenderer.ts +80 -0
  21. package/src/plugins/CypherPatternTool/CypherPatternTool.ts +84 -0
  22. package/src/plugins/CypherPatternTool/index.ts +2 -0
  23. package/src/plugins/ExtendedLineTool/ExtendedLineDrawingRenderer.ts +73 -0
  24. package/src/plugins/ExtendedLineTool/ExtendedLineTool.ts +173 -0
  25. package/src/plugins/ExtendedLineTool/index.ts +2 -0
  26. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanDrawingRenderer.ts +163 -0
  27. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanTool.ts +210 -0
  28. package/src/plugins/FibSpeedResistanceFanTool/index.ts +2 -0
  29. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionDrawingRenderer.ts +141 -0
  30. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionTool.ts +188 -0
  31. package/src/plugins/FibTrendExtensionTool/index.ts +2 -0
  32. package/src/plugins/FibonacciChannelTool/FibonacciChannelDrawingRenderer.ts +128 -0
  33. package/src/plugins/FibonacciChannelTool/FibonacciChannelTool.ts +231 -0
  34. package/src/plugins/FibonacciChannelTool/index.ts +2 -0
  35. package/src/plugins/FibonacciTool/FibonacciDrawingRenderer.ts +107 -0
  36. package/src/plugins/{FibonacciTool.ts → FibonacciTool/FibonacciTool.ts} +195 -192
  37. package/src/plugins/FibonacciTool/index.ts +2 -0
  38. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersDrawingRenderer.ts +95 -0
  39. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersTool.ts +97 -0
  40. package/src/plugins/HeadAndShouldersTool/index.ts +2 -0
  41. package/src/plugins/HorizontalLineTool/HorizontalLineDrawingRenderer.ts +54 -0
  42. package/src/plugins/HorizontalLineTool/HorizontalLineTool.ts +52 -0
  43. package/src/plugins/HorizontalLineTool/index.ts +2 -0
  44. package/src/plugins/HorizontalRayTool/HorizontalRayDrawingRenderer.ts +34 -0
  45. package/src/plugins/HorizontalRayTool/HorizontalRayTool.ts +52 -0
  46. package/src/plugins/HorizontalRayTool/index.ts +2 -0
  47. package/src/plugins/InfoLineTool/InfoLineDrawingRenderer.ts +72 -0
  48. package/src/plugins/InfoLineTool/InfoLineTool.ts +130 -0
  49. package/src/plugins/InfoLineTool/index.ts +2 -0
  50. package/src/plugins/LineTool/LineDrawingRenderer.ts +49 -0
  51. package/src/plugins/{LineTool.ts → LineTool/LineTool.ts} +161 -190
  52. package/src/plugins/LineTool/index.ts +2 -0
  53. package/src/plugins/{MeasureTool.ts → MeasureTool/MeasureTool.ts} +324 -344
  54. package/src/plugins/MeasureTool/index.ts +1 -0
  55. package/src/plugins/RayTool/RayDrawingRenderer.ts +69 -0
  56. package/src/plugins/RayTool/RayTool.ts +162 -0
  57. package/src/plugins/RayTool/index.ts +2 -0
  58. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternDrawingRenderer.ts +106 -0
  59. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternTool.ts +98 -0
  60. package/src/plugins/ThreeDrivesPatternTool/index.ts +2 -0
  61. package/src/plugins/ToolGroup.ts +211 -0
  62. package/src/plugins/TrendAngleTool/TrendAngleDrawingRenderer.ts +87 -0
  63. package/src/plugins/TrendAngleTool/TrendAngleTool.ts +176 -0
  64. package/src/plugins/TrendAngleTool/index.ts +2 -0
  65. package/src/plugins/TrianglePatternTool/TrianglePatternDrawingRenderer.ts +107 -0
  66. package/src/plugins/TrianglePatternTool/TrianglePatternTool.ts +98 -0
  67. package/src/plugins/TrianglePatternTool/index.ts +2 -0
  68. package/src/plugins/VerticalLineTool/VerticalLineDrawingRenderer.ts +35 -0
  69. package/src/plugins/VerticalLineTool/VerticalLineTool.ts +52 -0
  70. package/src/plugins/VerticalLineTool/index.ts +2 -0
  71. package/src/plugins/XABCDPatternTool/XABCDPatternDrawingRenderer.ts +178 -0
  72. package/src/plugins/XABCDPatternTool/XABCDPatternTool.ts +213 -0
  73. package/src/plugins/XABCDPatternTool/index.ts +2 -0
  74. package/src/types.ts +39 -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
  },
@@ -267,6 +268,18 @@ export class LayoutManager {
267
268
 
268
269
  let mainHeightVal = 75; // Default if no separate pane
269
270
 
271
+ // Parse layout.mainPaneHeight option (e.g. '40%' or 40)
272
+ let configuredMainHeight: number | undefined;
273
+ if (options.layout?.mainPaneHeight !== undefined) {
274
+ const raw = options.layout.mainPaneHeight;
275
+ if (typeof raw === 'string') {
276
+ const parsed = parseFloat(raw);
277
+ if (!isNaN(parsed)) configuredMainHeight = parsed;
278
+ } else if (typeof raw === 'number') {
279
+ configuredMainHeight = raw as unknown as number;
280
+ }
281
+ }
282
+
270
283
  // Prepare separate panes configuration
271
284
  let paneConfigs: PaneConfiguration[] = [];
272
285
 
@@ -285,33 +298,54 @@ export class LayoutManager {
285
298
  };
286
299
  });
287
300
 
288
- // 2. Assign actual heights
289
- // If collapsed, use small fixed height (e.g. 3%)
290
- const resolvedPanes = panes.map((p) => ({
301
+ // 2. Assign raw heights (collapsed = 3%, otherwise use requested or default 15)
302
+ const rawPanes = panes.map((p) => ({
291
303
  ...p,
292
- height: p.isCollapsed ? 3 : p.requestedHeight !== undefined ? p.requestedHeight : 15,
304
+ rawHeight: p.isCollapsed ? 3 : p.requestedHeight !== undefined ? p.requestedHeight : 15,
293
305
  }));
294
306
 
295
- // 3. Calculate total space needed for indicators
296
- const totalIndicatorHeight = resolvedPanes.reduce((sum, p) => sum + p.height, 0);
297
- const totalGaps = resolvedPanes.length * gapPercent;
298
- const totalBottomSpace = totalIndicatorHeight + totalGaps;
299
-
300
- // 4. Calculate Main Chart Height
301
- // Available space = chartAreaBottom - mainPaneTop;
302
307
  const totalAvailable = chartAreaBottom - mainPaneTop;
303
- mainHeightVal = totalAvailable - totalBottomSpace;
308
+ const totalGaps = rawPanes.length * gapPercent;
304
309
 
305
- // Apply user-dragged main height override
310
+ // 4. Determine main chart height
306
311
  if (mainHeightOverride !== undefined && mainHeightOverride > 0 && !isMainCollapsed) {
312
+ // Drag-resize takes absolute priority
307
313
  mainHeightVal = mainHeightOverride;
308
314
  } else if (isMainCollapsed) {
309
315
  mainHeightVal = 3;
316
+ } else if (configuredMainHeight !== undefined && configuredMainHeight > 0) {
317
+ // User set mainPaneHeight — indicators fill remaining space proportionally
318
+ mainHeightVal = configuredMainHeight;
310
319
  } else {
311
- // Safety check: ensure main chart has at least some space (e.g. 20%)
312
- if (mainHeightVal < 20) {
313
- mainHeightVal = Math.max(mainHeightVal, 10);
314
- }
320
+ // Auto: subtract indicator heights from available space
321
+ const totalIndicatorHeight = rawPanes.reduce((sum, p) => sum + p.rawHeight, 0);
322
+ mainHeightVal = totalAvailable - totalIndicatorHeight - totalGaps;
323
+ if (mainHeightVal < 20) mainHeightVal = Math.max(mainHeightVal, 10);
324
+ }
325
+
326
+ // 3. Resolve indicator heights
327
+ // When mainPaneHeight is configured (or drag override active), distribute remaining space
328
+ // proportionally among non-collapsed panes using their rawHeight as weights.
329
+ const isMainHeightFixed = (mainHeightOverride !== undefined && mainHeightOverride > 0 && !isMainCollapsed)
330
+ || (configuredMainHeight !== undefined && configuredMainHeight > 0 && !isMainCollapsed);
331
+
332
+ type ResolvedPane = (typeof rawPanes)[number] & { height: number };
333
+ let resolvedPanes: ResolvedPane[];
334
+ if (isMainHeightFixed) {
335
+ const remainingForIndicators = totalAvailable - mainHeightVal - totalGaps;
336
+ const totalWeights = rawPanes
337
+ .filter((p) => !p.isCollapsed)
338
+ .reduce((sum, p) => sum + p.rawHeight, 0);
339
+ resolvedPanes = rawPanes.map((p) => ({
340
+ ...p,
341
+ height: p.isCollapsed
342
+ ? 3
343
+ : totalWeights > 0
344
+ ? Math.max(5, (p.rawHeight / totalWeights) * remainingForIndicators)
345
+ : remainingForIndicators / rawPanes.filter((x) => !x.isCollapsed).length,
346
+ }));
347
+ } else {
348
+ resolvedPanes = rawPanes.map((p) => ({ ...p, height: p.rawHeight }));
315
349
  }
316
350
 
317
351
  // 5. Calculate positions
@@ -331,6 +365,7 @@ export class LayoutManager {
331
365
  return config;
332
366
  });
333
367
  } else {
368
+ // No secondary panes — mainPaneHeight is ignored, fill all available space
334
369
  mainHeightVal = chartAreaBottom - mainPaneTop;
335
370
  if (isMainCollapsed) {
336
371
  mainHeightVal = 3;
@@ -407,9 +442,8 @@ export class LayoutManager {
407
442
  if (options.yAxisLabelFormatter) {
408
443
  return options.yAxisLabelFormatter(value);
409
444
  }
410
- const decimals = options.yAxisDecimalPlaces !== undefined
411
- ? options.yAxisDecimalPlaces
412
- : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
445
+ const decimals =
446
+ options.yAxisDecimalPlaces !== undefined ? options.yAxisDecimalPlaces : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
413
447
  return AxisUtils.formatValue(value, decimals);
414
448
  },
415
449
  },
@@ -483,9 +517,8 @@ export class LayoutManager {
483
517
  if (options.yAxisLabelFormatter) {
484
518
  return options.yAxisLabelFormatter(value);
485
519
  }
486
- const decimals = options.yAxisDecimalPlaces !== undefined
487
- ? options.yAxisDecimalPlaces
488
- : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
520
+ const decimals =
521
+ options.yAxisDecimalPlaces !== undefined ? options.yAxisDecimalPlaces : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
489
522
  return AxisUtils.formatValue(value, decimals);
490
523
  },
491
524
  },
@@ -521,7 +554,11 @@ export class LayoutManager {
521
554
 
522
555
  // Check if this is a shape with price-relative positioning
523
556
  const isShapeWithPriceLocation =
524
- plot.options.style === 'shape' && (plot.options.location === 'abovebar' || plot.options.location === 'AboveBar' || plot.options.location === 'belowbar' || plot.options.location === 'BelowBar');
557
+ plot.options.style === 'shape' &&
558
+ (plot.options.location === 'abovebar' ||
559
+ plot.options.location === 'AboveBar' ||
560
+ plot.options.location === 'belowbar' ||
561
+ plot.options.location === 'BelowBar');
525
562
 
526
563
  if (visualOnlyStyles.includes(plot.options.style)) {
527
564
  // Assign these to a separate Y-axis so they don't affect price scale
@@ -582,7 +619,7 @@ export class LayoutManager {
582
619
  // Create Y-axes for incompatible plots
583
620
  // nextYAxisIndex already incremented in the loop above, so we know how many axes we need
584
621
  const numOverlayAxes = overlayYAxisMap.size > 0 ? nextYAxisIndex - 1 : 0;
585
-
622
+
586
623
  // Track which overlay axes are for visual-only plots (background, barcolor, etc.)
587
624
  const visualOnlyAxes = new Set<number>();
588
625
  overlayYAxisMap.forEach((yAxisIdx, plotKey) => {
@@ -596,11 +633,11 @@ export class LayoutManager {
596
633
  });
597
634
  });
598
635
  });
599
-
636
+
600
637
  for (let i = 0; i < numOverlayAxes; i++) {
601
638
  const yAxisIndex = i + 1; // Y-axis indices start at 1 for overlays
602
639
  const isVisualOnly = visualOnlyAxes.has(yAxisIndex);
603
-
640
+
604
641
  yAxis.push({
605
642
  position: 'left',
606
643
  scale: !isVisualOnly, // Disable scaling for visual-only plots
@@ -636,9 +673,10 @@ export class LayoutManager {
636
673
  if (options.yAxisLabelFormatter) {
637
674
  return options.yAxisLabelFormatter(value);
638
675
  }
639
- const decimals = options.yAxisDecimalPlaces !== undefined
640
- ? options.yAxisDecimalPlaces
641
- : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
676
+ const decimals =
677
+ options.yAxisDecimalPlaces !== undefined
678
+ ? options.yAxisDecimalPlaces
679
+ : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
642
680
  return AxisUtils.formatValue(value, decimals);
643
681
  },
644
682
  },
@@ -648,19 +686,21 @@ export class LayoutManager {
648
686
 
649
687
  // --- Generate DataZoom ---
650
688
  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
- }
689
+ const zoomOnTouch = options.dataZoom?.zoomOnTouch ?? true;
690
+ const pannable = options.dataZoom?.pannable ?? true;
691
+
692
+ // 'inside' zoom provides pan/drag — enabled independently of slider visibility
693
+ if (zoomOnTouch && pannable) {
694
+ dataZoom.push({
695
+ type: 'inside',
696
+ xAxisIndex: allXAxisIndices,
697
+ start: dzStart,
698
+ end: dzEnd,
699
+ filterMode: 'weakFilter',
700
+ });
701
+ }
663
702
 
703
+ if (dzVisible) {
664
704
  if (dzPosition === 'top') {
665
705
  dataZoom.push({
666
706
  type: 'slider',
@@ -708,7 +748,7 @@ export class LayoutManager {
708
748
  private static calculateMaximized(
709
749
  containerHeight: number,
710
750
  options: QFChartOptions,
711
- targetPaneIndex: number // 0 for main, 1+ for indicators
751
+ targetPaneIndex: number, // 0 for main, 1+ for indicators
712
752
  ): LayoutResult {
713
753
  return {
714
754
  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,25 @@
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/RayTool";
6
+ export * from "./plugins/InfoLineTool";
7
+ export * from "./plugins/ExtendedLineTool";
8
+ export * from "./plugins/TrendAngleTool";
9
+ export * from "./plugins/HorizontalLineTool";
10
+ export * from "./plugins/HorizontalRayTool";
11
+ export * from "./plugins/VerticalLineTool";
12
+ export * from "./plugins/CrossLineTool";
13
+ export * from "./plugins/FibonacciTool";
14
+ export * from "./plugins/FibonacciChannelTool";
15
+ export * from "./plugins/FibSpeedResistanceFanTool";
16
+ export * from "./plugins/FibTrendExtensionTool";
17
+ export * from "./plugins/XABCDPatternTool";
18
+ export * from "./plugins/ABCDPatternTool";
19
+ export * from "./plugins/CypherPatternTool";
20
+ export * from "./plugins/HeadAndShouldersTool";
21
+ export * from "./plugins/TrianglePatternTool";
22
+ export * from "./plugins/ThreeDrivesPatternTool";
23
+ export * from "./plugins/ToolGroup";
24
+ export * from "./components/AbstractPlugin";
25
+ 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';