@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.
@@ -1,97 +1,97 @@
1
- import { QFChartOptions } from "../types";
2
-
3
- export class TooltipFormatter {
4
- public static format(params: any[], options: QFChartOptions): string {
5
- if (!params || params.length === 0) return "";
6
-
7
- const marketName = options.title || "Market";
8
- const upColor = options.upColor || "#00da3c";
9
- const downColor = options.downColor || "#ec0000";
10
- const fontFamily = options.fontFamily || "sans-serif";
11
-
12
- // 1. Header: Date/Time (from the first param)
13
- const date = params[0].axisValue;
14
- let html = `<div style="font-weight: bold; margin-bottom: 5px; color: #cbd5e1; font-family: ${fontFamily};">${date}</div>`;
15
-
16
- // 2. Separate Market Data (Candlestick) from Indicators
17
- const marketSeries = params.find(
18
- (p: any) => p.seriesType === "candlestick"
19
- );
20
- const indicatorParams = params.filter(
21
- (p: any) => p.seriesType !== "candlestick"
22
- );
23
-
24
- // 3. Market Data Section
25
- if (marketSeries) {
26
- const [_, open, close, low, high] = marketSeries.value;
27
- const color = close >= open ? upColor : downColor;
28
-
29
- html += `
30
- <div style="margin-bottom: 8px; font-family: ${fontFamily};">
31
- <div style="display:flex; justify-content:space-between; color:${color}; font-weight:bold;">
32
- <span>${marketName}</span>
33
- </div>
34
- <div style="display: grid; grid-template-columns: auto auto; gap: 2px 15px; font-size: 0.9em; color: #cbd5e1;">
35
- <span>Open:</span> <span style="text-align: right; color: ${
36
- close >= open ? upColor : downColor
37
- }">${open}</span>
38
- <span>High:</span> <span style="text-align: right; color: ${upColor}">${high}</span>
39
- <span>Low:</span> <span style="text-align: right; color: ${downColor}">${low}</span>
40
- <span>Close:</span> <span style="text-align: right; color: ${
41
- close >= open ? upColor : downColor
42
- }">${close}</span>
43
- </div>
44
- </div>
45
- `;
46
- }
47
-
48
- // 4. Indicators Section
49
- if (indicatorParams.length > 0) {
50
- html += `<div style="border-top: 1px solid #334155; margin: 5px 0; padding-top: 5px;"></div>`;
51
-
52
- // Group by Indicator ID (extracted from seriesName "ID::PlotName")
53
- const indicators: { [key: string]: any[] } = {};
54
-
55
- indicatorParams.forEach((p: any) => {
56
- const parts = p.seriesName.split("::");
57
- const indId = parts.length > 1 ? parts[0] : "Unknown";
58
- const plotName = parts.length > 1 ? parts[1] : p.seriesName;
59
-
60
- if (!indicators[indId]) indicators[indId] = [];
61
- indicators[indId].push({ ...p, displayName: plotName });
62
- });
63
-
64
- // Render groups
65
- Object.keys(indicators).forEach((indId) => {
66
- html += `
67
- <div style="margin-top: 8px; font-family: ${fontFamily};">
68
- <div style="font-weight:bold; color: #fff; margin-bottom: 2px;">${indId}</div>
69
- `;
70
-
71
- indicators[indId].forEach((p) => {
72
- let val = p.value;
73
- if (Array.isArray(val)) {
74
- val = val[1]; // Assuming [index, value]
75
- }
76
-
77
- if (val === null || val === undefined) return;
78
-
79
- const valStr =
80
- typeof val === "number"
81
- ? val.toLocaleString(undefined, { maximumFractionDigits: 4 })
82
- : val;
83
-
84
- html += `
85
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px; padding-left: 8px;">
86
- <div>${p.marker} <span style="color: #cbd5e1;">${p.displayName}</span></div>
87
- <div style="font-size: 10px; color: #fff;padding-left:10px;">${valStr}</div>
88
- </div>`;
89
- });
90
-
91
- html += `</div>`;
92
- });
93
- }
94
-
95
- return html;
96
- }
97
- }
1
+ import { QFChartOptions } from "../types";
2
+
3
+ export class TooltipFormatter {
4
+ public static format(params: any[], options: QFChartOptions): string {
5
+ if (!params || params.length === 0) return "";
6
+
7
+ const marketName = options.title || "Market";
8
+ const upColor = options.upColor || "#00da3c";
9
+ const downColor = options.downColor || "#ec0000";
10
+ const fontFamily = options.fontFamily || "sans-serif";
11
+
12
+ // 1. Header: Date/Time (from the first param)
13
+ const date = params[0].axisValue;
14
+ let html = `<div style="font-weight: bold; margin-bottom: 5px; color: #cbd5e1; font-family: ${fontFamily};">${date}</div>`;
15
+
16
+ // 2. Separate Market Data (Candlestick) from Indicators
17
+ const marketSeries = params.find(
18
+ (p: any) => p.seriesType === "candlestick"
19
+ );
20
+ const indicatorParams = params.filter(
21
+ (p: any) => p.seriesType !== "candlestick"
22
+ );
23
+
24
+ // 3. Market Data Section
25
+ if (marketSeries) {
26
+ const [_, open, close, low, high] = marketSeries.value;
27
+ const color = close >= open ? upColor : downColor;
28
+
29
+ html += `
30
+ <div style="margin-bottom: 8px; font-family: ${fontFamily};">
31
+ <div style="display:flex; justify-content:space-between; color:${color}; font-weight:bold;">
32
+ <span>${marketName}</span>
33
+ </div>
34
+ <div style="display: grid; grid-template-columns: auto auto; gap: 2px 15px; font-size: 0.9em; color: #cbd5e1;">
35
+ <span>Open:</span> <span style="text-align: right; color: ${
36
+ close >= open ? upColor : downColor
37
+ }">${open}</span>
38
+ <span>High:</span> <span style="text-align: right; color: ${upColor}">${high}</span>
39
+ <span>Low:</span> <span style="text-align: right; color: ${downColor}">${low}</span>
40
+ <span>Close:</span> <span style="text-align: right; color: ${
41
+ close >= open ? upColor : downColor
42
+ }">${close}</span>
43
+ </div>
44
+ </div>
45
+ `;
46
+ }
47
+
48
+ // 4. Indicators Section
49
+ if (indicatorParams.length > 0) {
50
+ html += `<div style="border-top: 1px solid #334155; margin: 5px 0; padding-top: 5px;"></div>`;
51
+
52
+ // Group by Indicator ID (extracted from seriesName "ID::PlotName")
53
+ const indicators: { [key: string]: any[] } = {};
54
+
55
+ indicatorParams.forEach((p: any) => {
56
+ const parts = p.seriesName.split("::");
57
+ const indId = parts.length > 1 ? parts[0] : "Unknown";
58
+ const plotName = parts.length > 1 ? parts[1] : p.seriesName;
59
+
60
+ if (!indicators[indId]) indicators[indId] = [];
61
+ indicators[indId].push({ ...p, displayName: plotName });
62
+ });
63
+
64
+ // Render groups
65
+ Object.keys(indicators).forEach((indId) => {
66
+ html += `
67
+ <div style="margin-top: 8px; font-family: ${fontFamily};">
68
+ <div style="font-weight:bold; color: #fff; margin-bottom: 2px;">${indId}</div>
69
+ `;
70
+
71
+ indicators[indId].forEach((p) => {
72
+ let val = p.value;
73
+ if (Array.isArray(val)) {
74
+ val = val[1]; // Assuming [index, value]
75
+ }
76
+
77
+ if (val === null || val === undefined) return;
78
+
79
+ const valStr =
80
+ typeof val === "number"
81
+ ? val.toLocaleString(undefined, { maximumFractionDigits: 4 })
82
+ : val;
83
+
84
+ html += `
85
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px; padding-left: 8px;">
86
+ <div>${p.marker} <span style="color: #cbd5e1;">${p.displayName}</span></div>
87
+ <div style="font-size: 10px; color: #fff;padding-left:10px;">${valStr}</div>
88
+ </div>`;
89
+ });
90
+
91
+ html += `</div>`;
92
+ });
93
+ }
94
+
95
+ return html;
96
+ }
97
+ }
@@ -1,47 +1,59 @@
1
- import { SeriesRenderer, RenderContext } from './SeriesRenderer';
2
-
3
- export class BackgroundRenderer implements SeriesRenderer {
4
- render(context: RenderContext): any {
5
- const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray } = context;
6
-
7
- return {
8
- name: seriesName,
9
- type: 'custom',
10
- xAxisIndex: xAxisIndex,
11
- yAxisIndex: yAxisIndex,
12
- z: -10,
13
- renderItem: (params: any, api: any) => {
14
- const xVal = api.value(0);
15
- if (isNaN(xVal)) return;
16
-
17
- const start = api.coord([xVal, 0.5]); // Use 0.5 as a fixed Y-value within [0,1] range
18
- const size = api.size([1, 0]);
19
- const width = size[0];
20
- const sys = params.coordSys;
21
- const x = start[0] - width / 2;
22
- const barColor = colorArray[params.dataIndex];
23
- const val = api.value(1);
24
-
25
- if (!barColor || val === null || val === undefined || isNaN(val)) return;
26
-
27
- return {
28
- type: 'rect',
29
- shape: {
30
- x: x,
31
- y: sys.y,
32
- width: width,
33
- height: sys.height,
34
- },
35
- style: {
36
- fill: barColor,
37
- opacity: 0.3,
38
- },
39
- silent: true,
40
- };
41
- },
42
- // Normalize data values to 0.5 (middle of [0,1] range) to prevent Y-axis scaling issues
43
- // The actual value is only used to check if the background should render (non-null/non-NaN)
44
- data: dataArray.map((val, i) => [i, val !== null && val !== undefined && !isNaN(val) ? 0.5 : null]),
45
- };
46
- }
47
- }
1
+ import { SeriesRenderer, RenderContext } from './SeriesRenderer';
2
+ import { ColorUtils } from '../../utils/ColorUtils';
3
+
4
+ export class BackgroundRenderer implements SeriesRenderer {
5
+ render(context: RenderContext): any {
6
+ const { seriesName, xAxisIndex, yAxisIndex, dataArray, colorArray } = context;
7
+
8
+ // Pre-parse colors to extract embedded alpha (e.g. #RRGGBBAA → rgb + opacity)
9
+ // This avoids the double-opacity problem where ECharts multiplies a hardcoded
10
+ // opacity with the alpha already embedded in the color string.
11
+ const parsedColors: { color: string; opacity: number }[] = [];
12
+ for (let i = 0; i < colorArray.length; i++) {
13
+ parsedColors[i] = colorArray[i] ? ColorUtils.parseColor(colorArray[i]) : { color: '', opacity: 0 };
14
+ }
15
+
16
+ return {
17
+ name: seriesName,
18
+ type: 'custom',
19
+ xAxisIndex: xAxisIndex,
20
+ yAxisIndex: yAxisIndex,
21
+ z: -10,
22
+ renderItem: (params: any, api: any) => {
23
+ const xVal = api.value(0);
24
+ if (isNaN(xVal)) return;
25
+
26
+ const start = api.coord([xVal, 0.5]); // Use 0.5 as a fixed Y-value within [0,1] range
27
+ const size = api.size([1, 0]);
28
+ const width = size[0];
29
+ const sys = params.coordSys;
30
+ const x = start[0] - width / 2;
31
+ const barColor = colorArray[params.dataIndex];
32
+ const val = api.value(1);
33
+
34
+ if (!barColor || val === null || val === undefined || isNaN(val)) return;
35
+
36
+ const parsed = parsedColors[params.dataIndex];
37
+ if (!parsed || parsed.opacity <= 0) return; // Skip fully transparent
38
+
39
+ return {
40
+ type: 'rect',
41
+ shape: {
42
+ x: x,
43
+ y: sys.y,
44
+ width: width,
45
+ height: sys.height,
46
+ },
47
+ style: {
48
+ fill: parsed.color,
49
+ opacity: parsed.opacity,
50
+ },
51
+ silent: true,
52
+ };
53
+ },
54
+ // Normalize data values to 0.5 (middle of [0,1] range) to prevent Y-axis scaling issues
55
+ // The actual value is only used to check if the background should render (non-null/non-NaN)
56
+ data: dataArray.map((val, i) => [i, val !== null && val !== undefined && !isNaN(val) ? 0.5 : null]),
57
+ };
58
+ }
59
+ }
@@ -19,6 +19,38 @@ function normalizeColor(color: string | undefined): string | undefined {
19
19
  return color;
20
20
  }
