@qfo/qfchart 0.6.8 → 0.7.2

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.
@@ -158,6 +158,7 @@ export class SeriesBuilder {
158
158
  // Prepare data arrays
159
159
  // For 'fill' style, we don't use plot.data directly in the same way, but we initialize generic arrays
160
160
  const dataArray = new Array(totalDataLength).fill(null);
161
+ const rawDataArray = new Array(totalDataLength).fill(null); // Unmodified values for fill references
161
162
  const colorArray = new Array(totalDataLength).fill(null);
162
163
  const optionsArray = new Array(totalDataLength).fill(null); // Store per-point options
163
164
 
@@ -171,27 +172,35 @@ export class SeriesBuilder {
171
172
  let value = point.value;
172
173
  const pointColor = point.options?.color;
173
174
 
174
- // TradingView compatibility: if color is 'na' (NaN, null, or "na"), break the line
175
+ // Always store the raw value for fill plots to reference
176
+ // (fills need the actual data even when the line is invisible via color=na)
177
+ rawDataArray[offsetIndex] = value;
178
+
179
+ // TradingView compatibility: if color is 'na' (NaN, null, undefined, or "na"), break the line
180
+ // When the options object explicitly has a 'color' key set to undefined,
181
+ // this means PineTS evaluated the color expression to na (hidden segment).
182
+ const hasExplicitColorKey = point.options != null && 'color' in point.options;
175
183
  const isNaColor =
176
184
  pointColor === null ||
177
185
  pointColor === 'na' ||
178
186
  pointColor === 'NaN' ||
179
- (typeof pointColor === 'number' && isNaN(pointColor));
187
+ (typeof pointColor === 'number' && isNaN(pointColor)) ||
188
+ (hasExplicitColorKey && pointColor === undefined);
180
189
 
181
190
  if (isNaColor) {
182
191
  value = null;
183
192
  }
184
193
 
185
194
  dataArray[offsetIndex] = value;
186
- colorArray[offsetIndex] = pointColor || plot.options.color || SeriesBuilder.DEFAULT_COLOR;
195
+ colorArray[offsetIndex] = isNaColor ? null : (pointColor || plot.options.color || SeriesBuilder.DEFAULT_COLOR);
187
196
  optionsArray[offsetIndex] = point.options || {};
188
197
  }
189
198
  }
190
199
  });
191
200
 
192
- // Store data array for fill plots to reference
193
- // Only store for non-fill plots as fill plots don't produce data to be referenced by other fills (usually)
194
- plotDataArrays.set(`${id}::${plotName}`, dataArray);
201
+ // Store raw data array (before na-color nullification) for fill plots to reference
202
+ // Fill plots need the actual numeric values even when the referenced plot is invisible (color=na)
203
+ plotDataArrays.set(`${id}::${plotName}`, rawDataArray);
195
204
 
196
205
  if (plot.options?.style?.startsWith('style_')) {
197
206
  plot.options.style = plot.options.style.replace('style_', '') as IndicatorStyle;
@@ -223,6 +232,11 @@ export class SeriesBuilder {
223
232
  return; // Skip rendering a series for barcolor
224
233
  }
225
234
 
235
+ // Tables are rendered as DOM overlays, not ECharts series
236
+ if (plot.options.style === 'table') {
237
+ return;
238
+ }
239
+
226
240
  // Use Factory to get appropriate renderer
227
241
  const renderer = SeriesRendererFactory.get(plot.options.style);
228
242
  const seriesConfig = renderer.render({
@@ -10,6 +10,8 @@ import { FillRenderer } from './renderers/FillRenderer';
10
10
  import { LabelRenderer } from './renderers/LabelRenderer';
11
11
  import { DrawingLineRenderer } from './renderers/DrawingLineRenderer';
12
12
  import { LinefillRenderer } from './renderers/LinefillRenderer';
13
+ import { PolylineRenderer } from './renderers/PolylineRenderer';
14
+ import { BoxRenderer } from './renderers/BoxRenderer';
13
15
 
14
16
  export class SeriesRendererFactory {
15
17
  private static renderers: Map<string, SeriesRenderer> = new Map();
@@ -30,6 +32,8 @@ export class SeriesRendererFactory {
30
32
  this.register('label', new LabelRenderer());
31
33
  this.register('drawing_line', new DrawingLineRenderer());
32
34
  this.register('linefill', new LinefillRenderer());
35
+ this.register('drawing_polyline', new PolylineRenderer());
36
+ this.register('drawing_box', new BoxRenderer());
33
37
  }
34
38
 
35
39
  public static register(style: string, renderer: SeriesRenderer) {
@@ -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
+ }