@qfo/qfchart 0.8.2 → 0.8.4

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.2",
3
+ "version": "0.8.4",
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",
package/src/QFChart.ts CHANGED
@@ -984,6 +984,9 @@ export class QFChart implements ChartContext {
984
984
  return candle;
985
985
  });
986
986
 
987
+ // Build drawing range hints for Y-axis scaling
988
+ const updateDrawingRangeHints = this._buildDrawingRangeHints(layout, paddingPoints);
989
+
987
990
  // Update only the data arrays in the option, not the full config
988
991
  const updateOption: any = {
989
992
  xAxis: currentOption.xAxis.map((axis: any, index: number) => ({
@@ -995,6 +998,7 @@ export class QFChart implements ChartContext {
995
998
  markLine: candlestickSeries.markLine, // Ensure markLine is updated
996
999
  },
997
1000
  ...indicatorSeries,
1001
+ ...updateDrawingRangeHints,
998
1002
  ],
999
1003
  };
1000
1004
 
@@ -1232,6 +1236,132 @@ export class QFChart implements ChartContext {
1232
1236
  this._renderTableOverlays();
1233
1237
  }
1234
1238
 
1239
+ /**
1240
+ * Build invisible "scatter" series that carry the min/max Y values of Pine
1241
+ * Script drawing objects (lines, boxes, labels, polylines). ECharts includes
1242
+ * these points in its automatic Y-axis range calculation so drawings below
1243
+ * or above the candlestick range are no longer clipped.
1244
+ *
1245
+ * Returns one hidden series per pane that has drawing objects with Y-values
1246
+ * outside the default data range.
1247
+ */
1248
+ private _buildDrawingRangeHints(layout: any, paddingPoints: number): any[] {
1249
+ const hintSeries: any[] = [];
1250
+
1251
+ // Collect Y-value bounds per pane from all indicator drawing objects
1252
+ const boundsPerPane = new Map<number, { yMin: number; yMax: number }>();
1253
+
1254
+ for (const indicator of this.indicators) {
1255
+ if (!indicator.plots) continue;
1256
+ const paneIndex = indicator.paneIndex ?? 0;
1257
+ if (!boundsPerPane.has(paneIndex)) {
1258
+ boundsPerPane.set(paneIndex, { yMin: Infinity, yMax: -Infinity });
1259
+ }
1260
+ const bounds = boundsPerPane.get(paneIndex)!;
1261
+
1262
+ for (const [plotName, plot] of Object.entries(indicator.plots as Record<string, any>)) {
1263
+ if (!plot || !plot.options) continue;
1264
+ const style = plot.options?.style;
1265
+
1266
+ // Lines: y1, y2
1267
+ if (style === 'drawing_line' && plot.data) {
1268
+ for (const entry of plot.data) {
1269
+ const items = entry?.value ? (Array.isArray(entry.value) ? entry.value : [entry.value]) : [];
1270
+ for (const ln of items) {
1271
+ if (!ln || ln._deleted) continue;
1272
+ if (typeof ln.y1 === 'number' && isFinite(ln.y1)) {
1273
+ bounds.yMin = Math.min(bounds.yMin, ln.y1);
1274
+ bounds.yMax = Math.max(bounds.yMax, ln.y1);
1275
+ }
1276
+ if (typeof ln.y2 === 'number' && isFinite(ln.y2)) {
1277
+ bounds.yMin = Math.min(bounds.yMin, ln.y2);
1278
+ bounds.yMax = Math.max(bounds.yMax, ln.y2);
1279
+ }
1280
+ }
1281
+ }
1282
+ }
1283
+
1284
+ // Boxes: top, bottom
1285
+ if (style === 'drawing_box' && plot.data) {
1286
+ for (const entry of plot.data) {
1287
+ const items = entry?.value ? (Array.isArray(entry.value) ? entry.value : [entry.value]) : [];
1288
+ for (const bx of items) {
1289
+ if (!bx || bx._deleted) continue;
1290
+ if (typeof bx.top === 'number' && isFinite(bx.top)) {
1291
+ bounds.yMin = Math.min(bounds.yMin, bx.top);
1292
+ bounds.yMax = Math.max(bounds.yMax, bx.top);
1293
+ }
1294
+ if (typeof bx.bottom === 'number' && isFinite(bx.bottom)) {
1295
+ bounds.yMin = Math.min(bounds.yMin, bx.bottom);
1296
+ bounds.yMax = Math.max(bounds.yMax, bx.bottom);
1297
+ }
1298
+ }
1299
+ }
1300
+ }
1301
+
1302
+ // Labels: y
1303
+ if (style === 'label' && plot.data) {
1304
+ for (const entry of plot.data) {
1305
+ const items = entry?.value ? (Array.isArray(entry.value) ? entry.value : [entry.value]) : [];
1306
+ for (const lbl of items) {
1307
+ if (!lbl || lbl._deleted) continue;
1308
+ if (typeof lbl.y === 'number' && isFinite(lbl.y)) {
1309
+ bounds.yMin = Math.min(bounds.yMin, lbl.y);
1310
+ bounds.yMax = Math.max(bounds.yMax, lbl.y);
1311
+ }
1312
+ }
1313
+ }
1314
+ }
1315
+
1316
+ // Polylines: points[].price
1317
+ if (style === 'drawing_polyline' && plot.data) {
1318
+ for (const entry of plot.data) {
1319
+ const items = entry?.value ? (Array.isArray(entry.value) ? entry.value : [entry.value]) : [];
1320
+ for (const pl of items) {
1321
+ if (!pl || pl._deleted || !pl._points) continue;
1322
+ for (const pt of pl._points) {
1323
+ if (typeof pt?.price === 'number' && isFinite(pt.price)) {
1324
+ bounds.yMin = Math.min(bounds.yMin, pt.price);
1325
+ bounds.yMax = Math.max(bounds.yMax, pt.price);
1326
+ }
1327
+ }
1328
+ }
1329
+ }
1330
+ }
1331
+ }
1332
+ }
1333
+
1334
+ // Create a hidden scatter series per pane with min/max Y values
1335
+ const midIndex = paddingPoints + Math.floor((this.marketData?.length || 0) / 2);
1336
+ boundsPerPane.forEach((bounds, paneIndex) => {
1337
+ if (!isFinite(bounds.yMin) || !isFinite(bounds.yMax)) return;
1338
+
1339
+ // Determine Y-axis index for this pane
1340
+ const yAxisIndex = paneIndex === 0
1341
+ ? 0
1342
+ : (layout.separatePaneYAxisOffset || 1) + (paneIndex - 1);
1343
+
1344
+ hintSeries.push({
1345
+ name: `_drawingRange_pane${paneIndex}`,
1346
+ type: 'scatter',
1347
+ xAxisIndex: paneIndex,
1348
+ yAxisIndex,
1349
+ symbol: 'none',
1350
+ symbolSize: 0,
1351
+ silent: true,
1352
+ animation: false,
1353
+ // Two invisible points at min and max Y — ECharts includes them in axis scaling
1354
+ data: [
1355
+ [midIndex, bounds.yMin],
1356
+ [midIndex, bounds.yMax],
1357
+ ],
1358
+ tooltip: { show: false },
1359
+ });
1360
+ });
1361
+
1362
+ return hintSeries;
1363
+ }
1364
+
1235
1365
  /**
1236
1366
  * Build table canvas graphic elements from the current _lastTables.
1237
1367
  * Must be called AFTER setOption so grid rects are available from ECharts.
@@ -1602,6 +1732,10 @@ export class QFChart implements ChartContext {
1602
1732
  layout.separatePaneYAxisOffset, // Pass Y-axis offset for separate panes
1603
1733
  );
1604
1734
 
1735
+ // Create hidden range-hint series so Pine Script drawing objects
1736
+ // (lines, boxes, labels, polylines) contribute to Y-axis auto-scaling.
1737
+ const drawingRangeHints = this._buildDrawingRangeHints(layout, paddingPoints);
1738
+
1605
1739
  // Apply barColors (TradingView: barcolor() only changes body fill, borders/wicks stay default)
1606
1740
  candlestickSeries.data = candlestickSeries.data.map((candle: any, i: number) => {
1607
1741
  if (barColors[i]) {
@@ -1770,7 +1904,7 @@ export class QFChart implements ChartContext {
1770
1904
  xAxis: layout.xAxis,
1771
1905
  yAxis: layout.yAxis,
1772
1906
  dataZoom: layout.dataZoom,
1773
- series: [candlestickSeries, ...indicatorSeries, ...drawingSeriesList],
1907
+ series: [candlestickSeries, ...indicatorSeries, ...drawingRangeHints, ...drawingSeriesList],
1774
1908
  };
1775
1909
 
1776
1910
  this.chart.setOption(option, true); // true = not merge, replace.
@@ -418,7 +418,10 @@ export class LayoutManager {
418
418
  const xAxis: any[] = [];
419
419
 
420
420
  // Main X-Axis
421
+ // Hide date labels on the main chart when indicator panes exist below —
422
+ // the bottom-most pane's x-axis will show them instead.
421
423
  const isMainBottom = paneConfigs.length === 0;
424
+ const showMainXLabels = !isMainCollapsed && isMainBottom;
422
425
  xAxis.push({
423
426
  type: 'category',
424
427
  data: [], // Will be filled by SeriesBuilder or QFChart
@@ -435,7 +438,7 @@ export class LayoutManager {
435
438
  lineStyle: { color: gridLineColor, opacity: gridLineOpacity },
436
439
  },
437
440
  axisLabel: {
438
- show: !isMainCollapsed,
441
+ show: showMainXLabels,
439
442
  color: '#94a3b8',
440
443
  fontFamily: options.fontFamily || 'sans-serif',
441
444
  formatter: (value: number) => {
@@ -447,7 +450,7 @@ export class LayoutManager {
447
450
  return AxisUtils.formatValue(value, decimals);
448
451
  },
449
452
  },
450
- axisTick: { show: !isMainCollapsed },
453
+ axisTick: { show: showMainXLabels },
451
454
  axisPointer: {
452
455
  label: {
453
456
  show: isMainBottom,
@@ -458,15 +461,21 @@ export class LayoutManager {
458
461
  });
459
462
 
460
463
  // Separate Panes X-Axes
464
+ // Show date labels only on the bottom-most pane
461
465
  paneConfigs.forEach((pane, i) => {
462
466
  const isBottom = i === paneConfigs.length - 1;
467
+ const showLabels = isBottom && !pane.isCollapsed;
463
468
  xAxis.push({
464
469
  type: 'category',
465
470
  gridIndex: i + 1, // 0 is main
466
471
  data: [], // Shared data
467
- axisLabel: { show: false }, // Hide labels on indicator panes
472
+ axisLabel: {
473
+ show: showLabels,
474
+ color: '#94a3b8',
475
+ fontFamily: options.fontFamily || 'sans-serif',
476
+ },
468
477
  axisLine: { show: !pane.isCollapsed && gridBorderShow, lineStyle: { color: gridBorderColor } },
469
- axisTick: { show: false },
478
+ axisTick: { show: showLabels },
470
479
  splitLine: { show: false },
471
480
  axisPointer: {
472
481
  label: {
@@ -549,16 +558,21 @@ export class LayoutManager {
549
558
  const plotKey = `${id}::${plotName}`;
550
559
 
551
560
  // Skip visual-only plot types that should never affect Y-axis scaling
552
- // EXCEPTION: shapes with abovebar/belowbar must stay on main Y-axis
553
- const visualOnlyStyles = ['background', 'barcolor', 'char'];
561
+ // EXCEPTION: shapes/chars with price-relative locations must stay on main Y-axis
562
+ const visualOnlyStyles = ['background', 'barcolor'];
554
563
 
555
- // Check if this is a shape with price-relative positioning
564
+ // Check if this is a shape/char with price-relative positioning
565
+ // Includes abovebar/belowbar (relative to candle) and absolute (exact Y value)
556
566
  const isShapeWithPriceLocation =
557
- plot.options.style === 'shape' &&
567
+ (plot.options.style === 'shape' || plot.options.style === 'char') &&
558
568
  (plot.options.location === 'abovebar' ||
559
569
  plot.options.location === 'AboveBar' ||
570
+ plot.options.location === 'ab' ||
560
571
  plot.options.location === 'belowbar' ||
561
- plot.options.location === 'BelowBar');
572
+ plot.options.location === 'BelowBar' ||
573
+ plot.options.location === 'bl' ||
574
+ plot.options.location === 'absolute' ||
575
+ plot.options.location === 'Absolute');
562
576
 
563
577
  if (visualOnlyStyles.includes(plot.options.style)) {
564
578
  // Assign these to a separate Y-axis so they don't affect price scale
@@ -569,8 +583,8 @@ export class LayoutManager {
569
583
  return; // Skip further processing for this plot
570
584
  }
571
585
 
572
- // If it's a shape but NOT with price-relative positioning, treat as visual-only
573
- if (plot.options.style === 'shape' && !isShapeWithPriceLocation) {
586
+ // If it's a shape/char but NOT with price-relative positioning, treat as visual-only
587
+ if ((plot.options.style === 'shape' || plot.options.style === 'char') && !isShapeWithPriceLocation) {
574
588
  if (!overlayYAxisMap.has(plotKey)) {
575
589
  overlayYAxisMap.set(plotKey, nextYAxisIndex);
576
590
  nextYAxisIndex++;