hyperprop-charting-library 0.1.26 → 0.1.28

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
@@ -94,6 +94,17 @@ const chart = createChart(root, {
94
94
  });
95
95
  ```
96
96
 
97
+ ## Axis Label Density
98
+
99
+ ```ts
100
+ const chart = createChart(root, {
101
+ grid: {
102
+ xTickCount: 8, // bottom labels/grid intervals
103
+ yTickCount: 6 // right-side price labels/grid intervals
104
+ }
105
+ });
106
+ ```
107
+
97
108
  ## Crosshair "+" Button
98
109
 
99
110
  ```ts
@@ -142,6 +153,7 @@ Volume scaling tip (for large spikes):
142
153
  chart.updateIndicator(volumeId, {
143
154
  inputs: {
144
155
  scaleMode: "visible", // default; scales using current viewport only
156
+ scaleType: "sqrt", // default; balanced compression
145
157
  clampPercentile: 0.95 // optional outlier clamp
146
158
  }
147
159
  });
@@ -28,7 +28,9 @@ var DEFAULT_GRID_OPTIONS = {
28
28
  opacity: 0.38,
29
29
  horizontalLines: true,
30
30
  verticalLines: true,
31
- horizontalTickCount: 5
31
+ xTickCount: 8,
32
+ yTickCount: 6,
33
+ horizontalTickCount: 6
32
34
  };
