hyperprop-charting-library 0.1.25 → 0.1.27

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
@@ -136,6 +147,18 @@ chart.updateIndicator(emaId, { inputs: { length: 55 } });
136
147
  chart.updateIndicator(volumeId, { paneHeightRatio: 0.1 });
137
148
  ```
138
149
 
150
+ Volume scaling tip (for large spikes):
151
+
152
+ ```ts
153
+ chart.updateIndicator(volumeId, {
154
+ inputs: {
155
+ scaleMode: "visible", // default; scales using current viewport only
156
+ scaleType: "log", // default; better for large volume ranges
157
+ clampPercentile: 0.95 // optional outlier clamp
158
+ }
159
+ });
160
+ ```
161
+
139
162
  Autoscale controls per indicator:
140
163
 
141
164
  ```ts
@@ -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",
@@ -509,6 +511,15 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
509
511
  }
510
512
  ctx.restore();
511
513
  };
514
+ var getPercentileValue = (values, percentile) => {
515
+ if (values.length === 0) {
516
+ return 1;
517
+ }
518
+ const sorted = [...values].sort((a, b) => a - b);
519
+ const clamped = Math.min(1, Math.max(0, percentile));
520
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(clamped * sorted.length) - 1));
521
+ return sorted[index] ?? 1;
522
+ };
512
523
  var BUILTIN_VOLUME_INDICATOR = {
513
524
  id: "volume",
514
525
  name: "Volume",
@@ -520,7 +531,10 @@ var BUILTIN_VOLUME_INDICATOR = {
520
531
  upColor: "",
521
532
  downColor: "",
522
533
  minBarWidth: 1,
523
- overlayHeightRatio: 0.22
534
+ overlayHeightRatio: 0.22,
535
+ scaleMode: "visible",
536
+ scaleType: "log",
537
+ clampPercentile: 1
524
538
  },
525
539
  draw: (ctx, renderContext, inputs) => {
526
540
  const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
@@ -529,8 +543,12 @@ var BUILTIN_VOLUME_INDICATOR = {
529
543
  const overlayHeightRatio = Math.min(0.45, Math.max(0.08, Number(inputs.overlayHeightRatio) || 0.22));
530
544
  const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
531
545
  const paneBottom = chartBottom;
532
- const visiblePoints = data.slice(startIndex, endIndex + 1);
533
- const maxVolume = Math.max(1, ...visiblePoints.map((point) => point.v ?? 0));
546
+ const scaleMode = inputs.scaleMode === "full" ? "full" : "visible";
547
+ const scaleType = inputs.scaleType === "linear" ? "linear" : "log";
548
+ const scalingPoints = scaleMode === "full" ? data : data.slice(startIndex, endIndex + 1);
549
+ const scalingVolumes = scalingPoints.map((point) => Math.max(0, point.v ?? 0)).filter((value) => value > 0);
550
+ const clampPercentile = Number.isFinite(inputs.clampPercentile) ? Number(inputs.clampPercentile) : 1;
551
+ const maxVolume = Math.max(1, getPercentileValue(scalingVolumes, clampPercentile));
534
552
  const barWidth = Math.max(
535
553
  Math.max(1, Number(inputs.minBarWidth) || 1),
536
554
  Math.min(Math.max(1, candleSpacing - 1), Math.floor(candleSpacing * 0.7))
@@ -542,7 +560,8 @@ var BUILTIN_VOLUME_INDICATOR = {
542
560
  if (!point || point.v === void 0 || point.v <= 0) {
543
561
  continue;
544
562
  }
545
- 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)) : Math.min(1, Math.max(0, Math.log1p(clampedVolume) / Math.log1p(maxVolume)));
546
565
  const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
547
566
  const xCenter = xFromIndex(index);
548
567
  const barX = Math.round(xCenter - barWidth / 2);
@@ -1539,7 +1558,7 @@ function createChart(element, options = {}) {
1539
1558
  const fullChartBottom = chartTop + fullChartHeight;
1540
1559
  const chartRight = chartLeft + chartWidth;
1541
1560
  const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
1542
- const paneGap = 8;
1561
+ const paneGap = 0;
1543
1562
  const separatePaneSpacing = 6;
1544
1563
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1545
1564
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
@@ -1734,7 +1753,8 @@ function createChart(element, options = {}) {
1734
1753
  );
1735
1754
  const grid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
1736
1755
  const gridOpacity = clamp(grid.opacity, 0, 1);
1737
- 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));
1738
1758
  const gridColor = grid.color ?? mergedOptions.gridColor;
1739
1759
  if (grid.horizontalLines) {
1740
1760
  ctx.save();
@@ -1753,8 +1773,10 @@ function createChart(element, options = {}) {
1753
1773
  ctx.restore();
1754
1774
  }
1755
1775
  const minLabelSpacingPx = Math.max(72, candleSpacing * 6);
1756
- const approxLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
1757
- 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;
1758
1780
  const xStep = Math.max(1, Math.ceil(rawStep));
1759
1781
  const visibleTickStart = Math.floor(xStart);
1760
1782
  const visibleTickEnd = Math.ceil(xEnd) - 1;
@@ -1895,6 +1917,18 @@ function createChart(element, options = {}) {
1895
1917
  ctx.restore();
1896
1918
  });
1897
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
+ }
1898
1932
  ctx.strokeStyle = axis.lineColor;
1899
1933
  ctx.lineWidth = Math.max(1, axis.lineWidth);
1900
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",
@@ -485,6 +487,15 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
485
487
  }
