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 +23 -0
- package/dist/hyperprop-charting-library.cjs +43 -9
- package/dist/hyperprop-charting-library.d.ts +2 -0
- package/dist/hyperprop-charting-library.js +43 -9
- package/dist/index.cjs +43 -9
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +43 -9
- package/docs/API.md +6 -1
- package/docs/RECIPES.md +19 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
533
|
-
const
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
1757
|
-
const
|
|
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();
|
|
@@ -4,7 +4,9 @@ var DEFAULT_GRID_OPTIONS = {
|
|
|
4
4
|
opacity: 0.38,
|
|
5
5
|
horizontalLines: true,
|
|
6
6
|
verticalLines: true,
|
|
7
|
-
|
|
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
|
|
509
|
-
const
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
1733
|
-
const
|
|
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
|
-
|
|
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
|
|
533
|
-
const
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
1757
|
-
const
|
|
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
package/dist/index.d.ts
CHANGED
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
|
-
|
|
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
|
|
509
|
-
const
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
1733
|
-
const
|
|
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
|
-
- `
|
|
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,
|
|
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`
|