hyperprop-charting-library 0.1.34 → 0.1.35

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.
@@ -852,6 +852,41 @@ function createChart(element, options = {}) {
852
852
  let crosshairPoint = null;
853
853
  let doubleClickEnabled = mergedOptions.doubleClickEnabled;
854
854
  let doubleClickAction = mergedOptions.doubleClickAction;
855
+ let smoothedTickerPrice = null;
856
+ let tickerPriceTarget = null;
857
+ let smoothingRafId = null;
858
+ const tickerSmoothingLoop = () => {
859
+ smoothingRafId = null;
860
+ if (smoothedTickerPrice === null || tickerPriceTarget === null) return;
861
+ const diff = tickerPriceTarget - smoothedTickerPrice;
862
+ if (Math.abs(diff) < 1e-9) {
863
+ smoothedTickerPrice = tickerPriceTarget;
864
+ draw();
865
+ return;
866
+ }
867
+ const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
868
+ const speed = clamp(tickerOpts.smoothingSpeed ?? 8, 1, 60);
869
+ const dt = 1 / 60;
870
+ const lerp = 1 - Math.exp(-speed * dt);
871
+ smoothedTickerPrice += diff * lerp;
872
+ draw();
873
+ smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
874
+ };
875
+ const pushSmoothedPrice = (target) => {
876
+ const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
877
+ if (!tickerOpts.smoothing) {
878
+ smoothedTickerPrice = null;
879
+ tickerPriceTarget = null;
880
+ return;
881
+ }
882
+ tickerPriceTarget = target;
883
+ if (smoothedTickerPrice === null) {
884
+ smoothedTickerPrice = target;
885
+ }
886
+ if (smoothingRafId === null) {
887
+ smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
888
+ }
889
+ };
855
890
  const canvas = document.createElement("canvas");
856
891
  const ctx = canvas.getContext("2d");
857
892
  if (!ctx) {
@@ -1812,28 +1847,37 @@ function createChart(element, options = {}) {
1812
1847
  }
1813
1848
  ctx.restore();
1814
1849
  }
1850
+ const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1851
+ const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
1852
+ const lastDataIndex = data.length - 1;
1815
1853
  for (let index = startIndex; index <= endIndex; index += 1) {
1816
1854
  const point = data[index];
1817
1855
  if (!point) {
1818
1856
  continue;
1819
1857
  }
1858
+ const isLastCandle = useSmoothedCandle && index === lastDataIndex;
1859
+ const displayClose = isLastCandle ? smoothedTickerPrice : point.c;
1860
+ const displayHigh = isLastCandle ? Math.max(point.h, smoothedTickerPrice) : point.h;
1861
+ const displayLow = isLastCandle ? Math.min(point.l, smoothedTickerPrice) : point.l;
1820
1862
  const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
1821
1863
  const openY = yFromPrice(point.o);
1822
- const closeY = yFromPrice(point.c);
1823
- const highY = yFromPrice(point.h);
1824
- const lowY = yFromPrice(point.l);
1825
- const direction = getCandleDirectionByIndex(index);
1864
+ const closeY = yFromPrice(displayClose);
1865
+ const highY = yFromPrice(displayHigh);
1866
+ const lowY = yFromPrice(displayLow);
1867
+ const direction = isLastCandle ? displayClose >= point.o ? "up" : "down" : getCandleDirectionByIndex(index);
1826
1868
  const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
1869
+ const roundedCenterX = Math.round(centerX);
1827
1870
  ctx.strokeStyle = candleColor;
1828
1871
  ctx.lineWidth = candleWickWidth;
1829
1872
  ctx.beginPath();
1830
- ctx.moveTo(crisp(centerX), crisp(highY));
1831
- ctx.lineTo(crisp(centerX), crisp(lowY));
1873
+ ctx.moveTo(roundedCenterX + 0.5, crisp(highY));
1874
+ ctx.lineTo(roundedCenterX + 0.5, crisp(lowY));
1832
1875
  ctx.stroke();
1876
+ const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
1833
1877
  const bodyTop = Math.min(openY, closeY);
1834
1878
  const bodyHeight = Math.max(1, Math.abs(closeY - openY));
1835
1879
  ctx.fillStyle = candleColor;
1836
- ctx.fillRect(Math.round(centerX - bodyWidth / 2), Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
1880
+ ctx.fillRect(bodyLeft, Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
1837
1881
  }
1838
1882
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1839
1883
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
@@ -1967,10 +2011,10 @@ function createChart(element, options = {}) {
1967
2011
  const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1968
2012
  const lastPoint = data[data.length - 1];
1969
2013
  if ((ticker.visible ?? true) && lastPoint) {
1970
- const tickerPrice = lastPoint.c;
2014
+ const tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
1971
2015
  const tickerY = yFromPrice(tickerPrice);
1972
2016
  const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
1973
- const lastDirection = getCandleDirectionByIndex(data.length - 1);
2017
+ const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
1974
2018
  const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
1975
2019
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
1976
2020
  const tickerStyle = ticker.style ?? "solid";
@@ -2802,6 +2846,10 @@ function createChart(element, options = {}) {
2802
2846
  fitXViewport();
2803
2847
  }
2804
2848
  }
2849
+ const lastClose = data.length > 0 ? data[data.length - 1].c : null;
2850
+ if (lastClose !== null) {
2851
+ pushSmoothedPrice(lastClose);
2852
+ }
2805
2853
  draw();
2806
2854
  };
2807
2855
  const setPriceLines = (lines) => {
@@ -2952,6 +3000,10 @@ function createChart(element, options = {}) {
2952
3000
  draw();
2953
3001
  };
2954
3002
  const destroy = () => {
3003
+ if (smoothingRafId !== null) {
3004
+ cancelAnimationFrame(smoothingRafId);
3005
+ smoothingRafId = null;
3006
+ }
2955
3007
  canvas.removeEventListener("pointerdown", onPointerDown);
2956
3008
  canvas.removeEventListener("pointermove", onPointerMove);
2957
3009
  canvas.removeEventListener("pointerup", endPointerDrag);
@@ -259,6 +259,8 @@ interface TickerLineOptions {
259
259
  labelBackgroundColor?: string;
260
260
  labelTextColor?: string;
261
261
  labelBorderRadius?: number;
262
+ smoothing?: boolean;
263
+ smoothingSpeed?: number;
262
264
  }
263
265
  interface ChartInstance {
264
266
  setData: (data: OhlcDataPoint[]) => void;
@@ -828,6 +828,41 @@ function createChart(element, options = {}) {
828
828
  let crosshairPoint = null;
829
829
  let doubleClickEnabled = mergedOptions.doubleClickEnabled;
830
830
  let doubleClickAction = mergedOptions.doubleClickAction;
831
+ let smoothedTickerPrice = null;
832
+ let tickerPriceTarget = null;
833
+ let smoothingRafId = null;
834
+ const tickerSmoothingLoop = () => {
835
+ smoothingRafId = null;
836
+ if (smoothedTickerPrice === null || tickerPriceTarget === null) return;
837
+ const diff = tickerPriceTarget - smoothedTickerPrice;
838
+ if (Math.abs(diff) < 1e-9) {
839
+ smoothedTickerPrice = tickerPriceTarget;
840
+ draw();
841
+ return;
842
+ }
843
+ const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
844
+ const speed = clamp(tickerOpts.smoothingSpeed ?? 8, 1, 60);
845
+ const dt = 1 / 60;
846
+ const lerp = 1 - Math.exp(-speed * dt);
847
+ smoothedTickerPrice += diff * lerp;
848
+ draw();
849
+ smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
850
+ };
851
+ const pushSmoothedPrice = (target) => {
852
+ const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
853
+ if (!tickerOpts.smoothing) {
854
+ smoothedTickerPrice = null;
855
+ tickerPriceTarget = null;
856
+ return;
857
+ }
858
+ tickerPriceTarget = target;
859
+ if (smoothedTickerPrice === null) {
860
+ smoothedTickerPrice = target;
861
+ }
862
+ if (smoothingRafId === null) {
863
+ smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
864
+ }
865
+ };
831
866
  const canvas = document.createElement("canvas");
832
867
  const ctx = canvas.getContext("2d");
833
868
  if (!ctx) {
@@ -1788,28 +1823,37 @@ function createChart(element, options = {}) {
1788
1823
  }
1789
1824
  ctx.restore();
1790
1825
  }
1826
+ const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1827
+ const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
1828
+ const lastDataIndex = data.length - 1;
1791
1829
  for (let index = startIndex; index <= endIndex; index += 1) {
1792
1830
  const point = data[index];
1793
1831
  if (!point) {
1794
1832
  continue;
1795
1833
  }
1834
+ const isLastCandle = useSmoothedCandle && index === lastDataIndex;
1835
+ const displayClose = isLastCandle ? smoothedTickerPrice : point.c;
1836
+ const displayHigh = isLastCandle ? Math.max(point.h, smoothedTickerPrice) : point.h;
1837
+ const displayLow = isLastCandle ? Math.min(point.l, smoothedTickerPrice) : point.l;
1796
1838
  const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
1797
1839
  const openY = yFromPrice(point.o);
1798
- const closeY = yFromPrice(point.c);
1799
- const highY = yFromPrice(point.h);
1800
- const lowY = yFromPrice(point.l);
1801
- const direction = getCandleDirectionByIndex(index);
1840
+ const closeY = yFromPrice(displayClose);
1841
+ const highY = yFromPrice(displayHigh);
1842
+ const lowY = yFromPrice(displayLow);
1843
+ const direction = isLastCandle ? displayClose >= point.o ? "up" : "down" : getCandleDirectionByIndex(index);
1802
1844
  const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
1845
+ const roundedCenterX = Math.round(centerX);
1803
1846
  ctx.strokeStyle = candleColor;
1804
1847
  ctx.lineWidth = candleWickWidth;
1805
1848
  ctx.beginPath();
1806
- ctx.moveTo(crisp(centerX), crisp(highY));
1807
- ctx.lineTo(crisp(centerX), crisp(lowY));
1849
+ ctx.moveTo(roundedCenterX + 0.5, crisp(highY));
1850
+ ctx.lineTo(roundedCenterX + 0.5, crisp(lowY));
1808
1851
  ctx.stroke();
1852
+ const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
1809
1853
  const bodyTop = Math.min(openY, closeY);
1810
1854
  const bodyHeight = Math.max(1, Math.abs(closeY - openY));
1811
1855
  ctx.fillStyle = candleColor;
1812
- ctx.fillRect(Math.round(centerX - bodyWidth / 2), Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
1856
+ ctx.fillRect(bodyLeft, Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
1813
1857
  }
1814
1858
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1815
1859
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
@@ -1943,10 +1987,10 @@ function createChart(element, options = {}) {
1943
1987
  const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1944
1988
  const lastPoint = data[data.length - 1];
1945
1989
  if ((ticker.visible ?? true) && lastPoint) {
1946
- const tickerPrice = lastPoint.c;
1990
+ const tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
1947
1991
  const tickerY = yFromPrice(tickerPrice);
1948
1992
  const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
1949
- const lastDirection = getCandleDirectionByIndex(data.length - 1);
1993
+ const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
1950
1994
  const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
1951
1995
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
1952
1996
  const tickerStyle = ticker.style ?? "solid";
@@ -2778,6 +2822,10 @@ function createChart(element, options = {}) {
2778
2822
  fitXViewport();
2779
2823
  }
2780
2824
  }
2825
+ const lastClose = data.length > 0 ? data[data.length - 1].c : null;
2826
+ if (lastClose !== null) {
2827
+ pushSmoothedPrice(lastClose);
2828
+ }
2781
2829
  draw();
2782
2830
  };
2783
2831
  const setPriceLines = (lines) => {
@@ -2928,6 +2976,10 @@ function createChart(element, options = {}) {
2928
2976
  draw();
2929
2977
  };
2930
2978
  const destroy = () => {
2979
+ if (smoothingRafId !== null) {
2980
+ cancelAnimationFrame(smoothingRafId);
2981
+ smoothingRafId = null;
2982
+ }
2931
2983
  canvas.removeEventListener("pointerdown", onPointerDown);
2932
2984
  canvas.removeEventListener("pointermove", onPointerMove);
2933
2985
  canvas.removeEventListener("pointerup", endPointerDrag);
package/dist/index.cjs CHANGED
@@ -852,6 +852,41 @@ function createChart(element, options = {}) {
852
852
  let crosshairPoint = null;
853
853
  let doubleClickEnabled = mergedOptions.doubleClickEnabled;
854
854
  let doubleClickAction = mergedOptions.doubleClickAction;
855
+ let smoothedTickerPrice = null;
856
+ let tickerPriceTarget = null;
857
+ let smoothingRafId = null;
858
+ const tickerSmoothingLoop = () => {
859
+ smoothingRafId = null;
860
+ if (smoothedTickerPrice === null || tickerPriceTarget === null) return;
861
+ const diff = tickerPriceTarget - smoothedTickerPrice;
862
+ if (Math.abs(diff) < 1e-9) {
863
+ smoothedTickerPrice = tickerPriceTarget;
864
+ draw();
865
+ return;
866
+ }
867
+ const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
868
+ const speed = clamp(tickerOpts.smoothingSpeed ?? 8, 1, 60);
869
+ const dt = 1 / 60;
870
+ const lerp = 1 - Math.exp(-speed * dt);
871
+ smoothedTickerPrice += diff * lerp;
872
+ draw();
873
+ smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
874
+ };
875
+ const pushSmoothedPrice = (target) => {
876
+ const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
877
+ if (!tickerOpts.smoothing) {
878
+ smoothedTickerPrice = null;
879
+ tickerPriceTarget = null;
880
+ return;
881
+ }
882
+ tickerPriceTarget = target;
883
+ if (smoothedTickerPrice === null) {
884
+ smoothedTickerPrice = target;
885
+ }
886
+ if (smoothingRafId === null) {
887
+ smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
888
+ }
889
+ };
855
890
  const canvas = document.createElement("canvas");
856
891
  const ctx = canvas.getContext("2d");
857
892
  if (!ctx) {
@@ -1812,28 +1847,37 @@ function createChart(element, options = {}) {
1812
1847
  }
1813
1848
  ctx.restore();
1814
1849
  }
1850
+ const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1851
+ const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
1852
+ const lastDataIndex = data.length - 1;
1815
1853
  for (let index = startIndex; index <= endIndex; index += 1) {
1816
1854
  const point = data[index];
1817
1855
  if (!point) {
1818
1856
  continue;
1819
1857
  }
1858
+ const isLastCandle = useSmoothedCandle && index === lastDataIndex;
1859
+ const displayClose = isLastCandle ? smoothedTickerPrice : point.c;
1860
+ const displayHigh = isLastCandle ? Math.max(point.h, smoothedTickerPrice) : point.h;
1861
+ const displayLow = isLastCandle ? Math.min(point.l, smoothedTickerPrice) : point.l;
1820
1862
  const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
1821
1863
  const openY = yFromPrice(point.o);
1822
- const closeY = yFromPrice(point.c);
1823
- const highY = yFromPrice(point.h);
1824
- const lowY = yFromPrice(point.l);
1825
- const direction = getCandleDirectionByIndex(index);
1864
+ const closeY = yFromPrice(displayClose);
1865
+ const highY = yFromPrice(displayHigh);
1866
+ const lowY = yFromPrice(displayLow);
1867
+ const direction = isLastCandle ? displayClose >= point.o ? "up" : "down" : getCandleDirectionByIndex(index);
1826
1868
  const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
1869
+ const roundedCenterX = Math.round(centerX);
1827
1870
  ctx.strokeStyle = candleColor;
1828
1871
  ctx.lineWidth = candleWickWidth;
1829
1872
  ctx.beginPath();
1830
- ctx.moveTo(crisp(centerX), crisp(highY));
1831
- ctx.lineTo(crisp(centerX), crisp(lowY));
1873
+ ctx.moveTo(roundedCenterX + 0.5, crisp(highY));
1874
+ ctx.lineTo(roundedCenterX + 0.5, crisp(lowY));
1832
1875
  ctx.stroke();
1876
+ const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
1833
1877
  const bodyTop = Math.min(openY, closeY);
1834
1878
  const bodyHeight = Math.max(1, Math.abs(closeY - openY));
1835
1879
  ctx.fillStyle = candleColor;
1836
- ctx.fillRect(Math.round(centerX - bodyWidth / 2), Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
1880
+ ctx.fillRect(bodyLeft, Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
1837
1881
  }
1838
1882
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1839
1883
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
@@ -1967,10 +2011,10 @@ function createChart(element, options = {}) {
1967
2011
  const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1968
2012
  const lastPoint = data[data.length - 1];
1969
2013
  if ((ticker.visible ?? true) && lastPoint) {
1970
- const tickerPrice = lastPoint.c;
2014
+ const tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
1971
2015
  const tickerY = yFromPrice(tickerPrice);
1972
2016
  const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
1973
- const lastDirection = getCandleDirectionByIndex(data.length - 1);
2017
+ const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
1974
2018
  const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
1975
2019
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
1976
2020
  const tickerStyle = ticker.style ?? "solid";
@@ -2802,6 +2846,10 @@ function createChart(element, options = {}) {
2802
2846
  fitXViewport();
2803
2847
  }
2804
2848
  }
2849
+ const lastClose = data.length > 0 ? data[data.length - 1].c : null;
2850
+ if (lastClose !== null) {
2851
+ pushSmoothedPrice(lastClose);
2852
+ }
2805
2853
  draw();
2806
2854
  };
2807
2855
  const setPriceLines = (lines) => {
@@ -2952,6 +3000,10 @@ function createChart(element, options = {}) {
2952
3000
  draw();
2953
3001
  };
2954
3002
  const destroy = () => {
3003
+ if (smoothingRafId !== null) {
3004
+ cancelAnimationFrame(smoothingRafId);
3005
+ smoothingRafId = null;
3006
+ }
2955
3007
  canvas.removeEventListener("pointerdown", onPointerDown);
2956
3008
  canvas.removeEventListener("pointermove", onPointerMove);
2957
3009
  canvas.removeEventListener("pointerup", endPointerDrag);
package/dist/index.d.cts CHANGED
@@ -259,6 +259,8 @@ interface TickerLineOptions {
259
259
  labelBackgroundColor?: string;
260
260
  labelTextColor?: string;
261
261
  labelBorderRadius?: number;
262
+ smoothing?: boolean;
263
+ smoothingSpeed?: number;
262
264
  }
263
265
  interface ChartInstance {
264
266
  setData: (data: OhlcDataPoint[]) => void;
package/dist/index.d.ts CHANGED
@@ -259,6 +259,8 @@ interface TickerLineOptions {
259
259
  labelBackgroundColor?: string;
260
260
  labelTextColor?: string;
261
261
  labelBorderRadius?: number;
262
+ smoothing?: boolean;
263
+ smoothingSpeed?: number;
262
264
  }
263
265
  interface ChartInstance {
264
266
  setData: (data: OhlcDataPoint[]) => void;
package/dist/index.js CHANGED
@@ -828,6 +828,41 @@ function createChart(element, options = {}) {
828
828
  let crosshairPoint = null;
829
829
  let doubleClickEnabled = mergedOptions.doubleClickEnabled;
830
830
  let doubleClickAction = mergedOptions.doubleClickAction;
831
+ let smoothedTickerPrice = null;
832
+ let tickerPriceTarget = null;
833
+ let smoothingRafId = null;
834
+ const tickerSmoothingLoop = () => {
835
+ smoothingRafId = null;
836
+ if (smoothedTickerPrice === null || tickerPriceTarget === null) return;
837
+ const diff = tickerPriceTarget - smoothedTickerPrice;
838
+ if (Math.abs(diff) < 1e-9) {
839
+ smoothedTickerPrice = tickerPriceTarget;
840
+ draw();
841
+ return;
842
+ }
843
+ const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
844
+ const speed = clamp(tickerOpts.smoothingSpeed ?? 8, 1, 60);
845
+ const dt = 1 / 60;
846
+ const lerp = 1 - Math.exp(-speed * dt);
847
+ smoothedTickerPrice += diff * lerp;
848
+ draw();
849
+ smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
850
+ };
851
+ const pushSmoothedPrice = (target) => {
852
+ const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
853
+ if (!tickerOpts.smoothing) {
854
+ smoothedTickerPrice = null;
855
+ tickerPriceTarget = null;
856
+ return;
857
+ }
858
+ tickerPriceTarget = target;
859
+ if (smoothedTickerPrice === null) {
860
+ smoothedTickerPrice = target;
861
+ }
862
+ if (smoothingRafId === null) {
863
+ smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
864
+ }
865
+ };
831
866
  const canvas = document.createElement("canvas");
832
867
  const ctx = canvas.getContext("2d");
833
868
  if (!ctx) {
@@ -1788,28 +1823,37 @@ function createChart(element, options = {}) {
1788
1823
  }
1789
1824
  ctx.restore();
1790
1825
  }
1826
+ const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1827
+ const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
1828
+ const lastDataIndex = data.length - 1;
1791
1829
  for (let index = startIndex; index <= endIndex; index += 1) {
1792
1830
  const point = data[index];
1793
1831
  if (!point) {
1794
1832
  continue;
1795
1833
  }
1834
+ const isLastCandle = useSmoothedCandle && index === lastDataIndex;
1835
+ const displayClose = isLastCandle ? smoothedTickerPrice : point.c;
1836
+ const displayHigh = isLastCandle ? Math.max(point.h, smoothedTickerPrice) : point.h;
1837
+ const displayLow = isLastCandle ? Math.min(point.l, smoothedTickerPrice) : point.l;
1796
1838
  const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
1797
1839
  const openY = yFromPrice(point.o);
1798
- const closeY = yFromPrice(point.c);
1799
- const highY = yFromPrice(point.h);
1800
- const lowY = yFromPrice(point.l);
1801
- const direction = getCandleDirectionByIndex(index);
1840
+ const closeY = yFromPrice(displayClose);
1841
+ const highY = yFromPrice(displayHigh);
1842
+ const lowY = yFromPrice(displayLow);
1843
+ const direction = isLastCandle ? displayClose >= point.o ? "up" : "down" : getCandleDirectionByIndex(index);
1802
1844
  const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
1845
+ const roundedCenterX = Math.round(centerX);
1803
1846
  ctx.strokeStyle = candleColor;
1804
1847
  ctx.lineWidth = candleWickWidth;
1805
1848
  ctx.beginPath();
1806
- ctx.moveTo(crisp(centerX), crisp(highY));
1807
- ctx.lineTo(crisp(centerX), crisp(lowY));
1849
+ ctx.moveTo(roundedCenterX + 0.5, crisp(highY));
1850
+ ctx.lineTo(roundedCenterX + 0.5, crisp(lowY));
1808
1851
  ctx.stroke();
1852
+ const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
1809
1853
  const bodyTop = Math.min(openY, closeY);
1810
1854
  const bodyHeight = Math.max(1, Math.abs(closeY - openY));
1811
1855
  ctx.fillStyle = candleColor;
1812
- ctx.fillRect(Math.round(centerX - bodyWidth / 2), Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
1856
+ ctx.fillRect(bodyLeft, Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
1813
1857
  }
1814
1858
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1815
1859
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
@@ -1943,10 +1987,10 @@ function createChart(element, options = {}) {
1943
1987
  const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1944
1988
  const lastPoint = data[data.length - 1];
1945
1989
  if ((ticker.visible ?? true) && lastPoint) {
1946
- const tickerPrice = lastPoint.c;
1990
+ const tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
1947
1991
  const tickerY = yFromPrice(tickerPrice);
1948
1992
  const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
1949
- const lastDirection = getCandleDirectionByIndex(data.length - 1);
1993
+ const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
1950
1994
  const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
1951
1995
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
1952
1996
  const tickerStyle = ticker.style ?? "solid";
@@ -2778,6 +2822,10 @@ function createChart(element, options = {}) {
2778
2822
  fitXViewport();
2779
2823
  }
2780
2824
  }
2825
+ const lastClose = data.length > 0 ? data[data.length - 1].c : null;
2826
+ if (lastClose !== null) {
2827
+ pushSmoothedPrice(lastClose);
2828
+ }
2781
2829
  draw();
2782
2830
  };
2783
2831
  const setPriceLines = (lines) => {
@@ -2928,6 +2976,10 @@ function createChart(element, options = {}) {
2928
2976
  draw();
2929
2977
  };
2930
2978
  const destroy = () => {
2979
+ if (smoothingRafId !== null) {
2980
+ cancelAnimationFrame(smoothingRafId);
2981
+ smoothingRafId = null;
2982
+ }
2931
2983
  canvas.removeEventListener("pointerdown", onPointerDown);
2932
2984
  canvas.removeEventListener("pointermove", onPointerMove);
2933
2985
  canvas.removeEventListener("pointerup", endPointerDrag);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",