@qfo/qfchart 0.8.4 → 0.8.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qfo/qfchart",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "description": "Professional financial charting library built on Apache ECharts with candlestick charts, technical indicators, and interactive drawing tools",
5
5
  "keywords": [
6
6
  "chart",
@@ -184,6 +184,50 @@ export class GraphicBuilder {
184
184
  }
185
185
  }
186
186
 
187
+ // Pane Separator Lines (between main chart and indicator panes, and between indicators)
188
+ // Offset upward from center so the line doesn't overlap the lower pane's y-axis labels
189
+ if (!maximizedPaneId && layout.paneBoundaries.length > 0) {
190
+ const sepOffset = -8 * pixelToPercent; // shift 8px up from gap center
191
+ for (const boundary of layout.paneBoundaries) {
192
+ graphic.push({
193
+ type: 'group',
194
+ left: '10%',
195
+ top: (boundary.yPercent + sepOffset) + '%',
196
+ children: [
197
+ // Invisible wide hit target for easier hover/drag
198
+ {
199
+ type: 'rect',
200
+ shape: { width: 5000, height: 12, y: -6 },
201
+ style: { fill: 'transparent' },
202
+ cursor: 'row-resize',
203
+ },
204
+ // Visible line — moderately visible default, bright on hover
205
+ {
206
+ type: 'rect',
207
+ shape: { width: 5000, height: 2, y: -1 },
208
+ style: { fill: '#475569', opacity: 0.7 },
209
+ cursor: 'row-resize',
210
+ },
211
+ ],
212
+ z: 50,
213
+ onmouseover: function () {
214
+ const line = this.children()[1];
215
+ if (line) {
216
+ line.setStyle({ fill: '#94a3b8', opacity: 1.0 });
217
+ line.setShape({ height: 3, y: -1.5 });
218
+ }
219
+ },
220
+ onmouseout: function () {
221
+ const line = this.children()[1];
222
+ if (line) {
223
+ line.setStyle({ fill: '#475569', opacity: 0.7 });
224
+ line.setShape({ height: 2, y: -1 });
225
+ }
226
+ },
227
+ });
228
+ }
229
+ }
230
+
187
231
  // Indicator Panes
188
232
  layout.paneLayout.forEach((pane) => {
189
233
  // If maximizedPaneId is set, and this is NOT the maximized pane, skip rendering its controls
@@ -1,106 +1,106 @@
1
- import { Indicator as IndicatorInterface, IndicatorPlot, IndicatorPoint } from '../types';
2
-
3
- export class Indicator implements IndicatorInterface {
4
- public id: string;
5
- public plots: { [name: string]: IndicatorPlot };
6
- public paneIndex: number;
7
- public height?: number;
8
- public collapsed: boolean;
9
- public titleColor?: string;
10
- public controls?: { collapse?: boolean; maximize?: boolean };
11
-
12
- constructor(
13
- id: string,
14
- plots: { [name: string]: IndicatorPlot },
15
- paneIndex: number,
16
- options: {
17
- height?: number;
18
- collapsed?: boolean;
19
- titleColor?: string;
20
- controls?: { collapse?: boolean; maximize?: boolean };
21
- } = {}
22
- ) {
23
- this.id = id;
24
- this.plots = plots;
25
- this.paneIndex = paneIndex;
26
- this.height = options.height;
27
- this.collapsed = options.collapsed || false;
28
- this.titleColor = options.titleColor;
29
- this.controls = options.controls;
30
- }
31
-
32
- public toggleCollapse(): void {
33
- this.collapsed = !this.collapsed;
34
- }
35
-
36
- public isVisible(): boolean {
37
- return !this.collapsed;
38
- }
39
-
40
- /**
41
- * Update indicator data incrementally by merging new points
42
- *
43
- * @param plots - New plots data to merge (same structure as constructor)
44
- *
45
- * @remarks
46
- * This method merges new indicator data with existing data by timestamp.
47
- * - New timestamps are added
48
- * - Existing timestamps are updated with new values
49
- * - All data is automatically sorted by time after merge
50
- *
51
- * **Important**: This method only updates the indicator's internal data structure.
52
- * To see the changes reflected in the chart, you MUST call `chart.updateData()`
53
- * after updating indicator data.
54
- *
55
- * **Usage Pattern**:
56
- * ```typescript
57
- * // 1. Update indicator data first
58
- * indicator.updateData({
59
- * macd: { data: [{ time: 1234567890, value: 150 }], options: { style: 'line', color: '#2962FF' } }
60
- * });
61
- *
62
- * // 2. Then update chart data to trigger re-render
63
- * chart.updateData([
64
- * { time: 1234567890, open: 100, high: 105, low: 99, close: 103, volume: 1000 }
65
- * ]);
66
- * ```
67
- *
68
- * **Note**: If you update indicator data without corresponding market data changes,
69
- * this typically indicates a recalculation scenario. In normal workflows, indicator
70
- * values are derived from market data, so indicator updates should correspond to
71
- * new or modified market bars.
72
- */
73
- public updateData(plots: { [name: string]: IndicatorPlot }): void {
74
- Object.keys(plots).forEach((plotName) => {
75
- if (!this.plots[plotName]) {
76
- // New plot - add it
77
- this.plots[plotName] = plots[plotName];
78
- } else {
79
- // Existing plot - merge data points
80
- const existingPlot = this.plots[plotName];
81
- const newPlot = plots[plotName];
82
-
83
- if (!existingPlot.data) return;
84
-
85
- // Update options if provided
86
- if (newPlot.options) {
87
- existingPlot.options = { ...existingPlot.options, ...newPlot.options };
88
- }
89
-
90
- // Merge data points by time
91
- const existingTimeMap = new Map<number, IndicatorPoint>();
92
- existingPlot.data?.forEach((point) => {
93
- existingTimeMap.set(point.time, point);
94
- });
95
-
96
- // Update or add new points
97
- newPlot.data?.forEach((point) => {
98
- existingTimeMap.set(point.time, point);
99
- });
100
-
101
- // Rebuild data array sorted by time
102
- existingPlot.data = Array.from(existingTimeMap.values()).sort((a, b) => a.time - b.time);
103
- }
104
- });
105
- }
106
- }
1
+ import { Indicator as IndicatorInterface, IndicatorPlot, IndicatorPoint } from '../types';
2
+
3
+ export class Indicator implements IndicatorInterface {
4
+ public id: string;
5
+ public plots: { [name: string]: IndicatorPlot };
6
+ public paneIndex: number;
7
+ public height?: number;
8
+ public collapsed: boolean;
9
+ public titleColor?: string;
10
+ public controls?: { collapse?: boolean; maximize?: boolean };
11
+
12
+ constructor(
13
+ id: string,
14
+ plots: { [name: string]: IndicatorPlot },
15
+ paneIndex: number,
16
+ options: {
17
+ height?: number;
18
+ collapsed?: boolean;
19
+ titleColor?: string;
20
+ controls?: { collapse?: boolean; maximize?: boolean };
21
+ } = {}
22
+ ) {
23
+ this.id = id;
24
+ this.plots = plots;
25
+ this.paneIndex = paneIndex;
26
+ this.height = options.height;
27
+ this.collapsed = options.collapsed || false;
28
+ this.titleColor = options.titleColor;
29
+ this.controls = options.controls;
30
+ }
31
+
32
+ public toggleCollapse(): void {
33
+ this.collapsed = !this.collapsed;
34
+ }
35
+
36
+ public isVisible(): boolean {
37
+ return !this.collapsed;
38
+ }
39
+
40
+ /**
41
+ * Update indicator data incrementally by merging new points
42
+ *
43
+ * @param plots - New plots data to merge (same structure as constructor)
44
+ *
45
+ * @remarks
46
+ * This method merges new indicator data with existing data by timestamp.
47
+ * - New timestamps are added
48
+ * - Existing timestamps are updated with new values
49
+ * - All data is automatically sorted by time after merge
50
+ *
51
+ * **Important**: This method only updates the indicator's internal data structure.
52
+ * To see the changes reflected in the chart, you MUST call `chart.updateData()`
53
+ * after updating indicator data.
54
+ *
55
+ * **Usage Pattern**:
56
+ * ```typescript
57
+ * // 1. Update indicator data first
58
+ * indicator.updateData({
59
+ * macd: { data: [{ time: 1234567890, value: 150 }], options: { style: 'line', color: '#2962FF' } }
60
+ * });
61
+ *
62
+ * // 2. Then update chart data to trigger re-render
63
+ * chart.updateData([
64
+ * { time: 1234567890, open: 100, high: 105, low: 99, close: 103, volume: 1000 }
65
+ * ]);
66
+ * ```
67
+ *
68
+ * **Note**: If you update indicator data without corresponding market data changes,
69
+ * this typically indicates a recalculation scenario. In normal workflows, indicator
70
+ * values are derived from market data, so indicator updates should correspond to
71
+ * new or modified market bars.
72
+ */
73
+ public updateData(plots: { [name: string]: IndicatorPlot }): void {
74
+ Object.keys(plots).forEach((plotName) => {
75
+ if (!this.plots[plotName]) {
76
+ // New plot - add it
77
+ this.plots[plotName] = plots[plotName];
78
+ } else {
79
+ // Existing plot - merge data points
80
+ const existingPlot = this.plots[plotName];
81
+ const newPlot = plots[plotName];
82
+
83
+ if (!existingPlot.data) return;
84
+
85
+ // Update options if provided
86
+ if (newPlot.options) {
87
+ existingPlot.options = { ...existingPlot.options, ...newPlot.options };
88
+ }
89
+
90
+ // Merge data points by time
91
+ const existingTimeMap = new Map<number, IndicatorPoint>();
92
+ existingPlot.data?.forEach((point) => {
93
+ existingTimeMap.set(point.time, point);
94
+ });
95
+
96
+ // Update or add new points
97
+ newPlot.data?.forEach((point) => {
98
+ existingTimeMap.set(point.time, point);
99
+ });
100
+
101
+ // Rebuild data array sorted by time
102
+ existingPlot.data = Array.from(existingTimeMap.values()).sort((a, b) => a.time - b.time);
103
+ }
104
+ });
105
+ }
106
+ }
@@ -104,6 +104,9 @@ export class SeriesBuilder {
104
104
  const series: any[] = [];
105
105
  const barColors: (string | null)[] = new Array(totalDataLength).fill(null);
106
106
 
107
+ // Extract raw (non-null) market data for resolving xloc.bar_time coordinates
108
+ const rawMarketData = candlestickData?.filter((d): d is OHLCV => d != null && d.time !== undefined);
109
+
107
110
  // Store plot data arrays for fill plots to reference
108
111
  const plotDataArrays = new Map<string, number[]>();
109
112
 
@@ -348,6 +351,8 @@ export class SeriesBuilder {
348
351
  indicatorId: id,
349
352
  plotName: plotName,
350
353
  dataIndexOffset,
354
+ timeToIndex,
355
+ marketData: rawMarketData,
351
356
  });
352
357
 
353
358
  if (seriesConfig) {
@@ -1,4 +1,4 @@
1
- import { SeriesRenderer, RenderContext } from './SeriesRenderer';
1
+ import { SeriesRenderer, RenderContext, resolveXCoord } from './SeriesRenderer';
2
2
 
3
3
  /**
4
4
  * Convert any color string to a format ECharts canvas can render with opacity.
@@ -60,7 +60,7 @@ function luminance(r: number, g: number, b: number): number {
60
60
  */
61
61
  export class BoxRenderer implements SeriesRenderer {
62
62
  render(context: RenderContext): any {
63
- const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset } = context;
63
+ const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset, timeToIndex, marketData } = context;
64
64
  const offset = dataIndexOffset || 0;
65
65
 
66
66
  // Collect all non-deleted box objects from the sparse dataArray.
@@ -103,9 +103,11 @@ export class BoxRenderer implements SeriesRenderer {
103
103
  for (const bx of boxObjects) {
104
104
  if (bx._deleted) continue;
105
105
 
106
- const xOff = (bx.xloc === 'bar_index' || bx.xloc === 'bi') ? offset : 0;
107
- const pTopLeft = api.coord([bx.left + xOff, bx.top]);
108
- const pBottomRight = api.coord([bx.right + xOff, bx.bottom]);
106
+ const leftX = resolveXCoord(bx.left, bx.xloc, offset, timeToIndex, marketData);
107
+ const rightX = resolveXCoord(bx.right, bx.xloc, offset, timeToIndex, marketData);
108
+ if (isNaN(leftX) || isNaN(rightX)) continue;
109
+ const pTopLeft = api.coord([leftX, bx.top]);
110
+ const pBottomRight = api.coord([rightX, bx.bottom]);
109
111
 
110
112
  let x = pTopLeft[0];
111
113
  let y = pTopLeft[1];
@@ -1,4 +1,4 @@
1
- import { SeriesRenderer, RenderContext } from './SeriesRenderer';
1
+ import { SeriesRenderer, RenderContext, resolveXCoord } from './SeriesRenderer';
2
2
 
3
3
  /**
4
4
  * Renderer for Pine Script line.* drawing objects.
@@ -9,7 +9,7 @@ import { SeriesRenderer, RenderContext } from './SeriesRenderer';
9
9
  */
10
10
  export class DrawingLineRenderer implements SeriesRenderer {
11
11
  render(context: RenderContext): any {
12
- const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset } = context;
12
+ const { seriesName, xAxisIndex, yAxisIndex, dataArray, dataIndexOffset, timeToIndex, marketData } = context;
13
13
  const offset = dataIndexOffset || 0;
14
14
  const defaultColor = '#2962ff';
15
15
 
@@ -55,10 +55,12 @@ export class DrawingLineRenderer implements SeriesRenderer {
55
55
 
56
56
  for (const ln of lineObjects) {
57
57
  if (ln._deleted) continue;
58
- const xOff = (ln.xloc === 'bar_index' || ln.xloc === 'bi') ? offset : 0;
58
+ const x1Resolved = resolveXCoord(ln.x1, ln.xloc, offset, timeToIndex, marketData);
59
+ const x2Resolved = resolveXCoord(ln.x2, ln.xloc, offset, timeToIndex, marketData);
60
+ if (isNaN(x1Resolved) || isNaN(x2Resolved)) continue;
59
61
 
60
- let p1 = api.coord([ln.x1 + xOff, ln.y1]);
61
- let p2 = api.coord([ln.x2 + xOff, ln.y2]);
62
+ let p1 = api.coord([x1Resolved, ln.y1]);
63
+ let p2 = api.coord([x2Resolved, ln.y2]);
62
64
 
63
65
  // Handle extend (none/n | left/l | right/r | both/b)
64
66
  const extend = ln.extend || 'none';
@@ -201,8 +201,9 @@ export class FillRenderer implements SeriesRenderer {
201
201
 
202
202
  /**
203
203
  * Render a gradient fill between two plots.
204
- * Uses a vertical linear gradient from top_color (at the upper boundary)
205
- * to bottom_color (at the lower boundary) for each polygon segment.
204
+ * Uses per-bar top_value/bottom_value as the actual Y boundaries (not the raw plot values).
205
+ * A vertical linear gradient goes from top_color (at top_value) to bottom_color (at bottom_value).
206
+ * When top_value or bottom_value is na/NaN, the fill is hidden for that bar.
206
207
  */
207
208
  private renderGradientFill(
208
209
  seriesName: string,
@@ -214,40 +215,52 @@ export class FillRenderer implements SeriesRenderer {
214
215
  optionsArray: any[],
215
216
  plotOptions: any
216
217
  ): any {
217
- // Build per-bar gradient color arrays from optionsArray
218
- // Each entry in optionsArray has: { top_value, bottom_value, top_color, bottom_color }
219
- const gradientColors: { topColor: string; topOpacity: number; bottomColor: string; bottomOpacity: number }[] = [];
218
+ // Build per-bar gradient data from optionsArray
219
+ // Each entry has: { top_value, bottom_value, top_color, bottom_color }
220
+ interface GradientBar {
221
+ topValue: number | null;
222
+ bottomValue: number | null;
223
+ topColor: string;
224
+ topOpacity: number;
225
+ bottomColor: string;
226
+ bottomOpacity: number;
227
+ }
228
+ const gradientBars: (GradientBar | null)[] = [];
220
229
 
221
230
  for (let i = 0; i < totalDataLength; i++) {
222
231
  const opts = optionsArray?.[i];
223
232
  if (opts && opts.top_color !== undefined) {
233
+ const tv = opts.top_value;
234
+ const bv = opts.bottom_value;
235
+ // na/NaN/null/undefined → null (hidden bar)
236
+ const topVal = (tv == null || (typeof tv === 'number' && isNaN(tv))) ? null : tv;
237
+ const btmVal = (bv == null || (typeof bv === 'number' && isNaN(bv))) ? null : bv;
238
+
224
239
  const top = ColorUtils.parseColor(opts.top_color);
225
240
  const bottom = ColorUtils.parseColor(opts.bottom_color);
226
- gradientColors[i] = {
241
+ gradientBars[i] = {
242
+ topValue: topVal,
243
+ bottomValue: btmVal,
227
244
  topColor: top.color,
228
245
  topOpacity: top.opacity,
229
246
  bottomColor: bottom.color,
230
247
  bottomOpacity: bottom.opacity,
231
248
  };
232
249
  } else {
233
- // Fallback: use a default semi-transparent fill
234
- gradientColors[i] = {
235
- topColor: 'rgba(128,128,128,0.2)',
236
- topOpacity: 0.2,
237
- bottomColor: 'rgba(128,128,128,0.2)',
238
- bottomOpacity: 0.2,
239
- };
250
+ gradientBars[i] = null;
240
251
  }
241
252
  }
242
253
 
243
- // Create fill data with previous values
244
- const fillDataWithPrev: any[] = [];
254
+ // Create fill data using top_value/bottom_value as Y boundaries
255
+ const fillData: any[] = [];
245
256
  for (let i = 0; i < totalDataLength; i++) {
246
- const y1 = plot1Data[i];
247
- const y2 = plot2Data[i];
248
- const prevY1 = i > 0 ? plot1Data[i - 1] : null;
249
- const prevY2 = i > 0 ? plot2Data[i - 1] : null;
250
- fillDataWithPrev.push([i, y1, y2, prevY1, prevY2]);
257
+ const gb = gradientBars[i];
258
+ const prevGb = i > 0 ? gradientBars[i - 1] : null;
259
+ const topY = gb?.topValue ?? null;
260
+ const btmY = gb?.bottomValue ?? null;
261
+ const prevTopY = prevGb?.topValue ?? null;
262
+ const prevBtmY = prevGb?.bottomValue ?? null;
263
+ fillData.push([i, topY, btmY, prevTopY, prevBtmY]);
251
264
  }
252
265
 
253
266
  return {
@@ -263,57 +276,53 @@ export class FillRenderer implements SeriesRenderer {
263
276
  const index = params.dataIndex;
264
277
  if (index === 0) return null;
265
278
 
266
- const y1 = api.value(1);
267
- const y2 = api.value(2);
268
- const prevY1 = api.value(3);
269
- const prevY2 = api.value(4);
279
+ const topY = api.value(1);
280
+ const btmY = api.value(2);
281
+ const prevTopY = api.value(3);
282
+ const prevBtmY = api.value(4);
270
283
 
284
+ // Skip when any boundary is na (hidden bar)
271
285
  if (
272
- y1 === null || y2 === null || prevY1 === null || prevY2 === null ||
273
- isNaN(y1) || isNaN(y2) || isNaN(prevY1) || isNaN(prevY2)
286
+ topY == null || btmY == null || prevTopY == null || prevBtmY == null ||
287
+ isNaN(topY) || isNaN(btmY) || isNaN(prevTopY) || isNaN(prevBtmY)
274
288
  ) {
275
289
  return null;
276
290
  }
277
291
 
278
- const p1Prev = api.coord([index - 1, prevY1]);
279
- const p1Curr = api.coord([index, y1]);
280
- const p2Curr = api.coord([index, y2]);
281
- const p2Prev = api.coord([index - 1, prevY2]);
282
-
283
292
  // Get gradient colors for this bar
284
- const gc = gradientColors[index] || gradientColors[index - 1];
285
- if (!gc) return null;
293
+ const gb = gradientBars[index];
294
+ if (!gb) return null;
286
295
 
287
296
  // Skip fully transparent gradient fills
288
- if (gc.topOpacity < 0.01 && gc.bottomOpacity < 0.01) return null;
297
+ if (gb.topOpacity < 0.01 && gb.bottomOpacity < 0.01) return null;
289
298
 
290
- // Convert colors to rgba strings with their opacities
291
- const topRgba = ColorUtils.toRgba(gc.topColor, gc.topOpacity);
292
- const bottomRgba = ColorUtils.toRgba(gc.bottomColor, gc.bottomOpacity);
299
+ const topRgba = ColorUtils.toRgba(gb.topColor, gb.topOpacity);
300
+ const bottomRgba = ColorUtils.toRgba(gb.bottomColor, gb.bottomOpacity);
293
301
 
294
- // Determine if plot1 is above plot2 (in value space, higher value = higher on chart)
295
- // We want top_color at the higher value, bottom_color at the lower value
296
- const plot1IsAbove = y1 >= y2;
302
+ const pTopPrev = api.coord([index - 1, prevTopY]);
303
+ const pTopCurr = api.coord([index, topY]);
304
+ const pBtmCurr = api.coord([index, btmY]);
305
+ const pBtmPrev = api.coord([index - 1, prevBtmY]);
297
306
 
298
307
  return {
299
308
  type: 'polygon',
300
309
  shape: {
301
- points: [p1Prev, p1Curr, p2Curr, p2Prev],
310
+ points: [pTopPrev, pTopCurr, pBtmCurr, pBtmPrev],
302
311
  },
303
312
  style: {
304
313
  fill: {
305
314
  type: 'linear',
306
- x: 0, y: 0, x2: 0, y2: 1, // vertical gradient
315
+ x: 0, y: 0, x2: 0, y2: 1,
307
316
  colorStops: [
308
- { offset: 0, color: plot1IsAbove ? topRgba : bottomRgba },
309
- { offset: 1, color: plot1IsAbove ? bottomRgba : topRgba },
317
+ { offset: 0, color: topRgba },
318
+ { offset: 1, color: bottomRgba },
310
319
  ],
311
320
  },
312
321
  },
313
322
  silent: true,
314
323
  };
315
324
  },
316
- data: fillDataWithPrev,
325
+ data: fillData,
317
326
  silent: true,
318
327
  };
319
328
  }
@@ -1,9 +1,9 @@
1
- import { SeriesRenderer, RenderContext } from './SeriesRenderer';
1
+ import { SeriesRenderer, RenderContext, resolveXCoord } from './SeriesRenderer';
2
2
  import { ShapeUtils } from '../../utils/ShapeUtils';
3
3
 
4
4
  export class LabelRenderer implements SeriesRenderer {
5
5
  render(context: RenderContext): any {
6
- const { seriesName, xAxisIndex, yAxisIndex, dataArray, candlestickData, dataIndexOffset } = context;
6
+ const { seriesName, xAxisIndex, yAxisIndex, dataArray, candlestickData, dataIndexOffset, timeToIndex, marketData } = context;
7
7
  const offset = dataIndexOffset || 0;
8
8
 
9
9
  // Collect all non-null, non-deleted label objects from the sparse dataArray.
@@ -42,7 +42,8 @@ export class LabelRenderer implements SeriesRenderer {
42
42
  const shape = this.styleToShape(styleRaw);
43
43
 
44
44
  // Determine X position using label's own x coordinate
45
- const xPos = (lbl.xloc === 'bar_index' || lbl.xloc === 'bi') ? (lbl.x + offset) : lbl.x;
45
+ const xPos = resolveXCoord(lbl.x, lbl.xloc, offset, timeToIndex, marketData);
46
+ if (isNaN(xPos)) return null;
46
47
 
47
48
  // Determine Y value based on yloc
48
49
  let yValue = lbl.y;
@@ -126,6 +127,18 @@ export class LabelRenderer implements SeriesRenderer {
126
127
  labelTextOffset = [0, totalHeight * pointerRatio * 0.5];
127
128
  }
128
129
  }
130
+ } else if (shape === 'labelcenter') {
131
+ // label_center: no pointer, centered at exact coordinate.
132
+ // Size the bubble body to fit text but apply NO offset.
133
+ const lines = text.split('\n');
134
+ const longestLine = lines.reduce((a: string, b: string) => a.length > b.length ? a : b, '');
135
+ const textWidth = longestLine.length * fontSize * 0.65;
136
+ const minWidth = fontSize * 2.5;
137
+ const bubbleWidth = Math.max(minWidth, textWidth + fontSize * 1.6);
138
+ const lineHeight = fontSize * 1.4;
139
+ const bubbleHeight = Math.max(fontSize * 2.8, lines.length * lineHeight + fontSize * 1.2);
140
+ finalSize = [bubbleWidth, bubbleHeight];
141
+ // No symbolOffset — center exactly at the coordinate
129
142
  } else if (shape === 'none') {
130
143
  finalSize = 0;
131
144
  } else {
@@ -228,7 +241,7 @@ export class LabelRenderer implements SeriesRenderer {
228
241
  case 'label_upper_right':
229
242
  return 'labelup';
230
243
  case 'label_center':
231
- return 'labeldown';
244
+ return 'labelcenter';
232
245
  case 'circle':
233
246
  return 'circle';
234
247
  case 'square':
@@ -289,16 +302,16 @@ export class LabelRenderer implements SeriesRenderer {
289
302
  case 'tiny':
290
303
  return 8;
291
304
  case 'small':
292
- return 9;
305
+ return 11;
293
306
  case 'normal':
294
307
  case 'auto':
295
- return 10;
308
+ return 14;
296
309
  case 'large':
297
- return 12;
310
+ return 20;
298
311
  case 'huge':
299
- return 14;
312
+ return 36;
300
313
  default:
301
- return 10;
314
+ return 14;
302
315
  }
303
316
  }
304
317
  }