@qfo/qfchart 0.6.6 → 0.6.7

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,36 +1,38 @@
1
- import { SeriesRenderer } from './renderers/SeriesRenderer';
2
- import { LineRenderer } from './renderers/LineRenderer';
3
- import { StepRenderer } from './renderers/StepRenderer';
4
- import { HistogramRenderer } from './renderers/HistogramRenderer';
5
- import { ScatterRenderer } from './renderers/ScatterRenderer';
6
- import { OHLCBarRenderer } from './renderers/OHLCBarRenderer';
7
- import { ShapeRenderer } from './renderers/ShapeRenderer';
8
- import { BackgroundRenderer } from './renderers/BackgroundRenderer';
9
- import { FillRenderer } from './renderers/FillRenderer';
10
-
11
- export class SeriesRendererFactory {
12
- private static renderers: Map<string, SeriesRenderer> = new Map();
13
-
14
- static {
15
- this.register('line', new LineRenderer());
16
- this.register('step', new StepRenderer());
17
- this.register('histogram', new HistogramRenderer());
18
- this.register('columns', new HistogramRenderer());
19
- this.register('circles', new ScatterRenderer());
20
- this.register('cross', new ScatterRenderer());
21
- this.register('char', new ScatterRenderer());
22
- this.register('bar', new OHLCBarRenderer());
23
- this.register('candle', new OHLCBarRenderer());
24
- this.register('shape', new ShapeRenderer());
25
- this.register('background', new BackgroundRenderer());
26
- this.register('fill', new FillRenderer());
27
- }
28
-
29
- public static register(style: string, renderer: SeriesRenderer) {
30
- this.renderers.set(style, renderer);
31
- }
32
-
33
- public static get(style: string): SeriesRenderer {
34
- return this.renderers.get(style) || this.renderers.get('line')!; // Default to line
35
- }
36
- }
1
+ import { SeriesRenderer } from './renderers/SeriesRenderer';
2
+ import { LineRenderer } from './renderers/LineRenderer';
3
+ import { StepRenderer } from './renderers/StepRenderer';
4
+ import { HistogramRenderer } from './renderers/HistogramRenderer';
5
+ import { ScatterRenderer } from './renderers/ScatterRenderer';
6
+ import { OHLCBarRenderer } from './renderers/OHLCBarRenderer';
7
+ import { ShapeRenderer } from './renderers/ShapeRenderer';
8
+ import { BackgroundRenderer } from './renderers/BackgroundRenderer';
9
+ import { FillRenderer } from './renderers/FillRenderer';
10
+ import { LabelRenderer } from './renderers/LabelRenderer';
11
+
12
+ export class SeriesRendererFactory {
13
+ private static renderers: Map<string, SeriesRenderer> = new Map();
14
+
15
+ static {
16
+ this.register('line', new LineRenderer());
17
+ this.register('step', new StepRenderer());
18
+ this.register('histogram', new HistogramRenderer());
19
+ this.register('columns', new HistogramRenderer());
20
+ this.register('circles', new ScatterRenderer());
21
+ this.register('cross', new ScatterRenderer());
22
+ this.register('char', new ScatterRenderer());
23
+ this.register('bar', new OHLCBarRenderer());
24
+ this.register('candle', new OHLCBarRenderer());
25
+ this.register('shape', new ShapeRenderer());
26
+ this.register('background', new BackgroundRenderer());
27
+ this.register('fill', new FillRenderer());
28
+ this.register('label', new LabelRenderer());
29
+ }
30
+
31
+ public static register(style: string, renderer: SeriesRenderer) {
32
+ this.renderers.set(style, renderer);
33
+ }
34
+
35
+ public static get(style: string): SeriesRenderer {
36
+ return this.renderers.get(style) || this.renderers.get('line')!; // Default to line
37
+ }
38
+ }
@@ -0,0 +1,231 @@
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 } = context;
7
+
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;
15
+
16
+ const text = lbl.text || '';
17
+ const color = lbl.color || '#2962ff';
18
+ const textcolor = lbl.textcolor || '#ffffff';
19
+ const yloc = lbl.yloc || 'price';
20
+ const styleRaw = lbl.style || 'style_label_down';
21
+ const size = lbl.size || 'normal';
22
+ const textalign = lbl.textalign || 'align_center';
23
+ const tooltip = lbl.tooltip || '';
24
+
25
+ // Map Pine style string to shape name for ShapeUtils
26
+ const shape = this.styleToShape(styleRaw);
27
+
28
+ // Determine Y value based on yloc
29
+ let yValue = lbl.y;
30
+ let symbolOffset: (string | number)[] = [0, 0];
31
+
32
+ if (yloc === 'abovebar') {
33
+ if (candlestickData && candlestickData[i]) {
34
+ yValue = candlestickData[i].high;
35
+ }
36
+ symbolOffset = [0, '-150%'];
37
+ } else if (yloc === 'belowbar') {
38
+ if (candlestickData && candlestickData[i]) {
39
+ yValue = candlestickData[i].low;
40
+ }
41
+ symbolOffset = [0, '150%'];
42
+ }
43
+
44
+ // Get symbol from ShapeUtils
45
+ const symbol = ShapeUtils.getShapeSymbol(shape);
46
+ const symbolSize = ShapeUtils.getShapeSize(size);
47
+
48
+ // Compute font size for this label
49
+ const fontSize = this.getSizePx(size);
50
+
51
+ // Dynamically size the bubble to fit text content
52
+ let finalSize: number | number[];
53
+ if (shape === 'labeldown' || shape === 'labelup') {
54
+ // Approximate text width: chars * fontSize * avgCharWidthRatio (bold)
55
+ const textWidth = text.length * fontSize * 0.65;
56
+ const minWidth = fontSize * 2.5;
57
+ const bubbleWidth = Math.max(minWidth, textWidth + fontSize * 1.6);
58
+ 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];
67
+ } else {
68
+ symbolOffset = [symbolOffset[0], typeof symbolOffset[1] === 'string'
69
+ ? symbolOffset[1]
70
+ : (symbolOffset[1] as number) + bubbleHeight * 0.35];
71
+ }
72
+ } else if (shape === 'none') {
73
+ finalSize = 0;
74
+ } else {
75
+ if (Array.isArray(symbolSize)) {
76
+ finalSize = [symbolSize[0] * 1.5, symbolSize[1] * 1.5];
77
+ } else {
78
+ finalSize = symbolSize * 1.5;
79
+ }
80
+ }
81
+
82
+ // Determine label position based on style direction
83
+ const labelPosition = this.getLabelPosition(styleRaw, yloc);
84
+ const isInsideLabel = labelPosition === 'inside' ||
85
+ labelPosition.startsWith('inside');
86
+
87
+ const item: any = {
88
+ value: [i, yValue],
89
+ symbol: symbol,
90
+ symbolSize: finalSize,
91
+ symbolOffset: symbolOffset,
92
+ itemStyle: {
93
+ color: color,
94
+ },
95
+ label: {
96
+ show: !!text,
97
+ position: labelPosition,
98
+ distance: isInsideLabel ? 0 : 5,
99
+ formatter: text,
100
+ color: textcolor,
101
+ fontSize: fontSize,
102
+ fontWeight: 'bold',
103
+ align: isInsideLabel ? 'center'
104
+ : textalign === 'align_left' ? 'left'
105
+ : textalign === 'align_right' ? 'right'
106
+ : 'center',
107
+ verticalAlign: 'middle',
108
+ padding: [2, 6],
109
+ },
110
+ };
111
+
112
+ if (tooltip) {
113
+ item.tooltip = { formatter: tooltip };
114
+ }
115
+
116
+ return item;
117
+ })
118
+ .filter((item) => item !== null);
119
+
120
+ return {
121
+ name: seriesName,
122
+ type: 'scatter',
123
+ xAxisIndex: xAxisIndex,
124
+ yAxisIndex: yAxisIndex,
125
+ data: labelData,
126
+ z: 20,
127
+ };
128
+ }
129
+
130
+ private styleToShape(style: string): string {
131
+ // Strip 'style_' prefix
132
+ const s = style.startsWith('style_') ? style.substring(6) : style;
133
+
134
+ switch (s) {
135
+ case 'label_down':
136
+ return 'labeldown';
137
+ case 'label_up':
138
+ return 'labelup';
139
+ case 'label_left':
140
+ return 'labeldown'; // Use labeldown shape, position text left
141
+ case 'label_right':
142
+ return 'labeldown'; // Use labeldown shape, position text right
143
+ case 'label_lower_left':
144
+ return 'labeldown';
145
+ case 'label_lower_right':
146
+ return 'labeldown';
147
+ case 'label_upper_left':
148
+ return 'labelup';
149
+ case 'label_upper_right':
150
+ return 'labelup';
151
+ case 'label_center':
152
+ return 'labeldown';
153
+ case 'circle':
154
+ return 'circle';
155
+ case 'square':
156
+ return 'square';
157
+ case 'diamond':
158
+ return 'diamond';
159
+ case 'flag':
160
+ return 'flag';
161
+ case 'arrowup':
162
+ return 'arrowup';
163
+ case 'arrowdown':
164
+ return 'arrowdown';
165
+ case 'cross':
166
+ return 'cross';
167
+ case 'xcross':
168
+ return 'xcross';
169
+ case 'triangleup':
170
+ return 'triangleup';
171
+ case 'triangledown':
172
+ return 'triangledown';
173
+ case 'text_outline':
174
+ return 'none';
175
+ case 'none':
176
+ return 'none';
177
+ default:
178
+ return 'labeldown';
179
+ }
180
+ }
181
+
182
+ private getLabelPosition(style: string, yloc: string): string {
183
+ const s = style.startsWith('style_') ? style.substring(6) : style;
184
+
185
+ switch (s) {
186
+ case 'label_down':
187
+ return 'inside';
188
+ case 'label_up':
189
+ return 'inside';
190
+ case 'label_left':
191
+ return 'left';
192
+ case 'label_right':
193
+ return 'right';
194
+ case 'label_lower_left':
195
+ return 'insideBottomLeft';
196
+ case 'label_lower_right':
197
+ return 'insideBottomRight';
198
+ case 'label_upper_left':
199
+ return 'insideTopLeft';
200
+ case 'label_upper_right':
201
+ return 'insideTopRight';
202
+ case 'label_center':
203
+ return 'inside';
204
+ case 'text_outline':
205
+ case 'none':
206
+ // Text only, positioned based on yloc
207
+ return yloc === 'abovebar' ? 'top' : yloc === 'belowbar' ? 'bottom' : 'top';
208
+ default:
209
+ // For simple shapes (circle, diamond, etc.), text goes outside
210
+ return yloc === 'belowbar' ? 'bottom' : 'top';
211
+ }
212
+ }
213
+
214
+ private getSizePx(size: string): number {
215
+ switch (size) {
216
+ case 'tiny':
217
+ return 8;
218
+ case 'small':
219
+ return 9;
220
+ case 'normal':
221
+ case 'auto':
222
+ return 10;
223
+ case 'large':
224
+ return 12;
225
+ case 'huge':
226
+ return 14;
227
+ default:
228
+ return 10;
229
+ }
230
+ }
231
+ }