@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.
- package/dist/index.d.ts +49 -2
- package/dist/qfchart.min.browser.js +18 -16
- package/dist/qfchart.min.es.js +18 -16
- package/package.json +1 -1
- package/src/QFChart.ts +372 -59
- package/src/components/GraphicBuilder.ts +284 -263
- package/src/components/LayoutManager.ts +33 -22
- package/src/components/SeriesBuilder.ts +104 -0
- 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/types.ts +11 -2
- package/src/utils/ColorUtils.ts +1 -1
|
@@ -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
|
|
|
@@ -191,8 +202,8 @@ export class LayoutManager {
|
|
|
191
202
|
},
|
|
192
203
|
},
|
|
193
204
|
splitLine: {
|
|
194
|
-
show: isTarget,
|
|
195
|
-
lineStyle: { color:
|
|
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:
|
|
353
|
-
right:
|
|
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:
|
|
363
|
-
right:
|
|
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:
|
|
395
|
+
show: !isMainCollapsed && gridBorderShow,
|
|
396
|
+
lineStyle: { color: gridBorderColor },
|
|
386
397
|
},
|
|
387
398
|
splitLine: {
|
|
388
|
-
show: !isMainCollapsed,
|
|
389
|
-
lineStyle: { color:
|
|
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:
|
|
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',
|
|
@@ -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:
|
|
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:
|
|
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 };
|