@qfo/qfchart 0.7.2 → 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 +54 -2
- package/dist/qfchart.min.browser.js +16 -14
- package/dist/qfchart.min.es.js +16 -14
- package/package.json +1 -1
- package/src/QFChart.ts +533 -58
- package/src/components/GraphicBuilder.ts +284 -263
- package/src/components/LayoutManager.ts +67 -24
- package/src/components/SeriesBuilder.ts +122 -1
- package/src/components/TableCanvasRenderer.ts +467 -0
- package/src/components/TableOverlayRenderer.ts +76 -24
- package/src/components/TooltipFormatter.ts +97 -97
- package/src/components/renderers/BackgroundRenderer.ts +59 -47
- package/src/components/renderers/BoxRenderer.ts +133 -37
- package/src/components/renderers/DrawingLineRenderer.ts +12 -16
- package/src/components/renderers/FillRenderer.ts +118 -3
- package/src/components/renderers/HistogramRenderer.ts +67 -20
- package/src/components/renderers/LabelRenderer.ts +35 -9
- package/src/components/renderers/LinefillRenderer.ts +4 -12
- package/src/components/renderers/OHLCBarRenderer.ts +171 -161
- package/src/components/renderers/PolylineRenderer.ts +32 -32
- package/src/types.ts +11 -2
- package/src/utils/ColorUtils.ts +1 -1
|
@@ -14,6 +14,12 @@ export interface PaneConfiguration {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
|
|
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
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
export interface LayoutResult {
|
|
18
24
|
grid: any[];
|
|
19
25
|
xAxis: any[];
|
|
@@ -23,6 +29,7 @@ export interface LayoutResult {
|
|
|
23
29
|
mainPaneHeight: number;
|
|
24
30
|
mainPaneTop: number;
|
|
25
31
|
pixelToPercent: number;
|
|
32
|
+
paneBoundaries: PaneBoundary[];
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
export class LayoutManager {
|
|
@@ -32,7 +39,8 @@ export class LayoutManager {
|
|
|
32
39
|
options: QFChartOptions,
|
|
33
40
|
isMainCollapsed: boolean = false,
|
|
34
41
|
maximizedPaneId: string | null = null,
|
|
35
|
-
marketData?: import('../types').OHLCV[]
|
|
42
|
+
marketData?: import('../types').OHLCV[],
|
|
43
|
+
mainHeightOverride?: number
|
|
36
44
|
): LayoutResult & { overlayYAxisMap: Map<string, number>; separatePaneYAxisOffset: number } {
|
|
37
45
|
// Calculate pixelToPercent early for maximized logic
|
|
38
46
|
let pixelToPercent = 0;
|
|
@@ -43,6 +51,17 @@ export class LayoutManager {
|
|
|
43
51
|
// Get Y-axis padding percentage (default 5%)
|
|
44
52
|
const yAxisPaddingPercent = options.yAxisPadding !== undefined ? options.yAxisPadding : 5;
|
|
45
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
|
+
|
|
46
65
|
// Identify unique separate panes (indices > 0) and sort them
|
|
47
66
|
const separatePaneIndices = Array.from(indicators.values())
|
|
48
67
|
.map((ind) => ind.paneIndex)
|
|
@@ -114,8 +133,8 @@ export class LayoutManager {
|
|
|
114
133
|
|
|
115
134
|
// Grid
|
|
116
135
|
grid.push({
|
|
117
|
-
left:
|
|
118
|
-
right:
|
|
136
|
+
left: layoutLeft,
|
|
137
|
+
right: layoutRight,
|
|
119
138
|
top: isTarget ? '5%' : '0%',
|
|
120
139
|
height: isTarget ? '90%' : '0%',
|
|
121
140
|
show: isTarget,
|
|
@@ -133,10 +152,10 @@ export class LayoutManager {
|
|
|
133
152
|
color: '#94a3b8',
|
|
134
153
|
fontFamily: options.fontFamily,
|
|
135
154
|
},
|
|
136
|
-
axisLine: { show: isTarget, lineStyle: { color:
|
|
155
|
+
axisLine: { show: isTarget && gridBorderShow, lineStyle: { color: gridBorderColor } },
|
|
137
156
|
splitLine: {
|
|
138
|
-
show: isTarget,
|
|
139
|
-
lineStyle: { color:
|
|
157
|
+
show: isTarget && gridShow,
|
|
158
|
+
lineStyle: { color: gridLineColor, opacity: gridLineOpacity },
|
|
140
159
|
},
|
|
141
160
|
});
|
|
142
161
|
|
|
@@ -183,8 +202,8 @@ export class LayoutManager {
|
|
|
183
202
|
},
|
|
184
203
|
},
|
|
185
204
|
splitLine: {
|
|
186
|
-
show: isTarget,
|
|
187
|
-
lineStyle: { color:
|
|
205
|
+
show: isTarget && gridShow,
|
|
206
|
+
lineStyle: { color: gridLineColor, opacity: gridLineOpacity },
|
|
188
207
|
},
|
|
189
208
|
});
|
|
190
209
|
|
|
@@ -283,7 +302,10 @@ export class LayoutManager {
|
|
|
283
302
|
const totalAvailable = chartAreaBottom - mainPaneTop;
|
|
284
303
|
mainHeightVal = totalAvailable - totalBottomSpace;
|
|
285
304
|
|
|
286
|
-
|
|
305
|
+
// Apply user-dragged main height override
|
|
306
|
+
if (mainHeightOverride !== undefined && mainHeightOverride > 0 && !isMainCollapsed) {
|
|
307
|
+
mainHeightVal = mainHeightOverride;
|
|
308
|
+
} else if (isMainCollapsed) {
|
|
287
309
|
mainHeightVal = 3;
|
|
288
310
|
} else {
|
|
289
311
|
// Safety check: ensure main chart has at least some space (e.g. 20%)
|
|
@@ -315,12 +337,31 @@ export class LayoutManager {
|
|
|
315
337
|
}
|
|
316
338
|
}
|
|
317
339
|
|
|
340
|
+
// --- Build pane boundaries for drag-resize ---
|
|
341
|
+
const paneBoundaries: PaneBoundary[] = [];
|
|
342
|
+
if (paneConfigs.length > 0) {
|
|
343
|
+
// Boundary between main chart and first indicator
|
|
344
|
+
paneBoundaries.push({
|
|
345
|
+
yPercent: mainPaneTop + mainHeightVal + gapPercent / 2,
|
|
346
|
+
aboveId: 'main',
|
|
347
|
+
belowId: paneConfigs[0].indicatorId || '',
|
|
348
|
+
});
|
|
349
|
+
// Boundaries between consecutive indicators
|
|
350
|
+
for (let i = 0; i < paneConfigs.length - 1; i++) {
|
|
351
|
+
paneBoundaries.push({
|
|
352
|
+
yPercent: paneConfigs[i].top + paneConfigs[i].height + gapPercent / 2,
|
|
353
|
+
aboveId: paneConfigs[i].indicatorId || '',
|
|
354
|
+
belowId: paneConfigs[i + 1].indicatorId || '',
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
318
359
|
// --- Generate Grids ---
|
|
319
360
|
const grid: any[] = [];
|
|
320
361
|
// Main Grid (index 0)
|
|
321
362
|
grid.push({
|
|
322
|
-
left:
|
|
323
|
-
right:
|
|
363
|
+
left: layoutLeft,
|
|
364
|
+
right: layoutRight,
|
|
324
365
|
top: mainPaneTop + '%',
|
|
325
366
|
height: mainHeightVal + '%',
|
|
326
367
|
containLabel: false, // We handle margins explicitly
|
|
@@ -329,8 +370,8 @@ export class LayoutManager {
|
|
|
329
370
|
// Separate Panes Grids
|
|
330
371
|
paneConfigs.forEach((pane) => {
|
|
331
372
|
grid.push({
|
|
332
|
-
left:
|
|
333
|
-
right:
|
|
373
|
+
left: layoutLeft,
|
|
374
|
+
right: layoutRight,
|
|
334
375
|
top: pane.top + '%',
|
|
335
376
|
height: pane.height + '%',
|
|
336
377
|
containLabel: false,
|
|
@@ -351,12 +392,12 @@ export class LayoutManager {
|
|
|
351
392
|
// boundaryGap will be set in QFChart.ts based on padding option
|
|
352
393
|
axisLine: {
|
|
353
394
|
onZero: false,
|
|
354
|
-
show: !isMainCollapsed,
|
|
355
|
-
lineStyle: { color:
|
|
395
|
+
show: !isMainCollapsed && gridBorderShow,
|
|
396
|
+
lineStyle: { color: gridBorderColor },
|
|
356
397
|
},
|
|
357
398
|
splitLine: {
|
|
358
|
-
show: !isMainCollapsed,
|
|
359
|
-
lineStyle: { color:
|
|
399
|
+
show: !isMainCollapsed && gridShow,
|
|
400
|
+
lineStyle: { color: gridLineColor, opacity: gridLineOpacity },
|
|
360
401
|
},
|
|
361
402
|
axisLabel: {
|
|
362
403
|
show: !isMainCollapsed,
|
|
@@ -390,7 +431,7 @@ export class LayoutManager {
|
|
|
390
431
|
gridIndex: i + 1, // 0 is main
|
|
391
432
|
data: [], // Shared data
|
|
392
433
|
axisLabel: { show: false }, // Hide labels on indicator panes
|
|
393
|
-
axisLine: { show: !pane.isCollapsed, lineStyle: { color:
|
|
434
|
+
axisLine: { show: !pane.isCollapsed && gridBorderShow, lineStyle: { color: gridBorderColor } },
|
|
394
435
|
axisTick: { show: false },
|
|
395
436
|
splitLine: { show: false },
|
|
396
437
|
axisPointer: {
|
|
@@ -430,10 +471,10 @@ export class LayoutManager {
|
|
|
430
471
|
max: mainYAxisMax,
|
|
431
472
|
gridIndex: 0,
|
|
432
473
|
splitLine: {
|
|
433
|
-
show: !isMainCollapsed,
|
|
434
|
-
lineStyle: { color:
|
|
474
|
+
show: !isMainCollapsed && gridShow,
|
|
475
|
+
lineStyle: { color: gridLineColor, opacity: gridLineOpacity },
|
|
435
476
|
},
|
|
436
|
-
axisLine: { show: !isMainCollapsed, lineStyle: { color:
|
|
477
|
+
axisLine: { show: !isMainCollapsed && gridBorderShow, lineStyle: { color: gridBorderColor } },
|
|
437
478
|
axisLabel: {
|
|
438
479
|
show: !isMainCollapsed,
|
|
439
480
|
color: '#94a3b8',
|
|
@@ -583,8 +624,8 @@ export class LayoutManager {
|
|
|
583
624
|
max: AxisUtils.createMaxFunction(yAxisPaddingPercent),
|
|
584
625
|
gridIndex: i + 1,
|
|
585
626
|
splitLine: {
|
|
586
|
-
show: !pane.isCollapsed,
|
|
587
|
-
lineStyle: { color:
|
|
627
|
+
show: !pane.isCollapsed && gridShow,
|
|
628
|
+
lineStyle: { color: gridLineColor, opacity: gridLineOpacity * 0.6 },
|
|
588
629
|
},
|
|
589
630
|
axisLabel: {
|
|
590
631
|
show: !pane.isCollapsed,
|
|
@@ -601,7 +642,7 @@ export class LayoutManager {
|
|
|
601
642
|
return AxisUtils.formatValue(value, decimals);
|
|
602
643
|
},
|
|
603
644
|
},
|
|
604
|
-
axisLine: { show: !pane.isCollapsed, lineStyle: { color:
|
|
645
|
+
axisLine: { show: !pane.isCollapsed && gridBorderShow, lineStyle: { color: gridBorderColor } },
|
|
605
646
|
});
|
|
606
647
|
});
|
|
607
648
|
|
|
@@ -658,6 +699,7 @@ export class LayoutManager {
|
|
|
658
699
|
mainPaneHeight: mainHeightVal,
|
|
659
700
|
mainPaneTop,
|
|
660
701
|
pixelToPercent,
|
|
702
|
+
paneBoundaries,
|
|
661
703
|
overlayYAxisMap,
|
|
662
704
|
separatePaneYAxisOffset,
|
|
663
705
|
};
|
|
@@ -677,6 +719,7 @@ export class LayoutManager {
|
|
|
677
719
|
mainPaneHeight: 0,
|
|
678
720
|
mainPaneTop: 0,
|
|
679
721
|
pixelToPercent: 0,
|
|
722
|
+
paneBoundaries: [],
|
|
680
723
|
} as any;
|
|
681
724
|
}
|
|
682
725
|
}
|
|
@@ -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
|
|
@@ -133,7 +144,24 @@ export class SeriesBuilder {
|
|
|
133
144
|
// IMPORTANT: If indicator is overlay (paneIndex === 0), treat all plots as overlays
|
|
134
145
|
// This allows visual-only plots (background, barcolor) to have separate Y-axes while
|
|
135
146
|
// still being on the main chart pane
|
|
136
|
-
|
|
147
|
+
let plotOverlay = plot.options.overlay;
|
|
148
|
+
|
|
149
|
+
// Fill plots inherit overlay from their referenced plots.
|
|
150
|
+
// If both referenced plots are overlay, the fill should render on the
|
|
151
|
+
// overlay pane too — otherwise its price-scale data stretches the
|
|
152
|
+
// indicator sub-pane's y-axis to extreme ranges.
|
|
153
|
+
if (plot.options.style === 'fill' && plotOverlay === undefined) {
|
|
154
|
+
const p1Name = plot.options.plot1;
|
|
155
|
+
const p2Name = plot.options.plot2;
|
|
156
|
+
if (p1Name && p2Name) {
|
|
157
|
+
const p1 = indicator.plots[p1Name];
|
|
158
|
+
const p2 = indicator.plots[p2Name];
|
|
159
|
+
if (p1?.options?.overlay === true && p2?.options?.overlay === true) {
|
|
160
|
+
plotOverlay = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
137
165
|
const isPlotOverlay = indicator.paneIndex === 0 || plotOverlay === true;
|
|
138
166
|
|
|
139
167
|
if (isPlotOverlay) {
|
|
@@ -202,6 +230,9 @@ export class SeriesBuilder {
|
|
|
202
230
|
// Fill plots need the actual numeric values even when the referenced plot is invisible (color=na)
|
|
203
231
|
plotDataArrays.set(`${id}::${plotName}`, rawDataArray);
|
|
204
232
|
|
|
233
|
+
// display.none plots: data is now stored for fill references, skip rendering
|
|
234
|
+
if (isDisplayNone) return;
|
|
235
|
+
|
|
205
236
|
if (plot.options?.style?.startsWith('style_')) {
|
|
206
237
|
plot.options.style = plot.options.style.replace('style_', '') as IndicatorStyle;
|
|
207
238
|
}
|
|
@@ -237,6 +268,64 @@ export class SeriesBuilder {
|
|
|
237
268
|
return;
|
|
238
269
|
}
|
|
239
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
|
+
|
|
240
329
|
// Use Factory to get appropriate renderer
|
|
241
330
|
const renderer = SeriesRendererFactory.get(plot.options.style);
|
|
242
331
|
const seriesConfig = renderer.render({
|
|
@@ -258,6 +347,38 @@ export class SeriesBuilder {
|
|
|
258
347
|
series.push(seriesConfig);
|
|
259
348
|
}
|
|
260
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
|
+
}
|
|
261
382
|
});
|
|
262
383
|
|
|
263
384
|
return { series, barColors };
|