@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.
- package/dist/index.d.ts +524 -12
- package/dist/qfchart.min.browser.js +34 -18
- package/dist/qfchart.min.es.js +34 -18
- package/package.json +1 -1
- package/src/QFChart.ts +109 -272
- 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 +92 -52
- package/src/components/SeriesBuilder.ts +10 -10
- package/src/components/TooltipFormatter.ts +1 -1
- package/src/index.ts +25 -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/CrossLineTool/CrossLineDrawingRenderer.ts +49 -0
- package/src/plugins/CrossLineTool/CrossLineTool.ts +52 -0
- package/src/plugins/CrossLineTool/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/ExtendedLineTool/ExtendedLineDrawingRenderer.ts +73 -0
- package/src/plugins/ExtendedLineTool/ExtendedLineTool.ts +173 -0
- package/src/plugins/ExtendedLineTool/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/HorizontalLineTool/HorizontalLineDrawingRenderer.ts +54 -0
- package/src/plugins/HorizontalLineTool/HorizontalLineTool.ts +52 -0
- package/src/plugins/HorizontalLineTool/index.ts +2 -0
- package/src/plugins/HorizontalRayTool/HorizontalRayDrawingRenderer.ts +34 -0
- package/src/plugins/HorizontalRayTool/HorizontalRayTool.ts +52 -0
- package/src/plugins/HorizontalRayTool/index.ts +2 -0
- package/src/plugins/InfoLineTool/InfoLineDrawingRenderer.ts +72 -0
- package/src/plugins/InfoLineTool/InfoLineTool.ts +130 -0
- package/src/plugins/InfoLineTool/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/RayTool/RayDrawingRenderer.ts +69 -0
- package/src/plugins/RayTool/RayTool.ts +162 -0
- package/src/plugins/RayTool/index.ts +2 -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/TrendAngleTool/TrendAngleDrawingRenderer.ts +87 -0
- package/src/plugins/TrendAngleTool/TrendAngleTool.ts +176 -0
- package/src/plugins/TrendAngleTool/index.ts +2 -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/VerticalLineTool/VerticalLineDrawingRenderer.ts +35 -0
- package/src/plugins/VerticalLineTool/VerticalLineTool.ts +52 -0
- package/src/plugins/VerticalLineTool/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 +39 -11
|
@@ -15,9 +15,9 @@ export interface PaneConfiguration {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface PaneBoundary {
|
|
18
|
-
yPercent: number;
|
|
19
|
-
aboveId: string | 'main';
|
|
20
|
-
belowId: string;
|
|
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;
|
|
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;
|
|
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 =
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
+
const totalGaps = rawPanes.length * gapPercent;
|
|
304
309
|
|
|
305
|
-
//
|
|
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
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
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 =
|
|
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 =
|
|
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' &&
|
|
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 =
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
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 :
|
|
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
|
-
|
|
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 || "
|
|
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/
|
|
6
|
-
export * from "./
|
|
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
|
+
}
|