@qfo/qfchart 0.6.6 → 0.6.8

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,188 @@
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
+ const lineData: number[][] = [];
22
+
23
+ for (let i = 0; i < dataArray.length; i++) {
24
+ const val = dataArray[i];
25
+ if (!val) continue;
26
+
27
+ const items = Array.isArray(val) ? val : [val];
28
+ for (const ln of items) {
29
+ if (ln && typeof ln === 'object' && !ln._deleted) {
30
+ lineObjects.push(ln);
31
+ // Apply padding offset for bar_index-based coordinates
32
+ const xOff = ln.xloc === 'bar_index' ? offset : 0;
33
+ lineData.push([ln.x1 + xOff, ln.y1, ln.x2 + xOff, ln.y2]);
34
+ }
35
+ }
36
+ }
37
+
38
+ if (lineData.length === 0) {
39
+ return { name: seriesName, type: 'custom', xAxisIndex, yAxisIndex, data: [], silent: true };
40
+ }
41
+
42
+ return {
43
+ name: seriesName,
44
+ type: 'custom',
45
+ xAxisIndex,
46
+ yAxisIndex,
47
+ renderItem: (params: any, api: any) => {
48
+ const idx = params.dataIndex;
49
+ const ln = lineObjects[idx];
50
+ if (!ln || ln._deleted) return;
51
+
52
+ const x1 = api.value(0);
53
+ const y1 = api.value(1);
54
+ const x2 = api.value(2);
55
+ const y2 = api.value(3);
56
+
57
+ let p1 = api.coord([x1, y1]);
58
+ let p2 = api.coord([x2, y2]);
59
+
60
+ // Handle extend (none | left | right | both)
61
+ const extend = ln.extend || 'none';
62
+ if (extend !== 'none') {
63
+ const cs = params.coordSys;
64
+ const left = cs.x;
65
+ const right = cs.x + cs.width;
66
+ const top = cs.y;
67
+ const bottom = cs.y + cs.height;
68
+ [p1, p2] = this.extendLine(p1, p2, extend, left, right, top, bottom);
69
+ }
70
+
71
+ const children: any[] = [];
72
+ const color = ln.color || defaultColor;
73
+ const lineWidth = ln.width || 1;
74
+
75
+ // Main line segment
76
+ children.push({
77
+ type: 'line',
78
+ shape: { x1: p1[0], y1: p1[1], x2: p2[0], y2: p2[1] },
79
+ style: {
80
+ stroke: color,
81
+ lineWidth: lineWidth,
82
+ lineDash: this.getDashPattern(ln.style),
83
+ },
84
+ });
85
+
86
+ // Arrow heads based on style
87
+ const style = ln.style || 'style_solid';
88
+ if (style === 'style_arrow_left' || style === 'style_arrow_both') {
89
+ const arrow = this.arrowHead(p2, p1, lineWidth, color);
90
+ if (arrow) children.push(arrow);
91
+ }
92
+ if (style === 'style_arrow_right' || style === 'style_arrow_both') {
93
+ const arrow = this.arrowHead(p1, p2, lineWidth, color);
94
+ if (arrow) children.push(arrow);
95
+ }
96
+
97
+ return { type: 'group', children };
98
+ },
99
+ data: lineData,
100
+ z: 15,
101
+ silent: true,
102
+ emphasis: { disabled: true },
103
+ };
104
+ }
105
+
106
+ private getDashPattern(style: string): number[] | undefined {
107
+ switch (style) {
108
+ case 'style_dotted':
109
+ return [2, 2];
110
+ case 'style_dashed':
111
+ return [6, 4];
112
+ default:
113
+ return undefined;
114
+ }
115
+ }
116
+
117
+ private extendLine(
118
+ p1: number[],
119
+ p2: number[],
120
+ extend: string,
121
+ left: number,
122
+ right: number,
123
+ top: number,
124
+ bottom: number,
125
+ ): [number[], number[]] {
126
+ const dx = p2[0] - p1[0];
127
+ const dy = p2[1] - p1[1];
128
+
129
+ if (dx === 0 && dy === 0) return [p1, p2];
130
+
131
+ const extendPoint = (origin: number[], dir: number[]): number[] => {
132
+ let tMax = Infinity;
133
+ if (dir[0] !== 0) {
134
+ const tx = dir[0] > 0 ? (right - origin[0]) / dir[0] : (left - origin[0]) / dir[0];
135
+ tMax = Math.min(tMax, tx);
136
+ }
137
+ if (dir[1] !== 0) {
138
+ const ty = dir[1] > 0 ? (bottom - origin[1]) / dir[1] : (top - origin[1]) / dir[1];
139
+ tMax = Math.min(tMax, ty);
140
+ }
141
+ if (!isFinite(tMax)) tMax = 0;
142
+ return [origin[0] + tMax * dir[0], origin[1] + tMax * dir[1]];
143
+ };
144
+
145
+ let newP1 = p1;
146
+ let newP2 = p2;
147
+
148
+ if (extend === 'right' || extend === 'both') {
149
+ newP2 = extendPoint(p1, [dx, dy]);
150
+ }
151
+ if (extend === 'left' || extend === 'both') {
152
+ newP1 = extendPoint(p2, [-dx, -dy]);
153
+ }
154
+
155
+ return [newP1, newP2];
156
+ }
157
+
158
+ private arrowHead(from: number[], to: number[], lineWidth: number, color: string): any {
159
+ const dx = to[0] - from[0];
160
+ const dy = to[1] - from[1];
161
+ const len = Math.sqrt(dx * dx + dy * dy);
162
+ if (len < 1) return null;
163
+
164
+ const size = Math.max(8, lineWidth * 4);
165
+ const nx = dx / len;
166
+ const ny = dy / len;
167
+
168
+ // Arrow tip at `to`, base offset back by `size`
169
+ const bx = to[0] - nx * size;
170
+ const by = to[1] - ny * size;
171
+
172
+ // Perpendicular offset for arrowhead width
173
+ const px = -ny * size * 0.4;
174
+ const py = nx * size * 0.4;
175
+
176
+ return {
177
+ type: 'polygon',
178
+ shape: {
179
+ points: [
180
+ [to[0], to[1]],
181
+ [bx + px, by + py],
182
+ [bx - px, by - py],
183
+ ],
184
+ },
185
+ style: { fill: color },
186
+ };
187
+ }
188
+ }
@@ -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
+ }
@@ -0,0 +1,274 @@
1
+ import { SeriesRenderer, RenderContext } from './SeriesRenderer';
2
+ import { ShapeUtils } from '../../utils/ShapeUtils';
3
+
4
+ export class LabelRenderer implements SeriesRenderer {
5
+ render(context: RenderContext): any {
6
+ const { seriesName, xAxisIndex, yAxisIndex, dataArray, candlestickData, dataIndexOffset } = context;
7
+ const offset = dataIndexOffset || 0;
8
+
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
+ }
24
+
25
+ const labelData = labelObjects
26
+ .map((lbl) => {
27
+ const text = lbl.text || '';
28
+ const color = lbl.color || '#2962ff';
29
+ const textcolor = lbl.textcolor || '#ffffff';
30
+ const yloc = lbl.yloc || 'price';
31
+ const styleRaw = lbl.style || 'style_label_down';
32
+ const size = lbl.size || 'normal';
33
+ const textalign = lbl.textalign || 'align_center';
34
+ const tooltip = lbl.tooltip || '';
35
+
36
+ // Map Pine style string to shape name for ShapeUtils
37
+ const shape = this.styleToShape(styleRaw);
38
+
39
+ // Determine X position using label's own x coordinate
40
+ const xPos = lbl.xloc === 'bar_index' ? (lbl.x + offset) : lbl.x;
41
+
42
+ // Determine Y value based on yloc
43
+ let yValue = lbl.y;
44
+ let symbolOffset: (string | number)[] = [0, 0];
45
+
46
+ if (yloc === 'abovebar') {
47
+ if (candlestickData && candlestickData[xPos]) {
48
+ yValue = candlestickData[xPos].high;
49
+ }
50
+ symbolOffset = [0, '-150%'];
51
+ } else if (yloc === 'belowbar') {
52
+ if (candlestickData && candlestickData[xPos]) {
53
+ yValue = candlestickData[xPos].low;
54
+ }
55
+ symbolOffset = [0, '150%'];
56
+ }
57
+
58
+ // Get symbol from ShapeUtils
59
+ const symbol = ShapeUtils.getShapeSymbol(shape);
60
+ const symbolSize = ShapeUtils.getShapeSize(size);
61
+
62
+ // Compute font size for this label
63
+ const fontSize = this.getSizePx(size);
64
+
65
+ // Dynamically size the bubble to fit text content
66
+ let finalSize: number | number[];
67
+ const isBubble = shape === 'labeldown' || shape === 'labelup' ||
68
+ shape === 'labelleft' || shape === 'labelright';
69
+ // Track label text offset for centering text within the body
70
+ // (excluding the pointer area)
71
+ let labelTextOffset: [number, number] = [0, 0];
72
+
73
+ if (isBubble) {
74
+ // Approximate text width: chars * fontSize * avgCharWidthRatio (bold)
75
+ const textWidth = text.length * fontSize * 0.65;
76
+ const minWidth = fontSize * 2.5;
77
+ const bubbleWidth = Math.max(minWidth, textWidth + fontSize * 1.6);
78
+ const bubbleHeight = fontSize * 2.8;
79
+
80
+ // SVG pointer takes 3/24 = 12.5% of the path dimension
81
+ const pointerRatio = 3 / 24;
82
+
83
+ if (shape === 'labelleft' || shape === 'labelright') {
84
+ // Add extra width for the pointer
85
+ const totalWidth = bubbleWidth / (1 - pointerRatio);
86
+ finalSize = [totalWidth, bubbleHeight];
87
+
88
+ // Offset so the pointer tip sits at the anchor x position.
89
+ const xOff = typeof symbolOffset[0] === 'string' ? 0
90
+ : (symbolOffset[0] as number);
91
+ if (shape === 'labelleft') {
92
+ // Pointer on left → shift bubble body to the right
93
+ symbolOffset = [xOff + totalWidth * 0.42, symbolOffset[1]];
94
+ // Shift text right to center within body (not pointer)
95
+ labelTextOffset = [totalWidth * pointerRatio * 0.5, 0];
96
+ } else {
97
+ // Pointer on right → shift bubble body to the left
98
+ symbolOffset = [xOff - totalWidth * 0.42, symbolOffset[1]];
99
+ // Shift text left to center within body
100
+ labelTextOffset = [-totalWidth * pointerRatio * 0.5, 0];
101
+ }
102
+ } else {
103
+ // Vertical pointer (up/down)
104
+ const totalHeight = bubbleHeight / (1 - pointerRatio);
105
+ finalSize = [bubbleWidth, totalHeight];
106
+
107
+ // Offset bubble so the pointer tip sits at the anchor price.
108
+ if (shape === 'labeldown') {
109
+ symbolOffset = [symbolOffset[0], typeof symbolOffset[1] === 'string'
110
+ ? symbolOffset[1]
111
+ : (symbolOffset[1] as number) - totalHeight * 0.42];
112
+ labelTextOffset = [0, -totalHeight * pointerRatio * 0.5];
113
+ } else {
114
+ symbolOffset = [symbolOffset[0], typeof symbolOffset[1] === 'string'
115
+ ? symbolOffset[1]
116
+ : (symbolOffset[1] as number) + totalHeight * 0.42];
117
+ labelTextOffset = [0, totalHeight * pointerRatio * 0.5];
118
+ }
119
+ }
120
+ } else if (shape === 'none') {
121
+ finalSize = 0;
122
+ } else {
123
+ if (Array.isArray(symbolSize)) {
124
+ finalSize = [symbolSize[0] * 1.5, symbolSize[1] * 1.5];
125
+ } else {
126
+ finalSize = symbolSize * 1.5;
127
+ }
128
+ }
129
+
130
+ // Determine label position based on style direction
131
+ const labelPosition = this.getLabelPosition(styleRaw, yloc);
132
+ const isInsideLabel = labelPosition === 'inside' ||
133
+ labelPosition.startsWith('inside');
134
+
135
+ const item: any = {
136
+ value: [xPos, yValue],
137
+ symbol: symbol,
138
+ symbolSize: finalSize,
139
+ symbolOffset: symbolOffset,
140
+ itemStyle: {
141
+ color: color,
142
+ },
143
+ label: {
144
+ show: !!text,
145
+ position: labelPosition,
146
+ distance: isInsideLabel ? 0 : 5,
147
+ offset: labelTextOffset,
148
+ formatter: text,
149
+ color: textcolor,
150
+ fontSize: fontSize,
151
+ fontWeight: 'bold',
152
+ align: isInsideLabel ? 'center'
153
+ : textalign === 'align_left' ? 'left'
154
+ : textalign === 'align_right' ? 'right'
155
+ : 'center',
156
+ verticalAlign: 'middle',
157
+ padding: [2, 6],
158
+ },
159
+ };
160
+
161
+ if (tooltip) {
162
+ item.tooltip = { formatter: tooltip };
163
+ }
164
+
165
+ return item;
166
+ })
167
+ .filter((item) => item !== null);
168
+
169
+ return {
170
+ name: seriesName,
171
+ type: 'scatter',
172
+ xAxisIndex: xAxisIndex,
173
+ yAxisIndex: yAxisIndex,
174
+ data: labelData,
175
+ z: 20,
176
+ };
177
+ }
178
+
179
+ private styleToShape(style: string): string {
180
+ // Strip 'style_' prefix
181
+ const s = style.startsWith('style_') ? style.substring(6) : style;
182
+
183
+ switch (s) {
184
+ case 'label_down':
185
+ return 'labeldown';
186
+ case 'label_up':
187
+ return 'labelup';
188
+ case 'label_left':
189
+ return 'labelleft';
190
+ case 'label_right':
191
+ return 'labelright';
192
+ case 'label_lower_left':
193
+ return 'labeldown';
194
+ case 'label_lower_right':
195
+ return 'labeldown';
196
+ case 'label_upper_left':
197
+ return 'labelup';
198
+ case 'label_upper_right':
199
+ return 'labelup';
200
+ case 'label_center':
201
+ return 'labeldown';
202
+ case 'circle':
203
+ return 'circle';
204
+ case 'square':
205
+ return 'square';
206
+ case 'diamond':
207
+ return 'diamond';
208
+ case 'flag':
209
+ return 'flag';
210
+ case 'arrowup':
211
+ return 'arrowup';
212
+ case 'arrowdown':
213
+ return 'arrowdown';
214
+ case 'cross':
215
+ return 'cross';
216
+ case 'xcross':
217
+ return 'xcross';
218
+ case 'triangleup':
219
+ return 'triangleup';
220
+ case 'triangledown':
221
+ return 'triangledown';
222
+ case 'text_outline':
223
+ return 'none';
224
+ case 'none':
225
+ return 'none';
226
+ default:
227
+ return 'labeldown';
228
+ }
229
+ }
230
+
231
+ private getLabelPosition(style: string, yloc: string): string {
232
+ const s = style.startsWith('style_') ? style.substring(6) : style;
233
+
234
+ switch (s) {
235
+ // All label_* styles render text INSIDE the bubble (TradingView behavior).
236
+ // The left/right/up/down refers to the pointer direction, not text position.
237
+ case 'label_down':
238
+ case 'label_up':
239
+ case 'label_left':
240
+ case 'label_right':
241
+ case 'label_lower_left':
242
+ case 'label_lower_right':
243
+ case 'label_upper_left':
244
+ case 'label_upper_right':
245
+ case 'label_center':
246
+ return 'inside';
247
+ case 'text_outline':
248
+ case 'none':
249
+ // Text only, positioned based on yloc
250
+ return yloc === 'abovebar' ? 'top' : yloc === 'belowbar' ? 'bottom' : 'top';
251
+ default:
252
+ // For simple shapes (circle, diamond, etc.), text goes outside
253
+ return yloc === 'belowbar' ? 'bottom' : 'top';
254
+ }
255
+ }
256
+
257
+ private getSizePx(size: string): number {
258
+ switch (size) {
259
+ case 'tiny':
260
+ return 8;
261
+ case 'small':
262
+ return 9;
263
+ case 'normal':
264
+ case 'auto':
265
+ return 10;
266
+ case 'large':
267
+ return 12;
268
+ case 'huge':
269
+ return 14;
270
+ default:
271
+ return 10;
272
+ }
273
+ }
274
+ }