21
21
 
22
+ /**
23
+ * Parse a CSS color string into { r, g, b } (0-255 each).
24
+ * Supports #rgb, #rrggbb, #rrggbbaa, rgb(), rgba().
25
+ */
26
+ function parseRGB(color: string | null | undefined): { r: number; g: number; b: number } | null {
27
+ if (!color || typeof color !== 'string') return null;
28
+ if (color.startsWith('#')) {
29
+ const hex = color.slice(1);
30
+ if (hex.length >= 6) {
31
+ const r = parseInt(hex.slice(0, 2), 16);
32
+ const g = parseInt(hex.slice(2, 4), 16);
33
+ const b = parseInt(hex.slice(4, 6), 16);
34
+ if (!isNaN(r) && !isNaN(g) && !isNaN(b)) return { r, g, b };
35
+ }
36
+ if (hex.length === 3) {
37
+ const r = parseInt(hex[0] + hex[0], 16);
38
+ const g = parseInt(hex[1] + hex[1], 16);
39
+ const b = parseInt(hex[2] + hex[2], 16);
40
+ if (!isNaN(r) && !isNaN(g) && !isNaN(b)) return { r, g, b };
41
+ }
42
+ return null;
43
+ }
44
+ const m = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
45
+ if (m) return { r: +m[1], g: +m[2], b: +m[3] };
46
+ return null;
47
+ }
48
+
49
+ /** Relative luminance (0 = black, 1 = white). */
50
+ function luminance(r: number, g: number, b: number): number {
51
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
52
+ }
53
+
22
54
  /**
23
55
  * Renderer for Pine Script box.* drawing objects.
24
56
  * Each box is defined by two corners (left,top) → (right,bottom)
@@ -50,20 +82,13 @@ export class BoxRenderer implements SeriesRenderer {
50
82
  return { name: seriesName, type: 'custom', xAxisIndex, yAxisIndex, data: [], silent: true };
51
83
  }
52
84
 
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
85
  // Use a SINGLE data entry spanning the full x-range so renderItem is always called.
63
86
  // ECharts filters a data item only when ALL its x-dimensions are on the same side
64
87
  // of the visible window. With dims 0=0 and 1=lastBar the item always straddles
65
88
  // the viewport, so renderItem fires exactly once regardless of scroll position.
66
- // Dims 2/3 are yMin/yMax for axis scaling.
89
+ // Note: We do NOT encode y-dimensions — drawing objects should not influence the
90
+ // y-axis auto-scaling. Otherwise boxes drawn at the chart's end would prevent
91
+ // the y-axis from adapting when scrolling to earlier (lower-priced) history.
67
92
  const totalBars = (context.candlestickData?.length || 0) + offset;
68
93
  const lastBarIndex = Math.max(0, totalBars - 1);
69
94
 
@@ -87,33 +112,45 @@ export class BoxRenderer implements SeriesRenderer {
87
112
  let w = pBottomRight[0] - pTopLeft[0];
88
113
  let h = pBottomRight[1] - pTopLeft[1];
89
114
 
90
- // Handle extend (horizontal borders)
115
+ // Handle extend (none/n | left/l | right/r | both/b)
91
116
  const extend = bx.extend || 'none';
92
- if (extend !== 'none') {
117
+ if (extend !== 'none' && extend !== 'n') {
93
118
  const cs = params.coordSys;
94
- if (extend === 'left' || extend === 'both') {
119
+ if (extend === 'left' || extend === 'l' || extend === 'both' || extend === 'b') {
95
120
  x = cs.x;
96
- w = (extend === 'both') ? cs.width : (pBottomRight[0] - cs.x);
121
+ w = (extend === 'both' || extend === 'b') ? cs.width : (pBottomRight[0] - cs.x);
97
122
  }
98
- if (extend === 'right' || extend === 'both') {
99
- if (extend === 'right') {
123
+ if (extend === 'right' || extend === 'r' || extend === 'both' || extend === 'b') {
124
+ if (extend === 'right' || extend === 'r') {
100
125
  w = cs.x + cs.width - pTopLeft[0];
101
126
  }
102
127
  }
103
128
  }
104
129
 
105
130
  // Background fill rect
106
- const bgColor = normalizeColor(bx.bgcolor) || '#2962ff';
107
- children.push({
108
- type: 'rect',
109
- shape: { x, y, width: w, height: h },
110
- style: { fill: bgColor },
111
- });
112
-
113
- // Border rect (on top of fill)
114
- const borderColor = normalizeColor(bx.border_color) || '#2962ff';
131
+ // bgcolor = na means no fill (na resolves to NaN or undefined)
132
+ const rawBgColor = bx.bgcolor;
133
+ const isNaBgColor = rawBgColor === null || rawBgColor === undefined ||
134
+ (typeof rawBgColor === 'number' && isNaN(rawBgColor)) ||
135
+ rawBgColor === 'na' || rawBgColor === 'NaN' || rawBgColor === '';
136
+ const bgColor = isNaBgColor ? null : (normalizeColor(rawBgColor) || '#2962ff');
137
+ if (bgColor) {
138
+ children.push({
139
+ type: 'rect',
140
+ shape: { x, y, width: w, height: h },
141
+ style: { fill: bgColor, stroke: 'none' },
142
+ });
143
+ }
144
+
145
+ // Explicit border rect (on top of fill)
146
+ // border_color = na means no border (na resolves to NaN or undefined)
147
+ const rawBorderColor = bx.border_color;
148
+ const isNaBorder = rawBorderColor === null || rawBorderColor === undefined ||
149
+ (typeof rawBorderColor === 'number' && isNaN(rawBorderColor)) ||
150
+ rawBorderColor === 'na' || rawBorderColor === 'NaN';
151
+ const borderColor = isNaBorder ? null : (normalizeColor(rawBorderColor) || '#2962ff');
115
152
  const borderWidth = bx.border_width ?? 1;
116
- if (borderWidth > 0) {
153
+ if (borderWidth > 0 && borderColor) {
117
154
  children.push({
118
155
  type: 'rect',
119
156
  shape: { x, y, width: w, height: h },
@@ -130,16 +167,38 @@ export class BoxRenderer implements SeriesRenderer {
130
167
  if (bx.text) {
131
168
  const textX = this.getTextX(x, w, bx.text_halign);
132
169
  const textY = this.getTextY(y, h, bx.text_valign);
170
+
171
+ // Auto-contrast: TradingView renders box text as bold white on dark
172
+ // backgrounds. When text_color is the default black, compute luminance
173
+ // of bgcolor and use white text if the background is dark.
174
+ let textFill = normalizeColor(bx.text_color) || '#000000';
175
+ const isDefaultTextColor = !bx.text_color || bx.text_color === '#000000' ||
176
+ bx.text_color === 'black' || bx.text_color === 'color.black';
177
+ if (isDefaultTextColor && bgColor) {
178
+ const rgb = parseRGB(bgColor);
179
+ if (rgb && luminance(rgb.r, rgb.g, rgb.b) < 0.5) {
180
+ textFill = '#FFFFFF';
181
+ }
182
+ }
183
+
184
+ // TradingView renders box text bold by default (format_none → bold)
185
+ const isBold = !bx.text_formatting || bx.text_formatting === 'format_none' ||
186
+ bx.text_formatting === 'format_bold';
187
+
188
+ // Font size: for 'auto'/'size.auto', scale to fit within the box.
189
+ // For named sizes (tiny, small, etc.), use fixed values.
190
+ const fontSize = this.computeFontSize(bx.text_size, bx.text, Math.abs(w), Math.abs(h), isBold);
191
+
133
192
  children.push({
134
193
  type: 'text',
135
194
  style: {
136
195
  x: textX,
137
196
  y: textY,
138
197
  text: bx.text,
139
- fill: normalizeColor(bx.text_color) || '#000000',
140
- fontSize: this.getSizePixels(bx.text_size),
198
+ fill: textFill,
199
+ fontSize,
141
200
  fontFamily: bx.text_font_family === 'monospace' ? 'monospace' : 'sans-serif',
142
- fontWeight: (bx.text_formatting === 'format_bold') ? 'bold' : 'normal',
201
+ fontWeight: isBold ? 'bold' : 'normal',
143
202
  fontStyle: (bx.text_formatting === 'format_italic') ? 'italic' : 'normal',
144
203
  textAlign: this.mapHAlign(bx.text_halign),
145
204
  textVerticalAlign: this.mapVAlign(bx.text_valign),
@@ -150,9 +209,11 @@ export class BoxRenderer implements SeriesRenderer {
150
209
 
151
210
  return { type: 'group', children };
152
211
  },
153
- data: [[0, lastBarIndex, yMin, yMax]],
212
+ data: [[0, lastBarIndex]],
154
213
  clip: true,
155
- encode: { x: [0, 1], y: [2, 3] },
214
+ encode: { x: [0, 1] },
215
+ // Prevent ECharts visual system from overriding element colors with palette
216
+ itemStyle: { color: 'transparent', borderColor: 'transparent' },
156
217
  z: 14,
157
218
  silent: true,
158
219
  emphasis: { disabled: true },
@@ -170,12 +231,17 @@ export class BoxRenderer implements SeriesRenderer {
170
231
  }
171
232
  }
172
233
 
173
- private getSizePixels(size: string | number): number {
234
+ /**
235
+ * Compute font size for box text.
236
+ * For 'auto'/'size.auto' (the default), dynamically scale text to fit within
237
+ * the box dimensions with a small gap — matching TradingView behavior.
238
+ * For explicit named sizes, return fixed pixel values.
239
+ */
240
+ private computeFontSize(size: string | number, text: string, boxW: number, boxH: number, bold: boolean): number {
174
241
  if (typeof size === 'number' && size > 0) return size;
242
+
243
+ // Fixed named sizes
175
244
  switch (size) {
176
- case 'auto':
177
- case 'size.auto':
178
- return 12;
179
245
  case 'tiny':
180
246
  case 'size.tiny':
181
247
  return 8;
@@ -191,9 +257,39 @@ export class BoxRenderer implements SeriesRenderer {
191
257
  case 'huge':
192
258
  case 'size.huge':
193
259
  return 36;
194
- default:
195
- return 12;
196
260
  }
261
+
262
+ // 'auto' / 'size.auto' / default → scale to fit box
263
+ if (!text || boxW <= 0 || boxH <= 0) return 12;
264
+
265
+ const padding = 6; // px gap on each side
266
+ const availW = boxW - padding * 2;
267
+ const availH = boxH - padding * 2;
268
+ if (availW <= 0 || availH <= 0) return 6;
269
+
270
+ const lines = text.split('\n');
271
+ const numLines = lines.length;
272
+
273
+ // Find the longest line by character count
274
+ let maxChars = 1;
275
+ for (const line of lines) {
276
+ if (line.length > maxChars) maxChars = line.length;
277
+ }
278
+
279
+ // Average character width ratio (font-size relative).
280
+ // Bold sans-serif is ~0.62; regular is ~0.55.
281
+ const charWidthRatio = bold ? 0.62 : 0.55;
282
+
283
+ // Max font size constrained by width: availW = maxChars * fontSize * ratio
284
+ const maxByWidth = availW / (maxChars * charWidthRatio);
285
+
286
+ // Max font size constrained by height: availH = numLines * fontSize * lineHeight
287
+ const lineHeight = 1.3;
288
+ const maxByHeight = availH / (numLines * lineHeight);
289
+
290
+ // Use the smaller of the two, clamped to a reasonable range
291
+ const computed = Math.min(maxByWidth, maxByHeight);
292
+ return Math.max(6, Math.min(computed, 48));
197
293
  }
198
294
 
199
295
  private mapHAlign(align: string): string {
@@ -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