@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
package/src/QFChart.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as echarts from 'echarts';
|
|
2
2
|
import { OHLCV, IndicatorPlot, QFChartOptions, Indicator as IndicatorInterface, ChartContext, Plugin } from './types';
|
|
3
3
|
import { Indicator } from './components/Indicator';
|
|
4
|
-
import { LayoutManager } from './components/LayoutManager';
|
|
4
|
+
import { LayoutManager, LayoutResult, PaneBoundary } from './components/LayoutManager';
|
|
5
5
|
import { SeriesBuilder } from './components/SeriesBuilder';
|
|
6
6
|
import { GraphicBuilder } from './components/GraphicBuilder';
|
|
7
7
|
import { TooltipFormatter } from './components/TooltipFormatter';
|
|
@@ -10,6 +10,7 @@ import { DrawingEditor } from './components/DrawingEditor';
|
|
|
10
10
|
import { EventBus } from './utils/EventBus';
|
|
11
11
|
import { AxisUtils } from './utils/AxisUtils';
|
|
12
12
|
import { TableOverlayRenderer } from './components/TableOverlayRenderer';
|
|
13
|
+
import { TableCanvasRenderer } from './components/TableCanvasRenderer';
|
|
13
14
|
|
|
14
15
|
export class QFChart implements ChartContext {
|
|
15
16
|
private chart: echarts.ECharts;
|
|
@@ -54,9 +55,11 @@ export class QFChart implements ChartContext {
|
|
|
54
55
|
const pGrid = this.chart.convertFromPixel({ gridIndex: i }, [point.x, point.y]);
|
|
55
56
|
|
|
56
57
|
if (pGrid) {
|
|
57
|
-
// Store
|
|
58
|
-
// This
|
|
59
|
-
|
|
58
|
+
// Store in real data indices (subtract padding offset).
|
|
59
|
+
// This makes drawing coordinates independent of lazy padding
|
|
60
|
+
// expansion — when _resizePadding() changes dataIndexOffset,
|
|
61
|
+
// stored coordinates stay valid without manual updating.
|
|
62
|
+
return { timeIndex: Math.round(pGrid[0]) - this.dataIndexOffset, value: pGrid[1], paneIndex: i };
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
}
|
|
@@ -64,8 +67,8 @@ export class QFChart implements ChartContext {
|
|
|
64
67
|
},
|
|
65
68
|
dataToPixel: (point: { timeIndex: number; value: number; paneIndex?: number }) => {
|
|
66
69
|
const paneIdx = point.paneIndex || 0;
|
|
67
|
-
//
|
|
68
|
-
const p = this.chart.convertToPixel({ gridIndex: paneIdx }, [point.timeIndex, point.value]);
|
|
70
|
+
// Convert real data index back to padded space for ECharts
|
|
71
|
+
const p = this.chart.convertToPixel({ gridIndex: paneIdx }, [point.timeIndex + this.dataIndexOffset, point.value]);
|
|
69
72
|
if (p) {
|
|
70
73
|
return { x: p[0], y: p[1] };
|
|
71
74
|
}
|
|
@@ -79,6 +82,12 @@ export class QFChart implements ChartContext {
|
|
|
79
82
|
private readonly defaultPadding = 0.0;
|
|
80
83
|
private padding: number;
|
|
81
84
|
private dataIndexOffset: number = 0; // Offset for phantom padding data
|
|
85
|
+
private _paddingPoints: number = 0; // Current symmetric padding (empty bars per side)
|
|
86
|
+
private readonly LAZY_MIN_PADDING = 5; // Always have a tiny buffer so edge scroll triggers
|
|
87
|
+
private readonly LAZY_MAX_PADDING = 500; // Hard cap per side
|
|
88
|
+
private readonly LAZY_CHUNK_SIZE = 50; // Bars added per expansion
|
|
89
|
+
private readonly LAZY_EDGE_THRESHOLD = 10; // Bars from edge to trigger
|
|
90
|
+
private _expandScheduled: boolean = false; // Debounce flag
|
|
82
91
|
|
|
83
92
|
// DOM Elements for Layout
|
|
84
93
|
private rootContainer: HTMLElement;
|
|
@@ -89,6 +98,21 @@ export class QFChart implements ChartContext {
|
|
|
89
98
|
private chartContainer: HTMLElement;
|
|
90
99
|
private overlayContainer: HTMLElement;
|
|
91
100
|
private _lastTables: any[] = [];
|
|
101
|
+
private _tableGraphicIds: string[] = []; // Track canvas table graphic IDs for cleanup
|
|
102
|
+
private _baseGraphics: any[] = []; // Non-table graphic elements (title, watermark, pane labels)
|
|
103
|
+
private _labelTooltipEl: HTMLElement | null = null; // Floating tooltip for label.set_tooltip()
|
|
104
|
+
|
|
105
|
+
// Pane drag-resize state
|
|
106
|
+
private _lastLayout: (LayoutResult & { overlayYAxisMap: Map<string, number>; separatePaneYAxisOffset: number }) | null = null;
|
|
107
|
+
private _mainHeightOverride: number | null = null;
|
|
108
|
+
private _paneDragState: {
|
|
109
|
+
startY: number;
|
|
110
|
+
aboveId: string | 'main';
|
|
111
|
+
belowId: string;
|
|
112
|
+
startAboveHeight: number;
|
|
113
|
+
startBelowHeight: number;
|
|
114
|
+
} | null = null;
|
|
115
|
+
private _paneResizeRafId: number | null = null;
|
|
92
116
|
|
|
93
117
|
constructor(container: HTMLElement, options: QFChartOptions = {}) {
|
|
94
118
|
this.rootContainer = container;
|
|
@@ -183,7 +207,8 @@ export class QFChart implements ChartContext {
|
|
|
183
207
|
// Overlay container for table rendering (positioned above ECharts canvas)
|
|
184
208
|
this.chartContainer.style.position = 'relative';
|
|
185
209
|
this.overlayContainer = document.createElement('div');
|
|
186
|
-
this.overlayContainer.style.cssText =
|
|
210
|
+
this.overlayContainer.style.cssText =
|
|
211
|
+
'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:100;overflow:hidden;';
|
|
187
212
|
this.chartContainer.appendChild(this.overlayContainer);
|
|
188
213
|
|
|
189
214
|
this.pluginManager = new PluginManager(this, this.toolbarContainer);
|
|
@@ -197,26 +222,38 @@ export class QFChart implements ChartContext {
|
|
|
197
222
|
const triggerOn = this.options.databox?.triggerOn;
|
|
198
223
|
const position = this.options.databox?.position;
|
|
199
224
|
if (triggerOn === 'click' && position === 'floating') {
|
|
200
|
-
|
|
201
|
-
this.chart.dispatchAction({
|
|
202
|
-
type: 'hideTip',
|
|
203
|
-
});
|
|
225
|
+
this.chart.dispatchAction({ type: 'hideTip' });
|
|
204
226
|
}
|
|
227
|
+
|
|
228
|
+
// Lazy padding: check if user scrolled near an edge
|
|
229
|
+
this._checkEdgeAndExpand();
|
|
205
230
|
});
|
|
206
231
|
// @ts-ignore - ECharts event handler type mismatch
|
|
207
232
|
this.chart.on('finished', (params: any) => this.events.emit('chart:updated', params)); // General chart update
|
|
208
233
|
// @ts-ignore - ECharts ZRender event handler type mismatch
|
|
209
|
-
this.chart.getZr().on('mousedown', (params: any) =>
|
|
234
|
+
this.chart.getZr().on('mousedown', (params: any) => {
|
|
235
|
+
if (!this._paneDragState) this.events.emit('mouse:down', params);
|
|
236
|
+
});
|
|
210
237
|
// @ts-ignore - ECharts ZRender event handler type mismatch
|
|
211
|
-
this.chart.getZr().on('mousemove', (params: any) =>
|
|
238
|
+
this.chart.getZr().on('mousemove', (params: any) => {
|
|
239
|
+
if (!this._paneDragState) this.events.emit('mouse:move', params);
|
|
240
|
+
});
|
|
212
241
|
// @ts-ignore - ECharts ZRender event handler type mismatch
|
|
213
242
|
this.chart.getZr().on('mouseup', (params: any) => this.events.emit('mouse:up', params));
|
|
214
243
|
// @ts-ignore - ECharts ZRender event handler type mismatch
|
|
215
|
-
this.chart.getZr().on('click', (params: any) =>
|
|
244
|
+
this.chart.getZr().on('click', (params: any) => {
|
|
245
|
+
if (!this._paneDragState) this.events.emit('mouse:click', params);
|
|
246
|
+
});
|
|
216
247
|
|
|
217
248
|
const zr = this.chart.getZr();
|
|
218
249
|
const originalSetCursorStyle = zr.setCursorStyle;
|
|
250
|
+
const self = this;
|
|
219
251
|
zr.setCursorStyle = function (cursorStyle: string) {
|
|
252
|
+
// During pane drag, force row-resize cursor
|
|
253
|
+
if (self._paneDragState) {
|
|
254
|
+
originalSetCursorStyle.call(this, 'row-resize');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
220
257
|
// Change 'grab' (default roam cursor) to 'crosshair' (more suitable for candlestick chart)
|
|
221
258
|
if (cursorStyle === 'grab') {
|
|
222
259
|
cursorStyle = 'crosshair';
|
|
@@ -228,6 +265,9 @@ export class QFChart implements ChartContext {
|
|
|
228
265
|
// Bind Drawing Events
|
|
229
266
|
this.bindDrawingEvents();
|
|
230
267
|
|
|
268
|
+
// Bind pane border drag-resize
|
|
269
|
+
this.bindPaneResizeEvents();
|
|
270
|
+
|
|
231
271
|
window.addEventListener('resize', this.resize.bind(this));
|
|
232
272
|
|
|
233
273
|
// Listen for fullscreen change to restore state if exited via ESC
|
|
@@ -252,6 +292,133 @@ export class QFChart implements ChartContext {
|
|
|
252
292
|
this.render();
|
|
253
293
|
};
|
|
254
294
|
|
|
295
|
+
// ── Pane border drag-resize ────────────────────────────────
|
|
296
|
+
private bindPaneResizeEvents(): void {
|
|
297
|
+
const MIN_MAIN = 10; // minimum main pane height %
|
|
298
|
+
const MIN_INDICATOR = 5; // minimum indicator pane height %
|
|
299
|
+
const HIT_ZONE = 6; // hit-zone in pixels (±3px from boundary center)
|
|
300
|
+
|
|
301
|
+
const zr = this.chart.getZr();
|
|
302
|
+
|
|
303
|
+
/** Find a boundary near the mouse Y position (pixels). */
|
|
304
|
+
const findBoundary = (mouseY: number): PaneBoundary | null => {
|
|
305
|
+
if (!this._lastLayout || this._lastLayout.paneBoundaries.length === 0) return null;
|
|
306
|
+
if (this.maximizedPaneId) return null; // no resize when maximized
|
|
307
|
+
const containerH = this.chart.getHeight();
|
|
308
|
+
if (containerH <= 0) return null;
|
|
309
|
+
|
|
310
|
+
for (const b of this._lastLayout.paneBoundaries) {
|
|
311
|
+
const bY = (b.yPercent / 100) * containerH;
|
|
312
|
+
if (Math.abs(mouseY - bY) <= HIT_ZONE) {
|
|
313
|
+
// Don't allow resizing collapsed panes
|
|
314
|
+
if (b.aboveId === 'main' && this.isMainCollapsed) continue;
|
|
315
|
+
const belowInd = this.indicators.get(b.belowId);
|
|
316
|
+
if (belowInd?.collapsed) continue;
|
|
317
|
+
if (b.aboveId !== 'main') {
|
|
318
|
+
const aboveInd = this.indicators.get(b.aboveId);
|
|
319
|
+
if (aboveInd?.collapsed) continue;
|
|
320
|
+
}
|
|
321
|
+
return b;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/** Get current height of a pane. */
|
|
328
|
+
const getPaneHeight = (id: string | 'main'): number => {
|
|
329
|
+
if (id === 'main') {
|
|
330
|
+
return this._lastLayout?.mainPaneHeight ?? 50;
|
|
331
|
+
}
|
|
332
|
+
const ind = this.indicators.get(id);
|
|
333
|
+
return ind?.height ?? 15;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// --- ZR event handlers ---
|
|
337
|
+
|
|
338
|
+
zr.on('mousemove', (e: any) => {
|
|
339
|
+
if (this._paneDragState) {
|
|
340
|
+
// Active drag: compute new heights
|
|
341
|
+
const deltaY = e.offsetY - this._paneDragState.startY;
|
|
342
|
+
const containerH = this.chart.getHeight();
|
|
343
|
+
if (containerH <= 0) return;
|
|
344
|
+
const deltaPct = (deltaY / containerH) * 100;
|
|
345
|
+
|
|
346
|
+
const minAbove = this._paneDragState.aboveId === 'main' ? MIN_MAIN : MIN_INDICATOR;
|
|
347
|
+
const minBelow = MIN_INDICATOR;
|
|
348
|
+
|
|
349
|
+
let newAbove = this._paneDragState.startAboveHeight + deltaPct;
|
|
350
|
+
let newBelow = this._paneDragState.startBelowHeight - deltaPct;
|
|
351
|
+
|
|
352
|
+
// Clamp
|
|
353
|
+
if (newAbove < minAbove) {
|
|
354
|
+
newAbove = minAbove;
|
|
355
|
+
newBelow = this._paneDragState.startAboveHeight + this._paneDragState.startBelowHeight - minAbove;
|
|
356
|
+
}
|
|
357
|
+
if (newBelow < minBelow) {
|
|
358
|
+
newBelow = minBelow;
|
|
359
|
+
newAbove = this._paneDragState.startAboveHeight + this._paneDragState.startBelowHeight - minBelow;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Apply heights
|
|
363
|
+
if (this._paneDragState.aboveId === 'main') {
|
|
364
|
+
this._mainHeightOverride = newAbove;
|
|
365
|
+
} else {
|
|
366
|
+
const aboveInd = this.indicators.get(this._paneDragState.aboveId);
|
|
367
|
+
if (aboveInd) aboveInd.height = newAbove;
|
|
368
|
+
}
|
|
369
|
+
const belowInd = this.indicators.get(this._paneDragState.belowId);
|
|
370
|
+
if (belowInd) belowInd.height = newBelow;
|
|
371
|
+
|
|
372
|
+
// Throttle re-render via rAF
|
|
373
|
+
if (!this._paneResizeRafId) {
|
|
374
|
+
this._paneResizeRafId = requestAnimationFrame(() => {
|
|
375
|
+
this._paneResizeRafId = null;
|
|
376
|
+
this.render();
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Force row-resize cursor
|
|
381
|
+
zr.setCursorStyle('row-resize');
|
|
382
|
+
e.stop?.();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Not dragging: check hover over boundary
|
|
387
|
+
const boundary = findBoundary(e.offsetY);
|
|
388
|
+
if (boundary) {
|
|
389
|
+
zr.setCursorStyle('row-resize');
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
zr.on('mousedown', (e: any) => {
|
|
394
|
+
const boundary = findBoundary(e.offsetY);
|
|
395
|
+
if (!boundary) return;
|
|
396
|
+
|
|
397
|
+
// Start drag
|
|
398
|
+
this._paneDragState = {
|
|
399
|
+
startY: e.offsetY,
|
|
400
|
+
aboveId: boundary.aboveId,
|
|
401
|
+
belowId: boundary.belowId,
|
|
402
|
+
startAboveHeight: getPaneHeight(boundary.aboveId),
|
|
403
|
+
startBelowHeight: getPaneHeight(boundary.belowId),
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
zr.setCursorStyle('row-resize');
|
|
407
|
+
e.stop?.();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
zr.on('mouseup', () => {
|
|
411
|
+
if (this._paneDragState) {
|
|
412
|
+
this._paneDragState = null;
|
|
413
|
+
if (this._paneResizeRafId) {
|
|
414
|
+
cancelAnimationFrame(this._paneResizeRafId);
|
|
415
|
+
this._paneResizeRafId = null;
|
|
416
|
+
}
|
|
417
|
+
this.render();
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
255
422
|
private bindDrawingEvents() {
|
|
256
423
|
let hideTimeout: any = null;
|
|
257
424
|
let lastHoveredGroup: any = null;
|
|
@@ -450,6 +617,47 @@ export class QFChart implements ChartContext {
|
|
|
450
617
|
}
|
|
451
618
|
}
|
|
452
619
|
});
|
|
620
|
+
|
|
621
|
+
// --- Label Tooltip ---
|
|
622
|
+
// Create floating tooltip overlay for Pine Script label.set_tooltip()
|
|
623
|
+
this._labelTooltipEl = document.createElement('div');
|
|
624
|
+
this._labelTooltipEl.style.cssText =
|
|
625
|
+
'position:absolute;display:none;pointer-events:none;z-index:200;' +
|
|
626
|
+
'background:rgba(30,41,59,0.95);color:#fff;border:1px solid #475569;' +
|
|
627
|
+
'border-radius:4px;padding:6px 10px;font-size:12px;line-height:1.5;' +
|
|
628
|
+
'white-space:pre-wrap;max-width:350px;box-shadow:0 2px 8px rgba(0,0,0,0.3);' +
|
|
629
|
+
'font-family:' +
|
|
630
|
+
(this.options.fontFamily || 'sans-serif') +
|
|
631
|
+
';';
|
|
632
|
+
this.chartContainer.appendChild(this._labelTooltipEl);
|
|
633
|
+
|
|
634
|
+
// Show tooltip on scatter item hover (labels with tooltip text)
|
|
635
|
+
this.chart.on('mouseover', { seriesType: 'scatter' }, (params: any) => {
|
|
636
|
+
const tooltipText = params.data?._tooltipText;
|
|
637
|
+
if (!tooltipText || !this._labelTooltipEl) return;
|
|
638
|
+
|
|
639
|
+
this._labelTooltipEl.textContent = tooltipText;
|
|
640
|
+
this._labelTooltipEl.style.display = 'block';
|
|
641
|
+
|
|
642
|
+
// Position below the scatter point
|
|
643
|
+
const chartRect = this.chartContainer.getBoundingClientRect();
|
|
644
|
+
const event = params.event?.event;
|
|
645
|
+
if (event) {
|
|
646
|
+
const x = event.clientX - chartRect.left;
|
|
647
|
+
const y = event.clientY - chartRect.top;
|
|
648
|
+
// Show below and slightly left of cursor
|
|
649
|
+
const tipWidth = this._labelTooltipEl.offsetWidth;
|
|
650
|
+
const left = Math.min(x - tipWidth / 2, chartRect.width - tipWidth - 8);
|
|
651
|
+
this._labelTooltipEl.style.left = Math.max(4, left) + 'px';
|
|
652
|
+
this._labelTooltipEl.style.top = y + 18 + 'px';
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
this.chart.on('mouseout', { seriesType: 'scatter' }, () => {
|
|
657
|
+
if (this._labelTooltipEl) {
|
|
658
|
+
this._labelTooltipEl.style.display = 'none';
|
|
659
|
+
}
|
|
660
|
+
});
|
|
453
661
|
}
|
|
454
662
|
|
|
455
663
|
// --- Plugin System Integration ---
|
|
@@ -669,8 +877,10 @@ export class QFChart implements ChartContext {
|
|
|
669
877
|
this.options,
|
|
670
878
|
this.isMainCollapsed,
|
|
671
879
|
this.maximizedPaneId,
|
|
672
|
-
this.marketData
|
|
880
|
+
this.marketData,
|
|
881
|
+
this._mainHeightOverride ?? undefined,
|
|
673
882
|
);
|
|
883
|
+
this._lastLayout = layout;
|
|
674
884
|
|
|
675
885
|
// Pass full padded candlestick data for shape positioning
|
|
676
886
|
// But SeriesBuilder expects 'OHLCV[]', while paddedCandlestickData is array of arrays [open,close,low,high]
|
|
@@ -692,19 +902,20 @@ export class QFChart implements ChartContext {
|
|
|
692
902
|
paddingPoints,
|
|
693
903
|
paddedOHLCVForShapes, // Pass padded OHLCV data
|
|
694
904
|
layout.overlayYAxisMap, // Pass overlay Y-axis mapping
|
|
695
|
-
layout.separatePaneYAxisOffset // Pass Y-axis offset for separate panes
|
|
905
|
+
layout.separatePaneYAxisOffset, // Pass Y-axis offset for separate panes
|
|
696
906
|
);
|
|
697
907
|
|
|
698
908
|
// Apply barColors to candlestick data
|
|
909
|
+
// TradingView behavior: barcolor() only changes body fill; borders/wicks keep default colors (green/red)
|
|
699
910
|
const coloredCandlestickData = paddedCandlestickData.map((candle: any, i: number) => {
|
|
700
911
|
if (barColors[i]) {
|
|
912
|
+
const vals = candle.value || candle;
|
|
701
913
|
return {
|
|
702
|
-
value:
|
|
914
|
+
value: vals,
|
|
703
915
|
itemStyle: {
|
|
704
|
-
color: barColors[i],
|
|
705
|
-
color0: barColors[i],
|
|
706
|
-
borderColor
|
|
707
|
-
borderColor0: barColors[i],
|
|
916
|
+
color: barColors[i], // up-candle body fill
|
|
917
|
+
color0: barColors[i], // down-candle body fill
|
|
918
|
+
// borderColor/borderColor0 intentionally omitted → inherits series default (green/red)
|
|
708
919
|
},
|
|
709
920
|
};
|
|
710
921
|
}
|
|
@@ -721,16 +932,7 @@ export class QFChart implements ChartContext {
|
|
|
721
932
|
data: coloredCandlestickData,
|
|
722
933
|
markLine: candlestickSeries.markLine, // Ensure markLine is updated
|
|
723
934
|
},
|
|
724
|
-
...indicatorSeries
|
|
725
|
-
const update: any = { data: s.data };
|
|
726
|
-
// If the series has a renderItem function (custom series like background),
|
|
727
|
-
// we MUST update it because it likely closes over variables (colorArray)
|
|
728
|
-
// from the SeriesBuilder scope which have been recreated.
|
|
729
|
-
if (s.renderItem) {
|
|
730
|
-
update.renderItem = s.renderItem;
|
|
731
|
-
}
|
|
732
|
-
return update;
|
|
733
|
-
}),
|
|
935
|
+
...indicatorSeries,
|
|
734
936
|
],
|
|
735
937
|
};
|
|
736
938
|
|
|
@@ -745,7 +947,11 @@ export class QFChart implements ChartContext {
|
|
|
745
947
|
plot.data?.forEach((entry: any) => {
|
|
746
948
|
const tables = Array.isArray(entry.value) ? entry.value : [entry.value];
|
|
747
949
|
tables.forEach((t: any) => {
|
|
748
|
-
if (t && !t._deleted)
|
|
950
|
+
if (t && !t._deleted) {
|
|
951
|
+
// Tag table with its indicator's pane for correct positioning
|
|
952
|
+
t._paneIndex = t.force_overlay ? 0 : indicator.paneIndex;
|
|
953
|
+
allTables.push(t);
|
|
954
|
+
}
|
|
749
955
|
});
|
|
750
956
|
});
|
|
751
957
|
}
|
|
@@ -762,14 +968,23 @@ export class QFChart implements ChartContext {
|
|
|
762
968
|
// Stop existing timer
|
|
763
969
|
this.stopCountdown();
|
|
764
970
|
|
|
765
|
-
if (!this.options.lastPriceLine?.showCountdown ||
|
|
971
|
+
if (!this.options.lastPriceLine?.showCountdown || this.marketData.length === 0) {
|
|
766
972
|
return;
|
|
767
973
|
}
|
|
768
974
|
|
|
975
|
+
// Auto-detect interval from market data if not explicitly set
|
|
976
|
+
let interval = this.options.interval;
|
|
977
|
+
if (!interval && this.marketData.length >= 2) {
|
|
978
|
+
const last = this.marketData[this.marketData.length - 1];
|
|
979
|
+
const prev = this.marketData[this.marketData.length - 2];
|
|
980
|
+
interval = last.time - prev.time;
|
|
981
|
+
}
|
|
982
|
+
if (!interval) return;
|
|
983
|
+
|
|
769
984
|
const updateLabel = () => {
|
|
770
985
|
if (this.marketData.length === 0) return;
|
|
771
986
|
const lastBar = this.marketData[this.marketData.length - 1];
|
|
772
|
-
const nextCloseTime = lastBar.time +
|
|
987
|
+
const nextCloseTime = lastBar.time + interval!;
|
|
773
988
|
const now = Date.now();
|
|
774
989
|
const diff = nextCloseTime - now;
|
|
775
990
|
|
|
@@ -820,9 +1035,8 @@ export class QFChart implements ChartContext {
|
|
|
820
1035
|
if (this.options.yAxisLabelFormatter) {
|
|
821
1036
|
priceStr = this.options.yAxisLabelFormatter(price);
|
|
822
1037
|
} else {
|
|
823
|
-
const decimals =
|
|
824
|
-
? this.options.yAxisDecimalPlaces
|
|
825
|
-
: AxisUtils.autoDetectDecimals(this.marketData);
|
|
1038
|
+
const decimals =
|
|
1039
|
+
this.options.yAxisDecimalPlaces !== undefined ? this.options.yAxisDecimalPlaces : AxisUtils.autoDetectDecimals(this.marketData);
|
|
826
1040
|
priceStr = AxisUtils.formatValue(price, decimals);
|
|
827
1041
|
}
|
|
828
1042
|
|
|
@@ -876,10 +1090,10 @@ export class QFChart implements ChartContext {
|
|
|
876
1090
|
height?: number;
|
|
877
1091
|
titleColor?: string;
|
|
878
1092
|
controls?: { collapse?: boolean; maximize?: boolean };
|
|
879
|
-
} = {}
|
|
1093
|
+
} = {},
|
|
880
1094
|
): Indicator {
|
|
881
1095
|
// Handle backward compatibility: prefer 'overlay' over 'isOverlay'
|
|
882
|
-
const isOverlay = options.overlay !== undefined ? options.overlay : options.isOverlay ?? false;
|
|
1096
|
+
const isOverlay = options.overlay !== undefined ? options.overlay : (options.isOverlay ?? false);
|
|
883
1097
|
let paneIndex = 0;
|
|
884
1098
|
if (!isOverlay) {
|
|
885
1099
|
// Find the next available pane index
|
|
@@ -956,9 +1170,42 @@ export class QFChart implements ChartContext {
|
|
|
956
1170
|
this._renderTableOverlays();
|
|
957
1171
|
}
|
|
958
1172
|
|
|
1173
|
+
/**
|
|
1174
|
+
* Build table canvas graphic elements from the current _lastTables.
|
|
1175
|
+
* Must be called AFTER setOption so grid rects are available from ECharts.
|
|
1176
|
+
* Returns an array of ECharts graphic elements.
|
|
1177
|
+
*/
|
|
1178
|
+
private _buildTableGraphics(): any[] {
|
|
1179
|
+
const model = this.chart.getModel() as any;
|
|
1180
|
+
const getGridRect = (paneIndex: number) => model.getComponent('grid', paneIndex)?.coordinateSystem?.getRect();
|
|
1181
|
+
const elements = TableCanvasRenderer.buildGraphicElements(this._lastTables, getGridRect);
|
|
1182
|
+
// Assign stable IDs for future merge/replace
|
|
1183
|
+
this._tableGraphicIds = [];
|
|
1184
|
+
for (let i = 0; i < elements.length; i++) {
|
|
1185
|
+
const id = `__qf_table_${i}`;
|
|
1186
|
+
elements[i].id = id;
|
|
1187
|
+
this._tableGraphicIds.push(id);
|
|
1188
|
+
}
|
|
1189
|
+
return elements;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Render table overlays after a non-replacing setOption (updateData, resize).
|
|
1194
|
+
* Uses replaceMerge to cleanly replace all graphic elements without disrupting
|
|
1195
|
+
* other interactive components (dataZoom, tooltip, etc.).
|
|
1196
|
+
*/
|
|
959
1197
|
private _renderTableOverlays(): void {
|
|
960
|
-
|
|
961
|
-
|
|
1198
|
+
// Build new table graphics
|
|
1199
|
+
const tableGraphics = this._buildTableGraphics();
|
|
1200
|
+
|
|
1201
|
+
// Combine base graphics (title, watermark) + table graphics and replace all at once.
|
|
1202
|
+
// Using replaceMerge: ['graphic'] replaces ONLY the graphic component,
|
|
1203
|
+
// leaving dataZoom, tooltip, series etc. untouched.
|
|
1204
|
+
const allGraphics = [...this._baseGraphics, ...tableGraphics];
|
|
1205
|
+
this.chart.setOption({ graphic: allGraphics }, { replaceMerge: ['graphic'] } as any);
|
|
1206
|
+
|
|
1207
|
+
// Clear DOM overlays (legacy) — keep overlay container empty
|
|
1208
|
+
TableOverlayRenderer.clearAll(this.overlayContainer);
|
|
962
1209
|
}
|
|
963
1210
|
|
|
964
1211
|
public destroy(): void {
|
|
@@ -977,10 +1224,198 @@ export class QFChart implements ChartContext {
|
|
|
977
1224
|
this.timeToIndex.set(k.time, index);
|
|
978
1225
|
});
|
|
979
1226
|
|
|
980
|
-
//
|
|
1227
|
+
// Calculate initial padding from user-configured ratio
|
|
1228
|
+
const dataLength = this.marketData.length;
|
|
1229
|
+
const initialPadding = Math.ceil(dataLength * this.padding);
|
|
1230
|
+
|
|
1231
|
+
// _paddingPoints can only grow (lazy expansion), never shrink below initial or minimum
|
|
1232
|
+
this._paddingPoints = Math.max(this._paddingPoints, initialPadding, this.LAZY_MIN_PADDING);
|
|
1233
|
+
this.dataIndexOffset = this._paddingPoints;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Expand symmetric padding to the given number of points per side.
|
|
1238
|
+
* No-op if newPaddingPoints <= current. Performs a full render() and
|
|
1239
|
+
* restores the viewport position so there is no visual jump.
|
|
1240
|
+
*/
|
|
1241
|
+
public expandPadding(newPaddingPoints: number): void {
|
|
1242
|
+
this._resizePadding(newPaddingPoints);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Resize symmetric padding to the given number of points per side.
|
|
1247
|
+
* Works for both growing and shrinking. Clamps to [min, max].
|
|
1248
|
+
* Uses merge-mode setOption to preserve drag/interaction state.
|
|
1249
|
+
*/
|
|
1250
|
+
private _resizePadding(newPaddingPoints: number): void {
|
|
1251
|
+
// Clamp to bounds
|
|
1252
|
+
const initialPadding = Math.ceil(this.marketData.length * this.padding);
|
|
1253
|
+
newPaddingPoints = Math.max(newPaddingPoints, initialPadding, this.LAZY_MIN_PADDING);
|
|
1254
|
+
newPaddingPoints = Math.min(newPaddingPoints, this.LAZY_MAX_PADDING);
|
|
1255
|
+
if (newPaddingPoints === this._paddingPoints) return;
|
|
1256
|
+
|
|
1257
|
+
// 1. Capture current viewport as absolute bar indices
|
|
1258
|
+
const oldPadding = this._paddingPoints;
|
|
1259
|
+
const oldTotal = this.marketData.length + 2 * oldPadding;
|
|
1260
|
+
const currentOption = this.chart.getOption() as any;
|
|
1261
|
+
const zoomComp = currentOption?.dataZoom?.find((dz: any) => dz.type === 'slider' || dz.type === 'inside');
|
|
1262
|
+
const oldStartIdx = zoomComp ? (zoomComp.start / 100) * oldTotal : 0;
|
|
1263
|
+
const oldEndIdx = zoomComp ? (zoomComp.end / 100) * oldTotal : oldTotal;
|
|
1264
|
+
|
|
1265
|
+
// 2. Update padding state (delta can be positive or negative)
|
|
1266
|
+
const delta = newPaddingPoints - oldPadding;
|
|
1267
|
+
this._paddingPoints = newPaddingPoints;
|
|
1268
|
+
this.dataIndexOffset = this._paddingPoints;
|
|
1269
|
+
const paddingPoints = this._paddingPoints;
|
|
1270
|
+
|
|
1271
|
+
// 3. Rebuild all data arrays with new padding
|
|
1272
|
+
const emptyCandle = { value: [NaN, NaN, NaN, NaN], itemStyle: { opacity: 0 } };
|
|
1273
|
+
const candlestickSeries = SeriesBuilder.buildCandlestickSeries(this.marketData, this.options);
|
|
1274
|
+
const paddedCandlestickData = [
|
|
1275
|
+
...Array(paddingPoints).fill(emptyCandle),
|
|
1276
|
+
...candlestickSeries.data,
|
|
1277
|
+
...Array(paddingPoints).fill(emptyCandle),
|
|
1278
|
+
];
|
|
1279
|
+
const categoryData = [
|
|
1280
|
+
...Array(paddingPoints).fill(''),
|
|
1281
|
+
...this.marketData.map((k) => new Date(k.time).toLocaleString()),
|
|
1282
|
+
...Array(paddingPoints).fill(''),
|
|
1283
|
+
];
|
|
1284
|
+
const paddedOHLCVForShapes = [...Array(paddingPoints).fill(null), ...this.marketData, ...Array(paddingPoints).fill(null)];
|
|
1285
|
+
|
|
1286
|
+
// Rebuild indicator series with new offset
|
|
1287
|
+
const layout = LayoutManager.calculate(
|
|
1288
|
+
this.chart.getHeight(),
|
|
1289
|
+
this.indicators,
|
|
1290
|
+
this.options,
|
|
1291
|
+
this.isMainCollapsed,
|
|
1292
|
+
this.maximizedPaneId,
|
|
1293
|
+
this.marketData,
|
|
1294
|
+
this._mainHeightOverride ?? undefined,
|
|
1295
|
+
);
|
|
1296
|
+
const { series: indicatorSeries, barColors } = SeriesBuilder.buildIndicatorSeries(
|
|
1297
|
+
this.indicators,
|
|
1298
|
+
this.timeToIndex,
|
|
1299
|
+
layout.paneLayout,
|
|
1300
|
+
categoryData.length,
|
|
1301
|
+
paddingPoints,
|
|
1302
|
+
paddedOHLCVForShapes,
|
|
1303
|
+
layout.overlayYAxisMap,
|
|
1304
|
+
layout.separatePaneYAxisOffset,
|
|
1305
|
+
);
|
|
1306
|
+
|
|
1307
|
+
// Apply barColors (TradingView: barcolor() only changes body fill, borders/wicks stay default)
|
|
1308
|
+
const coloredCandlestickData = paddedCandlestickData.map((candle: any, i: number) => {
|
|
1309
|
+
if (barColors[i]) {
|
|
1310
|
+
const vals = candle.value || candle;
|
|
1311
|
+
return {
|
|
1312
|
+
value: vals,
|
|
1313
|
+
itemStyle: {
|
|
1314
|
+
color: barColors[i],
|
|
1315
|
+
color0: barColors[i],
|
|
1316
|
+
},
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
return candle;
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
// 4. Calculate corrected zoom for new total length
|
|
1323
|
+
const newTotal = this.marketData.length + 2 * newPaddingPoints;
|
|
1324
|
+
const newStart = Math.max(0, ((oldStartIdx + delta) / newTotal) * 100);
|
|
1325
|
+
const newEnd = Math.min(100, ((oldEndIdx + delta) / newTotal) * 100);
|
|
1326
|
+
|
|
1327
|
+
// 5. Rebuild drawing series data with new offset so ECharts
|
|
1328
|
+
// viewport culling uses correct padded indices after expansion.
|
|
1329
|
+
const drawingSeriesUpdates: any[] = [];
|
|
1330
|
+
const drawingsByPane = new Map<number, import('./types').DrawingElement[]>();
|
|
1331
|
+
this.drawings.forEach((d) => {
|
|
1332
|
+
const paneIdx = d.paneIndex || 0;
|
|
1333
|
+
if (!drawingsByPane.has(paneIdx)) drawingsByPane.set(paneIdx, []);
|
|
1334
|
+
drawingsByPane.get(paneIdx)!.push(d);
|
|
1335
|
+
});
|
|
1336
|
+
drawingsByPane.forEach((paneDrawings) => {
|
|
1337
|
+
drawingSeriesUpdates.push({
|
|
1338
|
+
data: paneDrawings.map((d) => [
|
|
1339
|
+
d.points[0].timeIndex + this.dataIndexOffset,
|
|
1340
|
+
d.points[0].value,
|
|
1341
|
+
d.points[1].timeIndex + this.dataIndexOffset,
|
|
1342
|
+
d.points[1].value,
|
|
1343
|
+
]),
|
|
1344
|
+
});
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
// 6. Merge update — preserves drag/interaction state
|
|
1348
|
+
const updateOption: any = {
|
|
1349
|
+
xAxis: currentOption.xAxis.map(() => ({ data: categoryData })),
|
|
1350
|
+
dataZoom: [
|
|
1351
|
+
{ start: newStart, end: newEnd },
|
|
1352
|
+
{ start: newStart, end: newEnd },
|
|
1353
|
+
],
|
|
1354
|
+
series: [
|
|
1355
|
+
{ data: coloredCandlestickData, markLine: candlestickSeries.markLine },
|
|
1356
|
+
...indicatorSeries.map((s) => {
|
|
1357
|
+
const update: any = { data: s.data };
|
|
1358
|
+
if (s.renderItem) update.renderItem = s.renderItem;
|
|
1359
|
+
return update;
|
|
1360
|
+
}),
|
|
1361
|
+
...drawingSeriesUpdates,
|
|
1362
|
+
],
|
|
1363
|
+
};
|
|
1364
|
+
this.chart.setOption(updateOption, { notMerge: false });
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Check if user scrolled near an edge (expand) or away from edges (contract).
|
|
1369
|
+
* Uses requestAnimationFrame to avoid cascading re-renders inside
|
|
1370
|
+
* the ECharts dataZoom event callback.
|
|
1371
|
+
*/
|
|
1372
|
+
private _checkEdgeAndExpand(): void {
|
|
1373
|
+
if (this._expandScheduled) return;
|
|
1374
|
+
|
|
1375
|
+
const zoomComp = (this.chart.getOption() as any)?.dataZoom?.find((dz: any) => dz.type === 'slider' || dz.type === 'inside');
|
|
1376
|
+
if (!zoomComp) return;
|
|
1377
|
+
|
|
1378
|
+
const paddingPoints = this._paddingPoints;
|
|
981
1379
|
const dataLength = this.marketData.length;
|
|
982
|
-
const
|
|
983
|
-
|
|
1380
|
+
const totalLength = dataLength + 2 * paddingPoints;
|
|
1381
|
+
const startIdx = Math.round((zoomComp.start / 100) * totalLength);
|
|
1382
|
+
const endIdx = Math.round((zoomComp.end / 100) * totalLength);
|
|
1383
|
+
|
|
1384
|
+
// Count visible real candles (overlap between viewport and data range)
|
|
1385
|
+
const dataStart = paddingPoints;
|
|
1386
|
+
const dataEnd = paddingPoints + dataLength - 1;
|
|
1387
|
+
const visibleCandles = Math.max(0, Math.min(endIdx, dataEnd) - Math.max(startIdx, dataStart) + 1);
|
|
1388
|
+
|
|
1389
|
+
const nearLeftEdge = startIdx < this.LAZY_EDGE_THRESHOLD;
|
|
1390
|
+
const nearRightEdge = endIdx > totalLength - this.LAZY_EDGE_THRESHOLD;
|
|
1391
|
+
|
|
1392
|
+
// Don't expand when zoomed in very tight (fewer than 3 visible candles)
|
|
1393
|
+
if ((nearLeftEdge || nearRightEdge) && paddingPoints < this.LAZY_MAX_PADDING && visibleCandles >= 3) {
|
|
1394
|
+
this._expandScheduled = true;
|
|
1395
|
+
requestAnimationFrame(() => {
|
|
1396
|
+
this._expandScheduled = false;
|
|
1397
|
+
this._resizePadding(paddingPoints + this.LAZY_CHUNK_SIZE);
|
|
1398
|
+
});
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Contract if far from both edges and padding is larger than needed
|
|
1403
|
+
// Calculate how many padding bars are visible/near-visible on each side
|
|
1404
|
+
const leftPadUsed = Math.max(0, paddingPoints - startIdx);
|
|
1405
|
+
const rightPadUsed = Math.max(0, endIdx - (paddingPoints + dataLength - 1));
|
|
1406
|
+
const neededPadding = Math.max(
|
|
1407
|
+
leftPadUsed + this.LAZY_CHUNK_SIZE, // keep one chunk of buffer
|
|
1408
|
+
rightPadUsed + this.LAZY_CHUNK_SIZE,
|
|
1409
|
+
);
|
|
1410
|
+
|
|
1411
|
+
// Only contract if we have at least one full chunk of excess
|
|
1412
|
+
if (paddingPoints > neededPadding + this.LAZY_CHUNK_SIZE) {
|
|
1413
|
+
this._expandScheduled = true;
|
|
1414
|
+
requestAnimationFrame(() => {
|
|
1415
|
+
this._expandScheduled = false;
|
|
1416
|
+
this._resizePadding(neededPadding);
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
984
1419
|
}
|
|
985
1420
|
|
|
986
1421
|
private render(): void {
|
|
@@ -1040,8 +1475,10 @@ export class QFChart implements ChartContext {
|
|
|
1040
1475
|
this.options,
|
|
1041
1476
|
this.isMainCollapsed,
|
|
1042
1477
|
this.maximizedPaneId,
|
|
1043
|
-
this.marketData
|
|
1478
|
+
this.marketData,
|
|
1479
|
+
this._mainHeightOverride ?? undefined,
|
|
1044
1480
|
);
|
|
1481
|
+
this._lastLayout = layout;
|
|
1045
1482
|
|
|
1046
1483
|
// Convert user-provided dataZoom start/end to account for padding
|
|
1047
1484
|
// User's start/end refer to real data (0% = start of real data, 100% = end of real data)
|
|
@@ -1100,19 +1537,18 @@ export class QFChart implements ChartContext {
|
|
|
1100
1537
|
paddingPoints,
|
|
1101
1538
|
paddedOHLCVForShapes, // Pass padded OHLCV
|
|
1102
1539
|
layout.overlayYAxisMap, // Pass overlay Y-axis mapping
|
|
1103
|
-
layout.separatePaneYAxisOffset // Pass Y-axis offset for separate panes
|
|
1540
|
+
layout.separatePaneYAxisOffset, // Pass Y-axis offset for separate panes
|
|
1104
1541
|
);
|
|
1105
1542
|
|
|
1106
|
-
// Apply barColors
|
|
1543
|
+
// Apply barColors (TradingView: barcolor() only changes body fill, borders/wicks stay default)
|
|
1107
1544
|
candlestickSeries.data = candlestickSeries.data.map((candle: any, i: number) => {
|
|
1108
1545
|
if (barColors[i]) {
|
|
1546
|
+
const vals = candle.value || candle;
|
|
1109
1547
|
return {
|
|
1110
|
-
value:
|
|
1548
|
+
value: vals,
|
|
1111
1549
|
itemStyle: {
|
|
1112
1550
|
color: barColors[i],
|
|
1113
1551
|
color0: barColors[i],
|
|
1114
|
-
borderColor: barColors[i],
|
|
1115
|
-
borderColor0: barColors[i],
|
|
1116
1552
|
},
|
|
1117
1553
|
};
|
|
1118
1554
|
}
|
|
@@ -1120,7 +1556,20 @@ export class QFChart implements ChartContext {
|
|
|
1120
1556
|
});
|
|
1121
1557
|
|
|
1122
1558
|
// 3. Build Graphics
|
|
1123
|
-
const
|
|
1559
|
+
const overlayIndicators: { id: string; titleColor?: string }[] = [];
|
|
1560
|
+
this.indicators.forEach((ind, id) => {
|
|
1561
|
+
if (ind.paneIndex === 0) {
|
|
1562
|
+
overlayIndicators.push({ id, titleColor: ind.titleColor });
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
const graphic = GraphicBuilder.build(
|
|
1566
|
+
layout,
|
|
1567
|
+
this.options,
|
|
1568
|
+
this.toggleIndicator.bind(this),
|
|
1569
|
+
this.isMainCollapsed,
|
|
1570
|
+
this.maximizedPaneId,
|
|
1571
|
+
overlayIndicators,
|
|
1572
|
+
);
|
|
1124
1573
|
|
|
1125
1574
|
// 4. Build Drawings Series (One Custom Series per Pane used)
|
|
1126
1575
|
const drawingsByPane = new Map<number, import('./types').DrawingElement[]>();
|
|
@@ -1149,9 +1598,10 @@ export class QFChart implements ChartContext {
|
|
|
1149
1598
|
|
|
1150
1599
|
if (!start || !end) return;
|
|
1151
1600
|
|
|
1152
|
-
//
|
|
1153
|
-
const
|
|
1154
|
-
const
|
|
1601
|
+
// Convert real data indices to padded space for ECharts rendering
|
|
1602
|
+
const drawingOffset = this.dataIndexOffset;
|
|
1603
|
+
const p1 = api.coord([start.timeIndex + drawingOffset, start.value]);
|
|
1604
|
+
const p2 = api.coord([end.timeIndex + drawingOffset, end.value]);
|
|
1155
1605
|
|
|
1156
1606
|
const isSelected = drawing.id === this.selectedDrawingId;
|
|
1157
1607
|
|
|
@@ -1390,7 +1840,12 @@ export class QFChart implements ChartContext {
|
|
|
1390
1840
|
};
|
|
1391
1841
|
}
|
|
1392
1842
|
},
|
|
1393
|
-
data: drawings.map((d) => [
|
|
1843
|
+
data: drawings.map((d) => [
|
|
1844
|
+
d.points[0].timeIndex + this.dataIndexOffset,
|
|
1845
|
+
d.points[0].value,
|
|
1846
|
+
d.points[1].timeIndex + this.dataIndexOffset,
|
|
1847
|
+
d.points[1].value,
|
|
1848
|
+
]),
|
|
1394
1849
|
encode: { x: [0, 2], y: [1, 3] },
|
|
1395
1850
|
z: 100,
|
|
1396
1851
|
silent: false,
|
|
@@ -1427,7 +1882,11 @@ export class QFChart implements ChartContext {
|
|
|
1427
1882
|
plot.data?.forEach((entry: any) => {
|
|
1428
1883
|
const tables = Array.isArray(entry.value) ? entry.value : [entry.value];
|
|
1429
1884
|
tables.forEach((t: any) => {
|
|
1430
|
-
if (t && !t._deleted)
|
|
1885
|
+
if (t && !t._deleted) {
|
|
1886
|
+
// Tag table with its indicator's pane for correct positioning
|
|
1887
|
+
t._paneIndex = t.force_overlay ? 0 : indicator.paneIndex;
|
|
1888
|
+
allTables.push(t);
|
|
1889
|
+
}
|
|
1431
1890
|
});
|
|
1432
1891
|
});
|
|
1433
1892
|
}
|
|
@@ -1479,8 +1938,24 @@ export class QFChart implements ChartContext {
|
|
|
1479
1938
|
|
|
1480
1939
|
this.chart.setOption(option, true); // true = not merge, replace.
|
|
1481
1940
|
|
|
1482
|
-
//
|
|
1941
|
+
// Store base graphics (title, watermark, pane labels) for later re-use
|
|
1942
|
+
// in _renderTableOverlays so we can do a clean replaceMerge.
|
|
1943
|
+
this._baseGraphics = graphic;
|
|
1944
|
+
|
|
1945
|
+
// Render table graphics AFTER setOption so we can query the computed grid rects.
|
|
1946
|
+
// Uses replaceMerge to cleanly set all graphics without disrupting interactive components.
|
|
1483
1947
|
this._lastTables = allTables;
|
|
1484
|
-
|
|
1948
|
+
if (allTables.length > 0) {
|
|
1949
|
+
const tableGraphics = this._buildTableGraphics();
|
|
1950
|
+
if (tableGraphics.length > 0) {
|
|
1951
|
+
const allGraphics = [...graphic, ...tableGraphics];
|
|
1952
|
+
this.chart.setOption({ graphic: allGraphics }, { replaceMerge: ['graphic'] } as any);
|
|
1953
|
+
}
|
|
1954
|
+
} else {
|
|
1955
|
+
this._tableGraphicIds = [];
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// Clear DOM overlays (legacy)
|
|
1959
|
+
TableOverlayRenderer.clearAll(this.overlayContainer);
|
|
1485
1960
|
}
|
|
1486
1961
|
}
|