hyperprop-charting-library 0.1.24 → 0.1.26

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.js CHANGED
@@ -109,6 +109,8 @@ var DEFAULT_OPTIONS = {
109
109
  backgroundColor: "#101114",
110
110
  axisColor: "#7f8289",
111
111
  axis: DEFAULT_AXIS_OPTIONS,
112
+ xAxis: DEFAULT_AXIS_OPTIONS,
113
+ yAxis: DEFAULT_AXIS_OPTIONS,
112
114
  priceDecimals: 2,
113
115
  stabilizePriceLabels: true,
114
116
  priceLabelMinIntegerDigits: 3,
@@ -127,6 +129,7 @@ var DEFAULT_OPTIONS = {
127
129
  candleBodyWidthRatio: 0.7,
128
130
  candleMinWidth: 0.5,
129
131
  candleWickWidth: 1,
132
+ tickSize: 0,
130
133
  candleColorMode: "openClose",
131
134
  candleColorEpsilon: -1,
132
135
  autoScaleSmoothing: 0.16,
@@ -364,6 +367,35 @@ var computeAtrSeries = (data, length) => {
364
367
  }
365
368
  return result;
366
369
  };
370
+ var builtInSeriesCache = /* @__PURE__ */ new Map();
371
+ var getSeriesFingerprint = (data) => {
372
+ const length = data.length;
373
+ const last = length > 0 ? data[length - 1] : void 0;
374
+ const prev = length > 1 ? data[length - 2] : void 0;
375
+ if (!last) return "empty";
376
+ return [
377
+ length,
378
+ last.time.getTime(),
379
+ last.o,
380
+ last.h,
381
+ last.l,
382
+ last.c,
383
+ last.v ?? "",
384
+ prev?.time.getTime() ?? "",
385
+ prev?.c ?? "",
386
+ prev?.v ?? ""
387
+ ].join("|");
388
+ };
389
+ var withCachedSeries = (key, data, compute) => {
390
+ const fingerprint = getSeriesFingerprint(data);
391
+ const existing = builtInSeriesCache.get(key);
392
+ if (existing && existing.fingerprint === fingerprint) {
393
+ return existing.values;
394
+ }
395
+ const values = compute();
396
+ builtInSeriesCache.set(key, { fingerprint, values });
397
+ return values;
398
+ };
367
399
  var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
368
400
  if (!renderContext.yFromPrice) return;
369
401
  const yFromPrice = renderContext.yFromPrice;
@@ -453,6 +485,15 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
453
485
  }
454
486
  ctx.restore();
455
487
  };