486
488
  ctx.restore();
487
489
  };
490
+ var getPercentileValue = (values, percentile) => {
491
+ if (values.length === 0) {
492
+ return 1;
493
+ }
494
+ const sorted = [...values].sort((a, b) => a - b);
495
+ const clamped = Math.min(1, Math.max(0, percentile));
496
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(clamped * sorted.length) - 1));
497
+ return sorted[index] ?? 1;
498
+ };
488
499
  var BUILTIN_VOLUME_INDICATOR = {
489
500
  id: "volume",
490
501
  name: "Volume",
@@ -496,7 +507,10 @@ var BUILTIN_VOLUME_INDICATOR = {
496
507
  upColor: "",
497
508
  downColor: "",
498
509
  minBarWidth: 1,
499
- overlayHeightRatio: 0.22
510
+ overlayHeightRatio: 0.22,
511
+ scaleMode: "visible",
512
+ scaleType: "log",
513
+ clampPercentile: 1
500
514
  },
501
515
  draw: (ctx, renderContext, inputs) => {
502
516
  const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
@@ -505,8 +519,12 @@ var BUILTIN_VOLUME_INDICATOR = {
505
519
  const overlayHeightRatio = Math.min(0.45, Math.max(0.08, Number(inputs.overlayHeightRatio) || 0.22));
506
520
  const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
507
521
  const paneBottom = chartBottom;
508
- const visiblePoints = data.slice(startIndex, endIndex + 1);
509
- const maxVolume = Math.max(1, ...visiblePoints.map((point) => point.v ?? 0));
522
+ const scaleMode = inputs.scaleMode === "full" ? "full" : "visible";
523
+ const scaleType = inputs.scaleType === "linear" ? "linear" : "log";
524
+ const scalingPoints = scaleMode === "full" ? data : data.slice(startIndex, endIndex + 1);
525
+ const scalingVolumes = scalingPoints.map((point) => Math.max(0, point.v ?? 0)).filter((value) => value > 0);
526
+ const clampPercentile = Number.isFinite(inputs.clampPercentile) ? Number(inputs.clampPercentile) : 1;
527
+ const maxVolume = Math.max(1, getPercentileValue(scalingVolumes, clampPercentile));
510
528
  const barWidth = Math.max(
511
529
  Math.max(1, Number(inputs.minBarWidth) || 1),
512
530
  Math.min(Math.max(1, candleSpacing - 1), Math.floor(candleSpacing * 0.7))
@@ -518,7 +536,8 @@ var BUILTIN_VOLUME_INDICATOR = {
518
536
  if (!point || point.v === void 0 || point.v <= 0) {
519
537
  continue;
520
538
  }
521
- 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)) : Math.min(1, Math.max(0, Math.log1p(clampedVolume) / Math.log1p(maxVolume)));
522
541
  const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
523
542
  const xCenter = xFromIndex(index);
524
543
  const barX = Math.round(xCenter - barWidth / 2);
@@ -1515,7 +1534,7 @@ function createChart(element, options = {}) {
1515
1534
  const fullChartBottom = chartTop + fullChartHeight;
1516
1535
  const chartRight = chartLeft + chartWidth;
1517
1536
  const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
1518
- const paneGap = 8;
1537
+ const paneGap = 0;
1519
1538
  const separatePaneSpacing = 6;
1520
1539
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1521
1540
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
@@ -1710,7 +1729,8 @@ function createChart(element, options = {}) {
1710
1729
  );
1711
1730
  const grid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
1712
1731
  const gridOpacity = clamp(grid.opacity, 0, 1);
1713
- 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));
1714
1734
  const gridColor = grid.color ?? mergedOptions.gridColor;
1715
1735
  if (grid.horizontalLines) {
1716
1736
  ctx.save();
@@ -1729,8 +1749,10 @@ function createChart(element, options = {}) {
1729
1749
  ctx.restore();
1730
1750
  }
1731
1751
  const minLabelSpacingPx = Math.max(72, candleSpacing * 6);
1732
- const approxLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
1733
- 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;
1734
1756
  const xStep = Math.max(1, Math.ceil(rawStep));
1735
1757
  const visibleTickStart = Math.floor(xStart);
1736
1758
  const visibleTickEnd = Math.ceil(xEnd) - 1;
@@ -1871,6 +1893,18 @@ function createChart(element, options = {}) {
1871
1893
  ctx.restore();
1872
1894
  });
1873
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
+ }
1874
1908
  ctx.strokeStyle = axis.lineColor;
1875
1909
  ctx.lineWidth = Math.max(1, axis.lineWidth);
1876
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",
@@ -509,6 +511,15 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
509
511
  }
