hyperprop-charting-library 0.1.23 → 0.1.25

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
@@ -60,6 +60,40 @@ const chart = createChart(root, {
60
60
  });
61
61
  ```
62
62
 
63
+ ## Candle Color Behavior
64
+
65
+ You can control how up/down candle color is decided:
66
+
67
+ ```ts
68
+ const chart = createChart(root, {
69
+ // "openClose" (default) or "prevClose"
70
+ candleColorMode: "prevClose",
71
+ // -1 = auto epsilon from priceDecimals, 0 = strict compare
72
+ candleColorEpsilon: -1
73
+ });
74
+ ```
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
+
63
97
  ## Crosshair "+" Button
64
98
 
65
99
  ```ts
@@ -102,6 +136,15 @@ chart.updateIndicator(emaId, { inputs: { length: 55 } });
102
136
  chart.updateIndicator(volumeId, { paneHeightRatio: 0.1 });
103
137
  ```
104
138
 
139
+ Autoscale controls per indicator:
140
+
141
+ ```ts
142
+ chart.addIndicator("ema", { length: 50 }, {
143
+ excludeFromAutoscale: false,
144
+ overlayScaleWeight: 0.25
145
+ });
146
+ ```
147
+
105
148
  Volume note:
106
149
 
107
150
  - `volume` and `vwma` require `OhlcDataPoint.v` for best results.
@@ -142,6 +185,7 @@ const myId = chart.addIndicator("my-line");
142
185
  - `chart.onOrderAction(handler)` / `chart.onChartClick(handler)` / `chart.onCrosshairMove(handler)` / `chart.onCrosshairPriceAction(handler)`
143
186
  - `chart.setDoubleClickEnabled(enabled)` / `chart.setDoubleClickAction(action)`
144
187
  - `chart.registerIndicator(plugin)` / `chart.addIndicator(type, inputs?, options?)` / `chart.updateIndicator(id, patch)` / `chart.removeIndicator(id)`
188
+ - `chart.listBuiltInIndicators()` / `chart.getIndicators()`
145
189
  - `chart.zoomInX()` / `chart.zoomOutX()` / `chart.panX(bars)` / `chart.resetViewport()`
146
190
  - `chart.resize(width, height)` / `chart.destroy()`
147
191
 
@@ -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,9 @@ var DEFAULT_OPTIONS = {
151
153
  candleBodyWidthRatio: 0.7,
152
154
  candleMinWidth: 0.5,
153
155
  candleWickWidth: 1,
156
+ tickSize: 0,
157
+ candleColorMode: "openClose",
158
+ candleColorEpsilon: -1,
154
159
  autoScaleSmoothing: 0.16,
155
160
  autoScaleIgnoreLatestCandle: true,
156
161
  doubleClickEnabled: true,
@@ -386,6 +391,35 @@ var computeAtrSeries = (data, length) => {
386
391
  }
387
392
  return result;
388
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
+ };
389
423
  var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
390
424
  if (!renderContext.yFromPrice) return;
391
425
  const yFromPrice = renderContext.yFromPrice;
@@ -483,6 +517,8 @@ var BUILTIN_VOLUME_INDICATOR = {
483
517
  defaultInputs: {
484
518
  upOpacity: 0.7,
485
519
  downOpacity: 0.7,
520
+ upColor: "",
521
+ downColor: "",
486
522
  minBarWidth: 1,
487
523
  overlayHeightRatio: 0.22
488
524
  },
@@ -511,11 +547,13 @@ var BUILTIN_VOLUME_INDICATOR = {
511
547
  const xCenter = xFromIndex(index);
512
548
  const barX = Math.round(xCenter - barWidth / 2);
513
549
  const barY = Math.round(paneBottom - volumeHeight);
514
- const isUp = point.c >= point.o;
515
- const opacity = isUp ? upOpacity : downOpacity;
550
+ const direction = renderContext.getCandleDirectionByIndex(index);
551
+ const opacity = direction === "up" ? upOpacity : downOpacity;
552
+ const upBarColor = inputs.upColor && inputs.upColor.trim().length > 0 ? inputs.upColor : upColor;
553
+ const downBarColor = inputs.downColor && inputs.downColor.trim().length > 0 ? inputs.downColor : downColor;
516
554
  ctx.save();
517
555
  ctx.globalAlpha = opacity;
518
- ctx.fillStyle = isUp ? upColor : downColor;
556
+ ctx.fillStyle = direction === "up" ? upBarColor : downBarColor;
519
557
  ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
520
558
  ctx.restore();
521
559
  }
@@ -528,7 +566,11 @@ var BUILTIN_SMA_INDICATOR = {
528
566
  defaultInputs: { length: 20, source: "close", color: "#60a5fa", width: 2 },
529
567
  draw: (ctx, renderContext, inputs) => {
530
568
  const length = clampIndicatorLength(inputs.length, 20);
531
- const values = computeSmaSeries(renderContext.data, length, inputs.source ?? "close");
569
+ const values = withCachedSeries(
570
+ `sma|${length}|${inputs.source ?? "close"}`,
571
+ renderContext.data,
572
+ () => computeSmaSeries(renderContext.data, length, inputs.source ?? "close")
573
+ );
532
574
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
533
575
  }
534
576
  };
@@ -539,7 +581,11 @@ var BUILTIN_EMA_INDICATOR = {
539
581
  defaultInputs: { length: 20, source: "close", color: "#f59e0b", width: 2 },
540
582
  draw: (ctx, renderContext, inputs) => {
541
583
  const length = clampIndicatorLength(inputs.length, 20);
542
- const values = computeEmaSeries(renderContext.data, length, inputs.source ?? "close");
584
+ const values = withCachedSeries(
585
+ `ema|${length}|${inputs.source ?? "close"}`,
586
+ renderContext.data,
587
+ () => computeEmaSeries(renderContext.data, length, inputs.source ?? "close")
588
+ );
543
589
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
544
590
  }
545
591
  };
@@ -550,7 +596,11 @@ var BUILTIN_WMA_INDICATOR = {
550
596
  defaultInputs: { length: 20, source: "close", color: "#a78bfa", width: 2 },
551
597
  draw: (ctx, renderContext, inputs) => {
552
598
  const length = clampIndicatorLength(inputs.length, 20);
553
- const values = computeWmaSeries(renderContext.data, length, inputs.source ?? "close");
599
+ const values = withCachedSeries(
600
+ `wma|${length}|${inputs.source ?? "close"}`,
601
+ renderContext.data,
602
+ () => computeWmaSeries(renderContext.data, length, inputs.source ?? "close")
603
+ );
554
604
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
555
605
  }
556
606
  };
@@ -561,7 +611,11 @@ var BUILTIN_VWMA_INDICATOR = {
561
611
  defaultInputs: { length: 20, source: "close", color: "#ef4444", width: 2 },
562
612
  draw: (ctx, renderContext, inputs) => {
563
613
  const length = clampIndicatorLength(inputs.length, 20);
564
- const values = computeVwmaSeries(renderContext.data, length, inputs.source ?? "close");
614
+ const values = withCachedSeries(
615
+ `vwma|${length}|${inputs.source ?? "close"}`,
616
+ renderContext.data,
617
+ () => computeVwmaSeries(renderContext.data, length, inputs.source ?? "close")
618
+ );
565
619
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
566
620
  }
567
621
  };
@@ -572,7 +626,11 @@ var BUILTIN_RMA_INDICATOR = {
572
626
  defaultInputs: { length: 14, source: "close", color: "#22c55e", width: 2 },
573
627
  draw: (ctx, renderContext, inputs) => {
574
628
  const length = clampIndicatorLength(inputs.length, 14);
575
- const values = computeRmaSeries(renderContext.data, length, inputs.source ?? "close");
629
+ const values = withCachedSeries(
630
+ `rma|${length}|${inputs.source ?? "close"}`,
631
+ renderContext.data,
632
+ () => computeRmaSeries(renderContext.data, length, inputs.source ?? "close")
633
+ );
576
634
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
577
635
  }
578
636
  };
@@ -583,7 +641,11 @@ var BUILTIN_HMA_INDICATOR = {
583
641
  defaultInputs: { length: 21, source: "close", color: "#14b8a6", width: 2 },
584
642
  draw: (ctx, renderContext, inputs) => {
585
643
  const length = clampIndicatorLength(inputs.length, 21);
586
- const values = computeHmaSeries(renderContext.data, length, inputs.source ?? "close");
644
+ const values = withCachedSeries(
645
+ `hma|${length}|${inputs.source ?? "close"}`,
646
+ renderContext.data,
647
+ () => computeHmaSeries(renderContext.data, length, inputs.source ?? "close")
648
+ );
587
649
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
588
650
  }
589
651
  };
@@ -595,7 +657,11 @@ var BUILTIN_STDDEV_INDICATOR = {
595
657
  defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2 },
596
658
  draw: (ctx, renderContext, inputs) => {
597
659
  const length = clampIndicatorLength(inputs.length, 20);
598
- const values = computeStdDevSeries(renderContext.data, length, inputs.source ?? "close");
660
+ const values = withCachedSeries(
661
+ `stddev|${length}|${inputs.source ?? "close"}`,
662
+ renderContext.data,
663
+ () => computeStdDevSeries(renderContext.data, length, inputs.source ?? "close")
664
+ );
599
665
  drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2);
600
666
  }
601
667
  };
@@ -607,7 +673,7 @@ var BUILTIN_ATR_INDICATOR = {
607
673
  defaultInputs: { length: 14, color: "#eab308", width: 2 },
608
674
  draw: (ctx, renderContext, inputs) => {
609
675
  const length = clampIndicatorLength(inputs.length, 14);
610
- const values = computeAtrSeries(renderContext.data, length);
676
+ const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
611
677
  drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2);
612
678
  }
613
679
  };
@@ -619,7 +685,7 @@ var BUILTIN_RSI_INDICATOR = {
619
685
  defaultInputs: { length: 14, color: "#3b82f6", width: 2 },
620
686
  draw: (ctx, renderContext, inputs) => {
621
687
  const length = clampIndicatorLength(inputs.length, 14);
622
- const values = computeRsiSeries(renderContext.data, length);
688
+ const values = withCachedSeries(`rsi|${length}`, renderContext.data, () => computeRsiSeries(renderContext.data, length));
623
689
  drawSeparateSeries(
624
690
  ctx,
625
691
  renderContext,
@@ -653,6 +719,18 @@ function createChart(element, options = {}) {
653
719
  ...options.axis ?? {},
654
720
  ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {}
655
721
  },
722
+ xAxis: {
723
+ ...DEFAULT_AXIS_OPTIONS,
724
+ ...options.axis ?? {},
725
+ ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
726
+ ...options.xAxis ?? {}
727
+ },
728
+ yAxis: {
729
+ ...DEFAULT_AXIS_OPTIONS,
730
+ ...options.axis ?? {},
731
+ ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
732
+ ...options.yAxis ?? {}
733
+ },
656
734
  crosshair: {
657
735
  ...DEFAULT_CROSSHAIR_OPTIONS,
658
736
  ...options.crosshair ?? {}
@@ -708,6 +786,9 @@ function createChart(element, options = {}) {
708
786
  visible: indicator.visible ?? true,
709
787
  pane: indicator.pane ?? plugin?.pane ?? "overlay",
710
788
  ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
789
+ zIndex: Math.round(Number(indicator.zIndex) || 0),
790
+ excludeFromAutoscale: indicator.excludeFromAutoscale ?? true,
791
+ overlayScaleWeight: Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0.25)),
711
792
  inputs: {
712
793
  ...defaults,
713
794
  ...indicator.inputs ?? {}
@@ -863,9 +944,63 @@ function createChart(element, options = {}) {
863
944
  }
864
945
  return { min: nextMin, max: nextMax };
865
946
  };
947
+ const getConfiguredPriceDecimals = () => clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
948
+ const getConfiguredTickSize = () => {
949
+ const step = Number(mergedOptions.tickSize);
950
+ return Number.isFinite(step) && step > 0 ? step : 0;
951
+ };
952
+ const getTickSizeDecimals = () => {
953
+ const tickSize = getConfiguredTickSize();
954
+ if (tickSize <= 0) {
955
+ return 0;
956
+ }
957
+ let scaled = tickSize;
958
+ let decimals = 0;
959
+ while (decimals < 8 && Math.abs(Math.round(scaled) - scaled) > 1e-10) {
960
+ scaled *= 10;
961
+ decimals += 1;
962
+ }
963
+ return decimals;
964
+ };
965
+ const getDisplayPriceDecimals = () => {
966
+ const configured = getConfiguredPriceDecimals();
967
+ const tickDecimals = getTickSizeDecimals();
968
+ return Math.max(configured, tickDecimals);
969
+ };
970
+ const quantizeToTickSize = (price) => {
971
+ const tickSize = getConfiguredTickSize();
972
+ if (tickSize <= 0 || !Number.isFinite(price)) {
973
+ return price;
974
+ }
975
+ return Math.round(price / tickSize) * tickSize;
976
+ };
866
977
  const formatPrice = (price) => {
867
- const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
868
- return price.toFixed(decimals);
978
+ const rounded = quantizeToTickSize(price);
979
+ const decimals = getDisplayPriceDecimals();
980
+ return rounded.toFixed(decimals);
981
+ };
982
+ const roundToPricePrecision = (price) => {
983
+ const rounded = quantizeToTickSize(price);
984
+ const decimals = getDisplayPriceDecimals();
985
+ return Number(rounded.toFixed(decimals));
986
+ };
987
+ const getResolvedCandleColorEpsilon = () => {
988
+ const configured = mergedOptions.candleColorEpsilon;
989
+ if (configured >= 0) {
990
+ return configured;
991
+ }
992
+ const tickSize = getConfiguredTickSize();
993
+ if (tickSize > 0) {
994
+ return tickSize / 2;
995
+ }
996
+ const decimals = getDisplayPriceDecimals();
997
+ return decimals > 0 ? 0.5 / 10 ** decimals : 0;
998
+ };
999
+ const getDirectionFromDelta = (delta) => {
1000
+ const epsilon = Math.max(0, getResolvedCandleColorEpsilon());
1001
+ if (delta > epsilon) return 1;
1002
+ if (delta < -epsilon) return -1;
1003
+ return 0;
869
1004
  };
870
1005
  const getStabilizedPriceTemplate = () => {
871
1006
  const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
@@ -893,14 +1028,33 @@ function createChart(element, options = {}) {
893
1028
  return Math.max(measured, templateWidth);
894
1029
  };
895
1030
  const parseData = (nextData) => {
896
- return nextData.map((point) => ({
897
- time: new Date(point.t),
898
- o: point.o,
899
- h: point.h,
900
- l: point.l,
901
- c: point.c,
902
- ...point.v === void 0 ? {} : { v: point.v }
903
- })).filter((point) => Number.isFinite(point.time.getTime())).sort((a, b) => a.time.getTime() - b.time.getTime());
1031
+ const dedupedByTime = /* @__PURE__ */ new Map();
1032
+ for (const point of nextData) {
1033
+ const time = new Date(point.t);
1034
+ const timeMs = time.getTime();
1035
+ if (!Number.isFinite(timeMs)) {
1036
+ continue;
1037
+ }
1038
+ const open = Number(point.o);
1039
+ const close = Number(point.c);
1040
+ const highInput = Number(point.h);
1041
+ const lowInput = Number(point.l);
1042
+ if (!Number.isFinite(open) || !Number.isFinite(close) || !Number.isFinite(highInput) || !Number.isFinite(lowInput)) {
1043
+ continue;
1044
+ }
1045
+ const normalizedHigh = Math.max(highInput, open, close);
1046
+ const normalizedLow = Math.min(lowInput, open, close);
1047
+ const volumeValue = point.v === void 0 ? void 0 : Number(point.v);
1048
+ dedupedByTime.set(timeMs, {
1049
+ time,
1050
+ o: open,
1051
+ h: normalizedHigh,
1052
+ l: normalizedLow,
1053
+ c: close,
1054
+ ...volumeValue === void 0 || !Number.isFinite(volumeValue) ? {} : { v: volumeValue }
1055
+ });
1056
+ }
1057
+ return Array.from(dedupedByTime.values()).sort((a, b) => a.time.getTime() - b.time.getTime());
904
1058
  };
905
1059
  const getTimeStepMs = () => {
906
1060
  if (data.length < 2) {
@@ -977,6 +1131,26 @@ function createChart(element, options = {}) {
977
1131
  const upperDelta = Math.abs(upperPoint.time.getTime() - timeMs);
978
1132
  return lowerDelta <= upperDelta ? lower : upper;
979
1133
  };
1134
+ const getCandleDirectionByIndex = (index) => {
1135
+ const point = data[index];
1136
+ if (!point) {
1137
+ return "up";
1138
+ }
1139
+ const prevPoint = index > 0 ? data[index - 1] : void 0;
1140
+ const mode = mergedOptions.candleColorMode;
1141
+ const baseForMode = mode === "prevClose" && prevPoint ? prevPoint.c : point.o;
1142
+ let direction = getDirectionFromDelta(point.c - baseForMode);
1143
+ if (direction === 0 && mode === "prevClose") {
1144
+ direction = getDirectionFromDelta(point.c - point.o);
1145
+ }
1146
+ if (direction === 0 && prevPoint) {
1147
+ direction = getDirectionFromDelta(point.c - prevPoint.c);
1148
+ }
1149
+ if (direction === 0) {
1150
+ return point.c >= point.o ? "up" : "down";
1151
+ }
1152
+ return direction > 0 ? "up" : "down";
1153
+ };
980
1154
  const formatHoverTimeLabel = (time, mode) => {
981
1155
  if (mode === "time") {
982
1156
  return time.toLocaleTimeString(void 0, {
@@ -1351,6 +1525,10 @@ function createChart(element, options = {}) {
1351
1525
  canvas.height = Math.floor(height * pixelRatio);
1352
1526
  ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
1353
1527
  const axis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
1528
+ const xAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
1529
+ const yAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
1530
+ const xAxisFontSize = Math.max(8, xAxis.fontSize);
1531
+ const yAxisFontSize = Math.max(8, yAxis.fontSize);
1354
1532
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
1355
1533
  ctx.fillStyle = mergedOptions.backgroundColor;
1356
1534
  ctx.fillRect(0, 0, width, height);
@@ -1365,6 +1543,9 @@ function createChart(element, options = {}) {
1365
1543
  const separatePaneSpacing = 6;
1366
1544
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1367
1545
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
1546
+ ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
1547
+ const overlayIndicatorsForScale = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1548
+ (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
1368
1549
  );
1369
1550
  const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
1370
1551
  const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
@@ -1416,8 +1597,47 @@ function createChart(element, options = {}) {
1416
1597
  }
1417
1598
  }
1418
1599
  }
1419
- const minPrice = Math.min(...priceSource.map((point) => point.l));
1420
- const maxPrice = Math.max(...priceSource.map((point) => point.h));
1600
+ let minPrice = Math.min(...priceSource.map((point) => point.l));
1601
+ let maxPrice = Math.max(...priceSource.map((point) => point.h));
1602
+ if (overlayIndicatorsForScale.length > 0) {
1603
+ for (const { indicator } of overlayIndicatorsForScale) {
1604
+ if (indicator.excludeFromAutoscale) {
1605
+ continue;
1606
+ }
1607
+ const type = indicator.type;
1608
+ const inputs = indicator.inputs;
1609
+ const source = inputs.source ?? "close";
1610
+ const length = clampIndicatorLength(inputs.length ?? 14, 14);
1611
+ let series = null;
1612
+ if (type === "sma") series = withCachedSeries(`sma|${length}|${source}`, data, () => computeSmaSeries(data, length, source));
1613
+ if (type === "ema") series = withCachedSeries(`ema|${length}|${source}`, data, () => computeEmaSeries(data, length, source));
1614
+ if (type === "wma") series = withCachedSeries(`wma|${length}|${source}`, data, () => computeWmaSeries(data, length, source));
1615
+ if (type === "vwma") series = withCachedSeries(`vwma|${length}|${source}`, data, () => computeVwmaSeries(data, length, source));
1616
+ if (type === "rma") series = withCachedSeries(`rma|${length}|${source}`, data, () => computeRmaSeries(data, length, source));
1617
+ if (type === "hma") series = withCachedSeries(`hma|${length}|${source}`, data, () => computeHmaSeries(data, length, source));
1618
+ if (!series) {
1619
+ continue;
1620
+ }
1621
+ const visibleValues = [];
1622
+ for (let idx = startIndex; idx <= endIndex; idx += 1) {
1623
+ const value = series[idx];
1624
+ if (Number.isFinite(value ?? Number.NaN)) {
1625
+ visibleValues.push(value);
1626
+ }
1627
+ }
1628
+ if (visibleValues.length === 0) {
1629
+ continue;
1630
+ }
1631
+ const seriesMin = Math.min(...visibleValues);
1632
+ const seriesMax = Math.max(...visibleValues);
1633
+ const weight = Math.min(1, Math.max(0, indicator.overlayScaleWeight));
1634
+ const currentMid = (minPrice + maxPrice) / 2;
1635
+ const weightedMin = currentMid + (seriesMin - currentMid) * weight;
1636
+ const weightedMax = currentMid + (seriesMax - currentMid) * weight;
1637
+ minPrice = Math.min(minPrice, weightedMin);
1638
+ maxPrice = Math.max(maxPrice, weightedMax);
1639
+ }
1640
+ }
1421
1641
  const priceRange = maxPrice - minPrice || 1;
1422
1642
  const autoMin = minPrice - priceRange * 0.08;
1423
1643
  const autoMax = maxPrice + priceRange * 0.08;
@@ -1562,8 +1782,8 @@ function createChart(element, options = {}) {
1562
1782
  const closeY = yFromPrice(point.c);
1563
1783
  const highY = yFromPrice(point.h);
1564
1784
  const lowY = yFromPrice(point.l);
1565
- const isUp = point.c >= point.o;
1566
- const candleColor = isUp ? mergedOptions.upColor : mergedOptions.downColor;
1785
+ const direction = getCandleDirectionByIndex(index);
1786
+ const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
1567
1787
  ctx.strokeStyle = candleColor;
1568
1788
  ctx.lineWidth = candleWickWidth;
1569
1789
  ctx.beginPath();
@@ -1577,7 +1797,7 @@ function createChart(element, options = {}) {
1577
1797
  }
1578
1798
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1579
1799
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
1580
- );
1800
+ ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
1581
1801
  if (activeOverlayIndicators.length > 0) {
1582
1802
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
1583
1803
  activeOverlayIndicators.forEach(({ indicator, plugin }) => {
@@ -1597,6 +1817,7 @@ function createChart(element, options = {}) {
1597
1817
  chartHeight,
1598
1818
  xFromIndex,
1599
1819
  yFromPrice,
1820
+ getCandleDirectionByIndex,
1600
1821
  candleSpacing,
1601
1822
  upColor: mergedOptions.upColor,
1602
1823
  downColor: mergedOptions.downColor
@@ -1656,6 +1877,7 @@ function createChart(element, options = {}) {
1656
1877
  chartHeight: paneHeight,
1657
1878
  xFromIndex,
1658
1879
  yFromPrice: null,
1880
+ getCandleDirectionByIndex,
1659
1881
  candleSpacing,
1660
1882
  upColor: mergedOptions.upColor,
1661
1883
  downColor: mergedOptions.downColor
@@ -1685,7 +1907,10 @@ function createChart(element, options = {}) {
1685
1907
  const ratio = tick / yTicks;
1686
1908
  const price = yMin + yRange * ratio;
1687
1909
  const y = yFromPrice(price);
1688
- drawText(formatPrice(price), chartRight + 6, y, "left", "middle", axis.textColor);
1910
+ const prevFont = ctx.font;
1911
+ ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
1912
+ drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
1913
+ ctx.font = prevFont;
1689
1914
  }
1690
1915
  const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1691
1916
  const lastPoint = data[data.length - 1];
@@ -1693,7 +1918,8 @@ function createChart(element, options = {}) {
1693
1918
  const tickerPrice = lastPoint.c;
1694
1919
  const tickerY = yFromPrice(tickerPrice);
1695
1920
  const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
1696
- const tickerColor = ticker.color ?? (lastPoint.c >= lastPoint.o ? mergedOptions.upColor : mergedOptions.downColor);
1921
+ const lastDirection = getCandleDirectionByIndex(data.length - 1);
1922
+ const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
1697
1923
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
1698
1924
  const tickerStyle = ticker.style ?? "solid";
1699
1925
  ctx.save();
@@ -1741,7 +1967,10 @@ function createChart(element, options = {}) {
1741
1967
  month: "short",
1742
1968
  day: "numeric"
1743
1969
  });
1744
- drawText(timeLabel, x, fullChartBottom + 8, "center", "top", axis.textColor);
1970
+ const prevFont = ctx.font;
1971
+ ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
1972
+ drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
1973
+ ctx.font = prevFont;
1745
1974
  }
1746
1975
  if (crosshair.visible && crosshairPoint) {
1747
1976
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
@@ -2061,7 +2290,7 @@ function createChart(element, options = {}) {
2061
2290
  x,
2062
2291
  y,
2063
2292
  region,
2064
- ...region === "plot" ? { price: Number(priceFromCanvasY(y).toFixed(mergedOptions.priceDecimals)) } : {},
2293
+ ...region === "plot" ? { price: roundToPricePrecision(priceFromCanvasY(y)) } : {},
2065
2294
  ...index === null ? {} : { index },
2066
2295
  ...hoverTime ? { time: hoverTime.toISOString() } : {},
2067
2296
  ...point ? { point } : {}
@@ -2107,7 +2336,7 @@ function createChart(element, options = {}) {
2107
2336
  if (orderRegion) {
2108
2337
  if (orderRegion.draggable) {
2109
2338
  activePointerId = event.pointerId;
2110
- const startPrice = Number(orderRegion.line.price.toFixed(2));
2339
+ const startPrice = roundToPricePrecision(orderRegion.line.price);
2111
2340
  actionDragState = {
2112
2341
  orderId: orderRegion.orderId,
2113
2342
  action: orderRegion.action,
@@ -2166,7 +2395,7 @@ function createChart(element, options = {}) {
2166
2395
  if (activePointerId !== null && event.pointerId !== activePointerId) {
2167
2396
  return;
2168
2397
  }
2169
- const nextPrice = Number(priceFromCanvasY(point.y).toFixed(2));
2398
+ const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
2170
2399
  if (nextPrice !== orderDragState.lastPrice) {
2171
2400
  orderDragState.lastPrice = nextPrice;
2172
2401
  orderLines = orderLines.map((line) => {
@@ -2196,7 +2425,7 @@ function createChart(element, options = {}) {
2196
2425
  if (activePointerId !== null && event.pointerId !== activePointerId) {
2197
2426
  return;
2198
2427
  }
2199
- const nextPrice = Number(priceFromCanvasY(point.y).toFixed(2));
2428
+ const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
2200
2429
  if (nextPrice !== actionDragState.lastPrice) {
2201
2430
  actionDragState.lastPrice = nextPrice;
2202
2431
  actionDragState.moved = true;
@@ -2313,7 +2542,7 @@ function createChart(element, options = {}) {
2313
2542
  canvas.style.cursor = "default";
2314
2543
  if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
2315
2544
  if (!pointerDownInfo.moved) {
2316
- const clickPrice = pointerDownInfo.region === "plot" ? Number(priceFromCanvasY(pointerDownInfo.y).toFixed(2)) : void 0;
2545
+ const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
2317
2546
  chartClickHandler?.({
2318
2547
  x: pointerDownInfo.x,
2319
2548
  y: pointerDownInfo.y,
@@ -2373,7 +2602,7 @@ function createChart(element, options = {}) {
2373
2602
  }
2374
2603
  orderActionHandler?.({
2375
2604
  action: "createLimit",
2376
- price: Number(priceFromCanvasY(point.y).toFixed(2))
2605
+ price: roundToPricePrecision(priceFromCanvasY(point.y))
2377
2606
  });
2378
2607
  return;
2379
2608
  }
@@ -2512,6 +2741,27 @@ function createChart(element, options = {}) {
2512
2741
  indicators = indicators.filter((indicator) => indicator.type !== type);
2513
2742
  draw();
2514
2743
  };
2744
+ const listBuiltInIndicators = () => {
2745
+ return BUILTIN_INDICATORS.map((indicator) => ({
2746
+ id: indicator.id,
2747
+ name: indicator.name,
2748
+ pane: indicator.pane ?? "overlay",
2749
+ ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio }
2750
+ }));
2751
+ };
2752
+ const getIndicators = () => {
2753
+ return indicators.map((indicator) => ({
2754
+ id: indicator.id,
2755
+ type: indicator.type,
2756
+ visible: indicator.visible,
2757
+ pane: indicator.pane,
2758
+ ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
2759
+ zIndex: indicator.zIndex,
2760
+ excludeFromAutoscale: indicator.excludeFromAutoscale,
2761
+ overlayScaleWeight: indicator.overlayScaleWeight,
2762
+ inputs: { ...indicator.inputs }
2763
+ }));
2764
+ };
2515
2765
  const setIndicators = (nextIndicators) => {
2516
2766
  indicators = nextIndicators.map((indicator) => normalizeIndicatorState(indicator));
2517
2767
  draw();
@@ -2541,6 +2791,9 @@ function createChart(element, options = {}) {
2541
2791
  visible: patch.visible ?? indicator.visible,
2542
2792
  pane: patch.pane ?? indicator.pane ?? plugin?.pane ?? "overlay",
2543
2793
  ...patch.paneHeightRatio !== void 0 || indicator.paneHeightRatio !== void 0 ? { paneHeightRatio: patch.paneHeightRatio ?? indicator.paneHeightRatio } : {},
2794
+ zIndex: patch.zIndex === void 0 ? indicator.zIndex : Math.round(Number(patch.zIndex) || 0),
2795
+ excludeFromAutoscale: patch.excludeFromAutoscale ?? indicator.excludeFromAutoscale,
2796
+ overlayScaleWeight: patch.overlayScaleWeight === void 0 ? indicator.overlayScaleWeight : Math.min(1, Math.max(0, Number(patch.overlayScaleWeight) || 0)),
2544
2797
  type: patch.type ?? indicator.type,
2545
2798
  inputs: {
2546
2799
  ...indicator.inputs,
@@ -2590,6 +2843,8 @@ function createChart(element, options = {}) {
2590
2843
  setDoubleClickAction,
2591
2844
  registerIndicator,
2592
2845
  unregisterIndicator,
2846
+ listBuiltInIndicators,
2847
+ getIndicators,
2593
2848
  addIndicator,
2594
2849
  updateIndicator,
2595
2850
  removeIndicator,