@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.
- package/dist/index.d.ts +4 -1
- package/dist/qfchart.min.browser.js +14 -14
- package/dist/qfchart.min.es.js +14 -14
- package/package.json +81 -81
- package/src/QFChart.ts +52 -0
- package/src/components/LayoutManager.ts +682 -679
- package/src/components/SeriesBuilder.ts +260 -250
- package/src/components/SeriesRendererFactory.ts +8 -0
- package/src/components/TableOverlayRenderer.ts +322 -0
- package/src/components/renderers/BoxRenderer.ts +258 -0
- package/src/components/renderers/DrawingLineRenderer.ts +194 -0
- package/src/components/renderers/FillRenderer.ts +99 -99
- package/src/components/renderers/LabelRenderer.ts +85 -41
- package/src/components/renderers/LinefillRenderer.ts +155 -0
- package/src/components/renderers/PolylineRenderer.ts +197 -0
- package/src/components/renderers/SeriesRenderer.ts +21 -20
- package/src/components/renderers/ShapeRenderer.ts +121 -121
- package/src/types.ts +2 -1
- package/src/utils/ShapeUtils.ts +156 -140
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { ColorUtils } from '../utils/ColorUtils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders Pine Script table objects as HTML DOM overlays positioned
|
|
5
|
+
* absolutely over the ECharts chart canvas.
|
|
6
|
+
*
|
|
7
|
+
* Tables use fixed positions (top_left, bottom_center, etc.) rather
|
|
8
|
+
* than data coordinates, so they are rendered as HTML elements instead
|
|
9
|
+
* of ECharts custom series.
|
|
10
|
+
*/
|
|
11
|
+
export class TableOverlayRenderer {
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse a color value for table rendering.
|
|
15
|
+
* Unlike ColorUtils.parseColor (which defaults to 0.3 opacity for fills),
|
|
16
|
+
* tables treat hex/named colors as fully opaque — only rgba provides opacity.
|
|
17
|
+
*/
|
|
18
|
+
private static safeParseColor(val: any): { color: string; opacity: number } {
|
|
19
|
+
if (!val || typeof val !== 'string') {
|
|
20
|
+
return { color: '#888888', opacity: 1 };
|
|
21
|
+
}
|
|
22
|
+
// Extract opacity from rgba(), otherwise assume fully opaque
|
|
23
|
+
const rgbaMatch = val.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
24
|
+
if (rgbaMatch) {
|
|
25
|
+
const a = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1;
|
|
26
|
+
return { color: `rgb(${rgbaMatch[1]},${rgbaMatch[2]},${rgbaMatch[3]})`, opacity: a };
|
|
27
|
+
}
|
|
28
|
+
return { color: val, opacity: 1 };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Clear all existing table overlays and render new ones.
|
|
33
|
+
* @param gridRect The ECharts grid rect {x, y, width, height} in pixels,
|
|
34
|
+
* representing the actual plot area within the container.
|
|
35
|
+
*/
|
|
36
|
+
static render(container: HTMLElement, tables: any[], gridRect?: { x: number; y: number; width: number; height: number }): void {
|
|
37
|
+
TableOverlayRenderer.clearAll(container);
|
|
38
|
+
|
|
39
|
+
// Pine Script: only the last table at each position is displayed
|
|
40
|
+
const byPosition = new Map<string, any>();
|
|
41
|
+
for (const tbl of tables) {
|
|
42
|
+
if (tbl && !tbl._deleted) {
|
|
43
|
+
byPosition.set(tbl.position, tbl);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
byPosition.forEach((tbl) => {
|
|
48
|
+
const el = TableOverlayRenderer.buildTable(tbl);
|
|
49
|
+
TableOverlayRenderer.positionTable(el, tbl.position, gridRect);
|
|
50
|
+
container.appendChild(el);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static clearAll(container: HTMLElement): void {
|
|
55
|
+
while (container.firstChild) {
|
|
56
|
+
container.removeChild(container.firstChild);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private static buildTable(tbl: any): HTMLElement {
|
|
61
|
+
const table = document.createElement('table');
|
|
62
|
+
table.style.borderCollapse = 'separate';
|
|
63
|
+
table.style.borderSpacing = '0';
|
|
64
|
+
table.style.pointerEvents = 'auto';
|
|
65
|
+
table.style.fontSize = '14px';
|
|
66
|
+
table.style.lineHeight = '1.4';
|
|
67
|
+
table.style.fontFamily = 'sans-serif';
|
|
68
|
+
table.style.margin = '4px';
|
|
69
|
+
|
|
70
|
+
// Table background
|
|
71
|
+
if (tbl.bgcolor) {
|
|
72
|
+
const { color, opacity } = TableOverlayRenderer.safeParseColor(tbl.bgcolor);
|
|
73
|
+
table.style.backgroundColor = color;
|
|
74
|
+
if (opacity < 1) table.style.opacity = String(opacity);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Frame (outer border)
|
|
78
|
+
if (tbl.frame_width > 0 && tbl.frame_color) {
|
|
79
|
+
const { color: fc } = TableOverlayRenderer.safeParseColor(tbl.frame_color);
|
|
80
|
+
table.style.border = `${tbl.frame_width}px solid ${fc}`;
|
|
81
|
+
} else if (tbl.frame_width > 0) {
|
|
82
|
+
table.style.border = `${tbl.frame_width}px solid #999`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Build merge lookup: for each cell, determine colspan/rowspan
|
|
86
|
+
const mergeMap = new Map<string, { colspan: number; rowspan: number }>();
|
|
87
|
+
const mergedCells = new Set<string>();
|
|
88
|
+
|
|
89
|
+
if (tbl.merges) {
|
|
90
|
+
for (const m of tbl.merges) {
|
|
91
|
+
const key = `${m.startCol},${m.startRow}`;
|
|
92
|
+
mergeMap.set(key, {
|
|
93
|
+
colspan: m.endCol - m.startCol + 1,
|
|
94
|
+
rowspan: m.endRow - m.startRow + 1,
|
|
95
|
+
});
|
|
96
|
+
// Mark all cells covered by this merge (except the origin)
|
|
97
|
+
for (let r = m.startRow; r <= m.endRow; r++) {
|
|
98
|
+
for (let c = m.startCol; c <= m.endCol; c++) {
|
|
99
|
+
if (r === m.startRow && c === m.startCol) continue;
|
|
100
|
+
mergedCells.add(`${c},${r}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Build rows
|
|
107
|
+
const rows = tbl.rows || 0;
|
|
108
|
+
const cols = tbl.columns || 0;
|
|
109
|
+
|
|
110
|
+
for (let r = 0; r < rows; r++) {
|
|
111
|
+
const tr = document.createElement('tr');
|
|
112
|
+
|
|
113
|
+
for (let c = 0; c < cols; c++) {
|
|
114
|
+
const cellKey = `${c},${r}`;
|
|
115
|
+
|
|
116
|
+
// Skip cells that are covered by a merge
|
|
117
|
+
if (mergedCells.has(cellKey)) continue;
|
|
118
|
+
|
|
119
|
+
const td = document.createElement('td');
|
|
120
|
+
|
|
121
|
+
// Apply merge attributes
|
|
122
|
+
const merge = mergeMap.get(cellKey);
|
|
123
|
+
if (merge) {
|
|
124
|
+
if (merge.colspan > 1) td.colSpan = merge.colspan;
|
|
125
|
+
if (merge.rowspan > 1) td.rowSpan = merge.rowspan;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Cell borders
|
|
129
|
+
if (tbl.border_width > 0) {
|
|
130
|
+
const bc = tbl.border_color
|
|
131
|
+
? TableOverlayRenderer.safeParseColor(tbl.border_color).color
|
|
132
|
+
: '#999';
|
|
133
|
+
td.style.border = `${tbl.border_width}px solid ${bc}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Get cell data
|
|
137
|
+
const cellData = tbl.cells?.[r]?.[c];
|
|
138
|
+
if (cellData && !cellData._merged) {
|
|
139
|
+
// Cell text
|
|
140
|
+
td.textContent = cellData.text || '';
|
|
141
|
+
|
|
142
|
+
// Cell background
|
|
143
|
+
if (cellData.bgcolor) {
|
|
144
|
+
const { color: bg, opacity: bgOp } = TableOverlayRenderer.safeParseColor(cellData.bgcolor);
|
|
145
|
+
td.style.backgroundColor = bg;
|
|
146
|
+
if (bgOp < 1) {
|
|
147
|
+
// Use rgba for cell-level opacity to avoid affecting text
|
|
148
|
+
td.style.backgroundColor = cellData.bgcolor;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Text color
|
|
153
|
+
if (cellData.text_color) {
|
|
154
|
+
const { color: tc } = TableOverlayRenderer.safeParseColor(cellData.text_color);
|
|
155
|
+
td.style.color = tc;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Text size
|
|
159
|
+
td.style.fontSize = TableOverlayRenderer.getSizePixels(cellData.text_size) + 'px';
|
|
160
|
+
|
|
161
|
+
// Text alignment
|
|
162
|
+
td.style.textAlign = TableOverlayRenderer.mapHAlign(cellData.text_halign);
|
|
163
|
+
td.style.verticalAlign = TableOverlayRenderer.mapVAlign(cellData.text_valign);
|
|
164
|
+
|
|
165
|
+
// Font family
|
|
166
|
+
if (cellData.text_font_family === 'monospace') {
|
|
167
|
+
td.style.fontFamily = 'monospace';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Width/height (% of chart area, 0 = auto)
|
|
171
|
+
if (cellData.width > 0) {
|
|
172
|
+
td.style.width = cellData.width + '%';
|
|
173
|
+
}
|
|
174
|
+
if (cellData.height > 0) {
|
|
175
|
+
td.style.height = cellData.height + '%';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Tooltip
|
|
179
|
+
if (cellData.tooltip) {
|
|
180
|
+
td.title = cellData.tooltip;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Default padding
|
|
185
|
+
td.style.padding = '4px 6px';
|
|
186
|
+
td.style.whiteSpace = 'nowrap';
|
|
187
|
+
|
|
188
|
+
tr.appendChild(td);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
table.appendChild(tr);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return table;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private static positionTable(
|
|
198
|
+
el: HTMLElement,
|
|
199
|
+
position: string,
|
|
200
|
+
gridRect?: { x: number; y: number; width: number; height: number },
|
|
201
|
+
): void {
|
|
202
|
+
el.style.position = 'absolute';
|
|
203
|
+
|
|
204
|
+
// Use grid rect (actual plot area) if available, otherwise fall back to container edges.
|
|
205
|
+
// Inset bottom/right by a few pixels so tables don't touch the axis lines.
|
|
206
|
+
const PAD = 8;
|
|
207
|
+
const top = gridRect ? gridRect.y + 'px' : '0';
|
|
208
|
+
const left = gridRect ? gridRect.x + 'px' : '0';
|
|
209
|
+
const bottom = gridRect ? (gridRect.y + gridRect.height - PAD) + 'px' : '0';
|
|
210
|
+
const right = gridRect ? (gridRect.x + gridRect.width - PAD) + 'px' : '0';
|
|
211
|
+
const centerX = gridRect ? (gridRect.x + gridRect.width / 2) + 'px' : '50%';
|
|
212
|
+
const centerY = gridRect ? (gridRect.y + gridRect.height / 2) + 'px' : '50%';
|
|
213
|
+
|
|
214
|
+
switch (position) {
|
|
215
|
+
case 'top_left':
|
|
216
|
+
el.style.top = top;
|
|
217
|
+
el.style.left = left;
|
|
218
|
+
break;
|
|
219
|
+
case 'top_center':
|
|
220
|
+
el.style.top = top;
|
|
221
|
+
el.style.left = centerX;
|
|
222
|
+
el.style.transform = 'translateX(-50%)';
|
|
223
|
+
break;
|
|
224
|
+
case 'top_right':
|
|
225
|
+
el.style.top = top;
|
|
226
|
+
el.style.left = right;
|
|
227
|
+
el.style.transform = 'translateX(-100%)';
|
|
228
|
+
break;
|
|
229
|
+
case 'middle_left':
|
|
230
|
+
el.style.top = centerY;
|
|
231
|
+
el.style.left = left;
|
|
232
|
+
el.style.transform = 'translateY(-50%)';
|
|
233
|
+
break;
|
|
234
|
+
case 'middle_center':
|
|
235
|
+
el.style.top = centerY;
|
|
236
|
+
el.style.left = centerX;
|
|
237
|
+
el.style.transform = 'translate(-50%, -50%)';
|
|
238
|
+
break;
|
|
239
|
+
case 'middle_right':
|
|
240
|
+
el.style.top = centerY;
|
|
241
|
+
el.style.left = right;
|
|
242
|
+
el.style.transform = 'translate(-100%, -50%)';
|
|
243
|
+
break;
|
|
244
|
+
case 'bottom_left':
|
|
245
|
+
el.style.top = bottom;
|
|
246
|
+
el.style.left = left;
|
|
247
|
+
el.style.transform = 'translateY(-100%)';
|
|
248
|
+
break;
|
|
249
|
+
case 'bottom_center':
|
|
250
|
+
el.style.top = bottom;
|
|
251
|
+
el.style.left = centerX;
|
|
252
|
+
el.style.transform = 'translate(-50%, -100%)';
|
|
253
|
+
break;
|
|
254
|
+
case 'bottom_right':
|
|
255
|
+
el.style.top = bottom;
|
|
256
|
+
el.style.left = right;
|
|
257
|
+
el.style.transform = 'translate(-100%, -100%)';
|
|
258
|
+
break;
|
|
259
|
+
default:
|
|
260
|
+
el.style.top = top;
|
|
261
|
+
el.style.left = right;
|
|
262
|
+
el.style.transform = 'translateX(-100%)';
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private static getSizePixels(size: string | number): number {
|
|
268
|
+
if (typeof size === 'number' && size > 0) return size;
|
|
269
|
+
switch (size) {
|
|
270
|
+
case 'auto':
|
|
271
|
+
case 'size.auto':
|
|
272
|
+
return 12;
|
|
273
|
+
case 'tiny':
|
|
274
|
+
case 'size.tiny':
|
|
275
|
+
return 8;
|
|
276
|
+
case 'small':
|
|
277
|
+
case 'size.small':
|
|
278
|
+
return 10;
|
|
279
|
+
case 'normal':
|
|
280
|
+
case 'size.normal':
|
|
281
|
+
return 14;
|
|
282
|
+
case 'large':
|
|
283
|
+
case 'size.large':
|
|
284
|
+
return 20;
|
|
285
|
+
case 'huge':
|
|
286
|
+
case 'size.huge':
|
|
287
|
+
return 36;
|
|
288
|
+
default:
|
|
289
|
+
return 14;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private static mapHAlign(align: string): string {
|
|
294
|
+
switch (align) {
|
|
295
|
+
case 'left':
|
|
296
|
+
case 'text.align_left':
|
|
297
|
+
return 'left';
|
|
298
|
+
case 'right':
|
|
299
|
+
case 'text.align_right':
|
|
300
|
+
return 'right';
|
|
301
|
+
case 'center':
|
|
302
|
+
case 'text.align_center':
|
|
303
|
+
default:
|
|
304
|
+
return 'center';
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private static mapVAlign(align: string): string {
|
|
309
|
+
switch (align) {
|
|
310
|
+
case 'top':
|
|
311
|
+
case 'text.align_top':
|
|
312
|
+
return 'top';
|
|
313
|
+
case 'bottom':
|
|
314
|
+
case 'text.align_bottom':
|
|
315
|
+
return 'bottom';
|
|
316
|
+
case 'center':
|
|
317
|
+
case 'text.align_center':
|
|
318
|
+
default:
|
|
319
|
+
return 'middle';
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { SeriesRenderer, RenderContext } from './SeriesRenderer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert any color string to a format ECharts canvas can render with opacity.
|
|
5
|
+
* 8-digit hex (#RRGGBBAA) is not universally supported by canvas — convert to rgba().
|
|
6
|
+
*/
|
|
7
|
+
function normalizeColor(color: string | undefined): string | undefined {
|
|
8
|
+
if (!color || typeof color !== 'string') return color;
|
|
9
|
+
if (color.startsWith('#')) {
|
|
10
|
+
const hex = color.slice(1);
|
|
11
|
+
if (hex.length === 8) {
|
|
12
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
13
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
14
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
15
|
+
const a = parseInt(hex.slice(6, 8), 16) / 255;
|
|
16
|
+
return `rgba(${r},${g},${b},${a.toFixed(3)})`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return color;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Renderer for Pine Script box.* drawing objects.
|
|
24
|
+
* Each box is defined by two corners (left,top) → (right,bottom)
|
|
25
|
+
* with fill, border, optional text, and optional extend.
|
|
26
|
+
*
|
|
27
|
+
* Style name: 'drawing_box' (distinct from other styles).
|
|
28
|
+
*/
|
|
29
|
+
export class BoxRenderer implements SeriesRenderer {
|
|
30
|
+
render(context: RenderContext): any {
|
|
31
|
+
const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset } = context;
|
|
32
|
+
const offset = dataIndexOffset || 0;
|
|
33
|
+
|
|
34
|
+
// Collect all non-deleted box objects from the sparse dataArray.
|
|
35
|
+
const boxObjects: any[] = [];
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
38
|
+
const val = dataArray[i];
|
|
39
|
+
if (!val) continue;
|
|
40
|
+
|
|
41
|
+
const items = Array.isArray(val) ? val : [val];
|
|
42
|
+
for (const bx of items) {
|
|
43
|
+
if (bx && typeof bx === 'object' && !bx._deleted) {
|
|
44
|
+
boxObjects.push(bx);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (boxObjects.length === 0) {
|
|
50
|
+
return { name: seriesName, type: 'custom', xAxisIndex, yAxisIndex, data: [], silent: true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Compute y-range for axis scaling
|
|
54
|
+
let yMin = Infinity, yMax = -Infinity;
|
|
55
|
+
for (const bx of boxObjects) {
|
|
56
|
+
if (bx.top < yMin) yMin = bx.top;
|
|
57
|
+
if (bx.top > yMax) yMax = bx.top;
|
|
58
|
+
if (bx.bottom < yMin) yMin = bx.bottom;
|
|
59
|
+
if (bx.bottom > yMax) yMax = bx.bottom;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Use a SINGLE data entry spanning the full x-range so renderItem is always called.
|
|
63
|
+
// ECharts filters a data item only when ALL its x-dimensions are on the same side
|
|
64
|
+
// of the visible window. With dims 0=0 and 1=lastBar the item always straddles
|
|
65
|
+
// the viewport, so renderItem fires exactly once regardless of scroll position.
|
|
66
|
+
// Dims 2/3 are yMin/yMax for axis scaling.
|
|
67
|
+
const totalBars = (context.candlestickData?.length || 0) + offset;
|
|
68
|
+
const lastBarIndex = Math.max(0, totalBars - 1);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
name: seriesName,
|
|
72
|
+
type: 'custom',
|
|
73
|
+
xAxisIndex,
|
|
74
|
+
yAxisIndex,
|
|
75
|
+
renderItem: (params: any, api: any) => {
|
|
76
|
+
const children: any[] = [];
|
|
77
|
+
|
|
78
|
+
for (const bx of boxObjects) {
|
|
79
|
+
if (bx._deleted) continue;
|
|
80
|
+
|
|
81
|
+
const xOff = (bx.xloc === 'bar_index' || bx.xloc === 'bi') ? offset : 0;
|
|
82
|
+
const pTopLeft = api.coord([bx.left + xOff, bx.top]);
|
|
83
|
+
const pBottomRight = api.coord([bx.right + xOff, bx.bottom]);
|
|
84
|
+
|
|
85
|
+
let x = pTopLeft[0];
|
|
86
|
+
let y = pTopLeft[1];
|
|
87
|
+
let w = pBottomRight[0] - pTopLeft[0];
|
|
88
|
+
let h = pBottomRight[1] - pTopLeft[1];
|
|
89
|
+
|
|
90
|
+
// Handle extend (horizontal borders)
|
|
91
|
+
const extend = bx.extend || 'none';
|
|
92
|
+
if (extend !== 'none') {
|
|
93
|
+
const cs = params.coordSys;
|
|
94
|
+
if (extend === 'left' || extend === 'both') {
|
|
95
|
+
x = cs.x;
|
|
96
|
+
w = (extend === 'both') ? cs.width : (pBottomRight[0] - cs.x);
|
|
97
|
+
}
|
|
98
|
+
if (extend === 'right' || extend === 'both') {
|
|
99
|
+
if (extend === 'right') {
|
|
100
|
+
w = cs.x + cs.width - pTopLeft[0];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Background fill rect
|
|
106
|
+
const bgColor = normalizeColor(bx.bgcolor) || '#2962ff';
|
|
107
|
+
children.push({
|
|
108
|
+
type: 'rect',
|
|
109
|
+
shape: { x, y, width: w, height: h },
|
|
110
|
+
style: { fill: bgColor },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Border rect (on top of fill)
|
|
114
|
+
const borderColor = normalizeColor(bx.border_color) || '#2962ff';
|
|
115
|
+
const borderWidth = bx.border_width ?? 1;
|
|
116
|
+
if (borderWidth > 0) {
|
|
117
|
+
children.push({
|
|
118
|
+
type: 'rect',
|
|
119
|
+
shape: { x, y, width: w, height: h },
|
|
120
|
+
style: {
|
|
121
|
+
fill: 'none',
|
|
122
|
+
stroke: borderColor,
|
|
123
|
+
lineWidth: borderWidth,
|
|
124
|
+
lineDash: this.getDashPattern(bx.border_style),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Text inside box
|
|
130
|
+
if (bx.text) {
|
|
131
|
+
const textX = this.getTextX(x, w, bx.text_halign);
|
|
132
|
+
const textY = this.getTextY(y, h, bx.text_valign);
|
|
133
|
+
children.push({
|
|
134
|
+
type: 'text',
|
|
135
|
+
style: {
|
|
136
|
+
x: textX,
|
|
137
|
+
y: textY,
|
|
138
|
+
text: bx.text,
|
|
139
|
+
fill: normalizeColor(bx.text_color) || '#000000',
|
|
140
|
+
fontSize: this.getSizePixels(bx.text_size),
|
|
141
|
+
fontFamily: bx.text_font_family === 'monospace' ? 'monospace' : 'sans-serif',
|
|
142
|
+
fontWeight: (bx.text_formatting === 'format_bold') ? 'bold' : 'normal',
|
|
143
|
+
fontStyle: (bx.text_formatting === 'format_italic') ? 'italic' : 'normal',
|
|
144
|
+
textAlign: this.mapHAlign(bx.text_halign),
|
|
145
|
+
textVerticalAlign: this.mapVAlign(bx.text_valign),
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { type: 'group', children };
|
|
152
|
+
},
|
|
153
|
+
data: [[0, lastBarIndex, yMin, yMax]],
|
|
154
|
+
clip: true,
|
|
155
|
+
encode: { x: [0, 1], y: [2, 3] },
|
|
156
|
+
z: 14,
|
|
157
|
+
silent: true,
|
|
158
|
+
emphasis: { disabled: true },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private getDashPattern(style: string): number[] | undefined {
|
|
163
|
+
switch (style) {
|
|
164
|
+
case 'style_dotted':
|
|
165
|
+
return [2, 2];
|
|
166
|
+
case 'style_dashed':
|
|
167
|
+
return [6, 4];
|
|
168
|
+
default:
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private getSizePixels(size: string | number): number {
|
|
174
|
+
if (typeof size === 'number' && size > 0) return size;
|
|
175
|
+
switch (size) {
|
|
176
|
+
case 'auto':
|
|
177
|
+
case 'size.auto':
|
|
178
|
+
return 12;
|
|
179
|
+
case 'tiny':
|
|
180
|
+
case 'size.tiny':
|
|
181
|
+
return 8;
|
|
182
|
+
case 'small':
|
|
183
|
+
case 'size.small':
|
|
184
|
+
return 10;
|
|
185
|
+
case 'normal':
|
|
186
|
+
case 'size.normal':
|
|
187
|
+
return 14;
|
|
188
|
+
case 'large':
|
|
189
|
+
case 'size.large':
|
|
190
|
+
return 20;
|
|
191
|
+
case 'huge':
|
|
192
|
+
case 'size.huge':
|
|
193
|
+
return 36;
|
|
194
|
+
default:
|
|
195
|
+
return 12;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private mapHAlign(align: string): string {
|
|
200
|
+
switch (align) {
|
|
201
|
+
case 'left':
|
|
202
|
+
case 'text.align_left':
|
|
203
|
+
return 'left';
|
|
204
|
+
case 'right':
|
|
205
|
+
case 'text.align_right':
|
|
206
|
+
return 'right';
|
|
207
|
+
case 'center':
|
|
208
|
+
case 'text.align_center':
|
|
209
|
+
default:
|
|
210
|
+
return 'center';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private mapVAlign(align: string): string {
|
|
215
|
+
switch (align) {
|
|
216
|
+
case 'top':
|
|
217
|
+
case 'text.align_top':
|
|
218
|
+
return 'top';
|
|
219
|
+
case 'bottom':
|
|
220
|
+
case 'text.align_bottom':
|
|
221
|
+
return 'bottom';
|
|
222
|
+
case 'center':
|
|
223
|
+
case 'text.align_center':
|
|
224
|
+
default:
|
|
225
|
+
return 'middle';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private getTextX(x: number, w: number, halign: string): number {
|
|
230
|
+
switch (halign) {
|
|
231
|
+
case 'left':
|
|
232
|
+
case 'text.align_left':
|
|
233
|
+
return x + 4;
|
|
234
|
+
case 'right':
|
|
235
|
+
case 'text.align_right':
|
|
236
|
+
return x + w - 4;
|
|
237
|
+
case 'center':
|
|
238
|
+
case 'text.align_center':
|
|
239
|
+
default:
|
|
240
|
+
return x + w / 2;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private getTextY(y: number, h: number, valign: string): number {
|
|
245
|
+
switch (valign) {
|
|
246
|
+
case 'top':
|
|
247
|
+
case 'text.align_top':
|
|
248
|
+
return y + 4;
|
|
249
|
+
case 'bottom':
|
|
250
|
+
case 'text.align_bottom':
|
|
251
|
+
return y + h - 4;
|
|
252
|
+
case 'center':
|
|
253
|
+
case 'text.align_center':
|
|
254
|
+
default:
|
|
255
|
+
return y + h / 2;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|