510
512
  ctx.restore();
511
513
  };
514
+ var getPercentileValue = (values, percentile) => {
515
+ if (values.length === 0) {
516
+ return 1;
517
+ }
518
+ const sorted = [...values].sort((a, b) => a - b);
519
+ const clamped = Math.min(1, Math.max(0, percentile));
520
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(clamped * sorted.length) - 1));
521
+ return sorted[index] ?? 1;
522
+ };
512
523
  var BUILTIN_VOLUME_INDICATOR = {
513
524
  id: "volume",
514
525
  name: "Volume",
@@ -520,7 +531,10 @@ var BUILTIN_VOLUME_INDICATOR = {
520
531
  upColor: "",
521
532
  downColor: "",
522
533
  minBarWidth: 1,
523
- overlayHeightRatio: 0.22
534
+ overlayHeightRatio: 0.22,
535
+ scaleMode: "visible",
536
+ scaleType: "log",
537
+ clampPercentile: 1
524
538
  },
525
539
  draw: (ctx, renderContext, inputs) => {
526
540
  const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
@@ -529,8 +543,12 @@ var BUILTIN_VOLUME_INDICATOR = {
529
543
  const overlayHeightRatio = Math.min(0.45, Math.max(0.08, Number(inputs.overlayHeightRatio) || 0.22));
530
544
  const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
531
545
  const paneBottom = chartBottom;
532
- const visiblePoints = data.slice(startIndex, endIndex + 1);
533
- const maxVolume = Math.max(1, ...visiblePoints.map((point) => point.v ?? 0));
546
+ const scaleMode = inputs.scaleMode === "full" ? "full" : "visible";
547
+ const scaleType = inputs.scaleType === "linear" ? "linear" : "log";
548
+ const scalingPoints = scaleMode === "full" ? data : data.slice(startIndex, endIndex + 1);
549
+ const scalingVolumes = scalingPoints.map((point) => Math.max(0, point.v ?? 0)).filter((value) => value > 0);
550
+ const clampPercentile = Number.isFinite(inputs.clampPercentile) ? Number(inputs.clampPercentile) : 1;
551
+ const maxVolume = Math.max(1, getPercentileValue(scalingVolumes, clampPercentile));
534
552
  const barWidth = Math.max(
535
553
  Math.max(1, Number(inputs.minBarWidth) || 1),
536
554
  Math.min(Math.max(1, candleSpacing - 1), Math.floor(candleSpacing * 0.7))
@@ -542,7 +560,8 @@ var BUILTIN_VOLUME_INDICATOR = {
542
560
  if (!point || point.v === void 0 || point.v <= 0) {
543
561
  continue;
544
562
  }
545
- 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)) : Math.min(1, Math.max(0, Math.log1p(clampedVolume) / Math.log1p(maxVolume)));
546
565
  const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
547
566
  const xCenter = xFromIndex(index);
548
567
  const barX = Math.round(xCenter - barWidth / 2);
@@ -1539,7 +1558,7 @@ function createChart(element, options = {}) {
1539
1558
  const fullChartBottom = chartTop + fullChartHeight;
1540
1559
  const chartRight = chartLeft + chartWidth;
1541
1560
  const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
1542
- const paneGap = 8;
1561
+ const paneGap = 0;
1543
1562
  const separatePaneSpacing = 6;
1544
1563
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1545
1564
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
@@ -1734,7 +1753,8 @@ function createChart(element, options = {}) {
1734
1753
  );
1735
1754
  const grid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
1736
1755
  const gridOpacity = clamp(grid.opacity, 0, 1);
1737
- 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));
1738
1758
  const gridColor = grid.color ?? mergedOptions.gridColor;
1739
1759
  if (grid.horizontalLines) {
1740
1760
  ctx.save();
@@ -1753,8 +1773,10 @@ function createChart(element, options = {}) {
1753
1773
  ctx.restore();
1754
1774
  }
1755
1775
  const minLabelSpacingPx = Math.max(72, candleSpacing * 6);
1756
- const approxLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
1757
- 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;
1758
1780
  const xStep = Math.max(1, Math.ceil(rawStep));
1759
1781
  const visibleTickStart = Math.floor(xStart);
1760
1782
  const visibleTickEnd = Math.ceil(xEnd) - 1;
@@ -1895,6 +1917,18 @@ function createChart(element, options = {}) {
1895
1917
  ctx.restore();
1896
1918
  });
1897
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
+ }
1898
1932
  ctx.strokeStyle = axis.lineColor;
1899
1933
  ctx.lineWidth = Math.max(1, axis.lineWidth);
1900
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",
@@ -485,6 +487,15 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
485
487
  }
