@qfo/qfchart 0.6.7 → 0.7.1

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.
@@ -0,0 +1,194 @@
1
+ import { SeriesRenderer, RenderContext } from './SeriesRenderer';
2
+
3
+ /**
4
+ * Renderer for Pine Script line.* drawing objects.
5
+ * Each line is defined by two endpoints (x1,y1) → (x2,y2) with optional
6
+ * extend, dash style, and arrow heads.
7
+ *
8
+ * Style name: 'drawing_line' (distinct from 'line' used by plot()).
9
+ */
10
+ export class DrawingLineRenderer implements SeriesRenderer {
11
+ render(context: RenderContext): any {
12
+ const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset } = context;
13
+ const offset = dataIndexOffset || 0;
14
+ const defaultColor = '#2962ff';
15
+
16
+ // Collect all non-null, non-deleted line objects from the sparse dataArray.
17
+ // Drawing objects are stored as an array of all lines in a single data entry
18
+ // (since multiple objects at the same bar would overwrite each other in the
19
+ // sparse array). Handle both array-of-objects and single-object entries.
20
+ const lineObjects: any[] = [];
21
+
22
+ for (let i = 0; i < dataArray.length; i++) {
23
+ const val = dataArray[i];
24
+ if (!val) continue;
25
+
26
+ const items = Array.isArray(val) ? val : [val];
27
+ for (const ln of items) {
28
+ if (ln && typeof ln === 'object' && !ln._deleted) {
29
+ lineObjects.push(ln);
30
+ }
31
+ }
32
+ }
33
+
34
+ if (lineObjects.length === 0) {
35
+ return { name: seriesName, type: 'custom', xAxisIndex, yAxisIndex, data: [], silent: true };
36
+ }
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
+ // Use a SINGLE data entry spanning the full x-range so renderItem is always called.
48
+ // ECharts filters a data item only when ALL its x-dimensions are on the same side
49
+ // of the visible window. With dims 0=0 and 1=lastBar the item always straddles
50
+ // the viewport, so renderItem fires exactly once regardless of scroll position.
51
+ // Dims 2/3 are yMin/yMax for axis scaling.
52
+ const totalBars = (context.candlestickData?.length || 0) + offset;
53
+ const lastBarIndex = Math.max(0, totalBars - 1);
54
+
55
+ return {
56
+ name: seriesName,
57
+ type: 'custom',
58
+ xAxisIndex,
59
+ yAxisIndex,
60
+ renderItem: (params: any, api: any) => {
61
+ const children: any[] = [];
62
+
63
+ for (const ln of lineObjects) {
64
+ if (ln._deleted) continue;
65
+ const xOff = (ln.xloc === 'bar_index' || ln.xloc === 'bi') ? offset : 0;
66
+
67
+ let p1 = api.coord([ln.x1 + xOff, ln.y1]);
68
+ let p2 = api.coord([ln.x2 + xOff, ln.y2]);
69
+
70
+ // Handle extend (none | left | right | both)
71
+ const extend = ln.extend || 'none';
72
+ if (extend !== 'none') {
73
+ const cs = params.coordSys;
74
+ [p1, p2] = this.extendLine(p1, p2, extend, cs.x, cs.x + cs.width, cs.y, cs.y + cs.height);
75
+ }
76
+
77
+ const color = ln.color || defaultColor;
78
+ const lineWidth = ln.width || 1;
79
+
80
+ children.push({
81
+ type: 'line',
82
+ shape: { x1: p1[0], y1: p1[1], x2: p2[0], y2: p2[1] },
83
+ style: {
84
+ stroke: color,
85
+ lineWidth,
86
+ lineDash: this.getDashPattern(ln.style),
87
+ },
88
+ });
89
+
90
+ const style = ln.style || 'style_solid';
91
+ if (style === 'style_arrow_left' || style === 'style_arrow_both') {
92
+ const arrow = this.arrowHead(p2, p1, lineWidth, color);
93
+ if (arrow) children.push(arrow);
94
+ }
95
+ if (style === 'style_arrow_right' || style === 'style_arrow_both') {
96
+ const arrow = this.arrowHead(p1, p2, lineWidth, color);
97
+ if (arrow) children.push(arrow);
98
+ }
99
+ }
100
+
101
+ return { type: 'group', children };
102
+ },
103
+ data: [[0, lastBarIndex, yMin, yMax]],
104
+ clip: true,
105
+ encode: { x: [0, 1], y: [2, 3] },
106
+ z: 15,
107
+ silent: true,
108
+ emphasis: { disabled: true },
109
+ };
110
+ }
111
+
112
+ private getDashPattern(style: string): number[] | undefined {
113
+ switch (style) {
114
+ case 'style_dotted':
115
+ return [2, 2];
116
+ case 'style_dashed':
117
+ return [6, 4];
118
+ default:
119
+ return undefined;
120
+ }
121
+ }
122
+
123
+ private extendLine(
124
+ p1: number[],
125
+ p2: number[],
126
+ extend: string,
127
+ left: number,
128
+ right: number,
129
+ top: number,
130
+ bottom: number,
131
+ ): [number[], number[]] {
132
+ const dx = p2[0] - p1[0];
133
+ const dy = p2[1] - p1[1];
134
+
135
+ if (dx === 0 && dy === 0) return [p1, p2];
136
+
137
+ const extendPoint = (origin: number[], dir: number[]): number[] => {
138
+ let tMax = Infinity;
139
+ if (dir[0] !== 0) {
140
+ const tx = dir[0] > 0 ? (right - origin[0]) / dir[0] : (left - origin[0]) / dir[0];
141
+ tMax = Math.min(tMax, tx);
142
+ }
143
+ if (dir[1] !== 0) {
144
+ const ty = dir[1] > 0 ? (bottom - origin[1]) / dir[1] : (top - origin[1]) / dir[1];
145
+ tMax = Math.min(tMax, ty);
146
+ }
147
+ if (!isFinite(tMax)) tMax = 0;
148
+ return [origin[0] + tMax * dir[0], origin[1] + tMax * dir[1]];
149
+ };
150
+
151
+ let newP1 = p1;
152
+ let newP2 = p2;
153
+
154
+ if (extend === 'right' || extend === 'both') {
155
+ newP2 = extendPoint(p1, [dx, dy]);
156
+ }
157
+ if (extend === 'left' || extend === 'both') {
158
+ newP1 = extendPoint(p2, [-dx, -dy]);
159
+ }
160
+
161
+ return [newP1, newP2];
162
+ }
163
+
164
+ private arrowHead(from: number[], to: number[], lineWidth: number, color: string): any {
165
+ const dx = to[0] - from[0];
166
+ const dy = to[1] - from[1];
167
+ const len = Math.sqrt(dx * dx + dy * dy);
168
+ if (len < 1) return null;
169
+
170
+ const size = Math.max(8, lineWidth * 4);
171
+ const nx = dx / len;
172
+ const ny = dy / len;
173
+
174
+ // Arrow tip at `to`, base offset back by `size`
175
+ const bx = to[0] - nx * size;
176
+ const by = to[1] - ny * size;
177
+
178
+ // Perpendicular offset for arrowhead width
179
+ const px = -ny * size * 0.4;
180
+ const py = nx * size * 0.4;
181
+
182
+ return {
183
+ type: 'polygon',
184
+ shape: {
185
+ points: [
186
+ [to[0], to[1]],
187
+ [bx + px, by + py],
188
+ [bx - px, by - py],
189
+ ],
190
+ },
191
+ style: { fill: color },
192
+ };
193
+ }
194
+ }
@@ -1,99 +1,99 @@
1
- import { SeriesRenderer, RenderContext } from './SeriesRenderer';
2
- import { ColorUtils } from '../../utils/ColorUtils';
3
-
4
- export class FillRenderer implements SeriesRenderer {
5
- render(context: RenderContext): any {
6
- const { seriesName, xAxisIndex, yAxisIndex, plotOptions, plotDataArrays, indicatorId, plotName } = context;
7
- const totalDataLength = context.dataArray.length; // Use length from dataArray placeholder
8
-
9
- // Fill plots reference other plots to fill the area between them
10
- const plot1Key = plotOptions.plot1 ? `${indicatorId}::${plotOptions.plot1}` : null;
11
- const plot2Key = plotOptions.plot2 ? `${indicatorId}::${plotOptions.plot2}` : null;
12
-
13
- if (!plot1Key || !plot2Key) {
14
- console.warn(`Fill plot "${plotName}" missing plot1 or plot2 reference`);
15
- return null;
16
- }
17
-
18
- const plot1Data = plotDataArrays?.get(plot1Key);
19
- const plot2Data = plotDataArrays?.get(plot2Key);
20
-
21
- if (!plot1Data || !plot2Data) {
22
- console.warn(`Fill plot "${plotName}" references non-existent plots: ${plotOptions.plot1}, ${plotOptions.plot2}`);
23
- return null;
24
- }
25
-
26
- // Parse color to extract opacity
27
- const { color: fillColor, opacity: fillOpacity } = ColorUtils.parseColor(plotOptions.color || 'rgba(128, 128, 128, 0.2)');
28
-
29
- // Create fill data with previous values for smooth polygon rendering
30
- const fillDataWithPrev: any[] = [];
31
- for (let i = 0; i < totalDataLength; i++) {
32
- const y1 = plot1Data[i];
33
- const y2 = plot2Data[i];
34
- const prevY1 = i > 0 ? plot1Data[i - 1] : null;
35
- const prevY2 = i > 0 ? plot2Data[i - 1] : null;
36
-
37
- fillDataWithPrev.push([i, y1, y2, prevY1, prevY2]);
38
- }
39
-
40
- // Add fill series with smooth area rendering
41
- return {
42
- name: seriesName,
43
- type: 'custom',
44
- xAxisIndex: xAxisIndex,
45
- yAxisIndex: yAxisIndex,
46
- z: -5, // Render behind lines but above background
47
- renderItem: (params: any, api: any) => {
48
- const index = params.dataIndex;
49
-
50
- // Skip first point (no previous to connect to)
51
- if (index === 0) return null;
52
-
53
- const y1 = api.value(1); // Current upper
54
- const y2 = api.value(2); // Current lower
55
- const prevY1 = api.value(3); // Previous upper
56
- const prevY2 = api.value(4); // Previous lower
57
-
58
- // Skip if any value is null/NaN
59
- if (
60
- y1 === null ||
61
- y2 === null ||
62
- prevY1 === null ||
63
- prevY2 === null ||
64
- isNaN(y1) ||
65
- isNaN(y2) ||
66
- isNaN(prevY1) ||
67
- isNaN(prevY2)
68
- ) {
69
- return null;
70
- }
71
-
72
- // Get pixel coordinates for all 4 points
73
- const p1Prev = api.coord([index - 1, prevY1]); // Previous upper
74
- const p1Curr = api.coord([index, y1]); // Current upper
75
- const p2Curr = api.coord([index, y2]); // Current lower
76
- const p2Prev = api.coord([index - 1, prevY2]); // Previous lower
77
-
78
- // Create a smooth polygon connecting the segments
79
- return {
80
- type: 'polygon',
81
- shape: {
82
- points: [
83
- p1Prev, // Top-left
84
- p1Curr, // Top-right
85
- p2Curr, // Bottom-right
86
- p2Prev, // Bottom-left
87
- ],
88
- },
89
- style: {
90
- fill: fillColor,
91
- opacity: fillOpacity,
92
- },
93
- silent: true,
94
- };
95
- },
96
- data: fillDataWithPrev,
97
- };
98
- }
99
- }
1
+ import { SeriesRenderer, RenderContext } from './SeriesRenderer';
2
+ import { ColorUtils } from '../../utils/ColorUtils';
3
+
4
+ export class FillRenderer implements SeriesRenderer {
5
+ render(context: RenderContext): any {
6
+ const { seriesName, xAxisIndex, yAxisIndex, plotOptions, plotDataArrays, indicatorId, plotName } = context;
7
+ const totalDataLength = context.dataArray.length; // Use length from dataArray placeholder
8
+
9
+ // Fill plots reference other plots to fill the area between them
10
+ const plot1Key = plotOptions.plot1 ? `${indicatorId}::${plotOptions.plot1}` : null;
11
+ const plot2Key = plotOptions.plot2 ? `${indicatorId}::${plotOptions.plot2}` : null;
12
+
13
+ if (!plot1Key || !plot2Key) {
14
+ console.warn(`Fill plot "${plotName}" missing plot1 or plot2 reference`);
15
+ return null;
16
+ }
17
+
18
+ const plot1Data = plotDataArrays?.get(plot1Key);
19
+ const plot2Data = plotDataArrays?.get(plot2Key);
20
+
21
+ if (!plot1Data || !plot2Data) {
22
+ console.warn(`Fill plot "${plotName}" references non-existent plots: ${plotOptions.plot1}, ${plotOptions.plot2}`);
23
+ return null;
24
+ }
25
+
26
+ // Parse color to extract opacity
27
+ const { color: fillColor, opacity: fillOpacity } = ColorUtils.parseColor(plotOptions.color || 'rgba(128, 128, 128, 0.2)');
28
+
29
+ // Create fill data with previous values for smooth polygon rendering
30
+ const fillDataWithPrev: any[] = [];
31
+ for (let i = 0; i < totalDataLength; i++) {
32
+ const y1 = plot1Data[i];
33
+ const y2 = plot2Data[i];
34
+ const prevY1 = i > 0 ? plot1Data[i - 1] : null;
35
+ const prevY2 = i > 0 ? plot2Data[i - 1] : null;
36
+
37
+ fillDataWithPrev.push([i, y1, y2, prevY1, prevY2]);
38
+ }
39
+
40
+ // Add fill series with smooth area rendering
41
+ return {
42
+ name: seriesName,
43
+ type: 'custom',
44
+ xAxisIndex: xAxisIndex,
45
+ yAxisIndex: yAxisIndex,
46
+ z: 1, // Behind plot lines (z=2) and candles (z=5), above grid background
47
+ renderItem: (params: any, api: any) => {
48
+ const index = params.dataIndex;
49
+
50
+ // Skip first point (no previous to connect to)
51
+ if (index === 0) return null;
52
+
53
+ const y1 = api.value(1); // Current upper
54
+ const y2 = api.value(2); // Current lower
55
+ const prevY1 = api.value(3); // Previous upper
56
+ const prevY2 = api.value(4); // Previous lower
57
+
58
+ // Skip if any value is null/NaN
59
+ if (
60
+ y1 === null ||
61
+ y2 === null ||
62
+ prevY1 === null ||
63
+ prevY2 === null ||
64
+ isNaN(y1) ||
65
+ isNaN(y2) ||
66
+ isNaN(prevY1) ||
67
+ isNaN(prevY2)
68
+ ) {
69
+ return null;
70
+ }
71
+
72
+ // Get pixel coordinates for all 4 points
73
+ const p1Prev = api.coord([index - 1, prevY1]); // Previous upper
74
+ const p1Curr = api.coord([index, y1]); // Current upper
75
+ const p2Curr = api.coord([index, y2]); // Current lower
76
+ const p2Prev = api.coord([index - 1, prevY2]); // Previous lower
77
+
78
+ // Create a smooth polygon connecting the segments
79
+ return {
80
+ type: 'polygon',
81
+ shape: {
82
+ points: [
83
+ p1Prev, // Top-left
84
+ p1Curr, // Top-right
85
+ p2Curr, // Bottom-right
86
+ p2Prev, // Bottom-left
87
+ ],
88
+ },
89
+ style: {
90
+ fill: fillColor,
91
+ opacity: fillOpacity,
92
+ },
93
+ silent: true,
94
+ };
95
+ },
96
+ data: fillDataWithPrev,
97
+ };
98
+ }
99
+ }
@@ -3,16 +3,27 @@ import { ShapeUtils } from '../../utils/ShapeUtils';
3
3
 
4
4
  export class LabelRenderer implements SeriesRenderer {
5
5
  render(context: RenderContext): any {
6
- const { seriesName, xAxisIndex, yAxisIndex, dataArray, candlestickData } = context;
6
+ const { seriesName, xAxisIndex, yAxisIndex, dataArray, candlestickData, dataIndexOffset } = context;
7
+ const offset = dataIndexOffset || 0;
7
8
 
8
- const labelData = dataArray
9
- .map((val, i) => {
10
- if (val === null || val === undefined) return null;
11
-
12
- // val is a label object: {id, x, y, text, xloc, yloc, color, style, textcolor, size, textalign, tooltip}
13
- const lbl = typeof val === 'object' ? val : null;
14
- if (!lbl) return null;
9
+ // Collect all non-null, non-deleted label objects from the sparse dataArray.
10
+ // Drawing objects are stored as an array of all labels in a single data entry
11
+ // (since multiple objects at the same bar would overwrite each other in the
12
+ // sparse array). Handle both array-of-objects and single-object entries.
13
+ const labelObjects: any[] = [];
14
+ for (let i = 0; i < dataArray.length; i++) {
15
+ const val = dataArray[i];
16
+ if (!val) continue;
17
+ const items = Array.isArray(val) ? val : [val];
18
+ for (const lbl of items) {
19
+ if (lbl && typeof lbl === 'object' && !lbl._deleted) {
20
+ labelObjects.push(lbl);
21
+ }
22
+ }
23
+ }
15
24
 
25
+ const labelData = labelObjects
26
+ .map((lbl) => {
16
27
  const text = lbl.text || '';
17
28
  const color = lbl.color || '#2962ff';
18
29
  const textcolor = lbl.textcolor || '#ffffff';
@@ -25,18 +36,21 @@ export class LabelRenderer implements SeriesRenderer {
25
36
  // Map Pine style string to shape name for ShapeUtils
26
37
  const shape = this.styleToShape(styleRaw);
27
38
 
39
+ // Determine X position using label's own x coordinate
40
+ const xPos = (lbl.xloc === 'bar_index' || lbl.xloc === 'bi') ? (lbl.x + offset) : lbl.x;
41
+
28
42
  // Determine Y value based on yloc
29
43
  let yValue = lbl.y;
30
44
  let symbolOffset: (string | number)[] = [0, 0];
31
45
 
32
- if (yloc === 'abovebar') {
33
- if (candlestickData && candlestickData[i]) {
34
- yValue = candlestickData[i].high;
46
+ if (yloc === 'abovebar' || yloc === 'AboveBar' || yloc === 'ab') {
47
+ if (candlestickData && candlestickData[xPos]) {
48
+ yValue = candlestickData[xPos].high;
35
49
  }
36
50
  symbolOffset = [0, '-150%'];
37
- } else if (yloc === 'belowbar') {
38
- if (candlestickData && candlestickData[i]) {
39
- yValue = candlestickData[i].low;
51
+ } else if (yloc === 'belowbar' || yloc === 'BelowBar' || yloc === 'bl') {
52
+ if (candlestickData && candlestickData[xPos]) {
53
+ yValue = candlestickData[xPos].low;
40
54
  }
41
55
  symbolOffset = [0, '150%'];
42
56
  }
@@ -50,24 +64,59 @@ export class LabelRenderer implements SeriesRenderer {
50
64
 
51
65
  // Dynamically size the bubble to fit text content
52
66
  let finalSize: number | number[];
53
- if (shape === 'labeldown' || shape === 'labelup') {
67
+ const isBubble = shape === 'labeldown' || shape === 'shape_label_down' ||
68
+ shape === 'labelup' || shape === 'shape_label_up' ||
69
+ shape === 'labelleft' || shape === 'labelright';
70
+ // Track label text offset for centering text within the body
71
+ // (excluding the pointer area)
72
+ let labelTextOffset: [number, number] = [0, 0];
73
+
74
+ if (isBubble) {
54
75
  // Approximate text width: chars * fontSize * avgCharWidthRatio (bold)
55
76
  const textWidth = text.length * fontSize * 0.65;
56
77
  const minWidth = fontSize * 2.5;
57
78
  const bubbleWidth = Math.max(minWidth, textWidth + fontSize * 1.6);
58
79
  const bubbleHeight = fontSize * 2.8;
59
- finalSize = [bubbleWidth, bubbleHeight];
60
-
61
- // Offset bubble so the pointer tip sits at the anchor price.
62
- // The SVG path pointer is ~20% of total height.
63
- if (shape === 'labeldown') {
64
- symbolOffset = [symbolOffset[0], typeof symbolOffset[1] === 'string'
65
- ? symbolOffset[1]
66
- : (symbolOffset[1] as number) - bubbleHeight * 0.35];
80
+
81
+ // SVG pointer takes 3/24 = 12.5% of the path dimension
82
+ const pointerRatio = 3 / 24;
83
+
84
+ if (shape === 'labelleft' || shape === 'labelright') {
85
+ // Add extra width for the pointer
86
+ const totalWidth = bubbleWidth / (1 - pointerRatio);
87
+ finalSize = [totalWidth, bubbleHeight];
88
+
89
+ // Offset so the pointer tip sits at the anchor x position.
90
+ const xOff = typeof symbolOffset[0] === 'string' ? 0
91
+ : (symbolOffset[0] as number);
92
+ if (shape === 'labelleft') {
93
+ // Pointer on left → shift bubble body to the right
94
+ symbolOffset = [xOff + totalWidth * 0.42, symbolOffset[1]];
95
+ // Shift text right to center within body (not pointer)
96
+ labelTextOffset = [totalWidth * pointerRatio * 0.5, 0];
97
+ } else {
98
+ // Pointer on right → shift bubble body to the left
99
+ symbolOffset = [xOff - totalWidth * 0.42, symbolOffset[1]];
100
+ // Shift text left to center within body
101
+ labelTextOffset = [-totalWidth * pointerRatio * 0.5, 0];
102
+ }
67
103
  } else {
68
- symbolOffset = [symbolOffset[0], typeof symbolOffset[1] === 'string'
69
- ? symbolOffset[1]
70
- : (symbolOffset[1] as number) + bubbleHeight * 0.35];
104
+ // Vertical pointer (up/down)
105
+ const totalHeight = bubbleHeight / (1 - pointerRatio);
106
+ finalSize = [bubbleWidth, totalHeight];
107
+
108
+ // Offset bubble so the pointer tip sits at the anchor price.
109
+ if (shape === 'labeldown') {
110
+ symbolOffset = [symbolOffset[0], typeof symbolOffset[1] === 'string'
111
+ ? symbolOffset[1]
112
+ : (symbolOffset[1] as number) - totalHeight * 0.42];
113
+ labelTextOffset = [0, -totalHeight * pointerRatio * 0.5];
114
+ } else {
115
+ symbolOffset = [symbolOffset[0], typeof symbolOffset[1] === 'string'
116
+ ? symbolOffset[1]
117
+ : (symbolOffset[1] as number) + totalHeight * 0.42];
118
+ labelTextOffset = [0, totalHeight * pointerRatio * 0.5];
119
+ }
71
120
  }
72
121
  } else if (shape === 'none') {
73
122
  finalSize = 0;
@@ -85,7 +134,7 @@ export class LabelRenderer implements SeriesRenderer {
85
134
  labelPosition.startsWith('inside');
86
135
 
87
136
  const item: any = {
88
- value: [i, yValue],
137
+ value: [xPos, yValue],
89
138
  symbol: symbol,
90
139
  symbolSize: finalSize,
91
140
  symbolOffset: symbolOffset,
@@ -96,13 +145,14 @@ export class LabelRenderer implements SeriesRenderer {
96
145
  show: !!text,
97
146
  position: labelPosition,
98
147
  distance: isInsideLabel ? 0 : 5,
148
+ offset: labelTextOffset,
99
149
  formatter: text,
100
150
  color: textcolor,
101
151
  fontSize: fontSize,
102
152
  fontWeight: 'bold',
103
153
  align: isInsideLabel ? 'center'
104
- : textalign === 'align_left' ? 'left'
105
- : textalign === 'align_right' ? 'right'
154
+ : (textalign === 'align_left' || textalign === 'left') ? 'left'
155
+ : (textalign === 'align_right' || textalign === 'right') ? 'right'
106
156
  : 'center',
107
157
  verticalAlign: 'middle',
108
158
  padding: [2, 6],
@@ -137,9 +187,9 @@ export class LabelRenderer implements SeriesRenderer {
137
187
  case 'label_up':
138
188
  return 'labelup';
139
189
  case 'label_left':
140
- return 'labeldown'; // Use labeldown shape, position text left
190
+ return 'labelleft';
141
191
  case 'label_right':
142
- return 'labeldown'; // Use labeldown shape, position text right
192
+ return 'labelright';
143
193
  case 'label_lower_left':
144
194
  return 'labeldown';
145
195
  case 'label_lower_right':
@@ -183,31 +233,25 @@ export class LabelRenderer implements SeriesRenderer {
183
233
  const s = style.startsWith('style_') ? style.substring(6) : style;
184
234
 
185
235
  switch (s) {
236
+ // All label_* styles render text INSIDE the bubble (TradingView behavior).
237
+ // The left/right/up/down refers to the pointer direction, not text position.
186
238
  case 'label_down':
187
- return 'inside';
188
239
  case 'label_up':
189
- return 'inside';
190
240
  case 'label_left':
191
- return 'left';
192
241
  case 'label_right':
193
- return 'right';
194
242
  case 'label_lower_left':
195
- return 'insideBottomLeft';
196
243
  case 'label_lower_right':
197
- return 'insideBottomRight';
198
244
  case 'label_upper_left':
199
- return 'insideTopLeft';
200
245
  case 'label_upper_right':
201
- return 'insideTopRight';
202
246
  case 'label_center':
203
247
  return 'inside';
204
248
  case 'text_outline':
205
249
  case 'none':
206
250
  // Text only, positioned based on yloc
207
- return yloc === 'abovebar' ? 'top' : yloc === 'belowbar' ? 'bottom' : 'top';
251
+ return (yloc === 'abovebar' || yloc === 'AboveBar' || yloc === 'ab') ? 'top' : (yloc === 'belowbar' || yloc === 'BelowBar' || yloc === 'bl') ? 'bottom' : 'top';
208
252
  default:
209
253
  // For simple shapes (circle, diamond, etc.), text goes outside
210
- return yloc === 'belowbar' ? 'bottom' : 'top';
254
+ return (yloc === 'belowbar' || yloc === 'BelowBar' || yloc === 'bl') ? 'bottom' : 'top';
211
255
  }
212
256
  }
213
257