@qfo/qfchart 0.7.3 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +368 -14
- package/dist/qfchart.min.browser.js +34 -16
- package/dist/qfchart.min.es.js +34 -16
- package/package.json +1 -1
- package/src/QFChart.ts +460 -311
- 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 +284 -263
- package/src/components/LayoutManager.ts +72 -55
- package/src/components/SeriesBuilder.ts +110 -6
- package/src/components/TableCanvasRenderer.ts +467 -0
- package/src/components/TableOverlayRenderer.ts +38 -9
- package/src/components/TooltipFormatter.ts +97 -97
- package/src/components/renderers/BackgroundRenderer.ts +59 -47
- package/src/components/renderers/BoxRenderer.ts +113 -17
- package/src/components/renderers/FillRenderer.ts +118 -3
- package/src/components/renderers/LabelRenderer.ts +35 -9
- package/src/components/renderers/OHLCBarRenderer.ts +171 -161
- package/src/components/renderers/PolylineRenderer.ts +26 -19
- package/src/index.ts +17 -6
- package/src/plugins/ABCDPatternTool/ABCDPatternDrawingRenderer.ts +112 -0
- package/src/plugins/ABCDPatternTool/ABCDPatternTool.ts +136 -0
- package/src/plugins/ABCDPatternTool/index.ts +2 -0
- package/src/plugins/CypherPatternTool/CypherPatternDrawingRenderer.ts +80 -0
- package/src/plugins/CypherPatternTool/CypherPatternTool.ts +84 -0
- package/src/plugins/CypherPatternTool/index.ts +2 -0
- package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanDrawingRenderer.ts +163 -0
- package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanTool.ts +210 -0
- package/src/plugins/FibSpeedResistanceFanTool/index.ts +2 -0
- package/src/plugins/FibTrendExtensionTool/FibTrendExtensionDrawingRenderer.ts +141 -0
- package/src/plugins/FibTrendExtensionTool/FibTrendExtensionTool.ts +188 -0
- package/src/plugins/FibTrendExtensionTool/index.ts +2 -0
- package/src/plugins/FibonacciChannelTool/FibonacciChannelDrawingRenderer.ts +128 -0
- package/src/plugins/FibonacciChannelTool/FibonacciChannelTool.ts +231 -0
- package/src/plugins/FibonacciChannelTool/index.ts +2 -0
- package/src/plugins/FibonacciTool/FibonacciDrawingRenderer.ts +107 -0
- package/src/plugins/{FibonacciTool.ts → FibonacciTool/FibonacciTool.ts} +195 -192
- package/src/plugins/FibonacciTool/index.ts +2 -0
- package/src/plugins/HeadAndShouldersTool/HeadAndShouldersDrawingRenderer.ts +95 -0
- package/src/plugins/HeadAndShouldersTool/HeadAndShouldersTool.ts +97 -0
- package/src/plugins/HeadAndShouldersTool/index.ts +2 -0
- package/src/plugins/LineTool/LineDrawingRenderer.ts +49 -0
- package/src/plugins/{LineTool.ts → LineTool/LineTool.ts} +161 -190
- package/src/plugins/LineTool/index.ts +2 -0
- package/src/plugins/{MeasureTool.ts → MeasureTool/MeasureTool.ts} +324 -344
- package/src/plugins/MeasureTool/index.ts +1 -0
- package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternDrawingRenderer.ts +106 -0
- package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternTool.ts +98 -0
- package/src/plugins/ThreeDrivesPatternTool/index.ts +2 -0
- package/src/plugins/ToolGroup.ts +211 -0
- package/src/plugins/TrianglePatternTool/TrianglePatternDrawingRenderer.ts +107 -0
- package/src/plugins/TrianglePatternTool/TrianglePatternTool.ts +98 -0
- package/src/plugins/TrianglePatternTool/index.ts +2 -0
- package/src/plugins/XABCDPatternTool/XABCDPatternDrawingRenderer.ts +178 -0
- package/src/plugins/XABCDPatternTool/XABCDPatternTool.ts +213 -0
- package/src/plugins/XABCDPatternTool/index.ts +2 -0
- package/src/types.ts +39 -4
- package/src/utils/ColorUtils.ts +1 -1
|
@@ -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;
|
|
@@ -51,6 +51,17 @@ export class LayoutManager {
|
|
|
51
51
|
// Get Y-axis padding percentage (default 5%)
|
|
52
52
|
const yAxisPaddingPercent = options.yAxisPadding !== undefined ? options.yAxisPadding : 5;
|
|
53
53
|
|
|
54
|
+
// Grid styling options
|
|
55
|
+
const gridShow = options.grid?.show === true; // default false
|
|
56
|
+
const gridLineColor = options.grid?.lineColor ?? '#334155';
|
|
57
|
+
const gridLineOpacity = options.grid?.lineOpacity ?? 0.5;
|
|
58
|
+
const gridBorderColor = options.grid?.borderColor ?? '#334155';
|
|
59
|
+
const gridBorderShow = options.grid?.borderShow === true; // default false
|
|
60
|
+
|
|
61
|
+
// Layout margin options
|
|
62
|
+
const layoutLeft = options.layout?.left ?? '10%';
|
|
63
|
+
const layoutRight = options.layout?.right ?? '10%';
|
|
64
|
+
|
|
54
65
|
// Identify unique separate panes (indices > 0) and sort them
|
|
55
66
|
const separatePaneIndices = Array.from(indicators.values())
|
|
56
67
|
.map((ind) => ind.paneIndex)
|
|
@@ -122,8 +133,8 @@ export class LayoutManager {
|
|
|
122
133
|
|
|
123
134
|
// Grid
|
|
124
135
|
grid.push({
|
|
125
|
-
left:
|
|
126
|
-
right:
|
|
136
|
+
left: layoutLeft,
|
|
137
|
+
right: layoutRight,
|
|
127
138
|
top: isTarget ? '5%' : '0%',
|
|
128
139
|
height: isTarget ? '90%' : '0%',
|
|
129
140
|
show: isTarget,
|
|
@@ -141,10 +152,10 @@ export class LayoutManager {
|
|
|
141
152
|
color: '#94a3b8',
|
|
142
153
|
fontFamily: options.fontFamily,
|
|
143
154
|
},
|
|
144
|
-
axisLine: { show: isTarget, lineStyle: { color:
|
|
155
|
+
axisLine: { show: isTarget && gridBorderShow, lineStyle: { color: gridBorderColor } },
|
|
145
156
|
splitLine: {
|
|
146
|
-
show: isTarget,
|
|
147
|
-
lineStyle: { color:
|
|
157
|
+
show: isTarget && gridShow,
|
|
158
|
+
lineStyle: { color: gridLineColor, opacity: gridLineOpacity },
|
|
148
159
|
},
|
|
149
160
|
});
|
|
150
161
|
|
|
@@ -184,15 +195,16 @@ export class LayoutManager {
|
|
|
184
195
|
if (options.yAxisLabelFormatter) {
|
|
185
196
|
return options.yAxisLabelFormatter(value);
|
|
186
197
|
}
|
|
187
|
-
const decimals =
|
|
188
|
-
|
|
189
|
-
|
|
198
|
+
const decimals =
|
|
199
|
+
options.yAxisDecimalPlaces !== undefined
|
|
200
|
+
? options.yAxisDecimalPlaces
|
|
201
|
+
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
190
202
|
return AxisUtils.formatValue(value, decimals);
|
|
191
203
|
},
|
|
192
204
|
},
|
|
193
205
|
splitLine: {
|
|
194
|
-
show: isTarget,
|
|
195
|
-
lineStyle: { color:
|
|
206
|
+
show: isTarget && gridShow,
|
|
207
|
+
lineStyle: { color: gridLineColor, opacity: gridLineOpacity },
|
|
196
208
|
},
|
|
197
209
|
});
|
|
198
210
|
|
|
@@ -349,8 +361,8 @@ export class LayoutManager {
|
|
|
349
361
|
const grid: any[] = [];
|
|
350
362
|
// Main Grid (index 0)
|
|
351
363
|
grid.push({
|
|
352
|
-
left:
|
|
353
|
-
right:
|
|
364
|
+
left: layoutLeft,
|
|
365
|
+
right: layoutRight,
|
|
354
366
|
top: mainPaneTop + '%',
|
|
355
367
|
height: mainHeightVal + '%',
|
|
356
368
|
containLabel: false, // We handle margins explicitly
|
|
@@ -359,8 +371,8 @@ export class LayoutManager {
|
|
|
359
371
|
// Separate Panes Grids
|
|
360
372
|
paneConfigs.forEach((pane) => {
|
|
361
373
|
grid.push({
|
|
362
|
-
left:
|
|
363
|
-
right:
|
|
374
|
+
left: layoutLeft,
|
|
375
|
+
right: layoutRight,
|
|
364
376
|
top: pane.top + '%',
|
|
365
377
|
height: pane.height + '%',
|
|
366
378
|
containLabel: false,
|
|
@@ -381,12 +393,12 @@ export class LayoutManager {
|
|
|
381
393
|
// boundaryGap will be set in QFChart.ts based on padding option
|
|
382
394
|
axisLine: {
|
|
383
395
|
onZero: false,
|
|
384
|
-
show: !isMainCollapsed,
|
|
385
|
-
lineStyle: { color:
|
|
396
|
+
show: !isMainCollapsed && gridBorderShow,
|
|
397
|
+
lineStyle: { color: gridBorderColor },
|
|
386
398
|
},
|
|
387
399
|
splitLine: {
|
|
388
|
-
show: !isMainCollapsed,
|
|
389
|
-
lineStyle: { color:
|
|
400
|
+
show: !isMainCollapsed && gridShow,
|
|
401
|
+
lineStyle: { color: gridLineColor, opacity: gridLineOpacity },
|
|
390
402
|
},
|
|
391
403
|
axisLabel: {
|
|
392
404
|
show: !isMainCollapsed,
|
|
@@ -396,9 +408,8 @@ export class LayoutManager {
|
|
|
396
408
|
if (options.yAxisLabelFormatter) {
|
|
397
409
|
return options.yAxisLabelFormatter(value);
|
|
398
410
|
}
|
|
399
|
-
const decimals =
|
|
400
|
-
? options.yAxisDecimalPlaces
|
|
401
|
-
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
411
|
+
const decimals =
|
|
412
|
+
options.yAxisDecimalPlaces !== undefined ? options.yAxisDecimalPlaces : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
402
413
|
return AxisUtils.formatValue(value, decimals);
|
|
403
414
|
},
|
|
404
415
|
},
|
|
@@ -420,7 +431,7 @@ export class LayoutManager {
|
|
|
420
431
|
gridIndex: i + 1, // 0 is main
|
|
421
432
|
data: [], // Shared data
|
|
422
433
|
axisLabel: { show: false }, // Hide labels on indicator panes
|
|
423
|
-
axisLine: { show: !pane.isCollapsed, lineStyle: { color:
|
|
434
|
+
axisLine: { show: !pane.isCollapsed && gridBorderShow, lineStyle: { color: gridBorderColor } },
|
|
424
435
|
axisTick: { show: false },
|
|
425
436
|
splitLine: { show: false },
|
|
426
437
|
axisPointer: {
|
|
@@ -460,10 +471,10 @@ export class LayoutManager {
|
|
|
460
471
|
max: mainYAxisMax,
|
|
461
472
|
gridIndex: 0,
|
|
462
473
|
splitLine: {
|
|
463
|
-
show: !isMainCollapsed,
|
|
464
|
-
lineStyle: { color:
|
|
474
|
+
show: !isMainCollapsed && gridShow,
|
|
475
|
+
lineStyle: { color: gridLineColor, opacity: gridLineOpacity },
|
|
465
476
|
},
|
|
466
|
-
axisLine: { show: !isMainCollapsed, lineStyle: { color:
|
|
477
|
+
axisLine: { show: !isMainCollapsed && gridBorderShow, lineStyle: { color: gridBorderColor } },
|
|
467
478
|
axisLabel: {
|
|
468
479
|
show: !isMainCollapsed,
|
|
469
480
|
color: '#94a3b8',
|
|
@@ -472,9 +483,8 @@ export class LayoutManager {
|
|
|
472
483
|
if (options.yAxisLabelFormatter) {
|
|
473
484
|
return options.yAxisLabelFormatter(value);
|
|
474
485
|
}
|
|
475
|
-
const decimals =
|
|
476
|
-
? options.yAxisDecimalPlaces
|
|
477
|
-
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
486
|
+
const decimals =
|
|
487
|
+
options.yAxisDecimalPlaces !== undefined ? options.yAxisDecimalPlaces : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
478
488
|
return AxisUtils.formatValue(value, decimals);
|
|
479
489
|
},
|
|
480
490
|
},
|
|
@@ -510,7 +520,11 @@ export class LayoutManager {
|
|
|
510
520
|
|
|
511
521
|
// Check if this is a shape with price-relative positioning
|
|
512
522
|
const isShapeWithPriceLocation =
|
|
513
|
-
plot.options.style === 'shape' &&
|
|
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');
|
|
514
528
|
|
|
515
529
|
if (visualOnlyStyles.includes(plot.options.style)) {
|
|
516
530
|
// Assign these to a separate Y-axis so they don't affect price scale
|
|
@@ -571,7 +585,7 @@ export class LayoutManager {
|
|
|
571
585
|
// Create Y-axes for incompatible plots
|
|
572
586
|
// nextYAxisIndex already incremented in the loop above, so we know how many axes we need
|
|
573
587
|
const numOverlayAxes = overlayYAxisMap.size > 0 ? nextYAxisIndex - 1 : 0;
|
|
574
|
-
|
|
588
|
+
|
|
575
589
|
// Track which overlay axes are for visual-only plots (background, barcolor, etc.)
|
|
576
590
|
const visualOnlyAxes = new Set<number>();
|
|
577
591
|
overlayYAxisMap.forEach((yAxisIdx, plotKey) => {
|
|
@@ -585,11 +599,11 @@ export class LayoutManager {
|
|
|
585
599
|
});
|
|
586
600
|
});
|
|
587
601
|
});
|
|
588
|
-
|
|
602
|
+
|
|
589
603
|
for (let i = 0; i < numOverlayAxes; i++) {
|
|
590
604
|
const yAxisIndex = i + 1; // Y-axis indices start at 1 for overlays
|
|
591
605
|
const isVisualOnly = visualOnlyAxes.has(yAxisIndex);
|
|
592
|
-
|
|
606
|
+
|
|
593
607
|
yAxis.push({
|
|
594
608
|
position: 'left',
|
|
595
609
|
scale: !isVisualOnly, // Disable scaling for visual-only plots
|
|
@@ -613,8 +627,8 @@ export class LayoutManager {
|
|
|
613
627
|
max: AxisUtils.createMaxFunction(yAxisPaddingPercent),
|
|
614
628
|
gridIndex: i + 1,
|
|
615
629
|
splitLine: {
|
|
616
|
-
show: !pane.isCollapsed,
|
|
617
|
-
lineStyle: { color:
|
|
630
|
+
show: !pane.isCollapsed && gridShow,
|
|
631
|
+
lineStyle: { color: gridLineColor, opacity: gridLineOpacity * 0.6 },
|
|
618
632
|
},
|
|
619
633
|
axisLabel: {
|
|
620
634
|
show: !pane.isCollapsed,
|
|
@@ -625,31 +639,34 @@ export class LayoutManager {
|
|
|
625
639
|
if (options.yAxisLabelFormatter) {
|
|
626
640
|
return options.yAxisLabelFormatter(value);
|
|
627
641
|
}
|
|
628
|
-
const decimals =
|
|
629
|
-
|
|
630
|
-
|
|
642
|
+
const decimals =
|
|
643
|
+
options.yAxisDecimalPlaces !== undefined
|
|
644
|
+
? options.yAxisDecimalPlaces
|
|
645
|
+
: AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
|
|
631
646
|
return AxisUtils.formatValue(value, decimals);
|
|
632
647
|
},
|
|
633
648
|
},
|
|
634
|
-
axisLine: { show: !pane.isCollapsed, lineStyle: { color:
|
|
649
|
+
axisLine: { show: !pane.isCollapsed && gridBorderShow, lineStyle: { color: gridBorderColor } },
|
|
635
650
|
});
|
|
636
651
|
});
|
|
637
652
|
|
|
638
653
|
// --- Generate DataZoom ---
|
|
639
654
|
const dataZoom: any[] = [];
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
}
|
|
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
|
+
}
|
|
652
668
|
|
|
669
|
+
if (dzVisible) {
|
|
653
670
|
if (dzPosition === 'top') {
|
|
654
671
|
dataZoom.push({
|
|
655
672
|
type: 'slider',
|
|
@@ -697,7 +714,7 @@ export class LayoutManager {
|
|
|
697
714
|
private static calculateMaximized(
|
|
698
715
|
containerHeight: number,
|
|
699
716
|
options: QFChartOptions,
|
|
700
|
-
targetPaneIndex: number // 0 for main, 1+ for indicators
|
|
717
|
+
targetPaneIndex: number, // 0 for main, 1+ for indicators
|
|
701
718
|
): LayoutResult {
|
|
702
719
|
return {
|
|
703
720
|
grid: [],
|
|
@@ -2,6 +2,8 @@ import { OHLCV, Indicator as IndicatorType, QFChartOptions, IndicatorPlot, Indic
|
|
|
2
2
|
import { PaneConfiguration } from './LayoutManager';
|
|
3
3
|
import { SeriesRendererFactory } from './SeriesRendererFactory';
|
|
4
4
|
import { AxisUtils } from '../utils/AxisUtils';
|
|
5
|
+
import { FillRenderer, BatchedFillEntry } from './renderers/FillRenderer';
|
|
6
|
+
import { ColorUtils } from '../utils/ColorUtils';
|
|
5
7
|
|
|
6
8
|
export class SeriesBuilder {
|
|
7
9
|
private static readonly DEFAULT_COLOR = '#2962ff';
|
|
@@ -33,9 +35,7 @@ export class SeriesBuilder {
|
|
|
33
35
|
if (lineStyleType.startsWith('linestyle_')) {
|
|
34
36
|
lineStyleType = lineStyleType.replace('linestyle_', '') as any;
|
|
35
37
|
}
|
|
36
|
-
const decimals = options.yAxisDecimalPlaces !== undefined
|
|
37
|
-
? options.yAxisDecimalPlaces
|
|
38
|
-
: AxisUtils.autoDetectDecimals(marketData);
|
|
38
|
+
const decimals = options.yAxisDecimalPlaces !== undefined ? options.yAxisDecimalPlaces : AxisUtils.autoDetectDecimals(marketData);
|
|
39
39
|
|
|
40
40
|
markLine = {
|
|
41
41
|
symbol: ['none', 'none'],
|
|
@@ -75,7 +75,8 @@ export class SeriesBuilder {
|
|
|
75
75
|
|
|
76
76
|
return {
|
|
77
77
|
type: 'candlestick',
|
|
78
|
-
|
|
78
|
+
id: '__candlestick__',
|
|
79
|
+
name: options.title,
|
|
79
80
|
data: data,
|
|
80
81
|
itemStyle: {
|
|
81
82
|
color: upColor,
|
|
@@ -98,7 +99,7 @@ export class SeriesBuilder {
|
|
|
98
99
|
dataIndexOffset: number = 0,
|
|
99
100
|
candlestickData?: OHLCV[], // Add candlestick data to access High/Low for positioning
|
|
100
101
|
overlayYAxisMap?: Map<string, number>, // Map of overlay indicator IDs to their Y-axis indices
|
|
101
|
-
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)
|
|
102
103
|
): { series: any[]; barColors: (string | null)[] } {
|
|
103
104
|
const series: any[] = [];
|
|
104
105
|
const barColors: (string | null)[] = new Array(totalDataLength).fill(null);
|
|
@@ -121,8 +122,17 @@ export class SeriesBuilder {
|
|
|
121
122
|
return 0;
|
|
122
123
|
});
|
|
123
124
|
|
|
125
|
+
// Collect non-gradient fill plots for batching (performance: N series → 1 series)
|
|
126
|
+
// Keyed by "xAxisIndex:yAxisIndex" to batch fills on the same axis pair
|
|
127
|
+
const pendingFills = new Map<string, { entries: BatchedFillEntry[]; xAxisIndex: number; yAxisIndex: number }>();
|
|
128
|
+
|
|
124
129
|
sortedPlots.forEach((plotName) => {
|
|
125
130
|
const plot = indicator.plots[plotName];
|
|
131
|
+
|
|
132
|
+
// display.none: don't render visually, but still populate data arrays
|
|
133
|
+
// so that fill() plots referencing this plot can find the data.
|
|
134
|
+
const isDisplayNone = plot.options.display === 'none';
|
|
135
|
+
|
|
126
136
|
const seriesName = `${id}::${plotName}`;
|
|
127
137
|
|
|
128
138
|
// Find axis index for THIS SPECIFIC PLOT
|
|
@@ -209,7 +219,7 @@ export class SeriesBuilder {
|
|
|
209
219
|
}
|
|
210
220
|
|
|
211
221
|
dataArray[offsetIndex] = value;
|
|
212
|
-
colorArray[offsetIndex] = isNaColor ? null :
|
|
222
|
+
colorArray[offsetIndex] = isNaColor ? null : pointColor || plot.options.color || SeriesBuilder.DEFAULT_COLOR;
|
|
213
223
|
optionsArray[offsetIndex] = point.options || {};
|
|
214
224
|
}
|
|
215
225
|
}
|
|
@@ -219,6 +229,9 @@ export class SeriesBuilder {
|
|
|
219
229
|
// Fill plots need the actual numeric values even when the referenced plot is invisible (color=na)
|
|
220
230
|
plotDataArrays.set(`${id}::${plotName}`, rawDataArray);
|
|
221
231
|
|
|
232
|
+
// display.none plots: data is now stored for fill references, skip rendering
|
|
233
|
+
if (isDisplayNone) return;
|
|
234
|
+
|
|
222
235
|
if (plot.options?.style?.startsWith('style_')) {
|
|
223
236
|
plot.options.style = plot.options.style.replace('style_', '') as IndicatorStyle;
|
|
224
237
|
}
|
|
@@ -254,6 +267,65 @@ export class SeriesBuilder {
|
|
|
254
267
|
return;
|
|
255
268
|
}
|
|
256
269
|
|
|
270
|
+
// Batch non-gradient fill plots for performance
|
|
271
|
+
// Instead of creating N separate ECharts custom series (one per fill),
|
|
272
|
+
// collect them and render as a single batched series per axis pair.
|
|
273
|
+
if (plot.options.style === 'fill' && plot.options.gradient !== true) {
|
|
274
|
+
const plot1Key = plot.options.plot1 ? `${id}::${plot.options.plot1}` : null;
|
|
275
|
+
const plot2Key = plot.options.plot2 ? `${id}::${plot.options.plot2}` : null;
|
|
276
|
+
|
|
277
|
+
if (plot1Key && plot2Key) {
|
|
278
|
+
const plot1Data = plotDataArrays.get(plot1Key);
|
|
279
|
+
const plot2Data = plotDataArrays.get(plot2Key);
|
|
280
|
+
|
|
281
|
+
if (plot1Data && plot2Data) {
|
|
282
|
+
// Parse per-bar colors
|
|
283
|
+
const { color: defaultColor, opacity: defaultOpacity } = ColorUtils.parseColor(
|
|
284
|
+
plot.options.color || 'rgba(128, 128, 128, 0.2)',
|
|
285
|
+
);
|
|
286
|
+
const hasPerBarColor = optionsArray.some((o: any) => o && o.color !== undefined);
|
|
287
|
+
|
|
288
|
+
const fillBarColors: { color: string; opacity: number }[] = [];
|
|
289
|
+
for (let i = 0; i < totalDataLength; i++) {
|
|
290
|
+
const opts = optionsArray[i];
|
|
291
|
+
if (hasPerBarColor && opts && opts.color !== undefined) {
|
|
292
|
+
fillBarColors[i] = ColorUtils.parseColor(opts.color);
|
|
293
|
+
} else {
|
|
294
|
+
fillBarColors[i] = { color: defaultColor, opacity: defaultOpacity };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const axisKey = `${xAxisIndex}:${yAxisIndex}`;
|
|
299
|
+
if (!pendingFills.has(axisKey)) {
|
|
300
|
+
pendingFills.set(axisKey, { entries: [], xAxisIndex, yAxisIndex });
|
|
301
|
+
}
|
|
302
|
+
pendingFills.get(axisKey)!.entries.push({
|
|
303
|
+
plot1Data,
|
|
304
|
+
plot2Data,
|
|
305
|
+
barColors: fillBarColors,
|
|
306
|
+
});
|
|
307
|
+
return; // Defer series creation to batch step below
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Skip fully transparent plots — they exist only as data sources for fills.
|
|
313
|
+
// Their data is already stored in plotDataArrays for fill references.
|
|
314
|
+
if (plot.options.color && typeof plot.options.color === 'string') {
|
|
315
|
+
const parsed = ColorUtils.parseColor(plot.options.color);
|
|
316
|
+
if (parsed.opacity < 0.01) {
|
|
317
|
+
// Check that ALL per-bar colors are also transparent (or absent)
|
|
318
|
+
const hasVisibleBarColor = colorArray.some((c: any) => {
|
|
319
|
+
if (c == null) return false;
|
|
320
|
+
const pc = ColorUtils.parseColor(c);
|
|
321
|
+
return pc.opacity >= 0.01;
|
|
322
|
+
});
|
|
323
|
+
if (!hasVisibleBarColor) {
|
|
324
|
+
return; // Skip rendering — data already in plotDataArrays for fills
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
257
329
|
// Use Factory to get appropriate renderer
|
|
258
330
|
const renderer = SeriesRendererFactory.get(plot.options.style);
|
|
259
331
|
const seriesConfig = renderer.render({
|
|
@@ -275,6 +347,38 @@ export class SeriesBuilder {
|
|
|
275
347
|
series.push(seriesConfig);
|
|
276
348
|
}
|
|
277
349
|
});
|
|
350
|
+
|
|
351
|
+
// Batch pending fills: merge multiple fill series into single batched series per axis pair
|
|
352
|
+
if (pendingFills.size > 0) {
|
|
353
|
+
const fillRenderer = new FillRenderer();
|
|
354
|
+
pendingFills.forEach(({ entries, xAxisIndex, yAxisIndex }, axisKey) => {
|
|
355
|
+
if (entries.length >= 2) {
|
|
356
|
+
// Batch multiple fills into a single ECharts custom series
|
|
357
|
+
const batchedConfig = fillRenderer.renderBatched(
|
|
358
|
+
`${id}::fills_batch_${axisKey}`,
|
|
359
|
+
xAxisIndex,
|
|
360
|
+
yAxisIndex,
|
|
361
|
+
totalDataLength,
|
|
362
|
+
entries,
|
|
363
|
+
);
|
|
364
|
+
if (batchedConfig) {
|
|
365
|
+
series.push(batchedConfig);
|
|
366
|
+
}
|
|
367
|
+
} else if (entries.length === 1) {
|
|
368
|
+
// Single fill — still use batched renderer for consistency (clip + encode)
|
|
369
|
+
const batchedConfig = fillRenderer.renderBatched(
|
|
370
|
+
`${id}::fills_batch_${axisKey}`,
|
|
371
|
+
xAxisIndex,
|
|
372
|
+
yAxisIndex,
|
|
373
|
+
totalDataLength,
|
|
374
|
+
entries,
|
|
375
|
+
);
|
|
376
|
+
if (batchedConfig) {
|
|
377
|
+
series.push(batchedConfig);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
278
382
|
});
|
|
279
383
|
|
|
280
384
|
return { series, barColors };
|