@qfo/qfchart 0.7.2 → 0.7.3

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/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';
@@ -90,6 +90,18 @@ export class QFChart implements ChartContext {
90
90
  private overlayContainer: HTMLElement;
91
91
  private _lastTables: any[] = [];
92
92
 
93
+ // Pane drag-resize state
94
+ private _lastLayout: (LayoutResult & { overlayYAxisMap: Map<string, number>; separatePaneYAxisOffset: number }) | null = null;
95
+ private _mainHeightOverride: number | null = null;
96
+ private _paneDragState: {
97
+ startY: number;
98
+ aboveId: string | 'main';
99
+ belowId: string;
100
+ startAboveHeight: number;
101
+ startBelowHeight: number;
102
+ } | null = null;
103
+ private _paneResizeRafId: number | null = null;
104
+
93
105
  constructor(container: HTMLElement, options: QFChartOptions = {}) {
94
106
  this.rootContainer = container;
95
107
  this.options = {
@@ -206,17 +218,23 @@ export class QFChart implements ChartContext {
206
218
  // @ts-ignore - ECharts event handler type mismatch
207
219
  this.chart.on('finished', (params: any) => this.events.emit('chart:updated', params)); // General chart update
208
220
  // @ts-ignore - ECharts ZRender event handler type mismatch
209
- this.chart.getZr().on('mousedown', (params: any) => this.events.emit('mouse:down', params));
221
+ this.chart.getZr().on('mousedown', (params: any) => { if (!this._paneDragState) this.events.emit('mouse:down', params); });
210
222
  // @ts-ignore - ECharts ZRender event handler type mismatch
211
- this.chart.getZr().on('mousemove', (params: any) => this.events.emit('mouse:move', params));
223
+ this.chart.getZr().on('mousemove', (params: any) => { if (!this._paneDragState) this.events.emit('mouse:move', params); });
212
224
  // @ts-ignore - ECharts ZRender event handler type mismatch
213
225
  this.chart.getZr().on('mouseup', (params: any) => this.events.emit('mouse:up', params));
214
226
  // @ts-ignore - ECharts ZRender event handler type mismatch
215
- this.chart.getZr().on('click', (params: any) => this.events.emit('mouse:click', params));
227
+ this.chart.getZr().on('click', (params: any) => { if (!this._paneDragState) this.events.emit('mouse:click', params); });
216
228
 
217
229
  const zr = this.chart.getZr();
218
230
  const originalSetCursorStyle = zr.setCursorStyle;
231
+ const self = this;
219
232
  zr.setCursorStyle = function (cursorStyle: string) {
233
+ // During pane drag, force row-resize cursor
234
+ if (self._paneDragState) {
235
+ originalSetCursorStyle.call(this, 'row-resize');
236
+ return;
237
+ }
220
238
  // Change 'grab' (default roam cursor) to 'crosshair' (more suitable for candlestick chart)
221
239
  if (cursorStyle === 'grab') {
222
240
  cursorStyle = 'crosshair';
@@ -228,6 +246,9 @@ export class QFChart implements ChartContext {
228
246
  // Bind Drawing Events
229
247
  this.bindDrawingEvents();
230
248
 
249
+ // Bind pane border drag-resize
250
+ this.bindPaneResizeEvents();
251
+
231
252
  window.addEventListener('resize', this.resize.bind(this));
232
253
 
233
254
  // Listen for fullscreen change to restore state if exited via ESC
@@ -252,6 +273,133 @@ export class QFChart implements ChartContext {
252
273
  this.render();
253
274
  };
254
275
 
276
+ // ── Pane border drag-resize ────────────────────────────────
277
+ private bindPaneResizeEvents(): void {
278
+ const MIN_MAIN = 10; // minimum main pane height %
279
+ const MIN_INDICATOR = 5; // minimum indicator pane height %
280
+ const HIT_ZONE = 6; // hit-zone in pixels (±3px from boundary center)
281
+
282
+ const zr = this.chart.getZr();
283
+
284
+ /** Find a boundary near the mouse Y position (pixels). */
285
+ const findBoundary = (mouseY: number): PaneBoundary | null => {
286
+ if (!this._lastLayout || this._lastLayout.paneBoundaries.length === 0) return null;
287
+ if (this.maximizedPaneId) return null; // no resize when maximized
288
+ const containerH = this.chart.getHeight();
289
+ if (containerH <= 0) return null;
290
+
291
+ for (const b of this._lastLayout.paneBoundaries) {
292
+ const bY = (b.yPercent / 100) * containerH;
293
+ if (Math.abs(mouseY - bY) <= HIT_ZONE) {
294
+ // Don't allow resizing collapsed panes
295
+ if (b.aboveId === 'main' && this.isMainCollapsed) continue;
296
+ const belowInd = this.indicators.get(b.belowId);
297
+ if (belowInd?.collapsed) continue;
298
+ if (b.aboveId !== 'main') {
299
+ const aboveInd = this.indicators.get(b.aboveId);
300
+ if (aboveInd?.collapsed) continue;
301
+ }
302
+ return b;
303
+ }
304
+ }
305
+ return null;
306
+ };
307
+
308
+ /** Get current height of a pane. */
309
+ const getPaneHeight = (id: string | 'main'): number => {
310
+ if (id === 'main') {
311
+ return this._lastLayout?.mainPaneHeight ?? 50;
312
+ }
313
+ const ind = this.indicators.get(id);
314
+ return ind?.height ?? 15;
315
+ };
316
+
317
+ // --- ZR event handlers ---
318
+
319
+ zr.on('mousemove', (e: any) => {
320
+ if (this._paneDragState) {
321
+ // Active drag: compute new heights
322
+ const deltaY = e.offsetY - this._paneDragState.startY;
323
+ const containerH = this.chart.getHeight();
324
+ if (containerH <= 0) return;
325
+ const deltaPct = (deltaY / containerH) * 100;
326
+
327
+ const minAbove = this._paneDragState.aboveId === 'main' ? MIN_MAIN : MIN_INDICATOR;
328
+ const minBelow = MIN_INDICATOR;
329
+
330
+ let newAbove = this._paneDragState.startAboveHeight + deltaPct;
331
+ let newBelow = this._paneDragState.startBelowHeight - deltaPct;
332
+
333
+ // Clamp
334
+ if (newAbove < minAbove) {
335
+ newAbove = minAbove;
336
+ newBelow = this._paneDragState.startAboveHeight + this._paneDragState.startBelowHeight - minAbove;
337
+ }
338
+ if (newBelow < minBelow) {
339
+ newBelow = minBelow;
340
+ newAbove = this._paneDragState.startAboveHeight + this._paneDragState.startBelowHeight - minBelow;
341
+ }
342
+
343
+ // Apply heights
344
+ if (this._paneDragState.aboveId === 'main') {
345
+ this._mainHeightOverride = newAbove;
346
+ } else {
347
+ const aboveInd = this.indicators.get(this._paneDragState.aboveId);
348
+ if (aboveInd) aboveInd.height = newAbove;
349
+ }
350
+ const belowInd = this.indicators.get(this._paneDragState.belowId);
351
+ if (belowInd) belowInd.height = newBelow;
352
+
353
+ // Throttle re-render via rAF
354
+ if (!this._paneResizeRafId) {
355
+ this._paneResizeRafId = requestAnimationFrame(() => {
356
+ this._paneResizeRafId = null;
357
+ this.render();
358
+ });
359
+ }
360
+
361
+ // Force row-resize cursor
362
+ zr.setCursorStyle('row-resize');
363
+ e.stop?.();
364
+ return;
365
+ }
366
+
367
+ // Not dragging: check hover over boundary
368
+ const boundary = findBoundary(e.offsetY);
369
+ if (boundary) {
370
+ zr.setCursorStyle('row-resize');
371
+ }
372
+ });
373
+
374
+ zr.on('mousedown', (e: any) => {
375
+ const boundary = findBoundary(e.offsetY);
376
+ if (!boundary) return;
377
+
378
+ // Start drag
379
+ this._paneDragState = {
380
+ startY: e.offsetY,
381
+ aboveId: boundary.aboveId,
382
+ belowId: boundary.belowId,
383
+ startAboveHeight: getPaneHeight(boundary.aboveId),
384
+ startBelowHeight: getPaneHeight(boundary.belowId),
385
+ };
386
+
387
+ zr.setCursorStyle('row-resize');
388
+ e.stop?.();
389
+ });
390
+
391
+ zr.on('mouseup', () => {
392
+ if (this._paneDragState) {
393
+ this._paneDragState = null;
394
+ if (this._paneResizeRafId) {
395
+ cancelAnimationFrame(this._paneResizeRafId);
396
+ this._paneResizeRafId = null;
397
+ }
398
+ this.render();
399
+ }
400
+ });
401
+ }
402
+
255
403
  private bindDrawingEvents() {
256
404
  let hideTimeout: any = null;
257
405
  let lastHoveredGroup: any = null;
@@ -669,8 +817,10 @@ export class QFChart implements ChartContext {
669
817
  this.options,
670
818
  this.isMainCollapsed,
671
819
  this.maximizedPaneId,
672
- this.marketData
820
+ this.marketData,
821
+ this._mainHeightOverride ?? undefined,
673
822
  );
823
+ this._lastLayout = layout;
674
824
 
675
825
  // Pass full padded candlestick data for shape positioning
676
826
  // But SeriesBuilder expects 'OHLCV[]', while paddedCandlestickData is array of arrays [open,close,low,high]
@@ -745,7 +895,11 @@ export class QFChart implements ChartContext {
745
895
  plot.data?.forEach((entry: any) => {
746
896
  const tables = Array.isArray(entry.value) ? entry.value : [entry.value];
747
897
  tables.forEach((t: any) => {
748
- if (t && !t._deleted) allTables.push(t);
898
+ if (t && !t._deleted) {
899
+ // Tag table with its indicator's pane for correct positioning
900
+ t._paneIndex = (t.force_overlay) ? 0 : indicator.paneIndex;
901
+ allTables.push(t);
902
+ }
749
903
  });
750
904
  });
751
905
  }
@@ -957,8 +1111,10 @@ export class QFChart implements ChartContext {
957
1111
  }
958
1112
 
959
1113
  private _renderTableOverlays(): void {
960
- const gridRect = (this.chart.getModel() as any).getComponent('grid', 0)?.coordinateSystem?.getRect();
961
- TableOverlayRenderer.render(this.overlayContainer, this._lastTables, gridRect);
1114
+ const model = this.chart.getModel() as any;
1115
+ const getGridRect = (paneIndex: number) =>
1116
+ model.getComponent('grid', paneIndex)?.coordinateSystem?.getRect();
1117
+ TableOverlayRenderer.render(this.overlayContainer, this._lastTables, getGridRect);
962
1118
  }
963
1119
 
964
1120
  public destroy(): void {
@@ -1040,8 +1196,10 @@ export class QFChart implements ChartContext {
1040
1196
  this.options,
1041
1197
  this.isMainCollapsed,
1042
1198
  this.maximizedPaneId,
1043
- this.marketData
1199
+ this.marketData,
1200
+ this._mainHeightOverride ?? undefined,
1044
1201
  );
1202
+ this._lastLayout = layout;
1045
1203
 
1046
1204
  // Convert user-provided dataZoom start/end to account for padding
1047
1205
  // User's start/end refer to real data (0% = start of real data, 100% = end of real data)
@@ -1427,7 +1585,11 @@ export class QFChart implements ChartContext {
1427
1585
  plot.data?.forEach((entry: any) => {
1428
1586
  const tables = Array.isArray(entry.value) ? entry.value : [entry.value];
1429
1587
  tables.forEach((t: any) => {
1430
- if (t && !t._deleted) allTables.push(t);
1588
+ if (t && !t._deleted) {
1589
+ // Tag table with its indicator's pane for correct positioning
1590
+ t._paneIndex = (t.force_overlay) ? 0 : indicator.paneIndex;
1591
+ allTables.push(t);
1592
+ }
1431
1593
  });
1432
1594
  });
1433
1595
  }
@@ -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;
@@ -283,7 +291,10 @@ export class LayoutManager {
283
291
  const totalAvailable = chartAreaBottom - mainPaneTop;
284
292
  mainHeightVal = totalAvailable - totalBottomSpace;
285
293
 
286
- if (isMainCollapsed) {
294
+ // Apply user-dragged main height override
295
+ if (mainHeightOverride !== undefined && mainHeightOverride > 0 && !isMainCollapsed) {
296
+ mainHeightVal = mainHeightOverride;
297
+ } else if (isMainCollapsed) {
287
298
  mainHeightVal = 3;
288
299
  } else {
289
300
  // Safety check: ensure main chart has at least some space (e.g. 20%)
@@ -315,6 +326,25 @@ export class LayoutManager {
315
326
  }
316
327
  }
317
328
 
329
+ // --- Build pane boundaries for drag-resize ---
330
+ const paneBoundaries: PaneBoundary[] = [];
331
+ if (paneConfigs.length > 0) {
332
+ // Boundary between main chart and first indicator
333
+ paneBoundaries.push({
334
+ yPercent: mainPaneTop + mainHeightVal + gapPercent / 2,
335
+ aboveId: 'main',
336
+ belowId: paneConfigs[0].indicatorId || '',
337
+ });
338
+ // Boundaries between consecutive indicators
339
+ for (let i = 0; i < paneConfigs.length - 1; i++) {
340
+ paneBoundaries.push({
341
+ yPercent: paneConfigs[i].top + paneConfigs[i].height + gapPercent / 2,
342
+ aboveId: paneConfigs[i].indicatorId || '',
343
+ belowId: paneConfigs[i + 1].indicatorId || '',
344
+ });
345
+ }
346
+ }
347
+
318
348
  // --- Generate Grids ---
319
349
  const grid: any[] = [];
320
350
  // Main Grid (index 0)
@@ -658,6 +688,7 @@ export class LayoutManager {
658
688
  mainPaneHeight: mainHeightVal,
659
689
  mainPaneTop,
660
690
  pixelToPercent,
691
+ paneBoundaries,
661
692
  overlayYAxisMap,
662
693
  separatePaneYAxisOffset,
663
694
  };
@@ -677,6 +708,7 @@ export class LayoutManager {
677
708
  mainPaneHeight: 0,
678
709
  mainPaneTop: 0,
679
710
  pixelToPercent: 0,
711
+ paneBoundaries: [],
680
712
  } as any;
681
713
  }
682
714
  }
@@ -133,7 +133,24 @@ export class SeriesBuilder {
133
133
  // IMPORTANT: If indicator is overlay (paneIndex === 0), treat all plots as overlays
134
134
  // This allows visual-only plots (background, barcolor) to have separate Y-axes while
135
135
  // still being on the main chart pane
136
- const plotOverlay = plot.options.overlay;
136
+ let plotOverlay = plot.options.overlay;
137
+
138
+ // Fill plots inherit overlay from their referenced plots.
139
+ // If both referenced plots are overlay, the fill should render on the
140
+ // overlay pane too — otherwise its price-scale data stretches the
141
+ // indicator sub-pane's y-axis to extreme ranges.
142
+ if (plot.options.style === 'fill' && plotOverlay === undefined) {
143
+ const p1Name = plot.options.plot1;
144
+ const p2Name = plot.options.plot2;
145
+ if (p1Name && p2Name) {
146
+ const p1 = indicator.plots[p1Name];
147
+ const p2 = indicator.plots[p2Name];
148
+ if (p1?.options?.overlay === true && p2?.options?.overlay === true) {
149
+ plotOverlay = true;
150
+ }
151
+ }
152
+ }
153
+
137
154
  const isPlotOverlay = indicator.paneIndex === 0 || plotOverlay === true;
138
155
 
139
156
  if (isPlotOverlay) {
@@ -30,10 +30,13 @@ export class TableOverlayRenderer {
30
30
 
31
31
  /**
32
32
  * Clear all existing table overlays and render new ones.
33
- * @param gridRect The ECharts grid rect {x, y, width, height} in pixels,
34
- * representing the actual plot area within the container.
33
+ * @param getGridRect Function that returns the ECharts grid rect for a given pane index.
35
34
  */
36
- static render(container: HTMLElement, tables: any[], gridRect?: { x: number; y: number; width: number; height: number }): void {
35
+ static render(
36
+ container: HTMLElement,
37
+ tables: any[],
38
+ getGridRect?: (paneIndex: number) => { x: number; y: number; width: number; height: number } | undefined,
39
+ ): void {
37
40
  TableOverlayRenderer.clearAll(container);
38
41
 
39
42
  // Pine Script: only the last table at each position is displayed
@@ -45,6 +48,8 @@ export class TableOverlayRenderer {
45
48
  }
46
49
 
47
50
  byPosition.forEach((tbl) => {
51
+ const paneIndex = tbl._paneIndex ?? 0;
52
+ const gridRect = getGridRect ? getGridRect(paneIndex) : undefined;
48
53
  const el = TableOverlayRenderer.buildTable(tbl);
49
54
  TableOverlayRenderer.positionTable(el, tbl.position, gridRect);
50
55
  container.appendChild(el);
@@ -59,9 +64,18 @@ export class TableOverlayRenderer {
59
64
 
60
65
  private static buildTable(tbl: any): HTMLElement {
61
66
  const table = document.createElement('table');
62
- table.style.borderCollapse = 'separate';
63
- table.style.borderSpacing = '0';
64
- table.style.pointerEvents = 'auto';
67
+ const borderWidth = tbl.border_width ?? 0;
68
+ const frameWidth = tbl.frame_width ?? 0;
69
+ // Use collapse when no visible borders — prevents sub-pixel hairlines between cells.
70
+ // Use separate when visible borders are present — so each cell's border is drawn independently.
71
+ const hasVisibleBorders = (borderWidth > 0 && !!tbl.border_color) || (frameWidth > 0 && !!tbl.frame_color);
72
+ if (hasVisibleBorders) {
73
+ table.style.borderCollapse = 'separate';
74
+ table.style.borderSpacing = '0';
75
+ } else {
76
+ table.style.borderCollapse = 'collapse';
77
+ }
78
+ table.style.pointerEvents = 'none';
65
79
  table.style.fontSize = '14px';
66
80
  table.style.lineHeight = '1.4';
67
81
  table.style.fontFamily = 'sans-serif';
@@ -75,11 +89,13 @@ export class TableOverlayRenderer {
75
89
  }
76
90
 
77
91
  // Frame (outer border)
78
- if (tbl.frame_width > 0 && tbl.frame_color) {
92
+ // Pine Script default frame_color is "no color" (transparent), so only
93
+ // draw frame when an explicit color is provided.
94
+ if (frameWidth > 0 && tbl.frame_color) {
79
95
  const { color: fc } = TableOverlayRenderer.safeParseColor(tbl.frame_color);
80
- table.style.border = `${tbl.frame_width}px solid ${fc}`;
81
- } else if (tbl.frame_width > 0) {
82
- table.style.border = `${tbl.frame_width}px solid #999`;
96
+ table.style.border = `${frameWidth}px solid ${fc}`;
97
+ } else {
98
+ table.style.border = 'none';
83
99
  }
84
100
 
85
101
  // Build merge lookup: for each cell, determine colspan/rowspan
@@ -103,6 +119,14 @@ export class TableOverlayRenderer {
103
119
  }
104
120
  }
105
121
 
122
+ // Cell border settings
123
+ // Pine Script default border_color is "no color" (transparent), so only
124
+ // draw cell borders when an explicit color is provided.
125
+ const hasCellBorders = borderWidth > 0 && !!tbl.border_color;
126
+ const borderColorStr = hasCellBorders
127
+ ? TableOverlayRenderer.safeParseColor(tbl.border_color).color
128
+ : '';
129
+
106
130
  // Build rows
107
131
  const rows = tbl.rows || 0;
108
132
  const cols = tbl.columns || 0;
@@ -126,11 +150,10 @@ export class TableOverlayRenderer {
126
150
  }
127
151
 
128
152
  // Cell borders
129
- if (tbl.border_width > 0) {
130
- const bc = tbl.border_color
131
- ? TableOverlayRenderer.safeParseColor(tbl.border_color).color
132
- : '#999';
133
- td.style.border = `${tbl.border_width}px solid ${bc}`;
153
+ if (hasCellBorders) {
154
+ td.style.border = `${borderWidth}px solid ${borderColorStr}`;
155
+ } else {
156
+ td.style.border = 'none';
134
157
  }
135
158
 
136
159
  // Get cell data
@@ -50,20 +50,13 @@ export class BoxRenderer implements SeriesRenderer {
50
50
  return { name: seriesName, type: 'custom', xAxisIndex, yAxisIndex, data: [], silent: true };
51
51
  }
52
52
 
53
- // Compute y-range for axis scaling
54
- let yMin = Infinity, yMax = -Infinity;
55
- for (const bx of boxObjects) {
56
- if (bx.top < yMin) yMin = bx.top;
57
- if (bx.top > yMax) yMax = bx.top;
58
- if (bx.bottom < yMin) yMin = bx.bottom;
59
- if (bx.bottom > yMax) yMax = bx.bottom;
60
- }
61
-
62
53
  // Use a SINGLE data entry spanning the full x-range so renderItem is always called.
63
54
  // ECharts filters a data item only when ALL its x-dimensions are on the same side
64
55
  // of the visible window. With dims 0=0 and 1=lastBar the item always straddles
65
56
  // the viewport, so renderItem fires exactly once regardless of scroll position.
66
- // Dims 2/3 are yMin/yMax for axis scaling.
57
+ // Note: We do NOT encode y-dimensions — drawing objects should not influence the
58
+ // y-axis auto-scaling. Otherwise boxes drawn at the chart's end would prevent
59
+ // the y-axis from adapting when scrolling to earlier (lower-priced) history.
67
60
  const totalBars = (context.candlestickData?.length || 0) + offset;
68
61
  const lastBarIndex = Math.max(0, totalBars - 1);
69
62
 
@@ -87,16 +80,16 @@ export class BoxRenderer implements SeriesRenderer {
87
80
  let w = pBottomRight[0] - pTopLeft[0];
88
81
  let h = pBottomRight[1] - pTopLeft[1];
89
82
 
90
- // Handle extend (horizontal borders)
83
+ // Handle extend (none/n | left/l | right/r | both/b)
91
84
  const extend = bx.extend || 'none';
92
- if (extend !== 'none') {
85
+ if (extend !== 'none' && extend !== 'n') {
93
86
  const cs = params.coordSys;
94
- if (extend === 'left' || extend === 'both') {
87
+ if (extend === 'left' || extend === 'l' || extend === 'both' || extend === 'b') {
95
88
  x = cs.x;
96
- w = (extend === 'both') ? cs.width : (pBottomRight[0] - cs.x);
89
+ w = (extend === 'both' || extend === 'b') ? cs.width : (pBottomRight[0] - cs.x);
97
90
  }
98
- if (extend === 'right' || extend === 'both') {
99
- if (extend === 'right') {
91
+ if (extend === 'right' || extend === 'r' || extend === 'both' || extend === 'b') {
92
+ if (extend === 'right' || extend === 'r') {
100
93
  w = cs.x + cs.width - pTopLeft[0];
101
94
  }
102
95
  }
@@ -107,13 +100,18 @@ export class BoxRenderer implements SeriesRenderer {
107
100
  children.push({
108
101
  type: 'rect',
109
102
  shape: { x, y, width: w, height: h },
110
- style: { fill: bgColor },
103
+ style: { fill: bgColor, stroke: 'none' },
111
104
  });
112
105
 
113
106
  // Border rect (on top of fill)
114
- const borderColor = normalizeColor(bx.border_color) || '#2962ff';
107
+ // border_color = na means no border (na resolves to NaN or undefined)
108
+ const rawBorderColor = bx.border_color;
109
+ const isNaBorder = rawBorderColor === null || rawBorderColor === undefined ||
110
+ (typeof rawBorderColor === 'number' && isNaN(rawBorderColor)) ||
111
+ rawBorderColor === 'na' || rawBorderColor === 'NaN';
112
+ const borderColor = isNaBorder ? null : (normalizeColor(rawBorderColor) || '#2962ff');
115
113
  const borderWidth = bx.border_width ?? 1;
116
- if (borderWidth > 0) {
114
+ if (borderWidth > 0 && borderColor) {
117
115
  children.push({
118
116
  type: 'rect',
119
117
  shape: { x, y, width: w, height: h },
@@ -150,9 +148,11 @@ export class BoxRenderer implements SeriesRenderer {
150
148
 
151
149
  return { type: 'group', children };
152
150
  },
153
- data: [[0, lastBarIndex, yMin, yMax]],
151
+ data: [[0, lastBarIndex]],
154
152
  clip: true,
155
- encode: { x: [0, 1], y: [2, 3] },
153
+ encode: { x: [0, 1] },
154
+ // Prevent ECharts visual system from overriding element colors with palette
155
+ itemStyle: { color: 'transparent', borderColor: 'transparent' },
156
156
  z: 14,
157
157
  silent: true,
158
158
  emphasis: { disabled: true },
@@ -35,20 +35,13 @@ export class DrawingLineRenderer implements SeriesRenderer {
35
35
  return { name: seriesName, type: 'custom', xAxisIndex, yAxisIndex, data: [], silent: true };
36
36
  }
37
37
 
38
- // Compute y-range for axis scaling
39
- let yMin = Infinity, yMax = -Infinity;
40
- for (const ln of lineObjects) {
41
- if (ln.y1 < yMin) yMin = ln.y1;
42
- if (ln.y1 > yMax) yMax = ln.y1;
43
- if (ln.y2 < yMin) yMin = ln.y2;
44
- if (ln.y2 > yMax) yMax = ln.y2;
45
- }
46
-
47
38
  // Use a SINGLE data entry spanning the full x-range so renderItem is always called.
48
39
  // ECharts filters a data item only when ALL its x-dimensions are on the same side
49
40
  // of the visible window. With dims 0=0 and 1=lastBar the item always straddles
50
41
  // the viewport, so renderItem fires exactly once regardless of scroll position.
51
- // Dims 2/3 are yMin/yMax for axis scaling.
42
+ // Note: We do NOT encode y-dimensions — drawing objects should not influence the
43
+ // y-axis auto-scaling. Otherwise lines drawn at the chart's end would prevent
44
+ // the y-axis from adapting when scrolling to earlier (lower-priced) history.
52
45
  const totalBars = (context.candlestickData?.length || 0) + offset;
53
46
  const lastBarIndex = Math.max(0, totalBars - 1);
54
47
 
@@ -67,9 +60,9 @@ export class DrawingLineRenderer implements SeriesRenderer {
67
60
  let p1 = api.coord([ln.x1 + xOff, ln.y1]);
68
61
  let p2 = api.coord([ln.x2 + xOff, ln.y2]);
69
62
 
70
- // Handle extend (none | left | right | both)
63
+ // Handle extend (none/n | left/l | right/r | both/b)
71
64
  const extend = ln.extend || 'none';
72
- if (extend !== 'none') {
65
+ if (extend !== 'none' && extend !== 'n') {
73
66
  const cs = params.coordSys;
74
67
  [p1, p2] = this.extendLine(p1, p2, extend, cs.x, cs.x + cs.width, cs.y, cs.y + cs.height);
75
68
  }
@@ -81,6 +74,7 @@ export class DrawingLineRenderer implements SeriesRenderer {
81
74
  type: 'line',
82
75
  shape: { x1: p1[0], y1: p1[1], x2: p2[0], y2: p2[1] },
83
76
  style: {
77
+ fill: 'none',
84
78
  stroke: color,
85
79
  lineWidth,
86
80
  lineDash: this.getDashPattern(ln.style),
@@ -100,9 +94,11 @@ export class DrawingLineRenderer implements SeriesRenderer {
100
94
 
101
95
  return { type: 'group', children };
102
96
  },
103
- data: [[0, lastBarIndex, yMin, yMax]],
97
+ data: [[0, lastBarIndex]],
104
98
  clip: true,
105
- encode: { x: [0, 1], y: [2, 3] },
99
+ encode: { x: [0, 1] },
100
+ // Prevent ECharts visual system from overriding element colors with palette
101
+ itemStyle: { color: 'transparent', borderColor: 'transparent' },
106
102
  z: 15,
107
103
  silent: true,
108
104
  emphasis: { disabled: true },
@@ -151,10 +147,10 @@ export class DrawingLineRenderer implements SeriesRenderer {
151
147
  let newP1 = p1;
152
148
  let newP2 = p2;
153
149
 
154
- if (extend === 'right' || extend === 'both') {
150
+ if (extend === 'right' || extend === 'r' || extend === 'both' || extend === 'b') {
155
151
  newP2 = extendPoint(p1, [dx, dy]);
156
152
  }
157
- if (extend === 'left' || extend === 'both') {
153
+ if (extend === 'left' || extend === 'l' || extend === 'both' || extend === 'b') {
158
154
  newP1 = extendPoint(p2, [-dx, -dy]);
159
155
  }
160
156