@qfo/qfchart 0.7.3 → 0.8.0

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.
@@ -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
 
@@ -191,8 +202,8 @@ export class LayoutManager {
191
202
  },
192
203
  },
193
204
  splitLine: {
194
- show: isTarget,
195
- lineStyle: { color: '#334155', opacity: 0.5 },
205
+ show: isTarget && gridShow,
206
+ lineStyle: { color: gridLineColor, opacity: gridLineOpacity },
196
207
  },
197
208
  });
198
209
 
@@ -349,8 +360,8 @@ export class LayoutManager {
349
360
  const grid: any[] = [];
350
361
  // Main Grid (index 0)
351
362
  grid.push({
352
- left: '10%',
353
- right: '10%',
363
+ left: layoutLeft,
364
+ right: layoutRight,
354
365
  top: mainPaneTop + '%',
355
366
  height: mainHeightVal + '%',
356
367
  containLabel: false, // We handle margins explicitly
@@ -359,8 +370,8 @@ export class LayoutManager {
359
370
  // Separate Panes Grids
360
371
  paneConfigs.forEach((pane) => {
361
372
  grid.push({
362
- left: '10%',
363
- right: '10%',
373
+ left: layoutLeft,
374
+ right: layoutRight,
364
375
  top: pane.top + '%',
365
376
  height: pane.height + '%',
366
377
  containLabel: false,
@@ -381,12 +392,12 @@ export class LayoutManager {
381
392
  // boundaryGap will be set in QFChart.ts based on padding option
382
393
  axisLine: {
383
394
  onZero: false,
384
- show: !isMainCollapsed,
385
- lineStyle: { color: '#334155' },
395
+ show: !isMainCollapsed && gridBorderShow,
396
+ lineStyle: { color: gridBorderColor },
386
397
  },
387
398
  splitLine: {
388
- show: !isMainCollapsed,
389
- lineStyle: { color: '#334155', opacity: 0.5 },
399
+ show: !isMainCollapsed && gridShow,
400
+ lineStyle: { color: gridLineColor, opacity: gridLineOpacity },
390
401
  },
391
402
  axisLabel: {
392
403
  show: !isMainCollapsed,
@@ -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',
@@ -613,8 +624,8 @@ export class LayoutManager {
613
624
  max: AxisUtils.createMaxFunction(yAxisPaddingPercent),
614
625
  gridIndex: i + 1,
615
626
  splitLine: {
616
- show: !pane.isCollapsed,
617
- lineStyle: { color: '#334155', opacity: 0.3 },
627
+ show: !pane.isCollapsed && gridShow,
628
+ lineStyle: { color: gridLineColor, opacity: gridLineOpacity * 0.6 },
618
629
  },
619
630
  axisLabel: {
620
631
  show: !pane.isCollapsed,
@@ -631,7 +642,7 @@ export class LayoutManager {
631
642
  return AxisUtils.formatValue(value, decimals);
632
643
  },
633
644
  },
634
- axisLine: { show: !pane.isCollapsed, lineStyle: { color: '#334155' } },
645
+ axisLine: { show: !pane.isCollapsed && gridBorderShow, lineStyle: { color: gridBorderColor } },
635
646
  });
636
647
  });
637
648
 
@@ -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';
@@ -121,8 +123,17 @@ export class SeriesBuilder {
121
123
  return 0;
122
124
  });
123
125
 
126
+ // Collect non-gradient fill plots for batching (performance: N series → 1 series)
127
+ // Keyed by "xAxisIndex:yAxisIndex" to batch fills on the same axis pair
128
+ const pendingFills = new Map<string, { entries: BatchedFillEntry[]; xAxisIndex: number; yAxisIndex: number }>();
129
+
124
130
  sortedPlots.forEach((plotName) => {
125
131
  const plot = indicator.plots[plotName];
132
+
133
+ // display.none: don't render visually, but still populate data arrays
134
+ // so that fill() plots referencing this plot can find the data.
135
+ const isDisplayNone = plot.options.display === 'none';
136
+
126
137
  const seriesName = `${id}::${plotName}`;
127
138
 
128
139
  // Find axis index for THIS SPECIFIC PLOT
@@ -219,6 +230,9 @@ export class SeriesBuilder {
219
230
  // Fill plots need the actual numeric values even when the referenced plot is invisible (color=na)
220
231
  plotDataArrays.set(`${id}::${plotName}`, rawDataArray);
221
232
 
233
+ // display.none plots: data is now stored for fill references, skip rendering
234
+ if (isDisplayNone) return;
235
+
222
236
  if (plot.options?.style?.startsWith('style_')) {
223
237
  plot.options.style = plot.options.style.replace('style_', '') as IndicatorStyle;
224
238
  }
@@ -254,6 +268,64 @@ export class SeriesBuilder {
254
268
  return;
255
269
  }
256
270
 
271
+ // Batch non-gradient fill plots for performance
272
+ // Instead of creating N separate ECharts custom series (one per fill),
273
+ // collect them and render as a single batched series per axis pair.
274
+ if (plot.options.style === 'fill' && plot.options.gradient !== true) {
275
+ const plot1Key = plot.options.plot1 ? `${id}::${plot.options.plot1}` : null;
276
+ const plot2Key = plot.options.plot2 ? `${id}::${plot.options.plot2}` : null;
277
+
278
+ if (plot1Key && plot2Key) {
279
+ const plot1Data = plotDataArrays.get(plot1Key);
280
+ const plot2Data = plotDataArrays.get(plot2Key);
281
+
282
+ if (plot1Data && plot2Data) {
283
+ // Parse per-bar colors
284
+ const { color: defaultColor, opacity: defaultOpacity } =
285
+ ColorUtils.parseColor(plot.options.color || 'rgba(128, 128, 128, 0.2)');
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 };