33
35
  var DEFAULT_AXIS_OPTIONS = {
34
36
  lineColor: "#3b3f47",
@@ -531,6 +533,7 @@ var BUILTIN_VOLUME_INDICATOR = {
531
533
  minBarWidth: 1,
532
534
  overlayHeightRatio: 0.22,
533
535
  scaleMode: "visible",
536
+ scaleType: "sqrt",
534
537
  clampPercentile: 1
535
538
  },
536
539
  draw: (ctx, renderContext, inputs) => {
@@ -541,6 +544,7 @@ var BUILTIN_VOLUME_INDICATOR = {
541
544
  const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
542
545
  const paneBottom = chartBottom;
543
546
  const scaleMode = inputs.scaleMode === "full" ? "full" : "visible";
547
+ const scaleType = inputs.scaleType === "linear" ? "linear" : inputs.scaleType === "log" ? "log" : "sqrt";
544
548
  const scalingPoints = scaleMode === "full" ? data : data.slice(startIndex, endIndex + 1);
545
549
  const scalingVolumes = scalingPoints.map((point) => Math.max(0, point.v ?? 0)).filter((value) => value > 0);
546
550
  const clampPercentile = Number.isFinite(inputs.clampPercentile) ? Number(inputs.clampPercentile) : 1;
@@ -556,7 +560,8 @@ var BUILTIN_VOLUME_INDICATOR = {
556
560
  if (!point || point.v === void 0 || point.v <= 0) {
557
561
  continue;
558
562
  }
559
- const ratio = Math.min(1, Math.max(0, point.v / maxVolume));
563
+ const clampedVolume = Math.min(maxVolume, Math.max(0, point.v));
564
+ const ratio = scaleType === "linear" ? Math.min(1, Math.max(0, clampedVolume / maxVolume)) : scaleType === "log" ? Math.min(1, Math.max(0, Math.log1p(clampedVolume) / Math.log1p(maxVolume))) : Math.min(1, Math.max(0, Math.sqrt(clampedVolume) / Math.sqrt(maxVolume)));
560
565
  const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
561
566
  const xCenter = xFromIndex(index);
562
567
  const barX = Math.round(xCenter - barWidth / 2);
@@ -1553,7 +1558,7 @@ function createChart(element, options = {}) {
1553
1558
  const fullChartBottom = chartTop + fullChartHeight;
1554
1559
  const chartRight = chartLeft + chartWidth;
1555
1560
  const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
1556
- const paneGap = 8;
1561
+ const paneGap = 0;
1557
1562
  const separatePaneSpacing = 6;
1558
1563
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1559
1564
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
@@ -1748,7 +1753,8 @@ function createChart(element, options = {}) {
1748
1753
  );
1749
1754
  const grid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
1750
1755
  const gridOpacity = clamp(grid.opacity, 0, 1);
1751
- const yTicks = Math.max(1, Math.floor(grid.horizontalTickCount));
1756
+ const yTickCountInput = grid.yTickCount ?? grid.horizontalTickCount;
1757
+ const yTicks = Math.max(1, Math.floor(yTickCountInput));
1752
1758
  const gridColor = grid.color ?? mergedOptions.gridColor;
1753
1759
  if (grid.horizontalLines) {
1754
1760
  ctx.save();
@@ -1767,8 +1773,10 @@ function createChart(element, options = {}) {
1767
1773
  ctx.restore();
1768
1774
  }
1769
1775
  const minLabelSpacingPx = Math.max(72, candleSpacing * 6);
1770
- const approxLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
1771
- const rawStep = xSpan / approxLabelCount;
1776
+ const autoLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
1777
+ const xTickCountInput = grid.xTickCount ?? autoLabelCount;
1778
+ const xTickCount = Math.max(2, Math.floor(xTickCountInput));
1779
+ const rawStep = xSpan / xTickCount;
1772
1780
  const xStep = Math.max(1, Math.ceil(rawStep));
1773
1781
  const visibleTickStart = Math.floor(xStart);
1774
1782
  const visibleTickEnd = Math.ceil(xEnd) - 1;
@@ -1909,6 +1917,18 @@ function createChart(element, options = {}) {
1909
1917
  ctx.restore();
1910
1918
  });
1911
1919
  }
1920
+ if (crosshair.visible && crosshairPoint && crosshair.showVertical) {
1921
+ const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
1922
+ ctx.save();
1923
+ ctx.strokeStyle = crosshair.color;
1924
+ ctx.lineWidth = Math.max(1, crosshair.width);
1925
+ applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
1926
+ ctx.beginPath();
1927
+ ctx.moveTo(crisp(cx), crisp(chartTop));
1928
+ ctx.lineTo(crisp(cx), crisp(fullChartBottom));
1929
+ ctx.stroke();
1930
+ ctx.restore();
1931
+ }
1912
1932
  ctx.strokeStyle = axis.lineColor;
1913
1933
  ctx.lineWidth = Math.max(1, axis.lineWidth);
1914
1934
  ctx.beginPath();
@@ -111,6 +111,8 @@ interface GridOptions {
111
111
  opacity?: number;
112
112
  horizontalLines?: boolean;
113
113
  verticalLines?: boolean;
114
+ xTickCount?: number;
115
+ yTickCount?: number;
114
116
  horizontalTickCount?: number;
115
117
  }
116
118
  interface CrosshairOptions {
@@ -4,7 +4,9 @@ var DEFAULT_GRID_OPTIONS = {
4
4
  opacity: 0.38,
5
5
  horizontalLines: true,
6
6
  verticalLines: true,
7
- horizontalTickCount: 5
7
+ xTickCount: 8,
8
+ yTickCount: 6,
9
+ horizontalTickCount: 6
8
10
  };
9
11
  var DEFAULT_AXIS_OPTIONS = {
10
12
  lineColor: "#3b3f47",
@@ -507,6 +509,7 @@ var BUILTIN_VOLUME_INDICATOR = {
507
509
  minBarWidth: 1,
508
510
  overlayHeightRatio: 0.22,
509
511
  scaleMode: "visible",
512
+ scaleType: "sqrt",
510
513
  clampPercentile: 1
511
514
  },
512
515
  draw: (ctx, renderContext, inputs) => {
@@ -517,6 +520,7 @@ var BUILTIN_VOLUME_INDICATOR = {
517
520
  const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
518
521
  const paneBottom = chartBottom;
519
522
  const scaleMode = inputs.scaleMode === "full" ? "full" : "visible";
523
+ const scaleType = inputs.scaleType === "linear" ? "linear" : inputs.scaleType === "log" ? "log" : "sqrt";
520
524
  const scalingPoints = scaleMode === "full" ? data : data.slice(startIndex, endIndex + 1);
521
525
  const scalingVolumes = scalingPoints.map((point) => Math.max(0, point.v ?? 0)).filter((value) => value > 0);
522
526
  const clampPercentile = Number.isFinite(inputs.clampPercentile) ? Number(inputs.clampPercentile) : 1;
@@ -532,7 +536,8 @@ var BUILTIN_VOLUME_INDICATOR = {
532
536
  if (!point || point.v === void 0 || point.v <= 0) {
533
537
  continue;
534
538
  }
535
- const ratio = Math.min(1, Math.max(0, point.v / maxVolume));
539
+ const clampedVolume = Math.min(maxVolume, Math.max(0, point.v));
540
+ const ratio = scaleType === "linear" ? Math.min(1, Math.max(0, clampedVolume / maxVolume)) : scaleType === "log" ? Math.min(1, Math.max(0, Math.log1p(clampedVolume) / Math.log1p(maxVolume))) : Math.min(1, Math.max(0, Math.sqrt(clampedVolume) / Math.sqrt(maxVolume)));
536
541
  const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
537
542
  const xCenter = xFromIndex(index);
538
543
  const barX = Math.round(xCenter - barWidth / 2);
@@ -1529,7 +1534,7 @@ function createChart(element, options = {}) {
1529
1534
  const fullChartBottom = chartTop + fullChartHeight;
1530
1535
  const chartRight = chartLeft + chartWidth;
1531
1536
  const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
1532
- const paneGap = 8;
1537
+ const paneGap = 0;
1533
1538
  const separatePaneSpacing = 6;
1534
1539
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1535
1540
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
@@ -1724,7 +1729,8 @@ function createChart(element, options = {}) {
1724
1729
  );
1725
1730
  const grid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
1726
1731
  const gridOpacity = clamp(grid.opacity, 0, 1);
1727
- const yTicks = Math.max(1, Math.floor(grid.horizontalTickCount));
1732
+ const yTickCountInput = grid.yTickCount ?? grid.horizontalTickCount;
1733
+ const yTicks = Math.max(1, Math.floor(yTickCountInput));
1728
1734
  const gridColor = grid.color ?? mergedOptions.gridColor;
1729
1735
  if (grid.horizontalLines) {
1730
1736
  ctx.save();
@@ -1743,8 +1749,10 @@ function createChart(element, options = {}) {
1743
1749
  ctx.restore();
1744
1750
  }
1745
1751
  const minLabelSpacingPx = Math.max(72, candleSpacing * 6);
1746
- const approxLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
1747
- const rawStep = xSpan / approxLabelCount;
1752
+ const autoLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
1753
+ const xTickCountInput = grid.xTickCount ?? autoLabelCount;
1754
+ const xTickCount = Math.max(2, Math.floor(xTickCountInput));
1755
+ const rawStep = xSpan / xTickCount;
1748
1756
  const xStep = Math.max(1, Math.ceil(rawStep));
1749
1757
  const visibleTickStart = Math.floor(xStart);
1750
1758
  const visibleTickEnd = Math.ceil(xEnd) - 1;
@@ -1885,6 +1893,18 @@ function createChart(element, options = {}) {
1885
1893
  ctx.restore();
1886
1894
  });
1887
1895
  }
1896
+ if (crosshair.visible && crosshairPoint && crosshair.showVertical) {
1897
+ const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
1898
+ ctx.save();
1899
+ ctx.strokeStyle = crosshair.color;
1900
+ ctx.lineWidth = Math.max(1, crosshair.width);
1901
+ applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
1902
+ ctx.beginPath();
1903
+ ctx.moveTo(crisp(cx), crisp(chartTop));
1904
+ ctx.lineTo(crisp(cx), crisp(fullChartBottom));
1905
+ ctx.stroke();
1906
+ ctx.restore();
1907
+ }
1888
1908
  ctx.strokeStyle = axis.lineColor;
1889
1909
  ctx.lineWidth = Math.max(1, axis.lineWidth);
1890
1910
  ctx.beginPath();
package/dist/index.cjs CHANGED
@@ -28,7 +28,9 @@ var DEFAULT_GRID_OPTIONS = {
28
28
  opacity: 0.38,
29
29
  horizontalLines: true,
30
30
  verticalLines: true,
31
- horizontalTickCount: 5
31
+ xTickCount: 8,
32
+ yTickCount: 6,
33
+ horizontalTickCount: 6
32
34
  };
33
35
  var DEFAULT_AXIS_OPTIONS = {
34
36
  lineColor: "#3b3f47",
@@ -531,6 +533,7 @@ var BUILTIN_VOLUME_INDICATOR = {
531
533
  minBarWidth: 1,
532
534
  overlayHeightRatio: 0.22,
533
535
  scaleMode: "visible",
536
+ scaleType: "sqrt",
534
537
  clampPercentile: 1
535
538
  },
536
539
  draw: (ctx, renderContext, inputs) => {
@@ -541,6 +544,7 @@ var BUILTIN_VOLUME_INDICATOR = {
541
544
  const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
542
545
  const paneBottom = chartBottom;
543
546
  const scaleMode = inputs.scaleMode === "full" ? "full" : "visible";
547
+ const scaleType = inputs.scaleType === "linear" ? "linear" : inputs.scaleType === "log" ? "log" : "sqrt";
544
548
  const scalingPoints = scaleMode === "full" ? data : data.slice(startIndex, endIndex + 1);
545
549
  const scalingVolumes = scalingPoints.map((point) => Math.max(0, point.v ?? 0)).filter((value) => value > 0);
546
550
  const clampPercentile = Number.isFinite(inputs.clampPercentile) ? Number(inputs.clampPercentile) : 1;
@@ -556,7 +560,8 @@ var BUILTIN_VOLUME_INDICATOR = {
556
560
  if (!point || point.v === void 0 || point.v <= 0) {
557
561
  continue;
558
562
  }
559
- const ratio = Math.min(1, Math.max(0, point.v / maxVolume));
563
+ const clampedVolume = Math.min(maxVolume, Math.max(0, point.v));
564
+ const ratio = scaleType === "linear" ? Math.min(1, Math.max(0, clampedVolume / maxVolume)) : scaleType === "log" ? Math.min(1, Math.max(0, Math.log1p(clampedVolume) / Math.log1p(maxVolume))) : Math.min(1, Math.max(0, Math.sqrt(clampedVolume) / Math.sqrt(maxVolume)));
560
565
  const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
561
566
  const xCenter = xFromIndex(index);
562
567
  const barX = Math.round(xCenter - barWidth / 2);
@@ -1553,7 +1558,7 @@ function createChart(element, options = {}) {
1553
1558
  const fullChartBottom = chartTop + fullChartHeight;
1554
1559
  const chartRight = chartLeft + chartWidth;
1555
1560
  const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
1556
- const paneGap = 8;
1561
+ const paneGap = 0;
1557
1562
  const separatePaneSpacing = 6;
1558
1563
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1559
1564
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
@@ -1748,7 +1753,8 @@ function createChart(element, options = {}) {
1748
1753
  );
1749
1754
  const grid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
1750
1755
  const gridOpacity = clamp(grid.opacity, 0, 1);
1751
- const yTicks = Math.max(1, Math.floor(grid.horizontalTickCount));
1756
+ const yTickCountInput = grid.yTickCount ?? grid.horizontalTickCount;
1757
+ const yTicks = Math.max(1, Math.floor(yTickCountInput));
1752
1758
  const gridColor = grid.color ?? mergedOptions.gridColor;
1753
1759
  if (grid.horizontalLines) {
1754
1760
  ctx.save();
@@ -1767,8 +1773,10 @@ function createChart(element, options = {}) {
1767
1773
  ctx.restore();
1768
1774
  }
1769
1775
  const minLabelSpacingPx = Math.max(72, candleSpacing * 6);
1770
- const approxLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
1771
- const rawStep = xSpan / approxLabelCount;
1776
+ const autoLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
1777
+ const xTickCountInput = grid.xTickCount ?? autoLabelCount;
1778
+ const xTickCount = Math.max(2, Math.floor(xTickCountInput));
1779
+ const rawStep = xSpan / xTickCount;
1772
1780
  const xStep = Math.max(1, Math.ceil(rawStep));
1773
1781
  const visibleTickStart = Math.floor(xStart);
1774
1782
  const visibleTickEnd = Math.ceil(xEnd) - 1;
@@ -1909,6 +1917,18 @@ function createChart(element, options = {}) {
1909
1917
  ctx.restore();
1910
1918
  });
1911
1919
  }
1920
+ if (crosshair.visible && crosshairPoint && crosshair.showVertical) {
1921
+ const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
1922
+ ctx.save();
1923
+ ctx.strokeStyle = crosshair.color;
1924
+ ctx.lineWidth = Math.max(1, crosshair.width);
1925
+ applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
1926
+ ctx.beginPath();
1927
+ ctx.moveTo(crisp(cx), crisp(chartTop));
1928
+ ctx.lineTo(crisp(cx), crisp(fullChartBottom));
1929
+ ctx.stroke();
1930
+ ctx.restore();
1931
+ }
1912
1932
  ctx.strokeStyle = axis.lineColor;
1913
1933
  ctx.lineWidth = Math.max(1, axis.lineWidth);
1914
1934
  ctx.beginPath();
package/dist/index.d.cts CHANGED
@@ -111,6 +111,8 @@ interface GridOptions {
111
111
  opacity?: number;
112
112
  horizontalLines?: boolean;
113
113
  verticalLines?: boolean;
114
+ xTickCount?: number;
115
+ yTickCount?: number;
114
116
  horizontalTickCount?: number;
115
117
  }
116
118
  interface CrosshairOptions {
package/dist/index.d.ts CHANGED
@@ -111,6 +111,8 @@ interface GridOptions {
111
111
  opacity?: number;
112
112
  horizontalLines?: boolean;
113
113
  verticalLines?: boolean;
114
+ xTickCount?: number;
115
+ yTickCount?: number;
114
116
  horizontalTickCount?: number;
115
117
  }
116
118
  interface CrosshairOptions {
package/dist/index.js CHANGED
@@ -4,7 +4,9 @@ var DEFAULT_GRID_OPTIONS = {
4
4
  opacity: 0.38,
5
5
  horizontalLines: true,
6
6
  verticalLines: true,
7
- horizontalTickCount: 5
7
+ xTickCount: 8,
8
+ yTickCount: 6,
9
+ horizontalTickCount: 6
8
10
  };
9
11
  var DEFAULT_AXIS_OPTIONS = {
10
12
  lineColor: "#3b3f47",
@@ -507,6 +509,7 @@ var BUILTIN_VOLUME_INDICATOR = {
507
509
  minBarWidth: 1,
508
510
  overlayHeightRatio: 0.22,
509
511
  scaleMode: "visible",
512
+ scaleType: "sqrt",
510
513
  clampPercentile: 1
511
514
  },
512
515
  draw: (ctx, renderContext, inputs) => {
@@ -517,6 +520,7 @@ var BUILTIN_VOLUME_INDICATOR = {
517
520
  const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
518
521
  const paneBottom = chartBottom;
519
522
  const scaleMode = inputs.scaleMode === "full" ? "full" : "visible";
523
+ const scaleType = inputs.scaleType === "linear" ? "linear" : inputs.scaleType === "log" ? "log" : "sqrt";
520
524
  const scalingPoints = scaleMode === "full" ? data : data.slice(startIndex, endIndex + 1);
521
525
  const scalingVolumes = scalingPoints.map((point) => Math.max(0, point.v ?? 0)).filter((value) => value > 0);
522
526
  const clampPercentile = Number.isFinite(inputs.clampPercentile) ? Number(inputs.clampPercentile) : 1;
@@ -532,7 +536,8 @@ var BUILTIN_VOLUME_INDICATOR = {
532
536
  if (!point || point.v === void 0 || point.v <= 0) {
533
537
  continue;
534
538
  }
535
- const ratio = Math.min(1, Math.max(0, point.v / maxVolume));
539
+ const clampedVolume = Math.min(maxVolume, Math.max(0, point.v));
540
+ const ratio = scaleType === "linear" ? Math.min(1, Math.max(0, clampedVolume / maxVolume)) : scaleType === "log" ? Math.min(1, Math.max(0, Math.log1p(clampedVolume) / Math.log1p(maxVolume))) : Math.min(1, Math.max(0, Math.sqrt(clampedVolume) / Math.sqrt(maxVolume)));
536
541
  const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
537
542
  const xCenter = xFromIndex(index);
538
543
  const barX = Math.round(xCenter - barWidth / 2);
@@ -1529,7 +1534,7 @@ function createChart(element, options = {}) {
1529
1534
  const fullChartBottom = chartTop + fullChartHeight;
1530
1535
  const chartRight = chartLeft + chartWidth;
1531
1536
  const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
1532
- const paneGap = 8;
1537
+ const paneGap = 0;
1533
1538
  const separatePaneSpacing = 6;
1534
1539
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1535
1540
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
@@ -1724,7 +1729,8 @@ function createChart(element, options = {}) {
1724
1729
  );
1725
1730
  const grid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
1726
1731
  const gridOpacity = clamp(grid.opacity, 0, 1);
1727
- const yTicks = Math.max(1, Math.floor(grid.horizontalTickCount));
1732
+ const yTickCountInput = grid.yTickCount ?? grid.horizontalTickCount;
1733
+ const yTicks = Math.max(1, Math.floor(yTickCountInput));
1728
1734
  const gridColor = grid.color ?? mergedOptions.gridColor;
1729
1735
  if (grid.horizontalLines) {
1730
1736
  ctx.save();
@@ -1743,8 +1749,10 @@ function createChart(element, options = {}) {
1743
1749
  ctx.restore();
1744
1750
  }
1745
1751
  const minLabelSpacingPx = Math.max(72, candleSpacing * 6);
1746
- const approxLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
1747
- const rawStep = xSpan / approxLabelCount;
1752
+ const autoLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
1753
+ const xTickCountInput = grid.xTickCount ?? autoLabelCount;
1754
+ const xTickCount = Math.max(2, Math.floor(xTickCountInput));
1755
+ const rawStep = xSpan / xTickCount;
1748
1756
  const xStep = Math.max(1, Math.ceil(rawStep));
1749
1757
  const visibleTickStart = Math.floor(xStart);
1750
1758
  const visibleTickEnd = Math.ceil(xEnd) - 1;
@@ -1885,6 +1893,18 @@ function createChart(element, options = {}) {
1885
1893
  ctx.restore();
1886
1894
  });
1887
1895
  }
1896
+ if (crosshair.visible && crosshairPoint && crosshair.showVertical) {
1897
+ const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
1898
+ ctx.save();
1899
+ ctx.strokeStyle = crosshair.color;
1900
+ ctx.lineWidth = Math.max(1, crosshair.width);
1901
+ applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
1902
+ ctx.beginPath();
1903
+ ctx.moveTo(crisp(cx), crisp(chartTop));
1904
+ ctx.lineTo(crisp(cx), crisp(fullChartBottom));
1905
+ ctx.stroke();
1906
+ ctx.restore();
1907
+ }
1888
1908
  ctx.strokeStyle = axis.lineColor;
1889
1909
  ctx.lineWidth = Math.max(1, axis.lineWidth);
1890
1910
  ctx.beginPath();
package/docs/API.md CHANGED
@@ -81,7 +81,9 @@ Top-level options:
81
81
  - `opacity` (default `0.38`)
82
82
  - `horizontalLines` (default `true`)
83
83
  - `verticalLines` (default `true`)
84
- - `horizontalTickCount` (default `5`)
84
+ - `xTickCount` (default `8`, x-axis label/grid density)
85
+ - `yTickCount` (default `6`, y-axis label/grid density)
86
+ - `horizontalTickCount` (legacy alias for `yTickCount`)
85
87
 
86
88
  ### `CrosshairOptions`
87
89
 
@@ -310,6 +312,7 @@ Volume style inputs:
310
312
  - `minBarWidth`
311
313
  - `overlayHeightRatio` (when used as overlay)
312
314
  - `scaleMode` (`"visible"` default, or `"full"`)
315
+ - `scaleType` (`"sqrt"` default, or `"log"` / `"linear"`)
313
316
  - `clampPercentile` (`0..1`, default `1`; e.g. `0.95` to reduce outlier crush)
314
317
 
315
318
  ---
package/docs/RECIPES.md CHANGED
@@ -11,7 +11,15 @@ const chart = createChart(rootEl, {
11
11
  backgroundColor: "#101114",
12
12
  upColor: "#2fb171",
13
13
  downColor: "#d35a5a",
14
- grid: { opacity: 0.35, horizontalTickCount: 4 }
14
+ grid: { opacity: 0.35, xTickCount: 8, yTickCount: 6 }
15
+ });
16
+ ```
17
+
18
+ Tune axis density when creating the chart:
19
+
20
+ ```ts
21
+ const chartDense = createChart(rootEl, {
22
+ grid: { xTickCount: 10, yTickCount: 8 }
15
23
  });
16
24
  ```
17
25
 
@@ -143,6 +151,7 @@ const volumeId = chart.addIndicator("volume", { upOpacity: 0.72, downOpacity: 0.
143
151
  ```ts
144
152
  const volumeId = chart.addIndicator("volume", {
145
153
  scaleMode: "visible", // default behavior
154
+ scaleType: "sqrt", // default behavior
146
155
  clampPercentile: 0.95 // clamp scaling reference to p95 of visible bars
147
156
  });
148
157
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",