@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.
- package/dist/index.d.ts +1 -1
- package/dist/qfchart.min.browser.js +7 -7
- package/dist/qfchart.min.es.js +10 -10
- package/package.json +1 -1
- package/src/QFChart.ts +1434 -1434
- package/src/components/SeriesRendererFactory.ts +38 -36
- package/src/components/renderers/LabelRenderer.ts +231 -0
- package/src/types.ts +206 -205
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
this.register('
|
|
17
|
-
this.register('
|
|
18
|
-
this.register('
|
|
19
|
-
this.register('
|
|
20
|
-
this.register('
|
|
21
|
-
this.register('
|
|
22
|
-
this.register('
|
|
23
|
-
this.register('
|
|
24
|
-
this.register('
|
|
25
|
-
this.register('
|
|
26
|
-
this.register('
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
}
|