@qfo/qfchart 0.8.2 → 0.8.5

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,229 +1,229 @@
1
- import { ChartContext, Plugin } from '../types';
2
- // We need to import AbstractPlugin if we check instanceof, or just treat all as Plugin interface
3
-
4
- export class PluginManager {
5
- private plugins: Map<string, Plugin> = new Map();
6
- private activePluginId: string | null = null;
7
- private context: ChartContext;
8
- private toolbarContainer: HTMLElement;
9
- private tooltipElement: HTMLElement | null = null;
10
- private hideTimeout: any = null;
11
-
12
- constructor(context: ChartContext, toolbarContainer: HTMLElement) {
13
- this.context = context;
14
- this.toolbarContainer = toolbarContainer;
15
- this.createTooltip();
16
- this.renderToolbar();
17
- }
18
-
19
- private createTooltip() {
20
- this.tooltipElement = document.createElement('div');
21
- Object.assign(this.tooltipElement.style, {
22
- position: 'fixed',
23
- display: 'none',
24
- backgroundColor: '#1e293b',
25
- color: '#e2e8f0',
26
- padding: '6px 10px',
27
- borderRadius: '6px',
28
- fontSize: '13px',
29
- lineHeight: '1.4',
30
- fontWeight: '500',
31
- border: '1px solid #334155',
32
- zIndex: '9999',
33
- pointerEvents: 'none',
34
- whiteSpace: 'nowrap',
35
- boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.15)',
36
- fontFamily: this.context.getOptions().fontFamily || 'sans-serif',
37
- transition: 'opacity 0.15s ease-in-out, transform 0.15s ease-in-out',
38
- opacity: '0',
39
- transform: 'translateX(-5px)',
40
- });
41
- document.body.appendChild(this.tooltipElement);
42
- }
43
-
44
- public destroy() {
45
- if (this.tooltipElement && this.tooltipElement.parentNode) {
46
- this.tooltipElement.parentNode.removeChild(this.tooltipElement);
47
- }
48
- this.tooltipElement = null;
49
- }
50
-
51
- private showTooltip(target: HTMLElement, text: string) {
52
- if (!this.tooltipElement) return;
53
-
54
- // Clear any pending hide to prevent race conditions
55
- if (this.hideTimeout) {
56
- clearTimeout(this.hideTimeout);
57
- this.hideTimeout = null;
58
- }
59
-
60
- const rect = target.getBoundingClientRect();
61
- this.tooltipElement.textContent = text;
62
- this.tooltipElement.style.display = 'block';
63
-
64
- // Position to the right of the button, centered vertically
65
- const tooltipRect = this.tooltipElement.getBoundingClientRect();
66
- const top = rect.top + (rect.height - tooltipRect.height) / 2;
67
- const left = rect.right + 10; // 10px gap
68
-
69
- this.tooltipElement.style.top = `${top}px`;
70
- this.tooltipElement.style.left = `${left}px`;
71
-
72
- // Trigger animation
73
- requestAnimationFrame(() => {
74
- if (this.tooltipElement) {
75
- this.tooltipElement.style.opacity = '1';
76
- this.tooltipElement.style.transform = 'translateX(0)';
77
- }
78
- });
79
- }
80
-
81
- private hideTooltip() {
82
- if (!this.tooltipElement) return;
83
- this.tooltipElement.style.opacity = '0';
84
- this.tooltipElement.style.transform = 'translateX(-5px)';
85
-
86
- if (this.hideTimeout) {
87
- clearTimeout(this.hideTimeout);
88
- }
89
-
90
- // Wait for transition to finish before hiding
91
- this.hideTimeout = setTimeout(() => {
92
- if (this.tooltipElement) {
93
- this.tooltipElement.style.display = 'none';
94
- }
95
- this.hideTimeout = null;
96
- }, 150);
97
- }
98
-
99
- public register(plugin: Plugin): void {
100
- if (this.plugins.has(plugin.id)) {
101
- console.warn(`Plugin with id ${plugin.id} is already registered.`);
102
- return;
103
- }
104
- this.plugins.set(plugin.id, plugin);
105
- plugin.init(this.context);
106
- this.addButton(plugin);
107
- }
108
-
109
- public unregister(pluginId: string): void {
110
- const plugin = this.plugins.get(pluginId);
111
- if (plugin) {
112
- if (this.activePluginId === pluginId) {
113
- this.deactivatePlugin();
114
- }
115
- plugin.destroy?.();
116
- this.plugins.delete(pluginId);
117
- this.removeButton(pluginId);
118
- }
119
- }
120
-
121
- public activatePlugin(pluginId: string): void {
122
- // If same plugin is clicked, deactivate it (toggle)
123
- if (this.activePluginId === pluginId) {
124
- this.deactivatePlugin();
125
- return;
126
- }
127
-
128
- // Deactivate current active plugin
129
- if (this.activePluginId) {
130
- this.deactivatePlugin();
131
- }
132
-
133
- const plugin = this.plugins.get(pluginId);
134
- if (plugin) {
135
- this.activePluginId = pluginId;
136
- this.setButtonActive(pluginId, true);
137
- plugin.activate?.();
138
- }
139
- }
140
-
141
- public deactivatePlugin(): void {
142
- if (this.activePluginId) {
143
- const plugin = this.plugins.get(this.activePluginId);
144
- plugin?.deactivate?.();
145
- this.setButtonActive(this.activePluginId, false);
146
- this.activePluginId = null;
147
- }
148
- }
149
-
150
- // --- UI Handling ---
151
-
152
- private renderToolbar(): void {
153
- this.toolbarContainer.innerHTML = '';
154
- this.toolbarContainer.classList.add('qfchart-toolbar');
155
- this.toolbarContainer.style.display = 'flex';
156
- this.toolbarContainer.style.flexDirection = 'column';
157
- this.toolbarContainer.style.width = '40px';
158
- this.toolbarContainer.style.backgroundColor = this.context.getOptions().backgroundColor || '#1e293b';
159
- this.toolbarContainer.style.borderRight = '1px solid #334155';
160
- this.toolbarContainer.style.padding = '5px';
161
- this.toolbarContainer.style.boxSizing = 'border-box';
162
- this.toolbarContainer.style.gap = '5px';
163
- this.toolbarContainer.style.flexShrink = '0';
164
- }
165
-
166
- private addButton(plugin: Plugin): void {
167
- const btn = document.createElement('button');
168
- btn.id = `qfchart-plugin-btn-${plugin.id}`;
169
- // Removed native title to use custom tooltip
170
- // btn.title = plugin.name || plugin.id;
171
- btn.style.width = '30px';
172
- btn.style.height = '30px';
173
- btn.style.padding = '4px';
174
- btn.style.border = '1px solid transparent';
175
- btn.style.borderRadius = '4px';
176
- btn.style.backgroundColor = 'transparent';
177
- btn.style.cursor = 'pointer';
178
- btn.style.color = this.context.getOptions().fontColor || '#cbd5e1';
179
- btn.style.display = 'flex';
180
- btn.style.alignItems = 'center';
181
- btn.style.justifyContent = 'center';
182
-
183
- // Icon
184
- if (plugin.icon) {
185
- btn.innerHTML = plugin.icon;
186
- } else {
187
- btn.innerText = (plugin.name || plugin.id).substring(0, 2).toUpperCase();
188
- }
189
-
190
- // Hover effects and Tooltip
191
- btn.addEventListener('mouseenter', () => {
192
- if (this.activePluginId !== plugin.id) {
193
- btn.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
194
- }
195
- this.showTooltip(btn, plugin.name || plugin.id);
196
- });
197
-
198
- btn.addEventListener('mouseleave', () => {
199
- if (this.activePluginId !== plugin.id) {
200
- btn.style.backgroundColor = 'transparent';
201
- }
202
- this.hideTooltip();
203
- });
204
-
205
- btn.onclick = () => this.activatePlugin(plugin.id);
206
-
207
- this.toolbarContainer.appendChild(btn);
208
- }
209
-
210
- private removeButton(pluginId: string): void {
211
- const btn = this.toolbarContainer.querySelector(`#qfchart-plugin-btn-${pluginId}`);
212
- if (btn) {
213
- btn.remove();
214
- }
215
- }
216
-
217
- private setButtonActive(pluginId: string, active: boolean): void {
218
- const btn = this.toolbarContainer.querySelector(`#qfchart-plugin-btn-${pluginId}`) as HTMLElement;
219
- if (btn) {
220
- if (active) {
221
- btn.style.backgroundColor = '#2563eb'; // Blue highlight
222
- btn.style.color = '#ffffff';
223
- } else {
224
- btn.style.backgroundColor = 'transparent';
225
- btn.style.color = this.context.getOptions().fontColor || '#cbd5e1';
226
- }
227
- }
228
- }
229
- }
1
+ import { ChartContext, Plugin } from '../types';
2
+ // We need to import AbstractPlugin if we check instanceof, or just treat all as Plugin interface
3
+
4
+ export class PluginManager {
5
+ private plugins: Map<string, Plugin> = new Map();
6
+ private activePluginId: string | null = null;
7
+ private context: ChartContext;
8
+ private toolbarContainer: HTMLElement;
9
+ private tooltipElement: HTMLElement | null = null;
10
+ private hideTimeout: any = null;
11
+
12
+ constructor(context: ChartContext, toolbarContainer: HTMLElement) {
13
+ this.context = context;
14
+ this.toolbarContainer = toolbarContainer;
15
+ this.createTooltip();
16
+ this.renderToolbar();
17
+ }
18
+
19
+ private createTooltip() {
20
+ this.tooltipElement = document.createElement('div');
21
+ Object.assign(this.tooltipElement.style, {
22
+ position: 'fixed',
23
+ display: 'none',
24
+ backgroundColor: '#1e293b',
25
+ color: '#e2e8f0',
26
+ padding: '6px 10px',
27
+ borderRadius: '6px',
28
+ fontSize: '13px',
29
+ lineHeight: '1.4',
30
+ fontWeight: '500',
31
+ border: '1px solid #334155',
32
+ zIndex: '9999',
33
+ pointerEvents: 'none',
34
+ whiteSpace: 'nowrap',
35
+ boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.15)',
36
+ fontFamily: this.context.getOptions().fontFamily || 'sans-serif',
37
+ transition: 'opacity 0.15s ease-in-out, transform 0.15s ease-in-out',
38
+ opacity: '0',
39
+ transform: 'translateX(-5px)',
40
+ });
41
+ document.body.appendChild(this.tooltipElement);
42
+ }
43
+
44
+ public destroy() {
45
+ if (this.tooltipElement && this.tooltipElement.parentNode) {
46
+ this.tooltipElement.parentNode.removeChild(this.tooltipElement);
47
+ }
48
+ this.tooltipElement = null;
49
+ }
50
+
51
+ private showTooltip(target: HTMLElement, text: string) {
52
+ if (!this.tooltipElement) return;
53
+
54
+ // Clear any pending hide to prevent race conditions
55
+ if (this.hideTimeout) {
56
+ clearTimeout(this.hideTimeout);
57
+ this.hideTimeout = null;
58
+ }
59
+
60
+ const rect = target.getBoundingClientRect();
61
+ this.tooltipElement.textContent = text;
62
+ this.tooltipElement.style.display = 'block';
63
+
64
+ // Position to the right of the button, centered vertically
65
+ const tooltipRect = this.tooltipElement.getBoundingClientRect();
66
+ const top = rect.top + (rect.height - tooltipRect.height) / 2;
67
+ const left = rect.right + 10; // 10px gap
68
+
69
+ this.tooltipElement.style.top = `${top}px`;
70
+ this.tooltipElement.style.left = `${left}px`;
71
+
72
+ // Trigger animation
73
+ requestAnimationFrame(() => {
74
+ if (this.tooltipElement) {
75
+ this.tooltipElement.style.opacity = '1';
76
+ this.tooltipElement.style.transform = 'translateX(0)';
77
+ }
78
+ });
79
+ }
80
+
81
+ private hideTooltip() {
82
+ if (!this.tooltipElement) return;
83
+ this.tooltipElement.style.opacity = '0';
84
+ this.tooltipElement.style.transform = 'translateX(-5px)';
85
+
86
+ if (this.hideTimeout) {
87
+ clearTimeout(this.hideTimeout);
88
+ }
89
+
90
+ // Wait for transition to finish before hiding
91
+ this.hideTimeout = setTimeout(() => {
92
+ if (this.tooltipElement) {
93
+ this.tooltipElement.style.display = 'none';
94
+ }
95
+ this.hideTimeout = null;
96
+ }, 150);
97
+ }
98
+
99
+ public register(plugin: Plugin): void {
100
+ if (this.plugins.has(plugin.id)) {
101
+ console.warn(`Plugin with id ${plugin.id} is already registered.`);
102
+ return;
103
+ }
104
+ this.plugins.set(plugin.id, plugin);
105
+ plugin.init(this.context);
106
+ this.addButton(plugin);
107
+ }
108
+
109
+ public unregister(pluginId: string): void {
110
+ const plugin = this.plugins.get(pluginId);
111
+ if (plugin) {
112
+ if (this.activePluginId === pluginId) {
113
+ this.deactivatePlugin();
114
+ }
115
+ plugin.destroy?.();
116
+ this.plugins.delete(pluginId);
117
+ this.removeButton(pluginId);
118
+ }
119
+ }
120
+
121
+ public activatePlugin(pluginId: string): void {
122
+ // If same plugin is clicked, deactivate it (toggle)
123
+ if (this.activePluginId === pluginId) {
124
+ this.deactivatePlugin();
125
+ return;
126
+ }
127
+
128
+ // Deactivate current active plugin
129
+ if (this.activePluginId) {
130
+ this.deactivatePlugin();
131
+ }
132
+
133
+ const plugin = this.plugins.get(pluginId);
134
+ if (plugin) {
135
+ this.activePluginId = pluginId;
136
+ this.setButtonActive(pluginId, true);
137
+ plugin.activate?.();
138
+ }
139
+ }
140
+
141
+ public deactivatePlugin(): void {
142
+ if (this.activePluginId) {
143
+ const plugin = this.plugins.get(this.activePluginId);
144
+ plugin?.deactivate?.();
145
+ this.setButtonActive(this.activePluginId, false);
146
+ this.activePluginId = null;
147
+ }
148
+ }
149
+
150
+ // --- UI Handling ---
151
+
152
+ private renderToolbar(): void {
153
+ this.toolbarContainer.innerHTML = '';
154
+ this.toolbarContainer.classList.add('qfchart-toolbar');
155
+ this.toolbarContainer.style.display = 'flex';
156
+ this.toolbarContainer.style.flexDirection = 'column';
157
+ this.toolbarContainer.style.width = '40px';
158
+ this.toolbarContainer.style.backgroundColor = this.context.getOptions().backgroundColor || '#1e293b';
159
+ this.toolbarContainer.style.borderRight = '1px solid #334155';
160
+ this.toolbarContainer.style.padding = '5px';
161
+ this.toolbarContainer.style.boxSizing = 'border-box';
162
+ this.toolbarContainer.style.gap = '5px';
163
+ this.toolbarContainer.style.flexShrink = '0';
164
+ }
165
+
166
+ private addButton(plugin: Plugin): void {
167
+ const btn = document.createElement('button');
168
+ btn.id = `qfchart-plugin-btn-${plugin.id}`;
169
+ // Removed native title to use custom tooltip
170
+ // btn.title = plugin.name || plugin.id;
171
+ btn.style.width = '30px';
172
+ btn.style.height = '30px';
173
+ btn.style.padding = '4px';
174
+ btn.style.border = '1px solid transparent';
175
+ btn.style.borderRadius = '4px';
176
+ btn.style.backgroundColor = 'transparent';
177
+ btn.style.cursor = 'pointer';
178
+ btn.style.color = this.context.getOptions().fontColor || '#cbd5e1';
179
+ btn.style.display = 'flex';
180
+ btn.style.alignItems = 'center';
181
+ btn.style.justifyContent = 'center';
182
+
183
+ // Icon
184
+ if (plugin.icon) {
185
+ btn.innerHTML = plugin.icon;
186
+ } else {
187
+ btn.innerText = (plugin.name || plugin.id).substring(0, 2).toUpperCase();
188
+ }
189
+
190
+ // Hover effects and Tooltip
191
+ btn.addEventListener('mouseenter', () => {
192
+ if (this.activePluginId !== plugin.id) {
193
+ btn.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
194
+ }
195
+ this.showTooltip(btn, plugin.name || plugin.id);
196
+ });
197
+
198
+ btn.addEventListener('mouseleave', () => {
199
+ if (this.activePluginId !== plugin.id) {
200
+ btn.style.backgroundColor = 'transparent';
201
+ }
202
+ this.hideTooltip();
203
+ });
204
+
205
+ btn.onclick = () => this.activatePlugin(plugin.id);
206
+
207
+ this.toolbarContainer.appendChild(btn);
208
+ }
209
+
210
+ private removeButton(pluginId: string): void {
211
+ const btn = this.toolbarContainer.querySelector(`#qfchart-plugin-btn-${pluginId}`);
212
+ if (btn) {
213
+ btn.remove();
214
+ }
215
+ }
216
+
217
+ private setButtonActive(pluginId: string, active: boolean): void {
218
+ const btn = this.toolbarContainer.querySelector(`#qfchart-plugin-btn-${pluginId}`) as HTMLElement;
219
+ if (btn) {
220
+ if (active) {
221
+ btn.style.backgroundColor = '#2563eb'; // Blue highlight
222
+ btn.style.color = '#ffffff';
223
+ } else {
224
+ btn.style.backgroundColor = 'transparent';
225
+ btn.style.color = this.context.getOptions().fontColor || '#cbd5e1';
226
+ }
227
+ }
228
+ }
229
+ }
@@ -104,6 +104,9 @@ export class SeriesBuilder {
104
104
  const series: any[] = [];
105
105
  const barColors: (string | null)[] = new Array(totalDataLength).fill(null);
106
106
 
107
+ // Extract raw (non-null) market data for resolving xloc.bar_time coordinates
108
+ const rawMarketData = candlestickData?.filter((d): d is OHLCV => d != null && d.time !== undefined);
109
+
107
110
  // Store plot data arrays for fill plots to reference
108
111
  const plotDataArrays = new Map<string, number[]>();
109
112
 
@@ -146,7 +149,7 @@ export class SeriesBuilder {
146
149
  let plotOverlay = plot.options.overlay;
147
150
 
148
151
  // Fill plots inherit overlay from their referenced plots.
149
- // If both referenced plots are overlay, the fill should render on the
152
+ // If either referenced plot is overlay, the fill should render on the
150
153
  // overlay pane too — otherwise its price-scale data stretches the
151
154
  // indicator sub-pane's y-axis to extreme ranges.
152
155
  if (plot.options.style === 'fill' && plotOverlay === undefined) {
@@ -155,7 +158,7 @@ export class SeriesBuilder {
155
158
  if (p1Name && p2Name) {
156
159
  const p1 = indicator.plots[p1Name];
157
160
  const p2 = indicator.plots[p2Name];
158
- if (p1?.options?.overlay === true && p2?.options?.overlay === true) {
161
+ if (p1?.options?.overlay === true || p2?.options?.overlay === true) {
159
162
  plotOverlay = true;
160
163
  }
161
164
  }
@@ -309,21 +312,28 @@ export class SeriesBuilder {
309
312
  }
310
313
  }
311
314
 
312
- // Skip fully transparent plots — they exist only as data sources for fills.
315
+ // Skip fully transparent / invisible plots — they exist only as data sources for fills.
313
316
  // 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
317
+ // Covers: color(na) null, color.new(x, 100) → fully transparent string, etc.
318
+ {
319
+ const plotColor = plot.options.color;
320
+ let skipPlot = false;
321
+ if (plotColor == null) {
322
+ // color(na) plot-level color is null/undefined; skip if no bar has a visible color
323
+ const hasVisibleBarColor = colorArray.some((c: any) => c != null);
324
+ skipPlot = !hasVisibleBarColor;
325
+ } else if (typeof plotColor === 'string') {
326
+ const parsed = ColorUtils.parseColor(plotColor);
327
+ if (parsed.opacity < 0.01) {
328
+ const hasVisibleBarColor = colorArray.some((c: any) => {
329
+ if (c == null) return false;
330
+ const pc = ColorUtils.parseColor(c);
331
+ return pc.opacity >= 0.01;
332
+ });
333
+ skipPlot = !hasVisibleBarColor;
325
334
  }
326
335
  }
336
+ if (skipPlot) return;
327
337
  }
328
338
 
329
339
  // Use Factory to get appropriate renderer
@@ -341,6 +351,8 @@ export class SeriesBuilder {
341
351
  indicatorId: id,
342
352
  plotName: plotName,
343
353
  dataIndexOffset,
354
+ timeToIndex,
355
+ marketData: rawMarketData,
344
356
  });
345
357
 
346
358
  if (seriesConfig) {
@@ -1,4 +1,4 @@
1
- import { SeriesRenderer, RenderContext } from './SeriesRenderer';
1
+ import { SeriesRenderer, RenderContext, resolveXCoord } from './SeriesRenderer';
2
2
 
3
3
  /**
4
4
  * Convert any color string to a format ECharts canvas can render with opacity.
@@ -60,7 +60,7 @@ function luminance(r: number, g: number, b: number): number {
60
60
  */
61
61
  export class BoxRenderer implements SeriesRenderer {
62
62
  render(context: RenderContext): any {
63
- const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset } = context;
63
+ const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset, timeToIndex, marketData } = context;
64
64
  const offset = dataIndexOffset || 0;
65
65
 
66
66
  // Collect all non-deleted box objects from the sparse dataArray.
@@ -103,9 +103,11 @@ export class BoxRenderer implements SeriesRenderer {
103
103
  for (const bx of boxObjects) {
104
104
  if (bx._deleted) continue;
105
105
 
106
- const xOff = (bx.xloc === 'bar_index' || bx.xloc === 'bi') ? offset : 0;
107
- const pTopLeft = api.coord([bx.left + xOff, bx.top]);
108
- const pBottomRight = api.coord([bx.right + xOff, bx.bottom]);
106
+ const leftX = resolveXCoord(bx.left, bx.xloc, offset, timeToIndex, marketData);
107
+ const rightX = resolveXCoord(bx.right, bx.xloc, offset, timeToIndex, marketData);
108
+ if (isNaN(leftX) || isNaN(rightX)) continue;
109
+ const pTopLeft = api.coord([leftX, bx.top]);
110
+ const pBottomRight = api.coord([rightX, bx.bottom]);
109
111
 
110
112
  let x = pTopLeft[0];
111
113
  let y = pTopLeft[1];
@@ -1,4 +1,4 @@
1
- import { SeriesRenderer, RenderContext } from './SeriesRenderer';
1
+ import { SeriesRenderer, RenderContext, resolveXCoord } from './SeriesRenderer';
2
2
 
3
3
  /**
4
4
  * Renderer for Pine Script line.* drawing objects.
@@ -9,7 +9,7 @@ import { SeriesRenderer, RenderContext } from './SeriesRenderer';
9
9
  */
10
10
  export class DrawingLineRenderer implements SeriesRenderer {
11
11
  render(context: RenderContext): any {
12
- const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset } = context;
12
+ const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset, timeToIndex, marketData } = context;
13
13
  const offset = dataIndexOffset || 0;
14
14
  const defaultColor = '#2962ff';
15
15
 
@@ -55,10 +55,12 @@ export class DrawingLineRenderer implements SeriesRenderer {
55
55
 
56
56
  for (const ln of lineObjects) {
57
57
  if (ln._deleted) continue;
58
- const xOff = (ln.xloc === 'bar_index' || ln.xloc === 'bi') ? offset : 0;
58
+ const x1Resolved = resolveXCoord(ln.x1, ln.xloc, offset, timeToIndex, marketData);
59
+ const x2Resolved = resolveXCoord(ln.x2, ln.xloc, offset, timeToIndex, marketData);
60
+ if (isNaN(x1Resolved) || isNaN(x2Resolved)) continue;
59
61
 
60
- let p1 = api.coord([ln.x1 + xOff, ln.y1]);
61
- let p2 = api.coord([ln.x2 + xOff, ln.y2]);
62
+ let p1 = api.coord([x1Resolved, ln.y1]);
63
+ let p2 = api.coord([x2Resolved, ln.y2]);
62
64
 
63
65
  // Handle extend (none/n | left/l | right/r | both/b)
64
66
  const extend = ln.extend || 'none';