486
488
  ctx.restore();
487
489
  };
490
+ var getPercentileValue = (values, percentile) => {
491
+ if (values.length === 0) {
492
+ return 1;
493
+ }
494
+ const sorted = [...values].sort((a, b) => a - b);
495
+ const clamped = Math.min(1, Math.max(0, percentile));
496
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(clamped * sorted.length) - 1));
497
+ return sorted[index] ?? 1;
498
+ };
488
499
  var BUILTIN_VOLUME_INDICATOR = {
489
500
  id: "volume",
490
501
  name: "Volume",
@@ -496,7 +507,10 @@ var BUILTIN_VOLUME_INDICATOR = {
496
507
  upColor: "",
497
508
  downColor: "",
498
509
  minBarWidth: 1,
499
- overlayHeightRatio: 0.22
510
+ overlayHeightRatio: 0.22,
511
+ scaleMode: "visible",
512
+ scaleType: "log",
513
+ clampPercentile: 1
500
514
  },
501
515
  draw: (ctx, renderContext, inputs) => {
502
516
  const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
@@ -505,8 +519,12 @@ var BUILTIN_VOLUME_INDICATOR = {
505
519
  const overlayHeightRatio = Math.min(0.45, Math.max(0.08, Number(inputs.overlayHeightRatio) || 0.22));
506
520
  const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
507
521
  const paneBottom = chartBottom;
508
- const visiblePoints = data.slice(startIndex, endIndex + 1);
509
- const maxVolume = Math.max(1, ...visiblePoints.map((point) => point.v ?? 0));
522
+ const scaleMode = inputs.scaleMode === "full" ? "full" : "visible";
523
+ const scaleType = inputs.scaleType === "linear" ? "linear" : "log";
524
+ const scalingPoints = scaleMode === "full" ? data : data.slice(startIndex, endIndex + 1);
525
+ const scalingVolumes = scalingPoints.map((point) => Math.max(0, point.v ?? 0)).filter((value) => value > 0);
526
+ const clampPercentile = Number.isFinite(inputs.clampPercentile) ? Number(inputs.clampPercentile) : 1;
527
+ const maxVolume = Math.max(1, getPercentileValue(scalingVolumes, clampPercentile));
510
528
  const barWidth = Math.max(
511
529
  Math.max(1, Number(inputs.minBarWidth) || 1),
512
530
  Math.min(Math.max(1, candleSpacing - 1), Math.floor(candleSpacing * 0.7))
@@ -518,7 +536,8 @@ var BUILTIN_VOLUME_INDICATOR = {
518
536
  if (!point || point.v === void 0 || point.v <= 0) {
519
537
  continue;
520
538
  }
521
- 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)) : Math.min(1, Math.max(0, Math.log1p(clampedVolume) / Math.log1p(maxVolume)));
522
541
  const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
523
542
  const xCenter = xFromIndex(index);
524
543
  const barX = Math.round(xCenter - barWidth / 2);
@@ -1515,7 +1534,7 @@ function createChart(element, options = {}) {
1515
1534
  const fullChartBottom = chartTop + fullChartHeight;
1516
1535
  const chartRight = chartLeft + chartWidth;
1517
1536
  const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
1518
- const paneGap = 8;
1537
+ const paneGap = 0;
1519
1538
  const separatePaneSpacing = 6;
1520
1539
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1521
1540
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
@@ -1710,7 +1729,8 @@ function createChart(element, options = {}) {
1710
1729
  );
1711
1730
  const grid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
1712
1731
  const gridOpacity = clamp(grid.opacity, 0, 1);
1713
- 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));
1714
1734
  const gridColor = grid.color ?? mergedOptions.gridColor;
1715
1735
  if (grid.horizontalLines) {
1716
1736
  ctx.save();
@@ -1729,8 +1749,10 @@ function createChart(element, options = {}) {
1729
1749
  ctx.restore();
1730
1750
  }
1731
1751
  const minLabelSpacingPx = Math.max(72, candleSpacing * 6);
1732
- const approxLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
1733
- 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;
1734
1756
  const xStep = Math.max(1, Math.ceil(rawStep));
1735
1757
  const visibleTickStart = Math.floor(xStart);
1736
1758
  const visibleTickEnd = Math.ceil(xEnd) - 1;
@@ -1871,6 +1893,18 @@ function createChart(element, options = {}) {
1871
1893
  ctx.restore();
1872
1894
  });
1873
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
+ }
1874
1908
  ctx.strokeStyle = axis.lineColor;
1875
1909
  ctx.lineWidth = Math.max(1, axis.lineWidth);
1876
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
 
@@ -309,6 +311,9 @@ Volume style inputs:
309
311
  - `upColor`, `downColor`
310
312
  - `minBarWidth`
311
313
  - `overlayHeightRatio` (when used as overlay)
314
+ - `scaleMode` (`"visible"` default, or `"full"`)
315
+ - `scaleType` (`"log"` default, or `"linear"`)
316
+ - `clampPercentile` (`0..1`, default `1`; e.g. `0.95` to reduce outlier crush)
312
317
 
313
318
  ---
314
319
 
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
 
@@ -138,6 +146,16 @@ const rsiId = chart.addIndicator("rsi", { length: 14 }, { pane: "separate", pane
138
146
  const volumeId = chart.addIndicator("volume", { upOpacity: 0.72, downOpacity: 0.72 });
139
147
  ```
140
148
 
149
+ ## Prevent one volume spike from crushing all bars
150
+
151
+ ```ts
152
+ const volumeId = chart.addIndicator("volume", {
153
+ scaleMode: "visible", // default behavior
154
+ scaleType: "log", // default behavior
155
+ clampPercentile: 0.95 // clamp scaling reference to p95 of visible bars
156
+ });
157
+ ```
158
+
141
159
  Available built-ins:
142
160
 
143
161
  - `volume`, `sma`, `ema`, `rsi`, `wma`, `vwma`, `rma`, `hma`, `stddev`, `atr`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",