hyperprop-charting-library 0.1.24 → 0.1.26
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 +42 -0
- package/dist/hyperprop-charting-library.cjs +230 -23
- package/dist/hyperprop-charting-library.d.ts +15 -1
- package/dist/hyperprop-charting-library.js +230 -23
- package/dist/index.cjs +230 -23
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +230 -23
- package/docs/API.md +18 -1
- package/docs/RECIPES.md +48 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,27 @@ const chart = createChart(root, {
|
|
|
73
73
|
});
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
## Tick-Size Aware Precision
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const chart = createChart(root, {
|
|
80
|
+
tickSize: 0.25,
|
|
81
|
+
priceDecimals: 2
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This keeps labels, pointer prices, and color tolerance aligned to market tick steps.
|
|
86
|
+
|
|
87
|
+
## Per-Axis Label Styling
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const chart = createChart(root, {
|
|
91
|
+
axis: { lineColor: "#374151" },
|
|
92
|
+
xAxis: { textColor: "#9ca3af", fontSize: 11 },
|
|
93
|
+
yAxis: { textColor: "#e5e7eb", fontSize: 12 }
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
76
97
|
## Crosshair "+" Button
|
|
77
98
|
|
|
78
99
|
```ts
|
|
@@ -115,6 +136,26 @@ chart.updateIndicator(emaId, { inputs: { length: 55 } });
|
|
|
115
136
|
chart.updateIndicator(volumeId, { paneHeightRatio: 0.1 });
|
|
116
137
|
```
|
|
117
138
|
|
|
139
|
+
Volume scaling tip (for large spikes):
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
chart.updateIndicator(volumeId, {
|
|
143
|
+
inputs: {
|
|
144
|
+
scaleMode: "visible", // default; scales using current viewport only
|
|
145
|
+
clampPercentile: 0.95 // optional outlier clamp
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Autoscale controls per indicator:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
chart.addIndicator("ema", { length: 50 }, {
|
|
154
|
+
excludeFromAutoscale: false,
|
|
155
|
+
overlayScaleWeight: 0.25
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
118
159
|
Volume note:
|
|
119
160
|
|
|
120
161
|
- `volume` and `vwma` require `OhlcDataPoint.v` for best results.
|
|
@@ -155,6 +196,7 @@ const myId = chart.addIndicator("my-line");
|
|
|
155
196
|
- `chart.onOrderAction(handler)` / `chart.onChartClick(handler)` / `chart.onCrosshairMove(handler)` / `chart.onCrosshairPriceAction(handler)`
|
|
156
197
|
- `chart.setDoubleClickEnabled(enabled)` / `chart.setDoubleClickAction(action)`
|
|
157
198
|
- `chart.registerIndicator(plugin)` / `chart.addIndicator(type, inputs?, options?)` / `chart.updateIndicator(id, patch)` / `chart.removeIndicator(id)`
|
|
199
|
+
- `chart.listBuiltInIndicators()` / `chart.getIndicators()`
|
|
158
200
|
- `chart.zoomInX()` / `chart.zoomOutX()` / `chart.panX(bars)` / `chart.resetViewport()`
|
|
159
201
|
- `chart.resize(width, height)` / `chart.destroy()`
|
|
160
202
|
|
|
@@ -133,6 +133,8 @@ var DEFAULT_OPTIONS = {
|
|
|
133
133
|
backgroundColor: "#101114",
|
|
134
134
|
axisColor: "#7f8289",
|
|
135
135
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
136
|
+
xAxis: DEFAULT_AXIS_OPTIONS,
|
|
137
|
+
yAxis: DEFAULT_AXIS_OPTIONS,
|
|
136
138
|
priceDecimals: 2,
|
|
137
139
|
stabilizePriceLabels: true,
|
|
138
140
|
priceLabelMinIntegerDigits: 3,
|
|
@@ -151,6 +153,7 @@ var DEFAULT_OPTIONS = {
|
|
|
151
153
|
candleBodyWidthRatio: 0.7,
|
|
152
154
|
candleMinWidth: 0.5,
|
|
153
155
|
candleWickWidth: 1,
|
|
156
|
+
tickSize: 0,
|
|
154
157
|
candleColorMode: "openClose",
|
|
155
158
|
candleColorEpsilon: -1,
|
|
156
159
|
autoScaleSmoothing: 0.16,
|
|
@@ -388,6 +391,35 @@ var computeAtrSeries = (data, length) => {
|
|
|
388
391
|
}
|
|
389
392
|
return result;
|
|
390
393
|
};
|
|
394
|
+
var builtInSeriesCache = /* @__PURE__ */ new Map();
|
|
395
|
+
var getSeriesFingerprint = (data) => {
|
|
396
|
+
const length = data.length;
|
|
397
|
+
const last = length > 0 ? data[length - 1] : void 0;
|
|
398
|
+
const prev = length > 1 ? data[length - 2] : void 0;
|
|
399
|
+
if (!last) return "empty";
|
|
400
|
+
return [
|
|
401
|
+
length,
|
|
402
|
+
last.time.getTime(),
|
|
403
|
+
last.o,
|
|
404
|
+
last.h,
|
|
405
|
+
last.l,
|
|
406
|
+
last.c,
|
|
407
|
+
last.v ?? "",
|
|
408
|
+
prev?.time.getTime() ?? "",
|
|
409
|
+
prev?.c ?? "",
|
|
410
|
+
prev?.v ?? ""
|
|
411
|
+
].join("|");
|
|
412
|
+
};
|
|
413
|
+
var withCachedSeries = (key, data, compute) => {
|
|
414
|
+
const fingerprint = getSeriesFingerprint(data);
|
|
415
|
+
const existing = builtInSeriesCache.get(key);
|
|
416
|
+
if (existing && existing.fingerprint === fingerprint) {
|
|
417
|
+
return existing.values;
|
|
418
|
+
}
|
|
419
|
+
const values = compute();
|
|
420
|
+
builtInSeriesCache.set(key, { fingerprint, values });
|
|
421
|
+
return values;
|
|
422
|
+
};
|
|
391
423
|
var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
392
424
|
if (!renderContext.yFromPrice) return;
|
|
393
425
|
const yFromPrice = renderContext.yFromPrice;
|
|
@@ -477,6 +509,15 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
|
|
|
477
509
|
}
|
|
478
510
|
ctx.restore();
|
|
479
511
|
};
|
|
512
|
+
var getPercentileValue = (values, percentile) => {
|
|
513
|
+
if (values.length === 0) {
|
|
514
|
+
return 1;
|
|
515
|
+
}
|
|
516
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
517
|
+
const clamped = Math.min(1, Math.max(0, percentile));
|
|
518
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(clamped * sorted.length) - 1));
|
|
519
|
+
return sorted[index] ?? 1;
|
|
520
|
+
};
|
|
480
521
|
var BUILTIN_VOLUME_INDICATOR = {
|
|
481
522
|
id: "volume",
|
|
482
523
|
name: "Volume",
|
|
@@ -485,8 +526,12 @@ var BUILTIN_VOLUME_INDICATOR = {
|
|
|
485
526
|
defaultInputs: {
|
|
486
527
|
upOpacity: 0.7,
|
|
487
528
|
downOpacity: 0.7,
|
|
529
|
+
upColor: "",
|
|
530
|
+
downColor: "",
|
|
488
531
|
minBarWidth: 1,
|
|
489
|
-
overlayHeightRatio: 0.22
|
|
532
|
+
overlayHeightRatio: 0.22,
|
|
533
|
+
scaleMode: "visible",
|
|
534
|
+
clampPercentile: 1
|
|
490
535
|
},
|
|
491
536
|
draw: (ctx, renderContext, inputs) => {
|
|
492
537
|
const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
|
|
@@ -495,8 +540,11 @@ var BUILTIN_VOLUME_INDICATOR = {
|
|
|
495
540
|
const overlayHeightRatio = Math.min(0.45, Math.max(0.08, Number(inputs.overlayHeightRatio) || 0.22));
|
|
496
541
|
const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
|
|
497
542
|
const paneBottom = chartBottom;
|
|
498
|
-
const
|
|
499
|
-
const
|
|
543
|
+
const scaleMode = inputs.scaleMode === "full" ? "full" : "visible";
|
|
544
|
+
const scalingPoints = scaleMode === "full" ? data : data.slice(startIndex, endIndex + 1);
|
|
545
|
+
const scalingVolumes = scalingPoints.map((point) => Math.max(0, point.v ?? 0)).filter((value) => value > 0);
|
|
546
|
+
const clampPercentile = Number.isFinite(inputs.clampPercentile) ? Number(inputs.clampPercentile) : 1;
|
|
547
|
+
const maxVolume = Math.max(1, getPercentileValue(scalingVolumes, clampPercentile));
|
|
500
548
|
const barWidth = Math.max(
|
|
501
549
|
Math.max(1, Number(inputs.minBarWidth) || 1),
|
|
502
550
|
Math.min(Math.max(1, candleSpacing - 1), Math.floor(candleSpacing * 0.7))
|
|
@@ -515,9 +563,11 @@ var BUILTIN_VOLUME_INDICATOR = {
|
|
|
515
563
|
const barY = Math.round(paneBottom - volumeHeight);
|
|
516
564
|
const direction = renderContext.getCandleDirectionByIndex(index);
|
|
517
565
|
const opacity = direction === "up" ? upOpacity : downOpacity;
|
|
566
|
+
const upBarColor = inputs.upColor && inputs.upColor.trim().length > 0 ? inputs.upColor : upColor;
|
|
567
|
+
const downBarColor = inputs.downColor && inputs.downColor.trim().length > 0 ? inputs.downColor : downColor;
|
|
518
568
|
ctx.save();
|
|
519
569
|
ctx.globalAlpha = opacity;
|
|
520
|
-
ctx.fillStyle = direction === "up" ?
|
|
570
|
+
ctx.fillStyle = direction === "up" ? upBarColor : downBarColor;
|
|
521
571
|
ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
|
|
522
572
|
ctx.restore();
|
|
523
573
|
}
|
|
@@ -530,7 +580,11 @@ var BUILTIN_SMA_INDICATOR = {
|
|
|
530
580
|
defaultInputs: { length: 20, source: "close", color: "#60a5fa", width: 2 },
|
|
531
581
|
draw: (ctx, renderContext, inputs) => {
|
|
532
582
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
533
|
-
const values =
|
|
583
|
+
const values = withCachedSeries(
|
|
584
|
+
`sma|${length}|${inputs.source ?? "close"}`,
|
|
585
|
+
renderContext.data,
|
|
586
|
+
() => computeSmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
587
|
+
);
|
|
534
588
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
|
|
535
589
|
}
|
|
536
590
|
};
|
|
@@ -541,7 +595,11 @@ var BUILTIN_EMA_INDICATOR = {
|
|
|
541
595
|
defaultInputs: { length: 20, source: "close", color: "#f59e0b", width: 2 },
|
|
542
596
|
draw: (ctx, renderContext, inputs) => {
|
|
543
597
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
544
|
-
const values =
|
|
598
|
+
const values = withCachedSeries(
|
|
599
|
+
`ema|${length}|${inputs.source ?? "close"}`,
|
|
600
|
+
renderContext.data,
|
|
601
|
+
() => computeEmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
602
|
+
);
|
|
545
603
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
546
604
|
}
|
|
547
605
|
};
|
|
@@ -552,7 +610,11 @@ var BUILTIN_WMA_INDICATOR = {
|
|
|
552
610
|
defaultInputs: { length: 20, source: "close", color: "#a78bfa", width: 2 },
|
|
553
611
|
draw: (ctx, renderContext, inputs) => {
|
|
554
612
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
555
|
-
const values =
|
|
613
|
+
const values = withCachedSeries(
|
|
614
|
+
`wma|${length}|${inputs.source ?? "close"}`,
|
|
615
|
+
renderContext.data,
|
|
616
|
+
() => computeWmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
617
|
+
);
|
|
556
618
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
|
|
557
619
|
}
|
|
558
620
|
};
|
|
@@ -563,7 +625,11 @@ var BUILTIN_VWMA_INDICATOR = {
|
|
|
563
625
|
defaultInputs: { length: 20, source: "close", color: "#ef4444", width: 2 },
|
|
564
626
|
draw: (ctx, renderContext, inputs) => {
|
|
565
627
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
566
|
-
const values =
|
|
628
|
+
const values = withCachedSeries(
|
|
629
|
+
`vwma|${length}|${inputs.source ?? "close"}`,
|
|
630
|
+
renderContext.data,
|
|
631
|
+
() => computeVwmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
632
|
+
);
|
|
567
633
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
|
|
568
634
|
}
|
|
569
635
|
};
|
|
@@ -574,7 +640,11 @@ var BUILTIN_RMA_INDICATOR = {
|
|
|
574
640
|
defaultInputs: { length: 14, source: "close", color: "#22c55e", width: 2 },
|
|
575
641
|
draw: (ctx, renderContext, inputs) => {
|
|
576
642
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
577
|
-
const values =
|
|
643
|
+
const values = withCachedSeries(
|
|
644
|
+
`rma|${length}|${inputs.source ?? "close"}`,
|
|
645
|
+
renderContext.data,
|
|
646
|
+
() => computeRmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
647
|
+
);
|
|
578
648
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
|
|
579
649
|
}
|
|
580
650
|
};
|
|
@@ -585,7 +655,11 @@ var BUILTIN_HMA_INDICATOR = {
|
|
|
585
655
|
defaultInputs: { length: 21, source: "close", color: "#14b8a6", width: 2 },
|
|
586
656
|
draw: (ctx, renderContext, inputs) => {
|
|
587
657
|
const length = clampIndicatorLength(inputs.length, 21);
|
|
588
|
-
const values =
|
|
658
|
+
const values = withCachedSeries(
|
|
659
|
+
`hma|${length}|${inputs.source ?? "close"}`,
|
|
660
|
+
renderContext.data,
|
|
661
|
+
() => computeHmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
662
|
+
);
|
|
589
663
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
590
664
|
}
|
|
591
665
|
};
|
|
@@ -597,7 +671,11 @@ var BUILTIN_STDDEV_INDICATOR = {
|
|
|
597
671
|
defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2 },
|
|
598
672
|
draw: (ctx, renderContext, inputs) => {
|
|
599
673
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
600
|
-
const values =
|
|
674
|
+
const values = withCachedSeries(
|
|
675
|
+
`stddev|${length}|${inputs.source ?? "close"}`,
|
|
676
|
+
renderContext.data,
|
|
677
|
+
() => computeStdDevSeries(renderContext.data, length, inputs.source ?? "close")
|
|
678
|
+
);
|
|
601
679
|
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2);
|
|
602
680
|
}
|
|
603
681
|
};
|
|
@@ -609,7 +687,7 @@ var BUILTIN_ATR_INDICATOR = {
|
|
|
609
687
|
defaultInputs: { length: 14, color: "#eab308", width: 2 },
|
|
610
688
|
draw: (ctx, renderContext, inputs) => {
|
|
611
689
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
612
|
-
const values = computeAtrSeries(renderContext.data, length);
|
|
690
|
+
const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
|
|
613
691
|
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2);
|
|
614
692
|
}
|
|
615
693
|
};
|
|
@@ -621,7 +699,7 @@ var BUILTIN_RSI_INDICATOR = {
|
|
|
621
699
|
defaultInputs: { length: 14, color: "#3b82f6", width: 2 },
|
|
622
700
|
draw: (ctx, renderContext, inputs) => {
|
|
623
701
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
624
|
-
const values = computeRsiSeries(renderContext.data, length);
|
|
702
|
+
const values = withCachedSeries(`rsi|${length}`, renderContext.data, () => computeRsiSeries(renderContext.data, length));
|
|
625
703
|
drawSeparateSeries(
|
|
626
704
|
ctx,
|
|
627
705
|
renderContext,
|
|
@@ -655,6 +733,18 @@ function createChart(element, options = {}) {
|
|
|
655
733
|
...options.axis ?? {},
|
|
656
734
|
...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {}
|
|
657
735
|
},
|
|
736
|
+
xAxis: {
|
|
737
|
+
...DEFAULT_AXIS_OPTIONS,
|
|
738
|
+
...options.axis ?? {},
|
|
739
|
+
...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
|
|
740
|
+
...options.xAxis ?? {}
|
|
741
|
+
},
|
|
742
|
+
yAxis: {
|
|
743
|
+
...DEFAULT_AXIS_OPTIONS,
|
|
744
|
+
...options.axis ?? {},
|
|
745
|
+
...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
|
|
746
|
+
...options.yAxis ?? {}
|
|
747
|
+
},
|
|
658
748
|
crosshair: {
|
|
659
749
|
...DEFAULT_CROSSHAIR_OPTIONS,
|
|
660
750
|
...options.crosshair ?? {}
|
|
@@ -710,6 +800,9 @@ function createChart(element, options = {}) {
|
|
|
710
800
|
visible: indicator.visible ?? true,
|
|
711
801
|
pane: indicator.pane ?? plugin?.pane ?? "overlay",
|
|
712
802
|
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
|
|
803
|
+
zIndex: Math.round(Number(indicator.zIndex) || 0),
|
|
804
|
+
excludeFromAutoscale: indicator.excludeFromAutoscale ?? true,
|
|
805
|
+
overlayScaleWeight: Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0.25)),
|
|
713
806
|
inputs: {
|
|
714
807
|
...defaults,
|
|
715
808
|
...indicator.inputs ?? {}
|
|
@@ -865,20 +958,56 @@ function createChart(element, options = {}) {
|
|
|
865
958
|
}
|
|
866
959
|
return { min: nextMin, max: nextMax };
|
|
867
960
|
};
|
|
961
|
+
const getConfiguredPriceDecimals = () => clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
962
|
+
const getConfiguredTickSize = () => {
|
|
963
|
+
const step = Number(mergedOptions.tickSize);
|
|
964
|
+
return Number.isFinite(step) && step > 0 ? step : 0;
|
|
965
|
+
};
|
|
966
|
+
const getTickSizeDecimals = () => {
|
|
967
|
+
const tickSize = getConfiguredTickSize();
|
|
968
|
+
if (tickSize <= 0) {
|
|
969
|
+
return 0;
|
|
970
|
+
}
|
|
971
|
+
let scaled = tickSize;
|
|
972
|
+
let decimals = 0;
|
|
973
|
+
while (decimals < 8 && Math.abs(Math.round(scaled) - scaled) > 1e-10) {
|
|
974
|
+
scaled *= 10;
|
|
975
|
+
decimals += 1;
|
|
976
|
+
}
|
|
977
|
+
return decimals;
|
|
978
|
+
};
|
|
979
|
+
const getDisplayPriceDecimals = () => {
|
|
980
|
+
const configured = getConfiguredPriceDecimals();
|
|
981
|
+
const tickDecimals = getTickSizeDecimals();
|
|
982
|
+
return Math.max(configured, tickDecimals);
|
|
983
|
+
};
|
|
984
|
+
const quantizeToTickSize = (price) => {
|
|
985
|
+
const tickSize = getConfiguredTickSize();
|
|
986
|
+
if (tickSize <= 0 || !Number.isFinite(price)) {
|
|
987
|
+
return price;
|
|
988
|
+
}
|
|
989
|
+
return Math.round(price / tickSize) * tickSize;
|
|
990
|
+
};
|
|
868
991
|
const formatPrice = (price) => {
|
|
869
|
-
const
|
|
870
|
-
|
|
992
|
+
const rounded = quantizeToTickSize(price);
|
|
993
|
+
const decimals = getDisplayPriceDecimals();
|
|
994
|
+
return rounded.toFixed(decimals);
|
|
871
995
|
};
|
|
872
996
|
const roundToPricePrecision = (price) => {
|
|
873
|
-
const
|
|
874
|
-
|
|
997
|
+
const rounded = quantizeToTickSize(price);
|
|
998
|
+
const decimals = getDisplayPriceDecimals();
|
|
999
|
+
return Number(rounded.toFixed(decimals));
|
|
875
1000
|
};
|
|
876
1001
|
const getResolvedCandleColorEpsilon = () => {
|
|
877
1002
|
const configured = mergedOptions.candleColorEpsilon;
|
|
878
1003
|
if (configured >= 0) {
|
|
879
1004
|
return configured;
|
|
880
1005
|
}
|
|
881
|
-
const
|
|
1006
|
+
const tickSize = getConfiguredTickSize();
|
|
1007
|
+
if (tickSize > 0) {
|
|
1008
|
+
return tickSize / 2;
|
|
1009
|
+
}
|
|
1010
|
+
const decimals = getDisplayPriceDecimals();
|
|
882
1011
|
return decimals > 0 ? 0.5 / 10 ** decimals : 0;
|
|
883
1012
|
};
|
|
884
1013
|
const getDirectionFromDelta = (delta) => {
|
|
@@ -1410,6 +1539,10 @@ function createChart(element, options = {}) {
|
|
|
1410
1539
|
canvas.height = Math.floor(height * pixelRatio);
|
|
1411
1540
|
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
1412
1541
|
const axis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
|
|
1542
|
+
const xAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
|
|
1543
|
+
const yAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
|
|
1544
|
+
const xAxisFontSize = Math.max(8, xAxis.fontSize);
|
|
1545
|
+
const yAxisFontSize = Math.max(8, yAxis.fontSize);
|
|
1413
1546
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1414
1547
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
1415
1548
|
ctx.fillRect(0, 0, width, height);
|
|
@@ -1424,6 +1557,9 @@ function createChart(element, options = {}) {
|
|
|
1424
1557
|
const separatePaneSpacing = 6;
|
|
1425
1558
|
const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1426
1559
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
|
|
1560
|
+
).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
|
|
1561
|
+
const overlayIndicatorsForScale = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1562
|
+
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
1427
1563
|
);
|
|
1428
1564
|
const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
|
|
1429
1565
|
const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
|
|
@@ -1475,8 +1611,47 @@ function createChart(element, options = {}) {
|
|
|
1475
1611
|
}
|
|
1476
1612
|
}
|
|
1477
1613
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1614
|
+
let minPrice = Math.min(...priceSource.map((point) => point.l));
|
|
1615
|
+
let maxPrice = Math.max(...priceSource.map((point) => point.h));
|
|
1616
|
+
if (overlayIndicatorsForScale.length > 0) {
|
|
1617
|
+
for (const { indicator } of overlayIndicatorsForScale) {
|
|
1618
|
+
if (indicator.excludeFromAutoscale) {
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
const type = indicator.type;
|
|
1622
|
+
const inputs = indicator.inputs;
|
|
1623
|
+
const source = inputs.source ?? "close";
|
|
1624
|
+
const length = clampIndicatorLength(inputs.length ?? 14, 14);
|
|
1625
|
+
let series = null;
|
|
1626
|
+
if (type === "sma") series = withCachedSeries(`sma|${length}|${source}`, data, () => computeSmaSeries(data, length, source));
|
|
1627
|
+
if (type === "ema") series = withCachedSeries(`ema|${length}|${source}`, data, () => computeEmaSeries(data, length, source));
|
|
1628
|
+
if (type === "wma") series = withCachedSeries(`wma|${length}|${source}`, data, () => computeWmaSeries(data, length, source));
|
|
1629
|
+
if (type === "vwma") series = withCachedSeries(`vwma|${length}|${source}`, data, () => computeVwmaSeries(data, length, source));
|
|
1630
|
+
if (type === "rma") series = withCachedSeries(`rma|${length}|${source}`, data, () => computeRmaSeries(data, length, source));
|
|
1631
|
+
if (type === "hma") series = withCachedSeries(`hma|${length}|${source}`, data, () => computeHmaSeries(data, length, source));
|
|
1632
|
+
if (!series) {
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
const visibleValues = [];
|
|
1636
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
1637
|
+
const value = series[idx];
|
|
1638
|
+
if (Number.isFinite(value ?? Number.NaN)) {
|
|
1639
|
+
visibleValues.push(value);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
if (visibleValues.length === 0) {
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
const seriesMin = Math.min(...visibleValues);
|
|
1646
|
+
const seriesMax = Math.max(...visibleValues);
|
|
1647
|
+
const weight = Math.min(1, Math.max(0, indicator.overlayScaleWeight));
|
|
1648
|
+
const currentMid = (minPrice + maxPrice) / 2;
|
|
1649
|
+
const weightedMin = currentMid + (seriesMin - currentMid) * weight;
|
|
1650
|
+
const weightedMax = currentMid + (seriesMax - currentMid) * weight;
|
|
1651
|
+
minPrice = Math.min(minPrice, weightedMin);
|
|
1652
|
+
maxPrice = Math.max(maxPrice, weightedMax);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1480
1655
|
const priceRange = maxPrice - minPrice || 1;
|
|
1481
1656
|
const autoMin = minPrice - priceRange * 0.08;
|
|
1482
1657
|
const autoMax = maxPrice + priceRange * 0.08;
|
|
@@ -1636,7 +1811,7 @@ function createChart(element, options = {}) {
|
|
|
1636
1811
|
}
|
|
1637
1812
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1638
1813
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
1639
|
-
);
|
|
1814
|
+
).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
|
|
1640
1815
|
if (activeOverlayIndicators.length > 0) {
|
|
1641
1816
|
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
1642
1817
|
activeOverlayIndicators.forEach(({ indicator, plugin }) => {
|
|
@@ -1746,7 +1921,10 @@ function createChart(element, options = {}) {
|
|
|
1746
1921
|
const ratio = tick / yTicks;
|
|
1747
1922
|
const price = yMin + yRange * ratio;
|
|
1748
1923
|
const y = yFromPrice(price);
|
|
1749
|
-
|
|
1924
|
+
const prevFont = ctx.font;
|
|
1925
|
+
ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
1926
|
+
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
1927
|
+
ctx.font = prevFont;
|
|
1750
1928
|
}
|
|
1751
1929
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
1752
1930
|
const lastPoint = data[data.length - 1];
|
|
@@ -1803,7 +1981,10 @@ function createChart(element, options = {}) {
|
|
|
1803
1981
|
month: "short",
|
|
1804
1982
|
day: "numeric"
|
|
1805
1983
|
});
|
|
1806
|
-
|
|
1984
|
+
const prevFont = ctx.font;
|
|
1985
|
+
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
1986
|
+
drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
|
|
1987
|
+
ctx.font = prevFont;
|
|
1807
1988
|
}
|
|
1808
1989
|
if (crosshair.visible && crosshairPoint) {
|
|
1809
1990
|
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
@@ -2574,6 +2755,27 @@ function createChart(element, options = {}) {
|
|
|
2574
2755
|
indicators = indicators.filter((indicator) => indicator.type !== type);
|
|
2575
2756
|
draw();
|
|
2576
2757
|
};
|
|
2758
|
+
const listBuiltInIndicators = () => {
|
|
2759
|
+
return BUILTIN_INDICATORS.map((indicator) => ({
|
|
2760
|
+
id: indicator.id,
|
|
2761
|
+
name: indicator.name,
|
|
2762
|
+
pane: indicator.pane ?? "overlay",
|
|
2763
|
+
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio }
|
|
2764
|
+
}));
|
|
2765
|
+
};
|
|
2766
|
+
const getIndicators = () => {
|
|
2767
|
+
return indicators.map((indicator) => ({
|
|
2768
|
+
id: indicator.id,
|
|
2769
|
+
type: indicator.type,
|
|
2770
|
+
visible: indicator.visible,
|
|
2771
|
+
pane: indicator.pane,
|
|
2772
|
+
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
|
|
2773
|
+
zIndex: indicator.zIndex,
|
|
2774
|
+
excludeFromAutoscale: indicator.excludeFromAutoscale,
|
|
2775
|
+
overlayScaleWeight: indicator.overlayScaleWeight,
|
|
2776
|
+
inputs: { ...indicator.inputs }
|
|
2777
|
+
}));
|
|
2778
|
+
};
|
|
2577
2779
|
const setIndicators = (nextIndicators) => {
|
|
2578
2780
|
indicators = nextIndicators.map((indicator) => normalizeIndicatorState(indicator));
|
|
2579
2781
|
draw();
|
|
@@ -2603,6 +2805,9 @@ function createChart(element, options = {}) {
|
|
|
2603
2805
|
visible: patch.visible ?? indicator.visible,
|
|
2604
2806
|
pane: patch.pane ?? indicator.pane ?? plugin?.pane ?? "overlay",
|
|
2605
2807
|
...patch.paneHeightRatio !== void 0 || indicator.paneHeightRatio !== void 0 ? { paneHeightRatio: patch.paneHeightRatio ?? indicator.paneHeightRatio } : {},
|
|
2808
|
+
zIndex: patch.zIndex === void 0 ? indicator.zIndex : Math.round(Number(patch.zIndex) || 0),
|
|
2809
|
+
excludeFromAutoscale: patch.excludeFromAutoscale ?? indicator.excludeFromAutoscale,
|
|
2810
|
+
overlayScaleWeight: patch.overlayScaleWeight === void 0 ? indicator.overlayScaleWeight : Math.min(1, Math.max(0, Number(patch.overlayScaleWeight) || 0)),
|
|
2606
2811
|
type: patch.type ?? indicator.type,
|
|
2607
2812
|
inputs: {
|
|
2608
2813
|
...indicator.inputs,
|
|
@@ -2652,6 +2857,8 @@ function createChart(element, options = {}) {
|
|
|
2652
2857
|
setDoubleClickAction,
|
|
2653
2858
|
registerIndicator,
|
|
2654
2859
|
unregisterIndicator,
|
|
2860
|
+
listBuiltInIndicators,
|
|
2861
|
+
getIndicators,
|
|
2655
2862
|
addIndicator,
|
|
2656
2863
|
updateIndicator,
|
|
2657
2864
|
removeIndicator,
|
|
@@ -4,6 +4,8 @@ interface ChartOptions {
|
|
|
4
4
|
backgroundColor?: string;
|
|
5
5
|
axisColor?: string;
|
|
6
6
|
axis?: AxisOptions;
|
|
7
|
+
xAxis?: AxisOptions;
|
|
8
|
+
yAxis?: AxisOptions;
|
|
7
9
|
priceDecimals?: number;
|
|
8
10
|
stabilizePriceLabels?: boolean;
|
|
9
11
|
priceLabelMinIntegerDigits?: number;
|
|
@@ -22,6 +24,7 @@ interface ChartOptions {
|
|
|
22
24
|
candleBodyWidthRatio?: number;
|
|
23
25
|
candleMinWidth?: number;
|
|
24
26
|
candleWickWidth?: number;
|
|
27
|
+
tickSize?: number;
|
|
25
28
|
candleColorMode?: "openClose" | "prevClose";
|
|
26
29
|
candleColorEpsilon?: number;
|
|
27
30
|
autoScaleSmoothing?: number;
|
|
@@ -44,6 +47,9 @@ interface IndicatorInstanceOptions<TInputs extends Record<string, unknown> = Rec
|
|
|
44
47
|
visible?: boolean;
|
|
45
48
|
pane?: IndicatorPane;
|
|
46
49
|
paneHeightRatio?: number;
|
|
50
|
+
zIndex?: number;
|
|
51
|
+
excludeFromAutoscale?: boolean;
|
|
52
|
+
overlayScaleWeight?: number;
|
|
47
53
|
inputs?: Partial<TInputs>;
|
|
48
54
|
}
|
|
49
55
|
interface IndicatorRenderContext {
|
|
@@ -80,6 +86,12 @@ interface IndicatorPlugin<TInputs extends Record<string, unknown> = Record<strin
|
|
|
80
86
|
defaultInputs?: TInputs;
|
|
81
87
|
draw: (ctx: CanvasRenderingContext2D, renderContext: IndicatorRenderContext, inputs: TInputs) => void;
|
|
82
88
|
}
|
|
89
|
+
interface BuiltInIndicatorInfo {
|
|
90
|
+
id: string;
|
|
91
|
+
name: string;
|
|
92
|
+
pane: IndicatorPane;
|
|
93
|
+
paneHeightRatio?: number;
|
|
94
|
+
}
|
|
83
95
|
interface DashPatternOptions {
|
|
84
96
|
dotted: [number, number];
|
|
85
97
|
dashed: [number, number];
|
|
@@ -271,6 +283,8 @@ interface ChartInstance {
|
|
|
271
283
|
setDoubleClickAction: (action: "reset" | "placeLimitOrder") => void;
|
|
272
284
|
registerIndicator: (plugin: IndicatorPlugin<any>) => void;
|
|
273
285
|
unregisterIndicator: (type: string) => void;
|
|
286
|
+
listBuiltInIndicators: () => BuiltInIndicatorInfo[];
|
|
287
|
+
getIndicators: () => IndicatorInstanceOptions[];
|
|
274
288
|
addIndicator: (type: string, inputs?: Record<string, unknown>, options?: Partial<IndicatorInstanceOptions>) => string;
|
|
275
289
|
updateIndicator: (id: string, patch: Partial<IndicatorInstanceOptions>) => void;
|
|
276
290
|
removeIndicator: (id: string) => void;
|
|
@@ -288,4 +302,4 @@ interface OhlcDataPoint {
|
|
|
288
302
|
}
|
|
289
303
|
declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
|
|
290
304
|
|
|
291
|
-
export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
|
|
305
|
+
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
|