@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.
Files changed (59) hide show
  1. package/dist/index.d.ts +368 -14
  2. package/dist/qfchart.min.browser.js +34 -16
  3. package/dist/qfchart.min.es.js +34 -16
  4. package/package.json +1 -1
  5. package/src/QFChart.ts +460 -311
  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 +284 -263
  10. package/src/components/LayoutManager.ts +72 -55
  11. package/src/components/SeriesBuilder.ts +110 -6
  12. package/src/components/TableCanvasRenderer.ts +467 -0
  13. package/src/components/TableOverlayRenderer.ts +38 -9
  14. package/src/components/TooltipFormatter.ts +97 -97
  15. package/src/components/renderers/BackgroundRenderer.ts +59 -47
  16. package/src/components/renderers/BoxRenderer.ts +113 -17
  17. package/src/components/renderers/FillRenderer.ts +118 -3
  18. package/src/components/renderers/LabelRenderer.ts +35 -9
  19. package/src/components/renderers/OHLCBarRenderer.ts +171 -161
  20. package/src/components/renderers/PolylineRenderer.ts +26 -19
  21. package/src/index.ts +17 -6
  22. package/src/plugins/ABCDPatternTool/ABCDPatternDrawingRenderer.ts +112 -0
  23. package/src/plugins/ABCDPatternTool/ABCDPatternTool.ts +136 -0
  24. package/src/plugins/ABCDPatternTool/index.ts +2 -0
  25. package/src/plugins/CypherPatternTool/CypherPatternDrawingRenderer.ts +80 -0
  26. package/src/plugins/CypherPatternTool/CypherPatternTool.ts +84 -0
  27. package/src/plugins/CypherPatternTool/index.ts +2 -0
  28. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanDrawingRenderer.ts +163 -0
  29. package/src/plugins/FibSpeedResistanceFanTool/FibSpeedResistanceFanTool.ts +210 -0
  30. package/src/plugins/FibSpeedResistanceFanTool/index.ts +2 -0
  31. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionDrawingRenderer.ts +141 -0
  32. package/src/plugins/FibTrendExtensionTool/FibTrendExtensionTool.ts +188 -0
  33. package/src/plugins/FibTrendExtensionTool/index.ts +2 -0
  34. package/src/plugins/FibonacciChannelTool/FibonacciChannelDrawingRenderer.ts +128 -0
  35. package/src/plugins/FibonacciChannelTool/FibonacciChannelTool.ts +231 -0
  36. package/src/plugins/FibonacciChannelTool/index.ts +2 -0
  37. package/src/plugins/FibonacciTool/FibonacciDrawingRenderer.ts +107 -0
  38. package/src/plugins/{FibonacciTool.ts → FibonacciTool/FibonacciTool.ts} +195 -192
  39. package/src/plugins/FibonacciTool/index.ts +2 -0
  40. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersDrawingRenderer.ts +95 -0
  41. package/src/plugins/HeadAndShouldersTool/HeadAndShouldersTool.ts +97 -0
  42. package/src/plugins/HeadAndShouldersTool/index.ts +2 -0
  43. package/src/plugins/LineTool/LineDrawingRenderer.ts +49 -0
  44. package/src/plugins/{LineTool.ts → LineTool/LineTool.ts} +161 -190
  45. package/src/plugins/LineTool/index.ts +2 -0
  46. package/src/plugins/{MeasureTool.ts → MeasureTool/MeasureTool.ts} +324 -344
  47. package/src/plugins/MeasureTool/index.ts +1 -0
  48. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternDrawingRenderer.ts +106 -0
  49. package/src/plugins/ThreeDrivesPatternTool/ThreeDrivesPatternTool.ts +98 -0
  50. package/src/plugins/ThreeDrivesPatternTool/index.ts +2 -0
  51. package/src/plugins/ToolGroup.ts +211 -0
  52. package/src/plugins/TrianglePatternTool/TrianglePatternDrawingRenderer.ts +107 -0
  53. package/src/plugins/TrianglePatternTool/TrianglePatternTool.ts +98 -0
  54. package/src/plugins/TrianglePatternTool/index.ts +2 -0
  55. package/src/plugins/XABCDPatternTool/XABCDPatternDrawingRenderer.ts +178 -0
  56. package/src/plugins/XABCDPatternTool/XABCDPatternTool.ts +213 -0
  57. package/src/plugins/XABCDPatternTool/index.ts +2 -0
  58. package/src/types.ts +39 -4
  59. 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; // 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;
@@ -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: '10%',
126
- right: '10%',
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: '#334155' } },
155
+ axisLine: { show: isTarget && gridBorderShow, lineStyle: { color: gridBorderColor } },
145
156
  splitLine: {
146
- show: isTarget,
147
- lineStyle: { color: '#334155', opacity: 0.5 },
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 = options.yAxisDecimalPlaces !== undefined
188
- ? options.yAxisDecimalPlaces
189
- : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
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: '#334155', opacity: 0.5 },
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: '10%',
353
- right: '10%',
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: '10%',
363
- right: '10%',
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: '#334155' },
396
+ show: !isMainCollapsed && gridBorderShow,
397
+ lineStyle: { color: gridBorderColor },
386
398
  },
387
399
  splitLine: {
388
- show: !isMainCollapsed,
389
- lineStyle: { color: '#334155', opacity: 0.5 },
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 = options.yAxisDecimalPlaces !== undefined
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: '#334155' } },
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: '#334155', opacity: 0.5 },
474
+ show: !isMainCollapsed && gridShow,
475
+ lineStyle: { color: gridLineColor, opacity: gridLineOpacity },
465
476
  },
466
- axisLine: { show: !isMainCollapsed, lineStyle: { color: '#334155' } },
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 = options.yAxisDecimalPlaces !== undefined
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' && (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');
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: '#334155', opacity: 0.3 },
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 = options.yAxisDecimalPlaces !== undefined
629
- ? options.yAxisDecimalPlaces
630
- : AxisUtils.autoDetectDecimals(marketData as OHLCV[]);
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: '#334155' } },
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
- if (dzVisible) {
641
- // Add 'inside' zoom (pan/drag) only if zoomOnTouch is enabled (default true)
642
- const zoomOnTouch = options.dataZoom?.zoomOnTouch ?? true;
643
- if (zoomOnTouch) {
644
- dataZoom.push({
645
- type: 'inside',
646
- xAxisIndex: allXAxisIndices,
647
- start: dzStart,
648
- end: dzEnd,
649
- filterMode: 'weakFilter',
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
- name: options.title || 'Market',
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 : (pointColor || plot.options.color || SeriesBuilder.DEFAULT_COLOR);
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 };