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/README.md CHANGED
@@ -73,6 +73,27 @@ const chart = createChart(root, {
73
73
  });
74
74
  ```
75
75
 
76
+ ## Tick-Size Aware Precision
77
+
78
+ ```ts
79
+ const chart = createChart(root, {
80
+ tickSize: 0.25,
81
+ priceDecimals: 2
82
+ });
83
+ ```
84
+
85
+ This keeps labels, pointer prices, and color tolerance aligned to market tick steps.
86
+
87
+ ## Per-Axis Label Styling
88
+
89
+ ```ts
90
+ const chart = createChart(root, {
91
+ axis: { lineColor: "#374151" },
92
+ xAxis: { textColor: "#9ca3af", fontSize: 11 },
93
+ yAxis: { textColor: "#e5e7eb", fontSize: 12 }
94
+ });
95
+ ```
96
+
76
97
  ## Crosshair "+" Button
77
98
 
78
99
  ```ts
@@ -115,6 +136,26 @@ chart.updateIndicator(emaId, { inputs: { length: 55 } });
115
136
  chart.updateIndicator(volumeId, { paneHeightRatio: 0.1 });
116
137
  ```
117
138
 
139
+ Volume scaling tip (for large spikes):
140
+
141
+ ```ts
142
+ chart.updateIndicator(volumeId, {
143
+ inputs: {
144
+ scaleMode: "visible", // default; scales using current viewport only
145
+ clampPercentile: 0.95 // optional outlier clamp
146
+ }
147
+ });
148
+ ```
149
+
150
+ Autoscale controls per indicator:
151
+
152
+ ```ts
153
+ chart.addIndicator("ema", { length: 50 }, {
154
+ excludeFromAutoscale: false,
155
+ overlayScaleWeight: 0.25
156
+ });
157
+ ```
158
+
118
159
  Volume note:
119
160
 
120
161
  - `volume` and `vwma` require `OhlcDataPoint.v` for best results.
@@ -155,6 +196,7 @@ const myId = chart.addIndicator("my-line");
155
196
  - `chart.onOrderAction(handler)` / `chart.onChartClick(handler)` / `chart.onCrosshairMove(handler)` / `chart.onCrosshairPriceAction(handler)`
156
197
  - `chart.setDoubleClickEnabled(enabled)` / `chart.setDoubleClickAction(action)`
157
198
  - `chart.registerIndicator(plugin)` / `chart.addIndicator(type, inputs?, options?)` / `chart.updateIndicator(id, patch)` / `chart.removeIndicator(id)`
199
+ - `chart.listBuiltInIndicators()` / `chart.getIndicators()`
158
200
  - `chart.zoomInX()` / `chart.zoomOutX()` / `chart.panX(bars)` / `chart.resetViewport()`
159
201
  - `chart.resize(width, height)` / `chart.destroy()`
160
202
 
@@ -133,6 +133,8 @@ var DEFAULT_OPTIONS = {
133
133
  backgroundColor: "#101114",
134
134
  axisColor: "#7f8289",
135
135
  axis: DEFAULT_AXIS_OPTIONS,
136
+ xAxis: DEFAULT_AXIS_OPTIONS,
137
+ yAxis: DEFAULT_AXIS_OPTIONS,
136
138
  priceDecimals: 2,
137
139
  stabilizePriceLabels: true,
138
140
  priceLabelMinIntegerDigits: 3,
@@ -151,6 +153,7 @@ var DEFAULT_OPTIONS = {
151
153
  candleBodyWidthRatio: 0.7,
152
154
  candleMinWidth: 0.5,
153
155
  candleWickWidth: 1,
156
+ tickSize: 0,
154
157
  candleColorMode: "openClose",
155
158
  candleColorEpsilon: -1,
156
159
  autoScaleSmoothing: 0.16,
@@ -388,6 +391,35 @@ var computeAtrSeries = (data, length) => {
388
391
  }
389
392
  return result;
390
393
  };
394
+ var builtInSeriesCache = /* @__PURE__ */ new Map();
395
+ var getSeriesFingerprint = (data) => {
396
+ const length = data.length;
397
+ const last = length > 0 ? data[length - 1] : void 0;
398
+ const prev = length > 1 ? data[length - 2] : void 0;
399
+ if (!last) return "empty";
400
+ return [
401
+ length,
402
+ last.time.getTime(),
403
+ last.o,
404
+ last.h,
405
+ last.l,
406
+ last.c,
407
+ last.v ?? "",
408
+ prev?.time.getTime() ?? "",
409
+ prev?.c ?? "",
410
+ prev?.v ?? ""
411
+ ].join("|");
412
+ };
413
+ var withCachedSeries = (key, data, compute) => {
414
+ const fingerprint = getSeriesFingerprint(data);
415
+ const existing = builtInSeriesCache.get(key);
416
+ if (existing && existing.fingerprint === fingerprint) {
417
+ return existing.values;
418
+ }
419
+ const values = compute();
420
+ builtInSeriesCache.set(key, { fingerprint, values });
421
+ return values;
422
+ };
391
423
  var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
392
424
  if (!renderContext.yFromPrice) return;
393
425
  const yFromPrice = renderContext.yFromPrice;
@@ -477,6 +509,15 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
477
509
  }
478
510
  ctx.restore();
479
511
  };
512
+ var getPercentileValue = (values, percentile) => {
513
+ if (values.length === 0) {
514
+ return 1;
515
+ }
516
+ const sorted = [...values].sort((a, b) => a - b);
517
+ const clamped = Math.min(1, Math.max(0, percentile));
518
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(clamped * sorted.length) - 1));
519
+ return sorted[index] ?? 1;
520
+ };
480
521
  var BUILTIN_VOLUME_INDICATOR = {
481
522
  id: "volume",
482
523
  name: "Volume",
@@ -485,8 +526,12 @@ var BUILTIN_VOLUME_INDICATOR = {
485
526
  defaultInputs: {
486
527
  upOpacity: 0.7,
487
528
  downOpacity: 0.7,
529
+ upColor: "",
530
+ downColor: "",
488
531
  minBarWidth: 1,
489
- overlayHeightRatio: 0.22
532
+ overlayHeightRatio: 0.22,
533
+ scaleMode: "visible",
534
+ clampPercentile: 1
490
535
  },
491
536
  draw: (ctx, renderContext, inputs) => {
492
537
  const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
@@ -495,8 +540,11 @@ var BUILTIN_VOLUME_INDICATOR = {
495
540
  const overlayHeightRatio = Math.min(0.45, Math.max(0.08, Number(inputs.overlayHeightRatio) || 0.22));
496
541
  const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
497
542
  const paneBottom = chartBottom;
498
- const visiblePoints = data.slice(startIndex, endIndex + 1);
499
- const maxVolume = Math.max(1, ...visiblePoints.map((point) => point.v ?? 0));
543
+ const scaleMode = inputs.scaleMode === "full" ? "full" : "visible";
544
+ const scalingPoints = scaleMode === "full" ? data : data.slice(startIndex, endIndex + 1);
545
+ const scalingVolumes = scalingPoints.map((point) => Math.max(0, point.v ?? 0)).filter((value) => value > 0);
546
+ const clampPercentile = Number.isFinite(inputs.clampPercentile) ? Number(inputs.clampPercentile) : 1;
547
+ const maxVolume = Math.max(1, getPercentileValue(scalingVolumes, clampPercentile));
500
548
  const barWidth = Math.max(
501
549
  Math.max(1, Number(inputs.minBarWidth) || 1),
502
550
  Math.min(Math.max(1, candleSpacing - 1), Math.floor(candleSpacing * 0.7))
@@ -515,9 +563,11 @@ var BUILTIN_VOLUME_INDICATOR = {
515
563
  const barY = Math.round(paneBottom - volumeHeight);
516
564
  const direction = renderContext.getCandleDirectionByIndex(index);
517
565
  const opacity = direction === "up" ? upOpacity : downOpacity;
566
+ const upBarColor = inputs.upColor && inputs.upColor.trim().length > 0 ? inputs.upColor : upColor;
567
+ const downBarColor = inputs.downColor && inputs.downColor.trim().length > 0 ? inputs.downColor : downColor;
518
568
  ctx.save();
519
569
  ctx.globalAlpha = opacity;
520
- ctx.fillStyle = direction === "up" ? upColor : downColor;
570
+ ctx.fillStyle = direction === "up" ? upBarColor : downBarColor;
521
571
  ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
522
572
  ctx.restore();
523
573
  }
@@ -530,7 +580,11 @@ var BUILTIN_SMA_INDICATOR = {
530
580
  defaultInputs: { length: 20, source: "close", color: "#60a5fa", width: 2 },
531
581
  draw: (ctx, renderContext, inputs) => {
532
582
  const length = clampIndicatorLength(inputs.length, 20);
533
- const values = computeSmaSeries(renderContext.data, length, inputs.source ?? "close");
583
+ const values = withCachedSeries(
584
+ `sma|${length}|${inputs.source ?? "close"}`,
585
+ renderContext.data,
586
+ () => computeSmaSeries(renderContext.data, length, inputs.source ?? "close")
587
+ );
534
588
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
535
589
  }
536
590
  };
@@ -541,7 +595,11 @@ var BUILTIN_EMA_INDICATOR = {
541
595
  defaultInputs: { length: 20, source: "close", color: "#f59e0b", width: 2 },
542
596
  draw: (ctx, renderContext, inputs) => {
543
597
  const length = clampIndicatorLength(inputs.length, 20);
544
- const values = computeEmaSeries(renderContext.data, length, inputs.source ?? "close");
598
+ const values = withCachedSeries(
599
+ `ema|${length}|${inputs.source ?? "close"}`,
600
+ renderContext.data,
601
+ () => computeEmaSeries(renderContext.data, length, inputs.source ?? "close")
602
+ );
545
603
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
546
604
  }
547
605
  };
@@ -552,7 +610,11 @@ var BUILTIN_WMA_INDICATOR = {
552
610
  defaultInputs: { length: 20, source: "close", color: "#a78bfa", width: 2 },
553
611
  draw: (ctx, renderContext, inputs) => {
554
612
  const length = clampIndicatorLength(inputs.length, 20);
555
- const values = computeWmaSeries(renderContext.data, length, inputs.source ?? "close");
613
+ const values = withCachedSeries(
614
+ `wma|${length}|${inputs.source ?? "close"}`,
615
+ renderContext.data,
616
+ () => computeWmaSeries(renderContext.data, length, inputs.source ?? "close")
617
+ );
556
618
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
557
619
  }
558
620
  };
@@ -563,7 +625,11 @@ var BUILTIN_VWMA_INDICATOR = {
563
625
  defaultInputs: { length: 20, source: "close", color: "#ef4444", width: 2 },
564
626
  draw: (ctx, renderContext, inputs) => {
565
627
  const length = clampIndicatorLength(inputs.length, 20);
566
- const values = computeVwmaSeries(renderContext.data, length, inputs.source ?? "close");
628
+ const values = withCachedSeries(
629
+ `vwma|${length}|${inputs.source ?? "close"}`,
630
+ renderContext.data,
631
+ () => computeVwmaSeries(renderContext.data, length, inputs.source ?? "close")
632
+ );
567
633
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
568
634
  }
569
635
  };
@@ -574,7 +640,11 @@ var BUILTIN_RMA_INDICATOR = {
574
640
  defaultInputs: { length: 14, source: "close", color: "#22c55e", width: 2 },
575
641
  draw: (ctx, renderContext, inputs) => {
576
642
  const length = clampIndicatorLength(inputs.length, 14);
577
- const values = computeRmaSeries(renderContext.data, length, inputs.source ?? "close");
643
+ const values = withCachedSeries(
644
+ `rma|${length}|${inputs.source ?? "close"}`,
645
+ renderContext.data,
646
+ () => computeRmaSeries(renderContext.data, length, inputs.source ?? "close")
647
+ );
578
648
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
579
649
  }
580
650
  };
@@ -585,7 +655,11 @@ var BUILTIN_HMA_INDICATOR = {
585
655
  defaultInputs: { length: 21, source: "close", color: "#14b8a6", width: 2 },
586
656
  draw: (ctx, renderContext, inputs) => {
587
657
  const length = clampIndicatorLength(inputs.length, 21);
588
- const values = computeHmaSeries(renderContext.data, length, inputs.source ?? "close");
658
+ const values = withCachedSeries(
659
+ `hma|${length}|${inputs.source ?? "close"}`,
660
+ renderContext.data,
661
+ () => computeHmaSeries(renderContext.data, length, inputs.source ?? "close")
662
+ );
589
663
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
590
664
  }
591
665
  };
@@ -597,7 +671,11 @@ var BUILTIN_STDDEV_INDICATOR = {
597
671
  defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2 },
598
672
  draw: (ctx, renderContext, inputs) => {
599
673
  const length = clampIndicatorLength(inputs.length, 20);
600
- const values = computeStdDevSeries(renderContext.data, length, inputs.source ?? "close");
674
+ const values = withCachedSeries(
675
+ `stddev|${length}|${inputs.source ?? "close"}`,
676
+ renderContext.data,
677
+ () => computeStdDevSeries(renderContext.data, length, inputs.source ?? "close")
678
+ );
601
679
  drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2);
602
680
  }
603
681
  };
@@ -609,7 +687,7 @@ var BUILTIN_ATR_INDICATOR = {
609
687
  defaultInputs: { length: 14, color: "#eab308", width: 2 },
610
688
  draw: (ctx, renderContext, inputs) => {
611
689
  const length = clampIndicatorLength(inputs.length, 14);
612
- const values = computeAtrSeries(renderContext.data, length);
690
+ const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
613
691
  drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2);
614
692
  }
615
693
  };
@@ -621,7 +699,7 @@ var BUILTIN_RSI_INDICATOR = {
621
699
  defaultInputs: { length: 14, color: "#3b82f6", width: 2 },
622
700
  draw: (ctx, renderContext, inputs) => {
623
701
  const length = clampIndicatorLength(inputs.length, 14);
624
- const values = computeRsiSeries(renderContext.data, length);
702
+ const values = withCachedSeries(`rsi|${length}`, renderContext.data, () => computeRsiSeries(renderContext.data, length));
625
703
  drawSeparateSeries(
626
704
  ctx,
627
705
  renderContext,
@@ -655,6 +733,18 @@ function createChart(element, options = {}) {
655
733
  ...options.axis ?? {},
656
734
  ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {}
657
735
  },
736
+ xAxis: {
737
+ ...DEFAULT_AXIS_OPTIONS,
738
+ ...options.axis ?? {},
739
+ ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
740
+ ...options.xAxis ?? {}
741
+ },
742
+ yAxis: {
743
+ ...DEFAULT_AXIS_OPTIONS,
744
+ ...options.axis ?? {},
745
+ ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
746
+ ...options.yAxis ?? {}
747
+ },
658
748
  crosshair: {
659
749
  ...DEFAULT_CROSSHAIR_OPTIONS,
660
750
  ...options.crosshair ?? {}
@@ -710,6 +800,9 @@ function createChart(element, options = {}) {
710
800
  visible: indicator.visible ?? true,
711
801
  pane: indicator.pane ?? plugin?.pane ?? "overlay",
712
802
  ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
803
+ zIndex: Math.round(Number(indicator.zIndex) || 0),
804
+ excludeFromAutoscale: indicator.excludeFromAutoscale ?? true,
805
+ overlayScaleWeight: Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0.25)),
713
806
  inputs: {
714
807
  ...defaults,
715
808
  ...indicator.inputs ?? {}
@@ -865,20 +958,56 @@ function createChart(element, options = {}) {
865
958
  }
866
959
  return { min: nextMin, max: nextMax };
867
960
  };
961
+ const getConfiguredPriceDecimals = () => clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
962
+ const getConfiguredTickSize = () => {
963
+ const step = Number(mergedOptions.tickSize);
964
+ return Number.isFinite(step) && step > 0 ? step : 0;
965
+ };
966
+ const getTickSizeDecimals = () => {
967
+ const tickSize = getConfiguredTickSize();
968
+ if (tickSize <= 0) {
969
+ return 0;
970
+ }
971
+ let scaled = tickSize;
972
+ let decimals = 0;
973
+ while (decimals < 8 && Math.abs(Math.round(scaled) - scaled) > 1e-10) {
974
+ scaled *= 10;
975
+ decimals += 1;
976
+ }
977
+ return decimals;
978
+ };
979
+ const getDisplayPriceDecimals = () => {
980
+ const configured = getConfiguredPriceDecimals();
981
+ const tickDecimals = getTickSizeDecimals();
982
+ return Math.max(configured, tickDecimals);
983
+ };
984
+ const quantizeToTickSize = (price) => {
985
+ const tickSize = getConfiguredTickSize();
986
+ if (tickSize <= 0 || !Number.isFinite(price)) {
987
+ return price;
988
+ }
989
+ return Math.round(price / tickSize) * tickSize;
990
+ };
868
991
  const formatPrice = (price) => {
869
- const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
870
- return price.toFixed(decimals);
992
+ const rounded = quantizeToTickSize(price);
993
+ const decimals = getDisplayPriceDecimals();
994
+ return rounded.toFixed(decimals);
871
995
  };
872
996
  const roundToPricePrecision = (price) => {
873
- const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
874
- return Number(price.toFixed(decimals));
997
+ const rounded = quantizeToTickSize(price);
998
+ const decimals = getDisplayPriceDecimals();
999
+ return Number(rounded.toFixed(decimals));
875
1000
  };
876
1001
  const getResolvedCandleColorEpsilon = () => {
877
1002
  const configured = mergedOptions.candleColorEpsilon;
878
1003
  if (configured >= 0) {
879
1004
  return configured;
880
1005
  }
881
- const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
1006
+ const tickSize = getConfiguredTickSize();
1007
+ if (tickSize > 0) {
1008
+ return tickSize / 2;
1009
+ }
1010
+ const decimals = getDisplayPriceDecimals();
882
1011
  return decimals > 0 ? 0.5 / 10 ** decimals : 0;
883
1012
  };
884
1013
  const getDirectionFromDelta = (delta) => {
@@ -1410,6 +1539,10 @@ function createChart(element, options = {}) {
1410
1539
  canvas.height = Math.floor(height * pixelRatio);
1411
1540
  ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
1412
1541
  const axis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
1542
+ const xAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
1543
+ const yAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
1544
+ const xAxisFontSize = Math.max(8, xAxis.fontSize);
1545
+ const yAxisFontSize = Math.max(8, yAxis.fontSize);
1413
1546
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
1414
1547
  ctx.fillStyle = mergedOptions.backgroundColor;
1415
1548
  ctx.fillRect(0, 0, width, height);
@@ -1424,6 +1557,9 @@ function createChart(element, options = {}) {
1424
1557
  const separatePaneSpacing = 6;
1425
1558
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1426
1559
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
1560
+ ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
1561
+ const overlayIndicatorsForScale = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1562
+ (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
1427
1563
  );
1428
1564
  const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
1429
1565
  const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
@@ -1475,8 +1611,47 @@ function createChart(element, options = {}) {
1475
1611
  }
1476
1612
  }
1477
1613
  }
1478
- const minPrice = Math.min(...priceSource.map((point) => point.l));
1479
- const maxPrice = Math.max(...priceSource.map((point) => point.h));
1614
+ let minPrice = Math.min(...priceSource.map((point) => point.l));
1615
+ let maxPrice = Math.max(...priceSource.map((point) => point.h));
1616
+ if (overlayIndicatorsForScale.length > 0) {
1617
+ for (const { indicator } of overlayIndicatorsForScale) {
1618
+ if (indicator.excludeFromAutoscale) {
1619
+ continue;
1620
+ }
1621
+ const type = indicator.type;
1622
+ const inputs = indicator.inputs;
1623
+ const source = inputs.source ?? "close";
1624
+ const length = clampIndicatorLength(inputs.length ?? 14, 14);
1625
+ let series = null;
1626
+ if (type === "sma") series = withCachedSeries(`sma|${length}|${source}`, data, () => computeSmaSeries(data, length, source));
1627
+ if (type === "ema") series = withCachedSeries(`ema|${length}|${source}`, data, () => computeEmaSeries(data, length, source));
1628
+ if (type === "wma") series = withCachedSeries(`wma|${length}|${source}`, data, () => computeWmaSeries(data, length, source));
1629
+ if (type === "vwma") series = withCachedSeries(`vwma|${length}|${source}`, data, () => computeVwmaSeries(data, length, source));
1630
+ if (type === "rma") series = withCachedSeries(`rma|${length}|${source}`, data, () => computeRmaSeries(data, length, source));
1631
+ if (type === "hma") series = withCachedSeries(`hma|${length}|${source}`, data, () => computeHmaSeries(data, length, source));
1632
+ if (!series) {
1633
+ continue;
1634
+ }
1635
+ const visibleValues = [];
1636
+ for (let idx = startIndex; idx <= endIndex; idx += 1) {
1637
+ const value = series[idx];
1638
+ if (Number.isFinite(value ?? Number.NaN)) {
1639
+ visibleValues.push(value);
1640
+ }
1641
+ }
1642
+ if (visibleValues.length === 0) {
1643
+ continue;
1644
+ }
1645
+ const seriesMin = Math.min(...visibleValues);
1646
+ const seriesMax = Math.max(...visibleValues);
1647
+ const weight = Math.min(1, Math.max(0, indicator.overlayScaleWeight));
1648
+ const currentMid = (minPrice + maxPrice) / 2;
1649
+ const weightedMin = currentMid + (seriesMin - currentMid) * weight;
1650
+ const weightedMax = currentMid + (seriesMax - currentMid) * weight;
1651
+ minPrice = Math.min(minPrice, weightedMin);
1652
+ maxPrice = Math.max(maxPrice, weightedMax);
1653
+ }
1654
+ }
1480
1655
  const priceRange = maxPrice - minPrice || 1;
1481
1656
  const autoMin = minPrice - priceRange * 0.08;
1482
1657
  const autoMax = maxPrice + priceRange * 0.08;
@@ -1636,7 +1811,7 @@ function createChart(element, options = {}) {
1636
1811
  }
1637
1812
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1638
1813
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
1639
- );
1814
+ ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
1640
1815
  if (activeOverlayIndicators.length > 0) {
1641
1816
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
1642
1817
  activeOverlayIndicators.forEach(({ indicator, plugin }) => {
@@ -1746,7 +1921,10 @@ function createChart(element, options = {}) {
1746
1921
  const ratio = tick / yTicks;
1747
1922
  const price = yMin + yRange * ratio;
1748
1923
  const y = yFromPrice(price);
1749
- drawText(formatPrice(price), chartRight + 6, y, "left", "middle", axis.textColor);
1924
+ const prevFont = ctx.font;
1925
+ ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
1926
+ drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
1927
+ ctx.font = prevFont;
1750
1928
  }
1751
1929
  const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1752
1930
  const lastPoint = data[data.length - 1];
@@ -1803,7 +1981,10 @@ function createChart(element, options = {}) {
1803
1981
  month: "short",
1804
1982
  day: "numeric"
1805
1983
  });
1806
- drawText(timeLabel, x, fullChartBottom + 8, "center", "top", axis.textColor);
1984
+ const prevFont = ctx.font;
1985
+ ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
1986
+ drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
1987
+ ctx.font = prevFont;
1807
1988
  }
1808
1989
  if (crosshair.visible && crosshairPoint) {
1809
1990
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
@@ -2574,6 +2755,27 @@ function createChart(element, options = {}) {
2574
2755
  indicators = indicators.filter((indicator) => indicator.type !== type);
2575
2756
  draw();
2576
2757
  };
2758
+ const listBuiltInIndicators = () => {
2759
+ return BUILTIN_INDICATORS.map((indicator) => ({
2760
+ id: indicator.id,
2761
+ name: indicator.name,
2762
+ pane: indicator.pane ?? "overlay",
2763
+ ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio }
2764
+ }));
2765
+ };
2766
+ const getIndicators = () => {
2767
+ return indicators.map((indicator) => ({
2768
+ id: indicator.id,
2769
+ type: indicator.type,
2770
+ visible: indicator.visible,
2771
+ pane: indicator.pane,
2772
+ ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
2773
+ zIndex: indicator.zIndex,
2774
+ excludeFromAutoscale: indicator.excludeFromAutoscale,
2775
+ overlayScaleWeight: indicator.overlayScaleWeight,
2776
+ inputs: { ...indicator.inputs }
2777
+ }));
2778
+ };
2577
2779
  const setIndicators = (nextIndicators) => {
2578
2780
  indicators = nextIndicators.map((indicator) => normalizeIndicatorState(indicator));
2579
2781
  draw();
@@ -2603,6 +2805,9 @@ function createChart(element, options = {}) {
2603
2805
  visible: patch.visible ?? indicator.visible,
2604
2806
  pane: patch.pane ?? indicator.pane ?? plugin?.pane ?? "overlay",
2605
2807
  ...patch.paneHeightRatio !== void 0 || indicator.paneHeightRatio !== void 0 ? { paneHeightRatio: patch.paneHeightRatio ?? indicator.paneHeightRatio } : {},
2808
+ zIndex: patch.zIndex === void 0 ? indicator.zIndex : Math.round(Number(patch.zIndex) || 0),
2809
+ excludeFromAutoscale: patch.excludeFromAutoscale ?? indicator.excludeFromAutoscale,
2810
+ overlayScaleWeight: patch.overlayScaleWeight === void 0 ? indicator.overlayScaleWeight : Math.min(1, Math.max(0, Number(patch.overlayScaleWeight) || 0)),
2606
2811
  type: patch.type ?? indicator.type,
2607
2812
  inputs: {
2608
2813
  ...indicator.inputs,
@@ -2652,6 +2857,8 @@ function createChart(element, options = {}) {
2652
2857
  setDoubleClickAction,
2653
2858
  registerIndicator,
2654
2859
  unregisterIndicator,
2860
+ listBuiltInIndicators,
2861
+ getIndicators,
2655
2862
  addIndicator,
2656
2863
  updateIndicator,
2657
2864
  removeIndicator,
@@ -4,6 +4,8 @@ interface ChartOptions {
4
4
  backgroundColor?: string;
5
5
  axisColor?: string;
6
6
  axis?: AxisOptions;
7
+ xAxis?: AxisOptions;
8
+ yAxis?: AxisOptions;
7
9
  priceDecimals?: number;
8
10
  stabilizePriceLabels?: boolean;
9
11
  priceLabelMinIntegerDigits?: number;
@@ -22,6 +24,7 @@ interface ChartOptions {
22
24
  candleBodyWidthRatio?: number;
23
25
  candleMinWidth?: number;
24
26
  candleWickWidth?: number;
27
+ tickSize?: number;
25
28
  candleColorMode?: "openClose" | "prevClose";
26
29
  candleColorEpsilon?: number;
27
30
  autoScaleSmoothing?: number;
@@ -44,6 +47,9 @@ interface IndicatorInstanceOptions<TInputs extends Record<string, unknown> = Rec
44
47
  visible?: boolean;
45
48
  pane?: IndicatorPane;
46
49
  paneHeightRatio?: number;
50
+ zIndex?: number;
51
+ excludeFromAutoscale?: boolean;
52
+ overlayScaleWeight?: number;
47
53
  inputs?: Partial<TInputs>;
48
54
  }
49
55
  interface IndicatorRenderContext {
@@ -80,6 +86,12 @@ interface IndicatorPlugin<TInputs extends Record<string, unknown> = Record<strin
80
86
  defaultInputs?: TInputs;
81
87
  draw: (ctx: CanvasRenderingContext2D, renderContext: IndicatorRenderContext, inputs: TInputs) => void;
82
88
  }
89
+ interface BuiltInIndicatorInfo {
90
+ id: string;
91
+ name: string;
92
+ pane: IndicatorPane;
93
+ paneHeightRatio?: number;
94
+ }
83
95
  interface DashPatternOptions {
84
96
  dotted: [number, number];
85
97
  dashed: [number, number];
@@ -271,6 +283,8 @@ interface ChartInstance {
271
283
  setDoubleClickAction: (action: "reset" | "placeLimitOrder") => void;
272
284
  registerIndicator: (plugin: IndicatorPlugin<any>) => void;
273
285
  unregisterIndicator: (type: string) => void;
286
+ listBuiltInIndicators: () => BuiltInIndicatorInfo[];
287
+ getIndicators: () => IndicatorInstanceOptions[];
274
288
  addIndicator: (type: string, inputs?: Record<string, unknown>, options?: Partial<IndicatorInstanceOptions>) => string;
275
289
  updateIndicator: (id: string, patch: Partial<IndicatorInstanceOptions>) => void;
276
290
  removeIndicator: (id: string) => void;
@@ -288,4 +302,4 @@ interface OhlcDataPoint {
288
302
  }
289
303
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
290
304
 
291
- export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
305
+ export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };