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/dist/index.js
CHANGED
|
@@ -109,6 +109,8 @@ var DEFAULT_OPTIONS = {
|
|
|
109
109
|
backgroundColor: "#101114",
|
|
110
110
|
axisColor: "#7f8289",
|
|
111
111
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
112
|
+
xAxis: DEFAULT_AXIS_OPTIONS,
|
|
113
|
+
yAxis: DEFAULT_AXIS_OPTIONS,
|
|
112
114
|
priceDecimals: 2,
|
|
113
115
|
stabilizePriceLabels: true,
|
|
114
116
|
priceLabelMinIntegerDigits: 3,
|
|
@@ -127,6 +129,7 @@ var DEFAULT_OPTIONS = {
|
|
|
127
129
|
candleBodyWidthRatio: 0.7,
|
|
128
130
|
candleMinWidth: 0.5,
|
|
129
131
|
candleWickWidth: 1,
|
|
132
|
+
tickSize: 0,
|
|
130
133
|
candleColorMode: "openClose",
|
|
131
134
|
candleColorEpsilon: -1,
|
|
132
135
|
autoScaleSmoothing: 0.16,
|
|
@@ -364,6 +367,35 @@ var computeAtrSeries = (data, length) => {
|
|
|
364
367
|
}
|
|
365
368
|
return result;
|
|
366
369
|
};
|
|
370
|
+
var builtInSeriesCache = /* @__PURE__ */ new Map();
|
|
371
|
+
var getSeriesFingerprint = (data) => {
|
|
372
|
+
const length = data.length;
|
|
373
|
+
const last = length > 0 ? data[length - 1] : void 0;
|
|
374
|
+
const prev = length > 1 ? data[length - 2] : void 0;
|
|
375
|
+
if (!last) return "empty";
|
|
376
|
+
return [
|
|
377
|
+
length,
|
|
378
|
+
last.time.getTime(),
|
|
379
|
+
last.o,
|
|
380
|
+
last.h,
|
|
381
|
+
last.l,
|
|
382
|
+
last.c,
|
|
383
|
+
last.v ?? "",
|
|
384
|
+
prev?.time.getTime() ?? "",
|
|
385
|
+
prev?.c ?? "",
|
|
386
|
+
prev?.v ?? ""
|
|
387
|
+
].join("|");
|
|
388
|
+
};
|
|
389
|
+
var withCachedSeries = (key, data, compute) => {
|
|
390
|
+
const fingerprint = getSeriesFingerprint(data);
|
|
391
|
+
const existing = builtInSeriesCache.get(key);
|
|
392
|
+
if (existing && existing.fingerprint === fingerprint) {
|
|
393
|
+
return existing.values;
|
|
394
|
+
}
|
|
395
|
+
const values = compute();
|
|
396
|
+
builtInSeriesCache.set(key, { fingerprint, values });
|
|
397
|
+
return values;
|
|
398
|
+
};
|
|
367
399
|
var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
368
400
|
if (!renderContext.yFromPrice) return;
|
|
369
401
|
const yFromPrice = renderContext.yFromPrice;
|
|
@@ -453,6 +485,15 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
|
|
|
453
485
|
}
|
|
454
486
|
ctx.restore();
|
|
455
487
|
};
|
|
488
|
+
var getPercentileValue = (values, percentile) => {
|
|
489
|
+
if (values.length === 0) {
|
|
490
|
+
return 1;
|
|
491
|
+
}
|
|
492
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
493
|
+
const clamped = Math.min(1, Math.max(0, percentile));
|
|
494
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(clamped * sorted.length) - 1));
|
|
495
|
+
return sorted[index] ?? 1;
|
|
496
|
+
};
|
|
456
497
|
var BUILTIN_VOLUME_INDICATOR = {
|
|
457
498
|
id: "volume",
|
|
458
499
|
name: "Volume",
|
|
@@ -461,8 +502,12 @@ var BUILTIN_VOLUME_INDICATOR = {
|
|
|
461
502
|
defaultInputs: {
|
|
462
503
|
upOpacity: 0.7,
|
|
463
504
|
downOpacity: 0.7,
|
|
505
|
+
upColor: "",
|
|
506
|
+
downColor: "",
|
|
464
507
|
minBarWidth: 1,
|
|
465
|
-
overlayHeightRatio: 0.22
|
|
508
|
+
overlayHeightRatio: 0.22,
|
|
509
|
+
scaleMode: "visible",
|
|
510
|
+
clampPercentile: 1
|
|
466
511
|
},
|
|
467
512
|
draw: (ctx, renderContext, inputs) => {
|
|
468
513
|
const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
|
|
@@ -471,8 +516,11 @@ var BUILTIN_VOLUME_INDICATOR = {
|
|
|
471
516
|
const overlayHeightRatio = Math.min(0.45, Math.max(0.08, Number(inputs.overlayHeightRatio) || 0.22));
|
|
472
517
|
const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
|
|
473
518
|
const paneBottom = chartBottom;
|
|
474
|
-
const
|
|
475
|
-
const
|
|
519
|
+
const scaleMode = inputs.scaleMode === "full" ? "full" : "visible";
|
|
520
|
+
const scalingPoints = scaleMode === "full" ? data : data.slice(startIndex, endIndex + 1);
|
|
521
|
+
const scalingVolumes = scalingPoints.map((point) => Math.max(0, point.v ?? 0)).filter((value) => value > 0);
|
|
522
|
+
const clampPercentile = Number.isFinite(inputs.clampPercentile) ? Number(inputs.clampPercentile) : 1;
|
|
523
|
+
const maxVolume = Math.max(1, getPercentileValue(scalingVolumes, clampPercentile));
|
|
476
524
|
const barWidth = Math.max(
|
|
477
525
|
Math.max(1, Number(inputs.minBarWidth) || 1),
|
|
478
526
|
Math.min(Math.max(1, candleSpacing - 1), Math.floor(candleSpacing * 0.7))
|
|
@@ -491,9 +539,11 @@ var BUILTIN_VOLUME_INDICATOR = {
|
|
|
491
539
|
const barY = Math.round(paneBottom - volumeHeight);
|
|
492
540
|
const direction = renderContext.getCandleDirectionByIndex(index);
|
|
493
541
|
const opacity = direction === "up" ? upOpacity : downOpacity;
|
|
542
|
+
const upBarColor = inputs.upColor && inputs.upColor.trim().length > 0 ? inputs.upColor : upColor;
|
|
543
|
+
const downBarColor = inputs.downColor && inputs.downColor.trim().length > 0 ? inputs.downColor : downColor;
|
|
494
544
|
ctx.save();
|
|
495
545
|
ctx.globalAlpha = opacity;
|
|
496
|
-
ctx.fillStyle = direction === "up" ?
|
|
546
|
+
ctx.fillStyle = direction === "up" ? upBarColor : downBarColor;
|
|
497
547
|
ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
|
|
498
548
|
ctx.restore();
|
|
499
549
|
}
|
|
@@ -506,7 +556,11 @@ var BUILTIN_SMA_INDICATOR = {
|
|
|
506
556
|
defaultInputs: { length: 20, source: "close", color: "#60a5fa", width: 2 },
|
|
507
557
|
draw: (ctx, renderContext, inputs) => {
|
|
508
558
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
509
|
-
const values =
|
|
559
|
+
const values = withCachedSeries(
|
|
560
|
+
`sma|${length}|${inputs.source ?? "close"}`,
|
|
561
|
+
renderContext.data,
|
|
562
|
+
() => computeSmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
563
|
+
);
|
|
510
564
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
|
|
511
565
|
}
|
|
512
566
|
};
|
|
@@ -517,7 +571,11 @@ var BUILTIN_EMA_INDICATOR = {
|
|
|
517
571
|
defaultInputs: { length: 20, source: "close", color: "#f59e0b", width: 2 },
|
|
518
572
|
draw: (ctx, renderContext, inputs) => {
|
|
519
573
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
520
|
-
const values =
|
|
574
|
+
const values = withCachedSeries(
|
|
575
|
+
`ema|${length}|${inputs.source ?? "close"}`,
|
|
576
|
+
renderContext.data,
|
|
577
|
+
() => computeEmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
578
|
+
);
|
|
521
579
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
522
580
|
}
|
|
523
581
|
};
|
|
@@ -528,7 +586,11 @@ var BUILTIN_WMA_INDICATOR = {
|
|
|
528
586
|
defaultInputs: { length: 20, source: "close", color: "#a78bfa", width: 2 },
|
|
529
587
|
draw: (ctx, renderContext, inputs) => {
|
|
530
588
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
531
|
-
const values =
|
|
589
|
+
const values = withCachedSeries(
|
|
590
|
+
`wma|${length}|${inputs.source ?? "close"}`,
|
|
591
|
+
renderContext.data,
|
|
592
|
+
() => computeWmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
593
|
+
);
|
|
532
594
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
|
|
533
595
|
}
|
|
534
596
|
};
|
|
@@ -539,7 +601,11 @@ var BUILTIN_VWMA_INDICATOR = {
|
|
|
539
601
|
defaultInputs: { length: 20, source: "close", color: "#ef4444", width: 2 },
|
|
540
602
|
draw: (ctx, renderContext, inputs) => {
|
|
541
603
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
542
|
-
const values =
|
|
604
|
+
const values = withCachedSeries(
|
|
605
|
+
`vwma|${length}|${inputs.source ?? "close"}`,
|
|
606
|
+
renderContext.data,
|
|
607
|
+
() => computeVwmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
608
|
+
);
|
|
543
609
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
|
|
544
610
|
}
|
|
545
611
|
};
|
|
@@ -550,7 +616,11 @@ var BUILTIN_RMA_INDICATOR = {
|
|
|
550
616
|
defaultInputs: { length: 14, source: "close", color: "#22c55e", width: 2 },
|
|
551
617
|
draw: (ctx, renderContext, inputs) => {
|
|
552
618
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
553
|
-
const values =
|
|
619
|
+
const values = withCachedSeries(
|
|
620
|
+
`rma|${length}|${inputs.source ?? "close"}`,
|
|
621
|
+
renderContext.data,
|
|
622
|
+
() => computeRmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
623
|
+
);
|
|
554
624
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
|
|
555
625
|
}
|
|
556
626
|
};
|
|
@@ -561,7 +631,11 @@ var BUILTIN_HMA_INDICATOR = {
|
|
|
561
631
|
defaultInputs: { length: 21, source: "close", color: "#14b8a6", width: 2 },
|
|
562
632
|
draw: (ctx, renderContext, inputs) => {
|
|
563
633
|
const length = clampIndicatorLength(inputs.length, 21);
|
|
564
|
-
const values =
|
|
634
|
+
const values = withCachedSeries(
|
|
635
|
+
`hma|${length}|${inputs.source ?? "close"}`,
|
|
636
|
+
renderContext.data,
|
|
637
|
+
() => computeHmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
638
|
+
);
|
|
565
639
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
566
640
|
}
|
|
567
641
|
};
|
|
@@ -573,7 +647,11 @@ var BUILTIN_STDDEV_INDICATOR = {
|
|
|
573
647
|
defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2 },
|
|
574
648
|
draw: (ctx, renderContext, inputs) => {
|
|
575
649
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
576
|
-
const values =
|
|
650
|
+
const values = withCachedSeries(
|
|
651
|
+
`stddev|${length}|${inputs.source ?? "close"}`,
|
|
652
|
+
renderContext.data,
|
|
653
|
+
() => computeStdDevSeries(renderContext.data, length, inputs.source ?? "close")
|
|
654
|
+
);
|
|
577
655
|
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2);
|
|
578
656
|
}
|
|
579
657
|
};
|
|
@@ -585,7 +663,7 @@ var BUILTIN_ATR_INDICATOR = {
|
|
|
585
663
|
defaultInputs: { length: 14, color: "#eab308", width: 2 },
|
|
586
664
|
draw: (ctx, renderContext, inputs) => {
|
|
587
665
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
588
|
-
const values = computeAtrSeries(renderContext.data, length);
|
|
666
|
+
const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
|
|
589
667
|
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2);
|
|
590
668
|
}
|
|
591
669
|
};
|
|
@@ -597,7 +675,7 @@ var BUILTIN_RSI_INDICATOR = {
|
|
|
597
675
|
defaultInputs: { length: 14, color: "#3b82f6", width: 2 },
|
|
598
676
|
draw: (ctx, renderContext, inputs) => {
|
|
599
677
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
600
|
-
const values = computeRsiSeries(renderContext.data, length);
|
|
678
|
+
const values = withCachedSeries(`rsi|${length}`, renderContext.data, () => computeRsiSeries(renderContext.data, length));
|
|
601
679
|
drawSeparateSeries(
|
|
602
680
|
ctx,
|
|
603
681
|
renderContext,
|
|
@@ -631,6 +709,18 @@ function createChart(element, options = {}) {
|
|
|
631
709
|
...options.axis ?? {},
|
|
632
710
|
...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {}
|
|
633
711
|
},
|
|
712
|
+
xAxis: {
|
|
713
|
+
...DEFAULT_AXIS_OPTIONS,
|
|
714
|
+
...options.axis ?? {},
|
|
715
|
+
...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
|
|
716
|
+
...options.xAxis ?? {}
|
|
717
|
+
},
|
|
718
|
+
yAxis: {
|
|
719
|
+
...DEFAULT_AXIS_OPTIONS,
|
|
720
|
+
...options.axis ?? {},
|
|
721
|
+
...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
|
|
722
|
+
...options.yAxis ?? {}
|
|
723
|
+
},
|
|
634
724
|
crosshair: {
|
|
635
725
|
...DEFAULT_CROSSHAIR_OPTIONS,
|
|
636
726
|
...options.crosshair ?? {}
|
|
@@ -686,6 +776,9 @@ function createChart(element, options = {}) {
|
|
|
686
776
|
visible: indicator.visible ?? true,
|
|
687
777
|
pane: indicator.pane ?? plugin?.pane ?? "overlay",
|
|
688
778
|
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
|
|
779
|
+
zIndex: Math.round(Number(indicator.zIndex) || 0),
|
|
780
|
+
excludeFromAutoscale: indicator.excludeFromAutoscale ?? true,
|
|
781
|
+
overlayScaleWeight: Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0.25)),
|
|
689
782
|
inputs: {
|
|
690
783
|
...defaults,
|
|
691
784
|
...indicator.inputs ?? {}
|
|
@@ -841,20 +934,56 @@ function createChart(element, options = {}) {
|
|
|
841
934
|
}
|
|
842
935
|
return { min: nextMin, max: nextMax };
|
|
843
936
|
};
|
|
937
|
+
const getConfiguredPriceDecimals = () => clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
938
|
+
const getConfiguredTickSize = () => {
|
|
939
|
+
const step = Number(mergedOptions.tickSize);
|
|
940
|
+
return Number.isFinite(step) && step > 0 ? step : 0;
|
|
941
|
+
};
|
|
942
|
+
const getTickSizeDecimals = () => {
|
|
943
|
+
const tickSize = getConfiguredTickSize();
|
|
944
|
+
if (tickSize <= 0) {
|
|
945
|
+
return 0;
|
|
946
|
+
}
|
|
947
|
+
let scaled = tickSize;
|
|
948
|
+
let decimals = 0;
|
|
949
|
+
while (decimals < 8 && Math.abs(Math.round(scaled) - scaled) > 1e-10) {
|
|
950
|
+
scaled *= 10;
|
|
951
|
+
decimals += 1;
|
|
952
|
+
}
|
|
953
|
+
return decimals;
|
|
954
|
+
};
|
|
955
|
+
const getDisplayPriceDecimals = () => {
|
|
956
|
+
const configured = getConfiguredPriceDecimals();
|
|
957
|
+
const tickDecimals = getTickSizeDecimals();
|
|
958
|
+
return Math.max(configured, tickDecimals);
|
|
959
|
+
};
|
|
960
|
+
const quantizeToTickSize = (price) => {
|
|
961
|
+
const tickSize = getConfiguredTickSize();
|
|
962
|
+
if (tickSize <= 0 || !Number.isFinite(price)) {
|
|
963
|
+
return price;
|
|
964
|
+
}
|
|
965
|
+
return Math.round(price / tickSize) * tickSize;
|
|
966
|
+
};
|
|
844
967
|
const formatPrice = (price) => {
|
|
845
|
-
const
|
|
846
|
-
|
|
968
|
+
const rounded = quantizeToTickSize(price);
|
|
969
|
+
const decimals = getDisplayPriceDecimals();
|
|
970
|
+
return rounded.toFixed(decimals);
|
|
847
971
|
};
|
|
848
972
|
const roundToPricePrecision = (price) => {
|
|
849
|
-
const
|
|
850
|
-
|
|
973
|
+
const rounded = quantizeToTickSize(price);
|
|
974
|
+
const decimals = getDisplayPriceDecimals();
|
|
975
|
+
return Number(rounded.toFixed(decimals));
|
|
851
976
|
};
|
|
852
977
|
const getResolvedCandleColorEpsilon = () => {
|
|
853
978
|
const configured = mergedOptions.candleColorEpsilon;
|
|
854
979
|
if (configured >= 0) {
|
|
855
980
|
return configured;
|
|
856
981
|
}
|
|
857
|
-
const
|
|
982
|
+
const tickSize = getConfiguredTickSize();
|
|
983
|
+
if (tickSize > 0) {
|
|
984
|
+
return tickSize / 2;
|
|
985
|
+
}
|
|
986
|
+
const decimals = getDisplayPriceDecimals();
|
|
858
987
|
return decimals > 0 ? 0.5 / 10 ** decimals : 0;
|
|
859
988
|
};
|
|
860
989
|
const getDirectionFromDelta = (delta) => {
|
|
@@ -1386,6 +1515,10 @@ function createChart(element, options = {}) {
|
|
|
1386
1515
|
canvas.height = Math.floor(height * pixelRatio);
|
|
1387
1516
|
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
1388
1517
|
const axis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
|
|
1518
|
+
const xAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
|
|
1519
|
+
const yAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
|
|
1520
|
+
const xAxisFontSize = Math.max(8, xAxis.fontSize);
|
|
1521
|
+
const yAxisFontSize = Math.max(8, yAxis.fontSize);
|
|
1389
1522
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1390
1523
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
1391
1524
|
ctx.fillRect(0, 0, width, height);
|
|
@@ -1400,6 +1533,9 @@ function createChart(element, options = {}) {
|
|
|
1400
1533
|
const separatePaneSpacing = 6;
|
|
1401
1534
|
const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1402
1535
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
|
|
1536
|
+
).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
|
|
1537
|
+
const overlayIndicatorsForScale = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1538
|
+
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
1403
1539
|
);
|
|
1404
1540
|
const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
|
|
1405
1541
|
const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
|
|
@@ -1451,8 +1587,47 @@ function createChart(element, options = {}) {
|
|
|
1451
1587
|
}
|
|
1452
1588
|
}
|
|
1453
1589
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1590
|
+
let minPrice = Math.min(...priceSource.map((point) => point.l));
|
|
1591
|
+
let maxPrice = Math.max(...priceSource.map((point) => point.h));
|
|
1592
|
+
if (overlayIndicatorsForScale.length > 0) {
|
|
1593
|
+
for (const { indicator } of overlayIndicatorsForScale) {
|
|
1594
|
+
if (indicator.excludeFromAutoscale) {
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
const type = indicator.type;
|
|
1598
|
+
const inputs = indicator.inputs;
|
|
1599
|
+
const source = inputs.source ?? "close";
|
|
1600
|
+
const length = clampIndicatorLength(inputs.length ?? 14, 14);
|
|
1601
|
+
let series = null;
|
|
1602
|
+
if (type === "sma") series = withCachedSeries(`sma|${length}|${source}`, data, () => computeSmaSeries(data, length, source));
|
|
1603
|
+
if (type === "ema") series = withCachedSeries(`ema|${length}|${source}`, data, () => computeEmaSeries(data, length, source));
|
|
1604
|
+
if (type === "wma") series = withCachedSeries(`wma|${length}|${source}`, data, () => computeWmaSeries(data, length, source));
|
|
1605
|
+
if (type === "vwma") series = withCachedSeries(`vwma|${length}|${source}`, data, () => computeVwmaSeries(data, length, source));
|
|
1606
|
+
if (type === "rma") series = withCachedSeries(`rma|${length}|${source}`, data, () => computeRmaSeries(data, length, source));
|
|
1607
|
+
if (type === "hma") series = withCachedSeries(`hma|${length}|${source}`, data, () => computeHmaSeries(data, length, source));
|
|
1608
|
+
if (!series) {
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
const visibleValues = [];
|
|
1612
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
1613
|
+
const value = series[idx];
|
|
1614
|
+
if (Number.isFinite(value ?? Number.NaN)) {
|
|
1615
|
+
visibleValues.push(value);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
if (visibleValues.length === 0) {
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
const seriesMin = Math.min(...visibleValues);
|
|
1622
|
+
const seriesMax = Math.max(...visibleValues);
|
|
1623
|
+
const weight = Math.min(1, Math.max(0, indicator.overlayScaleWeight));
|
|
1624
|
+
const currentMid = (minPrice + maxPrice) / 2;
|
|
1625
|
+
const weightedMin = currentMid + (seriesMin - currentMid) * weight;
|
|
1626
|
+
const weightedMax = currentMid + (seriesMax - currentMid) * weight;
|
|
1627
|
+
minPrice = Math.min(minPrice, weightedMin);
|
|
1628
|
+
maxPrice = Math.max(maxPrice, weightedMax);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1456
1631
|
const priceRange = maxPrice - minPrice || 1;
|
|
1457
1632
|
const autoMin = minPrice - priceRange * 0.08;
|
|
1458
1633
|
const autoMax = maxPrice + priceRange * 0.08;
|
|
@@ -1612,7 +1787,7 @@ function createChart(element, options = {}) {
|
|
|
1612
1787
|
}
|
|
1613
1788
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1614
1789
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
1615
|
-
);
|
|
1790
|
+
).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
|
|
1616
1791
|
if (activeOverlayIndicators.length > 0) {
|
|
1617
1792
|
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
1618
1793
|
activeOverlayIndicators.forEach(({ indicator, plugin }) => {
|
|
@@ -1722,7 +1897,10 @@ function createChart(element, options = {}) {
|
|
|
1722
1897
|
const ratio = tick / yTicks;
|
|
1723
1898
|
const price = yMin + yRange * ratio;
|
|
1724
1899
|
const y = yFromPrice(price);
|
|
1725
|
-
|
|
1900
|
+
const prevFont = ctx.font;
|
|
1901
|
+
ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
1902
|
+
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
1903
|
+
ctx.font = prevFont;
|
|
1726
1904
|
}
|
|
1727
1905
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
1728
1906
|
const lastPoint = data[data.length - 1];
|
|
@@ -1779,7 +1957,10 @@ function createChart(element, options = {}) {
|
|
|
1779
1957
|
month: "short",
|
|
1780
1958
|
day: "numeric"
|
|
1781
1959
|
});
|
|
1782
|
-
|
|
1960
|
+
const prevFont = ctx.font;
|
|
1961
|
+
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
1962
|
+
drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
|
|
1963
|
+
ctx.font = prevFont;
|
|
1783
1964
|
}
|
|
1784
1965
|
if (crosshair.visible && crosshairPoint) {
|
|
1785
1966
|
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
@@ -2550,6 +2731,27 @@ function createChart(element, options = {}) {
|
|
|
2550
2731
|
indicators = indicators.filter((indicator) => indicator.type !== type);
|
|
2551
2732
|
draw();
|
|
2552
2733
|
};
|
|
2734
|
+
const listBuiltInIndicators = () => {
|
|
2735
|
+
return BUILTIN_INDICATORS.map((indicator) => ({
|
|
2736
|
+
id: indicator.id,
|
|
2737
|
+
name: indicator.name,
|
|
2738
|
+
pane: indicator.pane ?? "overlay",
|
|
2739
|
+
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio }
|
|
2740
|
+
}));
|
|
2741
|
+
};
|
|
2742
|
+
const getIndicators = () => {
|
|
2743
|
+
return indicators.map((indicator) => ({
|
|
2744
|
+
id: indicator.id,
|
|
2745
|
+
type: indicator.type,
|
|
2746
|
+
visible: indicator.visible,
|
|
2747
|
+
pane: indicator.pane,
|
|
2748
|
+
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
|
|
2749
|
+
zIndex: indicator.zIndex,
|
|
2750
|
+
excludeFromAutoscale: indicator.excludeFromAutoscale,
|
|
2751
|
+
overlayScaleWeight: indicator.overlayScaleWeight,
|
|
2752
|
+
inputs: { ...indicator.inputs }
|
|
2753
|
+
}));
|
|
2754
|
+
};
|
|
2553
2755
|
const setIndicators = (nextIndicators) => {
|
|
2554
2756
|
indicators = nextIndicators.map((indicator) => normalizeIndicatorState(indicator));
|
|
2555
2757
|
draw();
|
|
@@ -2579,6 +2781,9 @@ function createChart(element, options = {}) {
|
|
|
2579
2781
|
visible: patch.visible ?? indicator.visible,
|
|
2580
2782
|
pane: patch.pane ?? indicator.pane ?? plugin?.pane ?? "overlay",
|
|
2581
2783
|
...patch.paneHeightRatio !== void 0 || indicator.paneHeightRatio !== void 0 ? { paneHeightRatio: patch.paneHeightRatio ?? indicator.paneHeightRatio } : {},
|
|
2784
|
+
zIndex: patch.zIndex === void 0 ? indicator.zIndex : Math.round(Number(patch.zIndex) || 0),
|
|
2785
|
+
excludeFromAutoscale: patch.excludeFromAutoscale ?? indicator.excludeFromAutoscale,
|
|
2786
|
+
overlayScaleWeight: patch.overlayScaleWeight === void 0 ? indicator.overlayScaleWeight : Math.min(1, Math.max(0, Number(patch.overlayScaleWeight) || 0)),
|
|
2582
2787
|
type: patch.type ?? indicator.type,
|
|
2583
2788
|
inputs: {
|
|
2584
2789
|
...indicator.inputs,
|
|
@@ -2628,6 +2833,8 @@ function createChart(element, options = {}) {
|
|
|
2628
2833
|
setDoubleClickAction,
|
|
2629
2834
|
registerIndicator,
|
|
2630
2835
|
unregisterIndicator,
|
|
2836
|
+
listBuiltInIndicators,
|
|
2837
|
+
getIndicators,
|
|
2631
2838
|
addIndicator,
|
|
2632
2839
|
updateIndicator,
|
|
2633
2840
|
removeIndicator,
|
package/docs/API.md
CHANGED
|
@@ -32,6 +32,8 @@ Top-level options:
|
|
|
32
32
|
- `backgroundColor` (default `#101114`)
|
|
33
33
|
- `axisColor` (legacy shorthand for axis line/text color)
|
|
34
34
|
- `axis?: AxisOptions`
|
|
35
|
+
- `xAxis?: AxisOptions` (overrides bottom-axis label text/font)
|
|
36
|
+
- `yAxis?: AxisOptions` (overrides right-axis label text/font)
|
|
35
37
|
- `priceDecimals` (default `2`, used for axis/ticker/line price labels)
|
|
36
38
|
- `stabilizePriceLabels` (default `true`, prevents ticker/crosshair/price-tag width jitter)
|
|
37
39
|
- `priceLabelMinIntegerDigits` (default `3`, baseline integer-digit width for stabilized labels)
|
|
@@ -50,6 +52,7 @@ Top-level options:
|
|
|
50
52
|
- `candleBodyWidthRatio` (default `0.7`)
|
|
51
53
|
- `candleMinWidth` (default `0.5`)
|
|
52
54
|
- `candleWickWidth` (default `1`)
|
|
55
|
+
- `tickSize` (default `0`; when > 0, formatter/pointer prices snap to tick)
|
|
53
56
|
- `candleColorMode` (`"openClose" | "prevClose"`, default `"openClose"`)
|
|
54
57
|
- `candleColorEpsilon` (default `-1` = auto from `priceDecimals`; set `0` to disable tolerance)
|
|
55
58
|
- `autoScaleSmoothing` (default `0.16`)
|
|
@@ -248,6 +251,9 @@ Connector/fill visuals:
|
|
|
248
251
|
- `visible?: boolean` (default `true`)
|
|
249
252
|
- `pane?: "overlay" | "separate"`
|
|
250
253
|
- `paneHeightRatio?: number` (for separate panes; `0.08` to `0.45` recommended)
|
|
254
|
+
- `zIndex?: number` (render order; lower first)
|
|
255
|
+
- `excludeFromAutoscale?: boolean` (default `true` for indicator instances)
|
|
256
|
+
- `overlayScaleWeight?: number` (`0..1`; only used when `excludeFromAutoscale` is `false`)
|
|
251
257
|
- `inputs?: Record<string, unknown>` (plugin-specific parameters)
|
|
252
258
|
|
|
253
259
|
### `IndicatorPlugin`
|
|
@@ -269,7 +275,7 @@ Connector/fill visuals:
|
|
|
269
275
|
|
|
270
276
|
Built-in:
|
|
271
277
|
|
|
272
|
-
- `"volume"`:
|
|
278
|
+
- `"volume"`: overlay histogram by default (uses `OhlcDataPoint.v`; can be moved to separate pane)
|
|
273
279
|
- `"sma"`: Simple Moving Average (overlay)
|
|
274
280
|
- `"ema"`: Exponential Moving Average (overlay)
|
|
275
281
|
- `"rsi"`: Relative Strength Index (separate pane, 30/50/70 guides)
|
|
@@ -297,6 +303,15 @@ Volume/VWMA note:
|
|
|
297
303
|
|
|
298
304
|
- if your data has no `v`, `volume`/`vwma` will have limited or no output.
|
|
299
305
|
|
|
306
|
+
Volume style inputs:
|
|
307
|
+
|
|
308
|
+
- `upOpacity`, `downOpacity`
|
|
309
|
+
- `upColor`, `downColor`
|
|
310
|
+
- `minBarWidth`
|
|
311
|
+
- `overlayHeightRatio` (when used as overlay)
|
|
312
|
+
- `scaleMode` (`"visible"` default, or `"full"`)
|
|
313
|
+
- `clampPercentile` (`0..1`, default `1`; e.g. `0.95` to reduce outlier crush)
|
|
314
|
+
|
|
300
315
|
---
|
|
301
316
|
|
|
302
317
|
## `ChartInstance` Methods
|
|
@@ -325,6 +340,8 @@ Volume/VWMA note:
|
|
|
325
340
|
- `setDoubleClickAction(action: "reset" | "placeLimitOrder"): void`
|
|
326
341
|
- `registerIndicator(plugin: IndicatorPlugin): void`
|
|
327
342
|
- `unregisterIndicator(type: string): void`
|
|
343
|
+
- `listBuiltInIndicators(): BuiltInIndicatorInfo[]`
|
|
344
|
+
- `getIndicators(): IndicatorInstanceOptions[]`
|
|
328
345
|
- `addIndicator(type: string, inputs?: Record<string, unknown>, options?: Partial<IndicatorInstanceOptions>): string`
|
|
329
346
|
- `updateIndicator(id: string, patch: Partial<IndicatorInstanceOptions>): void`
|
|
330
347
|
- `removeIndicator(id: string): void`
|
package/docs/RECIPES.md
CHANGED
|
@@ -26,6 +26,25 @@ Use:
|
|
|
26
26
|
- `autoScaleSmoothing` for smoother scale transitions
|
|
27
27
|
- `autoScaleIgnoreLatestCandle` to reduce live-candle jitter
|
|
28
28
|
|
|
29
|
+
## Tick-size aware chart precision
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const chart = createChart(rootEl, {
|
|
33
|
+
tickSize: 0.25,
|
|
34
|
+
priceDecimals: 2
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Style x-axis and y-axis labels differently
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const chart = createChart(rootEl, {
|
|
42
|
+
axis: { lineColor: "#374151" },
|
|
43
|
+
xAxis: { textColor: "#9ca3af", fontSize: 11 },
|
|
44
|
+
yAxis: { textColor: "#e5e7eb", fontSize: 12 }
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
29
48
|
## Stabilize candle up/down coloring on tiny deltas
|
|
30
49
|
|
|
31
50
|
```ts
|
|
@@ -119,6 +138,15 @@ const rsiId = chart.addIndicator("rsi", { length: 14 }, { pane: "separate", pane
|
|
|
119
138
|
const volumeId = chart.addIndicator("volume", { upOpacity: 0.72, downOpacity: 0.72 });
|
|
120
139
|
```
|
|
121
140
|
|
|
141
|
+
## Prevent one volume spike from crushing all bars
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const volumeId = chart.addIndicator("volume", {
|
|
145
|
+
scaleMode: "visible", // default behavior
|
|
146
|
+
clampPercentile: 0.95 // clamp scaling reference to p95 of visible bars
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
122
150
|
Available built-ins:
|
|
123
151
|
|
|
124
152
|
- `volume`, `sma`, `ema`, `rsi`, `wma`, `vwma`, `rma`, `hma`, `stddev`, `atr`
|
|
@@ -138,6 +166,26 @@ Recommended range:
|
|
|
138
166
|
|
|
139
167
|
- `0.08` to `0.45` (library clamps to safe bounds)
|
|
140
168
|
|
|
169
|
+
## Use indicator z-order and autoscale influence
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
chart.addIndicator("ema", { length: 34 }, { zIndex: 5 });
|
|
173
|
+
chart.addIndicator("wma", { length: 55 }, { zIndex: 10 });
|
|
174
|
+
|
|
175
|
+
chart.addIndicator("sma", { length: 200 }, {
|
|
176
|
+
excludeFromAutoscale: false,
|
|
177
|
+
overlayScaleWeight: 0.25
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Query built-ins and active indicator instances
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const builtIns = chart.listBuiltInIndicators();
|
|
185
|
+
const active = chart.getIndicators();
|
|
186
|
+
console.log({ builtIns, active });
|
|
187
|
+
```
|
|
188
|
+
|
|
141
189
|
## Remove or hide indicators
|
|
142
190
|
|
|
143
191
|
```ts
|