488
+ var getPercentileValue = (values, percentile) => {
489
+ if (values.length === 0) {
490
+ return 1;
491
+ }
492
+ const sorted = [...values].sort((a, b) => a - b);
493
+ const clamped = Math.min(1, Math.max(0, percentile));
494
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(clamped * sorted.length) - 1));
495
+ return sorted[index] ?? 1;
496
+ };
456
497
  var BUILTIN_VOLUME_INDICATOR = {
457
498
  id: "volume",
458
499
  name: "Volume",
@@ -461,8 +502,12 @@ var BUILTIN_VOLUME_INDICATOR = {
461
502
  defaultInputs: {
462
503
  upOpacity: 0.7,
463
504
  downOpacity: 0.7,
505
+ upColor: "",
506
+ downColor: "",
464
507
  minBarWidth: 1,
465
- overlayHeightRatio: 0.22
508
+ overlayHeightRatio: 0.22,
509
+ scaleMode: "visible",
510
+ clampPercentile: 1
466
511
  },
467
512
  draw: (ctx, renderContext, inputs) => {
468
513
  const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
@@ -471,8 +516,11 @@ var BUILTIN_VOLUME_INDICATOR = {
471
516
  const overlayHeightRatio = Math.min(0.45, Math.max(0.08, Number(inputs.overlayHeightRatio) || 0.22));
472
517
  const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
473
518
  const paneBottom = chartBottom;
474
- const visiblePoints = data.slice(startIndex, endIndex + 1);
475
- const maxVolume = Math.max(1, ...visiblePoints.map((point) => point.v ?? 0));
519
+ const scaleMode = inputs.scaleMode === "full" ? "full" : "visible";
520
+ const scalingPoints = scaleMode === "full" ? data : data.slice(startIndex, endIndex + 1);
521
+ const scalingVolumes = scalingPoints.map((point) => Math.max(0, point.v ?? 0)).filter((value) => value > 0);
522
+ const clampPercentile = Number.isFinite(inputs.clampPercentile) ? Number(inputs.clampPercentile) : 1;
523
+ const maxVolume = Math.max(1, getPercentileValue(scalingVolumes, clampPercentile));
476
524
  const barWidth = Math.max(
477
525
  Math.max(1, Number(inputs.minBarWidth) || 1),
478
526
  Math.min(Math.max(1, candleSpacing - 1), Math.floor(candleSpacing * 0.7))
@@ -491,9 +539,11 @@ var BUILTIN_VOLUME_INDICATOR = {
491
539
  const barY = Math.round(paneBottom - volumeHeight);
492
540
  const direction = renderContext.getCandleDirectionByIndex(index);
493
541
  const opacity = direction === "up" ? upOpacity : downOpacity;
542
+ const upBarColor = inputs.upColor && inputs.upColor.trim().length > 0 ? inputs.upColor : upColor;
543
+ const downBarColor = inputs.downColor && inputs.downColor.trim().length > 0 ? inputs.downColor : downColor;
494
544
  ctx.save();
495
545
  ctx.globalAlpha = opacity;
496
- ctx.fillStyle = direction === "up" ? upColor : downColor;
546
+ ctx.fillStyle = direction === "up" ? upBarColor : downBarColor;
497
547
  ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
498
548
  ctx.restore();
499
549
  }
@@ -506,7 +556,11 @@ var BUILTIN_SMA_INDICATOR = {
506
556
  defaultInputs: { length: 20, source: "close", color: "#60a5fa", width: 2 },
507
557
  draw: (ctx, renderContext, inputs) => {
508
558
  const length = clampIndicatorLength(inputs.length, 20);
509
- const values = computeSmaSeries(renderContext.data, length, inputs.source ?? "close");
559
+ const values = withCachedSeries(
560
+ `sma|${length}|${inputs.source ?? "close"}`,
561
+ renderContext.data,
562
+ () => computeSmaSeries(renderContext.data, length, inputs.source ?? "close")
563
+ );
510
564
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
511
565
  }
512
566
  };
@@ -517,7 +571,11 @@ var BUILTIN_EMA_INDICATOR = {
517
571
  defaultInputs: { length: 20, source: "close", color: "#f59e0b", width: 2 },
518
572
  draw: (ctx, renderContext, inputs) => {
519
573
  const length = clampIndicatorLength(inputs.length, 20);
520
- const values = computeEmaSeries(renderContext.data, length, inputs.source ?? "close");
574
+ const values = withCachedSeries(
575
+ `ema|${length}|${inputs.source ?? "close"}`,
576
+ renderContext.data,
577
+ () => computeEmaSeries(renderContext.data, length, inputs.source ?? "close")
578
+ );
521
579
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
522
580
  }
523
581
  };
@@ -528,7 +586,11 @@ var BUILTIN_WMA_INDICATOR = {
528
586
  defaultInputs: { length: 20, source: "close", color: "#a78bfa", width: 2 },
529
587
  draw: (ctx, renderContext, inputs) => {
530
588
  const length = clampIndicatorLength(inputs.length, 20);
531
- const values = computeWmaSeries(renderContext.data, length, inputs.source ?? "close");
589
+ const values = withCachedSeries(
590
+ `wma|${length}|${inputs.source ?? "close"}`,
591
+ renderContext.data,
592
+ () => computeWmaSeries(renderContext.data, length, inputs.source ?? "close")
593
+ );
532
594
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
533
595
  }
534
596
  };
@@ -539,7 +601,11 @@ var BUILTIN_VWMA_INDICATOR = {
539
601
  defaultInputs: { length: 20, source: "close", color: "#ef4444", width: 2 },
540
602
  draw: (ctx, renderContext, inputs) => {
541
603
  const length = clampIndicatorLength(inputs.length, 20);
542
- const values = computeVwmaSeries(renderContext.data, length, inputs.source ?? "close");
604
+ const values = withCachedSeries(
605
+ `vwma|${length}|${inputs.source ?? "close"}`,
606
+ renderContext.data,
607
+ () => computeVwmaSeries(renderContext.data, length, inputs.source ?? "close")
608
+ );
543
609
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
544
610
  }
545
611
  };
@@ -550,7 +616,11 @@ var BUILTIN_RMA_INDICATOR = {
550
616
  defaultInputs: { length: 14, source: "close", color: "#22c55e", width: 2 },
551
617
  draw: (ctx, renderContext, inputs) => {
552
618
  const length = clampIndicatorLength(inputs.length, 14);
553
- const values = computeRmaSeries(renderContext.data, length, inputs.source ?? "close");
619
+ const values = withCachedSeries(
620
+ `rma|${length}|${inputs.source ?? "close"}`,
621
+ renderContext.data,
622
+ () => computeRmaSeries(renderContext.data, length, inputs.source ?? "close")
623
+ );
554
624
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
555
625
  }
556
626
  };
@@ -561,7 +631,11 @@ var BUILTIN_HMA_INDICATOR = {
561
631
  defaultInputs: { length: 21, source: "close", color: "#14b8a6", width: 2 },
562
632
  draw: (ctx, renderContext, inputs) => {
563
633
  const length = clampIndicatorLength(inputs.length, 21);
564
- const values = computeHmaSeries(renderContext.data, length, inputs.source ?? "close");
634
+ const values = withCachedSeries(
635
+ `hma|${length}|${inputs.source ?? "close"}`,
636
+ renderContext.data,
637
+ () => computeHmaSeries(renderContext.data, length, inputs.source ?? "close")
638
+ );
565
639
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
566
640
  }
567
641
  };
@@ -573,7 +647,11 @@ var BUILTIN_STDDEV_INDICATOR = {
573
647
  defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2 },
574
648
  draw: (ctx, renderContext, inputs) => {
575
649
  const length = clampIndicatorLength(inputs.length, 20);
576
- const values = computeStdDevSeries(renderContext.data, length, inputs.source ?? "close");
650
+ const values = withCachedSeries(
651
+ `stddev|${length}|${inputs.source ?? "close"}`,
652
+ renderContext.data,
653
+ () => computeStdDevSeries(renderContext.data, length, inputs.source ?? "close")
654
+ );
577
655
  drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2);
578
656
  }
579
657
  };
@@ -585,7 +663,7 @@ var BUILTIN_ATR_INDICATOR = {
585
663
  defaultInputs: { length: 14, color: "#eab308", width: 2 },
586
664
  draw: (ctx, renderContext, inputs) => {
587
665
  const length = clampIndicatorLength(inputs.length, 14);
588
- const values = computeAtrSeries(renderContext.data, length);
666
+ const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
589
667
  drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2);
590
668
  }
591
669
  };
@@ -597,7 +675,7 @@ var BUILTIN_RSI_INDICATOR = {
597
675
  defaultInputs: { length: 14, color: "#3b82f6", width: 2 },
598
676
  draw: (ctx, renderContext, inputs) => {
599
677
  const length = clampIndicatorLength(inputs.length, 14);
600
- const values = computeRsiSeries(renderContext.data, length);
678
+ const values = withCachedSeries(`rsi|${length}`, renderContext.data, () => computeRsiSeries(renderContext.data, length));
601
679
  drawSeparateSeries(
602
680
  ctx,
603
681
  renderContext,
@@ -631,6 +709,18 @@ function createChart(element, options = {}) {
631
709
  ...options.axis ?? {},
632
710
  ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {}
633
711
  },
712
+ xAxis: {
713
+ ...DEFAULT_AXIS_OPTIONS,
714
+ ...options.axis ?? {},
715
+ ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
716
+ ...options.xAxis ?? {}
717
+ },
718
+ yAxis: {
719
+ ...DEFAULT_AXIS_OPTIONS,
720
+ ...options.axis ?? {},
721
+ ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
722
+ ...options.yAxis ?? {}
723
+ },
634
724
  crosshair: {
635
725
  ...DEFAULT_CROSSHAIR_OPTIONS,
636
726
  ...options.crosshair ?? {}
@@ -686,6 +776,9 @@ function createChart(element, options = {}) {
686
776
  visible: indicator.visible ?? true,
687
777
  pane: indicator.pane ?? plugin?.pane ?? "overlay",
688
778
  ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
779
+ zIndex: Math.round(Number(indicator.zIndex) || 0),
780
+ excludeFromAutoscale: indicator.excludeFromAutoscale ?? true,
781
+ overlayScaleWeight: Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0.25)),
689
782
  inputs: {
690
783
  ...defaults,
691
784
  ...indicator.inputs ?? {}
@@ -841,20 +934,56 @@ function createChart(element, options = {}) {
841
934
  }
842
935
  return { min: nextMin, max: nextMax };
843
936
  };
937
+ const getConfiguredPriceDecimals = () => clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
938
+ const getConfiguredTickSize = () => {
939
+ const step = Number(mergedOptions.tickSize);
940
+ return Number.isFinite(step) && step > 0 ? step : 0;
941
+ };
942
+ const getTickSizeDecimals = () => {
943
+ const tickSize = getConfiguredTickSize();
944
+ if (tickSize <= 0) {
945
+ return 0;
946
+ }
947
+ let scaled = tickSize;
948
+ let decimals = 0;
949
+ while (decimals < 8 && Math.abs(Math.round(scaled) - scaled) > 1e-10) {
950
+ scaled *= 10;
951
+ decimals += 1;
952
+ }
953
+ return decimals;
954
+ };
955
+ const getDisplayPriceDecimals = () => {
956
+ const configured = getConfiguredPriceDecimals();
957
+ const tickDecimals = getTickSizeDecimals();
958
+ return Math.max(configured, tickDecimals);
959
+ };
960
+ const quantizeToTickSize = (price) => {
961
+ const tickSize = getConfiguredTickSize();
962
+ if (tickSize <= 0 || !Number.isFinite(price)) {
963
+ return price;
964
+ }
965
+ return Math.round(price / tickSize) * tickSize;
966
+ };
844
967
  const formatPrice = (price) => {
845
- const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
846
- return price.toFixed(decimals);
968
+ const rounded = quantizeToTickSize(price);
969
+ const decimals = getDisplayPriceDecimals();
970
+ return rounded.toFixed(decimals);
847
971
  };
848
972
  const roundToPricePrecision = (price) => {
849
- const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
850
- return Number(price.toFixed(decimals));
973
+ const rounded = quantizeToTickSize(price);
974
+ const decimals = getDisplayPriceDecimals();
975
+ return Number(rounded.toFixed(decimals));
851
976
  };
852
977
  const getResolvedCandleColorEpsilon = () => {
853
978
  const configured = mergedOptions.candleColorEpsilon;
854
979
  if (configured >= 0) {
855
980
  return configured;
856
981
  }
857
- const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
982
+ const tickSize = getConfiguredTickSize();
983
+ if (tickSize > 0) {
984
+ return tickSize / 2;
985
+ }
986
+ const decimals = getDisplayPriceDecimals();
858
987
  return decimals > 0 ? 0.5 / 10 ** decimals : 0;
859
988
  };
860
989
  const getDirectionFromDelta = (delta) => {
@@ -1386,6 +1515,10 @@ function createChart(element, options = {}) {
1386
1515
  canvas.height = Math.floor(height * pixelRatio);
1387
1516
  ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
1388
1517
  const axis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
1518
+ const xAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
1519
+ const yAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
1520
+ const xAxisFontSize = Math.max(8, xAxis.fontSize);
1521
+ const yAxisFontSize = Math.max(8, yAxis.fontSize);
1389
1522
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
1390
1523
  ctx.fillStyle = mergedOptions.backgroundColor;
1391
1524
  ctx.fillRect(0, 0, width, height);
@@ -1400,6 +1533,9 @@ function createChart(element, options = {}) {
1400
1533
  const separatePaneSpacing = 6;
1401
1534
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1402
1535
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
1536
+ ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
1537
+ const overlayIndicatorsForScale = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1538
+ (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
1403
1539
  );
1404
1540
  const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
1405
1541
  const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
@@ -1451,8 +1587,47 @@ function createChart(element, options = {}) {
1451
1587
  }
1452
1588
  }
1453
1589
  }
1454
- const minPrice = Math.min(...priceSource.map((point) => point.l));
1455
- const maxPrice = Math.max(...priceSource.map((point) => point.h));
1590
+ let minPrice = Math.min(...priceSource.map((point) => point.l));
1591
+ let maxPrice = Math.max(...priceSource.map((point) => point.h));
1592
+ if (overlayIndicatorsForScale.length > 0) {
1593
+ for (const { indicator } of overlayIndicatorsForScale) {
1594
+ if (indicator.excludeFromAutoscale) {
1595
+ continue;
1596
+ }
1597
+ const type = indicator.type;
1598
+ const inputs = indicator.inputs;
1599
+ const source = inputs.source ?? "close";
1600
+ const length = clampIndicatorLength(inputs.length ?? 14, 14);
1601
+ let series = null;
1602
+ if (type === "sma") series = withCachedSeries(`sma|${length}|${source}`, data, () => computeSmaSeries(data, length, source));
1603
+ if (type === "ema") series = withCachedSeries(`ema|${length}|${source}`, data, () => computeEmaSeries(data, length, source));
1604
+ if (type === "wma") series = withCachedSeries(`wma|${length}|${source}`, data, () => computeWmaSeries(data, length, source));
1605
+ if (type === "vwma") series = withCachedSeries(`vwma|${length}|${source}`, data, () => computeVwmaSeries(data, length, source));
1606
+ if (type === "rma") series = withCachedSeries(`rma|${length}|${source}`, data, () => computeRmaSeries(data, length, source));
1607
+ if (type === "hma") series = withCachedSeries(`hma|${length}|${source}`, data, () => computeHmaSeries(data, length, source));
1608
+ if (!series) {
1609
+ continue;
1610
+ }
1611
+ const visibleValues = [];
1612
+ for (let idx = startIndex; idx <= endIndex; idx += 1) {
1613
+ const value = series[idx];
1614
+ if (Number.isFinite(value ?? Number.NaN)) {
1615
+ visibleValues.push(value);
1616
+ }
1617
+ }
1618
+ if (visibleValues.length === 0) {
1619
+ continue;
1620
+ }
1621
+ const seriesMin = Math.min(...visibleValues);
1622
+ const seriesMax = Math.max(...visibleValues);
1623
+ const weight = Math.min(1, Math.max(0, indicator.overlayScaleWeight));
1624
+ const currentMid = (minPrice + maxPrice) / 2;
1625
+ const weightedMin = currentMid + (seriesMin - currentMid) * weight;
1626
+ const weightedMax = currentMid + (seriesMax - currentMid) * weight;
1627
+ minPrice = Math.min(minPrice, weightedMin);
1628
+ maxPrice = Math.max(maxPrice, weightedMax);
1629
+ }
1630
+ }
1456
1631
  const priceRange = maxPrice - minPrice || 1;
1457
1632
  const autoMin = minPrice - priceRange * 0.08;
1458
1633
  const autoMax = maxPrice + priceRange * 0.08;
@@ -1612,7 +1787,7 @@ function createChart(element, options = {}) {
1612
1787
  }
1613
1788
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1614
1789
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
1615
- );
1790
+ ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
1616
1791
  if (activeOverlayIndicators.length > 0) {
1617
1792
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
1618
1793
  activeOverlayIndicators.forEach(({ indicator, plugin }) => {
@@ -1722,7 +1897,10 @@ function createChart(element, options = {}) {
1722
1897
  const ratio = tick / yTicks;
1723
1898
  const price = yMin + yRange * ratio;
1724
1899
  const y = yFromPrice(price);
1725
- drawText(formatPrice(price), chartRight + 6, y, "left", "middle", axis.textColor);
1900
+ const prevFont = ctx.font;
1901
+ ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
1902
+ drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
1903
+ ctx.font = prevFont;
1726
1904
  }
1727
1905
  const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1728
1906
  const lastPoint = data[data.length - 1];
@@ -1779,7 +1957,10 @@ function createChart(element, options = {}) {
1779
1957
  month: "short",
1780
1958
  day: "numeric"
1781
1959
  });
1782
- drawText(timeLabel, x, fullChartBottom + 8, "center", "top", axis.textColor);
1960
+ const prevFont = ctx.font;
1961
+ ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
1962
+ drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
1963
+ ctx.font = prevFont;
1783
1964
  }
1784
1965
  if (crosshair.visible && crosshairPoint) {
1785
1966
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
@@ -2550,6 +2731,27 @@ function createChart(element, options = {}) {
2550
2731
  indicators = indicators.filter((indicator) => indicator.type !== type);
2551
2732
  draw();
2552
2733
  };
2734
+ const listBuiltInIndicators = () => {
2735
+ return BUILTIN_INDICATORS.map((indicator) => ({
2736
+ id: indicator.id,
2737
+ name: indicator.name,
2738
+ pane: indicator.pane ?? "overlay",
2739
+ ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio }
2740
+ }));
2741
+ };
2742
+ const getIndicators = () => {
2743
+ return indicators.map((indicator) => ({
2744
+ id: indicator.id,
2745
+ type: indicator.type,
2746
+ visible: indicator.visible,
2747
+ pane: indicator.pane,
2748
+ ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
2749
+ zIndex: indicator.zIndex,
2750
+ excludeFromAutoscale: indicator.excludeFromAutoscale,
2751
+ overlayScaleWeight: indicator.overlayScaleWeight,
2752
+ inputs: { ...indicator.inputs }
2753
+ }));
2754
+ };
2553
2755
  const setIndicators = (nextIndicators) => {
2554
2756
  indicators = nextIndicators.map((indicator) => normalizeIndicatorState(indicator));
2555
2757
  draw();
@@ -2579,6 +2781,9 @@ function createChart(element, options = {}) {
2579
2781
  visible: patch.visible ?? indicator.visible,
2580
2782
  pane: patch.pane ?? indicator.pane ?? plugin?.pane ?? "overlay",
2581
2783
  ...patch.paneHeightRatio !== void 0 || indicator.paneHeightRatio !== void 0 ? { paneHeightRatio: patch.paneHeightRatio ?? indicator.paneHeightRatio } : {},
2784
+ zIndex: patch.zIndex === void 0 ? indicator.zIndex : Math.round(Number(patch.zIndex) || 0),
2785
+ excludeFromAutoscale: patch.excludeFromAutoscale ?? indicator.excludeFromAutoscale,
2786
+ overlayScaleWeight: patch.overlayScaleWeight === void 0 ? indicator.overlayScaleWeight : Math.min(1, Math.max(0, Number(patch.overlayScaleWeight) || 0)),
2582
2787
  type: patch.type ?? indicator.type,
2583
2788
  inputs: {
2584
2789
  ...indicator.inputs,
@@ -2628,6 +2833,8 @@ function createChart(element, options = {}) {
2628
2833
  setDoubleClickAction,
2629
2834
  registerIndicator,
2630
2835
  unregisterIndicator,
2836
+ listBuiltInIndicators,
2837
+ getIndicators,
2631
2838
  addIndicator,
2632
2839
  updateIndicator,
2633
2840
  removeIndicator,
package/docs/API.md CHANGED
@@ -32,6 +32,8 @@ Top-level options:
32
32
  - `backgroundColor` (default `#101114`)
33
33
  - `axisColor` (legacy shorthand for axis line/text color)
34
34
  - `axis?: AxisOptions`
35
+ - `xAxis?: AxisOptions` (overrides bottom-axis label text/font)
36
+ - `yAxis?: AxisOptions` (overrides right-axis label text/font)
35
37
  - `priceDecimals` (default `2`, used for axis/ticker/line price labels)
36
38
  - `stabilizePriceLabels` (default `true`, prevents ticker/crosshair/price-tag width jitter)
37
39
  - `priceLabelMinIntegerDigits` (default `3`, baseline integer-digit width for stabilized labels)
@@ -50,6 +52,7 @@ Top-level options:
50
52
  - `candleBodyWidthRatio` (default `0.7`)
51
53
  - `candleMinWidth` (default `0.5`)
52
54
  - `candleWickWidth` (default `1`)
55
+ - `tickSize` (default `0`; when > 0, formatter/pointer prices snap to tick)
53
56
  - `candleColorMode` (`"openClose" | "prevClose"`, default `"openClose"`)
54
57
  - `candleColorEpsilon` (default `-1` = auto from `priceDecimals`; set `0` to disable tolerance)
55
58
  - `autoScaleSmoothing` (default `0.16`)
@@ -248,6 +251,9 @@ Connector/fill visuals:
248
251
  - `visible?: boolean` (default `true`)
249
252
  - `pane?: "overlay" | "separate"`
250
253
  - `paneHeightRatio?: number` (for separate panes; `0.08` to `0.45` recommended)
254
+ - `zIndex?: number` (render order; lower first)
255
+ - `excludeFromAutoscale?: boolean` (default `true` for indicator instances)
256
+ - `overlayScaleWeight?: number` (`0..1`; only used when `excludeFromAutoscale` is `false`)
251
257
  - `inputs?: Record<string, unknown>` (plugin-specific parameters)
252
258
 
253
259
  ### `IndicatorPlugin`
@@ -269,7 +275,7 @@ Connector/fill visuals:
269
275
 
270
276
  Built-in:
271
277
 
272
- - `"volume"`: separate pane histogram (uses `OhlcDataPoint.v`)
278
+ - `"volume"`: overlay histogram by default (uses `OhlcDataPoint.v`; can be moved to separate pane)
273
279
  - `"sma"`: Simple Moving Average (overlay)
274
280
  - `"ema"`: Exponential Moving Average (overlay)
275
281
  - `"rsi"`: Relative Strength Index (separate pane, 30/50/70 guides)
@@ -297,6 +303,15 @@ Volume/VWMA note:
297
303
 
298
304
  - if your data has no `v`, `volume`/`vwma` will have limited or no output.
299
305
 
306
+ Volume style inputs:
307
+
308
+ - `upOpacity`, `downOpacity`
309
+ - `upColor`, `downColor`
310
+ - `minBarWidth`
311
+ - `overlayHeightRatio` (when used as overlay)
312
+ - `scaleMode` (`"visible"` default, or `"full"`)
313
+ - `clampPercentile` (`0..1`, default `1`; e.g. `0.95` to reduce outlier crush)
314
+
300
315
  ---
301
316
 
302
317
  ## `ChartInstance` Methods
@@ -325,6 +340,8 @@ Volume/VWMA note:
325
340
  - `setDoubleClickAction(action: "reset" | "placeLimitOrder"): void`
326
341
  - `registerIndicator(plugin: IndicatorPlugin): void`
327
342
  - `unregisterIndicator(type: string): void`
343
+ - `listBuiltInIndicators(): BuiltInIndicatorInfo[]`
344
+ - `getIndicators(): IndicatorInstanceOptions[]`
328
345
  - `addIndicator(type: string, inputs?: Record<string, unknown>, options?: Partial<IndicatorInstanceOptions>): string`
329
346
  - `updateIndicator(id: string, patch: Partial<IndicatorInstanceOptions>): void`
330
347
  - `removeIndicator(id: string): void`
package/docs/RECIPES.md CHANGED
@@ -26,6 +26,25 @@ Use:
26
26
  - `autoScaleSmoothing` for smoother scale transitions
27
27
  - `autoScaleIgnoreLatestCandle` to reduce live-candle jitter
28
28
 
29
+ ## Tick-size aware chart precision
30
+
31
+ ```ts
32
+ const chart = createChart(rootEl, {
33
+ tickSize: 0.25,
34
+ priceDecimals: 2
35
+ });
36
+ ```
37
+
38
+ ## Style x-axis and y-axis labels differently
39
+
40
+ ```ts
41
+ const chart = createChart(rootEl, {
42
+ axis: { lineColor: "#374151" },
43
+ xAxis: { textColor: "#9ca3af", fontSize: 11 },
44
+ yAxis: { textColor: "#e5e7eb", fontSize: 12 }
45
+ });
46
+ ```
47
+
29
48
  ## Stabilize candle up/down coloring on tiny deltas
30
49
 
31
50
  ```ts
@@ -119,6 +138,15 @@ const rsiId = chart.addIndicator("rsi", { length: 14 }, { pane: "separate", pane
119
138
  const volumeId = chart.addIndicator("volume", { upOpacity: 0.72, downOpacity: 0.72 });
120
139
  ```
121
140
 
141
+ ## Prevent one volume spike from crushing all bars
142
+
143
+ ```ts
144
+ const volumeId = chart.addIndicator("volume", {
145
+ scaleMode: "visible", // default behavior
146
+ clampPercentile: 0.95 // clamp scaling reference to p95 of visible bars
147
+ });
148
+ ```
149
+
122
150
  Available built-ins:
123
151
 
124
152
  - `volume`, `sma`, `ema`, `rsi`, `wma`, `vwma`, `rma`, `hma`, `stddev`, `atr`
@@ -138,6 +166,26 @@ Recommended range:
138
166
 
139
167
  - `0.08` to `0.45` (library clamps to safe bounds)
140
168
 
169
+ ## Use indicator z-order and autoscale influence
170
+
171
+ ```ts
172
+ chart.addIndicator("ema", { length: 34 }, { zIndex: 5 });
173
+ chart.addIndicator("wma", { length: 55 }, { zIndex: 10 });
174
+
175
+ chart.addIndicator("sma", { length: 200 }, {
176
+ excludeFromAutoscale: false,
177
+ overlayScaleWeight: 0.25
178
+ });
179
+ ```
180
+
181
+ ## Query built-ins and active indicator instances
182
+
183
+ ```ts
184
+ const builtIns = chart.listBuiltInIndicators();
185
+ const active = chart.getIndicators();
186
+ console.log({ builtIns, active });
187
+ ```
188
+
141
189
  ## Remove or hide indicators
142
190
 
143
191
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",