hyperprop-charting-library 0.1.23 → 0.1.24
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 +13 -0
- package/dist/hyperprop-charting-library.cjs +82 -20
- package/dist/hyperprop-charting-library.d.ts +3 -0
- package/dist/hyperprop-charting-library.js +82 -20
- package/dist/index.cjs +82 -20
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +82 -20
- package/docs/API.md +2 -0
- package/docs/RECIPES.md +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,19 @@ const chart = createChart(root, {
|
|
|
60
60
|
});
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
## Candle Color Behavior
|
|
64
|
+
|
|
65
|
+
You can control how up/down candle color is decided:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const chart = createChart(root, {
|
|
69
|
+
// "openClose" (default) or "prevClose"
|
|
70
|
+
candleColorMode: "prevClose",
|
|
71
|
+
// -1 = auto epsilon from priceDecimals, 0 = strict compare
|
|
72
|
+
candleColorEpsilon: -1
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
63
76
|
## Crosshair "+" Button
|
|
64
77
|
|
|
65
78
|
```ts
|
|
@@ -151,6 +151,8 @@ var DEFAULT_OPTIONS = {
|
|
|
151
151
|
candleBodyWidthRatio: 0.7,
|
|
152
152
|
candleMinWidth: 0.5,
|
|
153
153
|
candleWickWidth: 1,
|
|
154
|
+
candleColorMode: "openClose",
|
|
155
|
+
candleColorEpsilon: -1,
|
|
154
156
|
autoScaleSmoothing: 0.16,
|
|
155
157
|
autoScaleIgnoreLatestCandle: true,
|
|
156
158
|
doubleClickEnabled: true,
|
|
@@ -511,11 +513,11 @@ var BUILTIN_VOLUME_INDICATOR = {
|
|
|
511
513
|
const xCenter = xFromIndex(index);
|
|
512
514
|
const barX = Math.round(xCenter - barWidth / 2);
|
|
513
515
|
const barY = Math.round(paneBottom - volumeHeight);
|
|
514
|
-
const
|
|
515
|
-
const opacity =
|
|
516
|
+
const direction = renderContext.getCandleDirectionByIndex(index);
|
|
517
|
+
const opacity = direction === "up" ? upOpacity : downOpacity;
|
|
516
518
|
ctx.save();
|
|
517
519
|
ctx.globalAlpha = opacity;
|
|
518
|
-
ctx.fillStyle =
|
|
520
|
+
ctx.fillStyle = direction === "up" ? upColor : downColor;
|
|
519
521
|
ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
|
|
520
522
|
ctx.restore();
|
|
521
523
|
}
|
|
@@ -867,6 +869,24 @@ function createChart(element, options = {}) {
|
|
|
867
869
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
868
870
|
return price.toFixed(decimals);
|
|
869
871
|
};
|
|
872
|
+
const roundToPricePrecision = (price) => {
|
|
873
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
874
|
+
return Number(price.toFixed(decimals));
|
|
875
|
+
};
|
|
876
|
+
const getResolvedCandleColorEpsilon = () => {
|
|
877
|
+
const configured = mergedOptions.candleColorEpsilon;
|
|
878
|
+
if (configured >= 0) {
|
|
879
|
+
return configured;
|
|
880
|
+
}
|
|
881
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
882
|
+
return decimals > 0 ? 0.5 / 10 ** decimals : 0;
|
|
883
|
+
};
|
|
884
|
+
const getDirectionFromDelta = (delta) => {
|
|
885
|
+
const epsilon = Math.max(0, getResolvedCandleColorEpsilon());
|
|
886
|
+
if (delta > epsilon) return 1;
|
|
887
|
+
if (delta < -epsilon) return -1;
|
|
888
|
+
return 0;
|
|
889
|
+
};
|
|
870
890
|
const getStabilizedPriceTemplate = () => {
|
|
871
891
|
const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
|
|
872
892
|
if (explicitTemplate.length > 0) {
|
|
@@ -893,14 +913,33 @@ function createChart(element, options = {}) {
|
|
|
893
913
|
return Math.max(measured, templateWidth);
|
|
894
914
|
};
|
|
895
915
|
const parseData = (nextData) => {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
916
|
+
const dedupedByTime = /* @__PURE__ */ new Map();
|
|
917
|
+
for (const point of nextData) {
|
|
918
|
+
const time = new Date(point.t);
|
|
919
|
+
const timeMs = time.getTime();
|
|
920
|
+
if (!Number.isFinite(timeMs)) {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
const open = Number(point.o);
|
|
924
|
+
const close = Number(point.c);
|
|
925
|
+
const highInput = Number(point.h);
|
|
926
|
+
const lowInput = Number(point.l);
|
|
927
|
+
if (!Number.isFinite(open) || !Number.isFinite(close) || !Number.isFinite(highInput) || !Number.isFinite(lowInput)) {
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
const normalizedHigh = Math.max(highInput, open, close);
|
|
931
|
+
const normalizedLow = Math.min(lowInput, open, close);
|
|
932
|
+
const volumeValue = point.v === void 0 ? void 0 : Number(point.v);
|
|
933
|
+
dedupedByTime.set(timeMs, {
|
|
934
|
+
time,
|
|
935
|
+
o: open,
|
|
936
|
+
h: normalizedHigh,
|
|
937
|
+
l: normalizedLow,
|
|
938
|
+
c: close,
|
|
939
|
+
...volumeValue === void 0 || !Number.isFinite(volumeValue) ? {} : { v: volumeValue }
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
return Array.from(dedupedByTime.values()).sort((a, b) => a.time.getTime() - b.time.getTime());
|
|
904
943
|
};
|
|
905
944
|
const getTimeStepMs = () => {
|
|
906
945
|
if (data.length < 2) {
|
|
@@ -977,6 +1016,26 @@ function createChart(element, options = {}) {
|
|
|
977
1016
|
const upperDelta = Math.abs(upperPoint.time.getTime() - timeMs);
|
|
978
1017
|
return lowerDelta <= upperDelta ? lower : upper;
|
|
979
1018
|
};
|
|
1019
|
+
const getCandleDirectionByIndex = (index) => {
|
|
1020
|
+
const point = data[index];
|
|
1021
|
+
if (!point) {
|
|
1022
|
+
return "up";
|
|
1023
|
+
}
|
|
1024
|
+
const prevPoint = index > 0 ? data[index - 1] : void 0;
|
|
1025
|
+
const mode = mergedOptions.candleColorMode;
|
|
1026
|
+
const baseForMode = mode === "prevClose" && prevPoint ? prevPoint.c : point.o;
|
|
1027
|
+
let direction = getDirectionFromDelta(point.c - baseForMode);
|
|
1028
|
+
if (direction === 0 && mode === "prevClose") {
|
|
1029
|
+
direction = getDirectionFromDelta(point.c - point.o);
|
|
1030
|
+
}
|
|
1031
|
+
if (direction === 0 && prevPoint) {
|
|
1032
|
+
direction = getDirectionFromDelta(point.c - prevPoint.c);
|
|
1033
|
+
}
|
|
1034
|
+
if (direction === 0) {
|
|
1035
|
+
return point.c >= point.o ? "up" : "down";
|
|
1036
|
+
}
|
|
1037
|
+
return direction > 0 ? "up" : "down";
|
|
1038
|
+
};
|
|
980
1039
|
const formatHoverTimeLabel = (time, mode) => {
|
|
981
1040
|
if (mode === "time") {
|
|
982
1041
|
return time.toLocaleTimeString(void 0, {
|
|
@@ -1562,8 +1621,8 @@ function createChart(element, options = {}) {
|
|
|
1562
1621
|
const closeY = yFromPrice(point.c);
|
|
1563
1622
|
const highY = yFromPrice(point.h);
|
|
1564
1623
|
const lowY = yFromPrice(point.l);
|
|
1565
|
-
const
|
|
1566
|
-
const candleColor =
|
|
1624
|
+
const direction = getCandleDirectionByIndex(index);
|
|
1625
|
+
const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
|
|
1567
1626
|
ctx.strokeStyle = candleColor;
|
|
1568
1627
|
ctx.lineWidth = candleWickWidth;
|
|
1569
1628
|
ctx.beginPath();
|
|
@@ -1597,6 +1656,7 @@ function createChart(element, options = {}) {
|
|
|
1597
1656
|
chartHeight,
|
|
1598
1657
|
xFromIndex,
|
|
1599
1658
|
yFromPrice,
|
|
1659
|
+
getCandleDirectionByIndex,
|
|
1600
1660
|
candleSpacing,
|
|
1601
1661
|
upColor: mergedOptions.upColor,
|
|
1602
1662
|
downColor: mergedOptions.downColor
|
|
@@ -1656,6 +1716,7 @@ function createChart(element, options = {}) {
|
|
|
1656
1716
|
chartHeight: paneHeight,
|
|
1657
1717
|
xFromIndex,
|
|
1658
1718
|
yFromPrice: null,
|
|
1719
|
+
getCandleDirectionByIndex,
|
|
1659
1720
|
candleSpacing,
|
|
1660
1721
|
upColor: mergedOptions.upColor,
|
|
1661
1722
|
downColor: mergedOptions.downColor
|
|
@@ -1693,7 +1754,8 @@ function createChart(element, options = {}) {
|
|
|
1693
1754
|
const tickerPrice = lastPoint.c;
|
|
1694
1755
|
const tickerY = yFromPrice(tickerPrice);
|
|
1695
1756
|
const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
|
|
1696
|
-
const
|
|
1757
|
+
const lastDirection = getCandleDirectionByIndex(data.length - 1);
|
|
1758
|
+
const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
|
|
1697
1759
|
const tickerThickness = Math.max(1, ticker.thickness ?? 1);
|
|
1698
1760
|
const tickerStyle = ticker.style ?? "solid";
|
|
1699
1761
|
ctx.save();
|
|
@@ -2061,7 +2123,7 @@ function createChart(element, options = {}) {
|
|
|
2061
2123
|
x,
|
|
2062
2124
|
y,
|
|
2063
2125
|
region,
|
|
2064
|
-
...region === "plot" ? { price:
|
|
2126
|
+
...region === "plot" ? { price: roundToPricePrecision(priceFromCanvasY(y)) } : {},
|
|
2065
2127
|
...index === null ? {} : { index },
|
|
2066
2128
|
...hoverTime ? { time: hoverTime.toISOString() } : {},
|
|
2067
2129
|
...point ? { point } : {}
|
|
@@ -2107,7 +2169,7 @@ function createChart(element, options = {}) {
|
|
|
2107
2169
|
if (orderRegion) {
|
|
2108
2170
|
if (orderRegion.draggable) {
|
|
2109
2171
|
activePointerId = event.pointerId;
|
|
2110
|
-
const startPrice =
|
|
2172
|
+
const startPrice = roundToPricePrecision(orderRegion.line.price);
|
|
2111
2173
|
actionDragState = {
|
|
2112
2174
|
orderId: orderRegion.orderId,
|
|
2113
2175
|
action: orderRegion.action,
|
|
@@ -2166,7 +2228,7 @@ function createChart(element, options = {}) {
|
|
|
2166
2228
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
2167
2229
|
return;
|
|
2168
2230
|
}
|
|
2169
|
-
const nextPrice =
|
|
2231
|
+
const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
|
|
2170
2232
|
if (nextPrice !== orderDragState.lastPrice) {
|
|
2171
2233
|
orderDragState.lastPrice = nextPrice;
|
|
2172
2234
|
orderLines = orderLines.map((line) => {
|
|
@@ -2196,7 +2258,7 @@ function createChart(element, options = {}) {
|
|
|
2196
2258
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
2197
2259
|
return;
|
|
2198
2260
|
}
|
|
2199
|
-
const nextPrice =
|
|
2261
|
+
const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
|
|
2200
2262
|
if (nextPrice !== actionDragState.lastPrice) {
|
|
2201
2263
|
actionDragState.lastPrice = nextPrice;
|
|
2202
2264
|
actionDragState.moved = true;
|
|
@@ -2313,7 +2375,7 @@ function createChart(element, options = {}) {
|
|
|
2313
2375
|
canvas.style.cursor = "default";
|
|
2314
2376
|
if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
|
|
2315
2377
|
if (!pointerDownInfo.moved) {
|
|
2316
|
-
const clickPrice = pointerDownInfo.region === "plot" ?
|
|
2378
|
+
const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
|
|
2317
2379
|
chartClickHandler?.({
|
|
2318
2380
|
x: pointerDownInfo.x,
|
|
2319
2381
|
y: pointerDownInfo.y,
|
|
@@ -2373,7 +2435,7 @@ function createChart(element, options = {}) {
|
|
|
2373
2435
|
}
|
|
2374
2436
|
orderActionHandler?.({
|
|
2375
2437
|
action: "createLimit",
|
|
2376
|
-
price:
|
|
2438
|
+
price: roundToPricePrecision(priceFromCanvasY(point.y))
|
|
2377
2439
|
});
|
|
2378
2440
|
return;
|
|
2379
2441
|
}
|
|
@@ -22,6 +22,8 @@ interface ChartOptions {
|
|
|
22
22
|
candleBodyWidthRatio?: number;
|
|
23
23
|
candleMinWidth?: number;
|
|
24
24
|
candleWickWidth?: number;
|
|
25
|
+
candleColorMode?: "openClose" | "prevClose";
|
|
26
|
+
candleColorEpsilon?: number;
|
|
25
27
|
autoScaleSmoothing?: number;
|
|
26
28
|
autoScaleIgnoreLatestCandle?: boolean;
|
|
27
29
|
doubleClickEnabled?: boolean;
|
|
@@ -65,6 +67,7 @@ interface IndicatorRenderContext {
|
|
|
65
67
|
chartHeight: number;
|
|
66
68
|
xFromIndex: (index: number) => number;
|
|
67
69
|
yFromPrice: ((price: number) => number) | null;
|
|
70
|
+
getCandleDirectionByIndex: (index: number) => "up" | "down";
|
|
68
71
|
candleSpacing: number;
|
|
69
72
|
upColor: string;
|
|
70
73
|
downColor: string;
|
|
@@ -127,6 +127,8 @@ var DEFAULT_OPTIONS = {
|
|
|
127
127
|
candleBodyWidthRatio: 0.7,
|
|
128
128
|
candleMinWidth: 0.5,
|
|
129
129
|
candleWickWidth: 1,
|
|
130
|
+
candleColorMode: "openClose",
|
|
131
|
+
candleColorEpsilon: -1,
|
|
130
132
|
autoScaleSmoothing: 0.16,
|
|
131
133
|
autoScaleIgnoreLatestCandle: true,
|
|
132
134
|
doubleClickEnabled: true,
|
|
@@ -487,11 +489,11 @@ var BUILTIN_VOLUME_INDICATOR = {
|
|
|
487
489
|
const xCenter = xFromIndex(index);
|
|
488
490
|
const barX = Math.round(xCenter - barWidth / 2);
|
|
489
491
|
const barY = Math.round(paneBottom - volumeHeight);
|
|
490
|
-
const
|
|
491
|
-
const opacity =
|
|
492
|
+
const direction = renderContext.getCandleDirectionByIndex(index);
|
|
493
|
+
const opacity = direction === "up" ? upOpacity : downOpacity;
|
|
492
494
|
ctx.save();
|
|
493
495
|
ctx.globalAlpha = opacity;
|
|
494
|
-
ctx.fillStyle =
|
|
496
|
+
ctx.fillStyle = direction === "up" ? upColor : downColor;
|
|
495
497
|
ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
|
|
496
498
|
ctx.restore();
|
|
497
499
|
}
|
|
@@ -843,6 +845,24 @@ function createChart(element, options = {}) {
|
|
|
843
845
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
844
846
|
return price.toFixed(decimals);
|
|
845
847
|
};
|
|
848
|
+
const roundToPricePrecision = (price) => {
|
|
849
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
850
|
+
return Number(price.toFixed(decimals));
|
|
851
|
+
};
|
|
852
|
+
const getResolvedCandleColorEpsilon = () => {
|
|
853
|
+
const configured = mergedOptions.candleColorEpsilon;
|
|
854
|
+
if (configured >= 0) {
|
|
855
|
+
return configured;
|
|
856
|
+
}
|
|
857
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
858
|
+
return decimals > 0 ? 0.5 / 10 ** decimals : 0;
|
|
859
|
+
};
|
|
860
|
+
const getDirectionFromDelta = (delta) => {
|
|
861
|
+
const epsilon = Math.max(0, getResolvedCandleColorEpsilon());
|
|
862
|
+
if (delta > epsilon) return 1;
|
|
863
|
+
if (delta < -epsilon) return -1;
|
|
864
|
+
return 0;
|
|
865
|
+
};
|
|
846
866
|
const getStabilizedPriceTemplate = () => {
|
|
847
867
|
const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
|
|
848
868
|
if (explicitTemplate.length > 0) {
|
|
@@ -869,14 +889,33 @@ function createChart(element, options = {}) {
|
|
|
869
889
|
return Math.max(measured, templateWidth);
|
|
870
890
|
};
|
|
871
891
|
const parseData = (nextData) => {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
892
|
+
const dedupedByTime = /* @__PURE__ */ new Map();
|
|
893
|
+
for (const point of nextData) {
|
|
894
|
+
const time = new Date(point.t);
|
|
895
|
+
const timeMs = time.getTime();
|
|
896
|
+
if (!Number.isFinite(timeMs)) {
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
const open = Number(point.o);
|
|
900
|
+
const close = Number(point.c);
|
|
901
|
+
const highInput = Number(point.h);
|
|
902
|
+
const lowInput = Number(point.l);
|
|
903
|
+
if (!Number.isFinite(open) || !Number.isFinite(close) || !Number.isFinite(highInput) || !Number.isFinite(lowInput)) {
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
const normalizedHigh = Math.max(highInput, open, close);
|
|
907
|
+
const normalizedLow = Math.min(lowInput, open, close);
|
|
908
|
+
const volumeValue = point.v === void 0 ? void 0 : Number(point.v);
|
|
909
|
+
dedupedByTime.set(timeMs, {
|
|
910
|
+
time,
|
|
911
|
+
o: open,
|
|
912
|
+
h: normalizedHigh,
|
|
913
|
+
l: normalizedLow,
|
|
914
|
+
c: close,
|
|
915
|
+
...volumeValue === void 0 || !Number.isFinite(volumeValue) ? {} : { v: volumeValue }
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
return Array.from(dedupedByTime.values()).sort((a, b) => a.time.getTime() - b.time.getTime());
|
|
880
919
|
};
|
|
881
920
|
const getTimeStepMs = () => {
|
|
882
921
|
if (data.length < 2) {
|
|
@@ -953,6 +992,26 @@ function createChart(element, options = {}) {
|
|
|
953
992
|
const upperDelta = Math.abs(upperPoint.time.getTime() - timeMs);
|
|
954
993
|
return lowerDelta <= upperDelta ? lower : upper;
|
|
955
994
|
};
|
|
995
|
+
const getCandleDirectionByIndex = (index) => {
|
|
996
|
+
const point = data[index];
|
|
997
|
+
if (!point) {
|
|
998
|
+
return "up";
|
|
999
|
+
}
|
|
1000
|
+
const prevPoint = index > 0 ? data[index - 1] : void 0;
|
|
1001
|
+
const mode = mergedOptions.candleColorMode;
|
|
1002
|
+
const baseForMode = mode === "prevClose" && prevPoint ? prevPoint.c : point.o;
|
|
1003
|
+
let direction = getDirectionFromDelta(point.c - baseForMode);
|
|
1004
|
+
if (direction === 0 && mode === "prevClose") {
|
|
1005
|
+
direction = getDirectionFromDelta(point.c - point.o);
|
|
1006
|
+
}
|
|
1007
|
+
if (direction === 0 && prevPoint) {
|
|
1008
|
+
direction = getDirectionFromDelta(point.c - prevPoint.c);
|
|
1009
|
+
}
|
|
1010
|
+
if (direction === 0) {
|
|
1011
|
+
return point.c >= point.o ? "up" : "down";
|
|
1012
|
+
}
|
|
1013
|
+
return direction > 0 ? "up" : "down";
|
|
1014
|
+
};
|
|
956
1015
|
const formatHoverTimeLabel = (time, mode) => {
|
|
957
1016
|
if (mode === "time") {
|
|
958
1017
|
return time.toLocaleTimeString(void 0, {
|
|
@@ -1538,8 +1597,8 @@ function createChart(element, options = {}) {
|
|
|
1538
1597
|
const closeY = yFromPrice(point.c);
|
|
1539
1598
|
const highY = yFromPrice(point.h);
|
|
1540
1599
|
const lowY = yFromPrice(point.l);
|
|
1541
|
-
const
|
|
1542
|
-
const candleColor =
|
|
1600
|
+
const direction = getCandleDirectionByIndex(index);
|
|
1601
|
+
const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
|
|
1543
1602
|
ctx.strokeStyle = candleColor;
|
|
1544
1603
|
ctx.lineWidth = candleWickWidth;
|
|
1545
1604
|
ctx.beginPath();
|
|
@@ -1573,6 +1632,7 @@ function createChart(element, options = {}) {
|
|
|
1573
1632
|
chartHeight,
|
|
1574
1633
|
xFromIndex,
|
|
1575
1634
|
yFromPrice,
|
|
1635
|
+
getCandleDirectionByIndex,
|
|
1576
1636
|
candleSpacing,
|
|
1577
1637
|
upColor: mergedOptions.upColor,
|
|
1578
1638
|
downColor: mergedOptions.downColor
|
|
@@ -1632,6 +1692,7 @@ function createChart(element, options = {}) {
|
|
|
1632
1692
|
chartHeight: paneHeight,
|
|
1633
1693
|
xFromIndex,
|
|
1634
1694
|
yFromPrice: null,
|
|
1695
|
+
getCandleDirectionByIndex,
|
|
1635
1696
|
candleSpacing,
|
|
1636
1697
|
upColor: mergedOptions.upColor,
|
|
1637
1698
|
downColor: mergedOptions.downColor
|
|
@@ -1669,7 +1730,8 @@ function createChart(element, options = {}) {
|
|
|
1669
1730
|
const tickerPrice = lastPoint.c;
|
|
1670
1731
|
const tickerY = yFromPrice(tickerPrice);
|
|
1671
1732
|
const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
|
|
1672
|
-
const
|
|
1733
|
+
const lastDirection = getCandleDirectionByIndex(data.length - 1);
|
|
1734
|
+
const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
|
|
1673
1735
|
const tickerThickness = Math.max(1, ticker.thickness ?? 1);
|
|
1674
1736
|
const tickerStyle = ticker.style ?? "solid";
|
|
1675
1737
|
ctx.save();
|
|
@@ -2037,7 +2099,7 @@ function createChart(element, options = {}) {
|
|
|
2037
2099
|
x,
|
|
2038
2100
|
y,
|
|
2039
2101
|
region,
|
|
2040
|
-
...region === "plot" ? { price:
|
|
2102
|
+
...region === "plot" ? { price: roundToPricePrecision(priceFromCanvasY(y)) } : {},
|
|
2041
2103
|
...index === null ? {} : { index },
|
|
2042
2104
|
...hoverTime ? { time: hoverTime.toISOString() } : {},
|
|
2043
2105
|
...point ? { point } : {}
|
|
@@ -2083,7 +2145,7 @@ function createChart(element, options = {}) {
|
|
|
2083
2145
|
if (orderRegion) {
|
|
2084
2146
|
if (orderRegion.draggable) {
|
|
2085
2147
|
activePointerId = event.pointerId;
|
|
2086
|
-
const startPrice =
|
|
2148
|
+
const startPrice = roundToPricePrecision(orderRegion.line.price);
|
|
2087
2149
|
actionDragState = {
|
|
2088
2150
|
orderId: orderRegion.orderId,
|
|
2089
2151
|
action: orderRegion.action,
|
|
@@ -2142,7 +2204,7 @@ function createChart(element, options = {}) {
|
|
|
2142
2204
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
2143
2205
|
return;
|
|
2144
2206
|
}
|
|
2145
|
-
const nextPrice =
|
|
2207
|
+
const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
|
|
2146
2208
|
if (nextPrice !== orderDragState.lastPrice) {
|
|
2147
2209
|
orderDragState.lastPrice = nextPrice;
|
|
2148
2210
|
orderLines = orderLines.map((line) => {
|
|
@@ -2172,7 +2234,7 @@ function createChart(element, options = {}) {
|
|
|
2172
2234
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
2173
2235
|
return;
|
|
2174
2236
|
}
|
|
2175
|
-
const nextPrice =
|
|
2237
|
+
const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
|
|
2176
2238
|
if (nextPrice !== actionDragState.lastPrice) {
|
|
2177
2239
|
actionDragState.lastPrice = nextPrice;
|
|
2178
2240
|
actionDragState.moved = true;
|
|
@@ -2289,7 +2351,7 @@ function createChart(element, options = {}) {
|
|
|
2289
2351
|
canvas.style.cursor = "default";
|
|
2290
2352
|
if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
|
|
2291
2353
|
if (!pointerDownInfo.moved) {
|
|
2292
|
-
const clickPrice = pointerDownInfo.region === "plot" ?
|
|
2354
|
+
const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
|
|
2293
2355
|
chartClickHandler?.({
|
|
2294
2356
|
x: pointerDownInfo.x,
|
|
2295
2357
|
y: pointerDownInfo.y,
|
|
@@ -2349,7 +2411,7 @@ function createChart(element, options = {}) {
|
|
|
2349
2411
|
}
|
|
2350
2412
|
orderActionHandler?.({
|
|
2351
2413
|
action: "createLimit",
|
|
2352
|
-
price:
|
|
2414
|
+
price: roundToPricePrecision(priceFromCanvasY(point.y))
|
|
2353
2415
|
});
|
|
2354
2416
|
return;
|
|
2355
2417
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -151,6 +151,8 @@ var DEFAULT_OPTIONS = {
|
|
|
151
151
|
candleBodyWidthRatio: 0.7,
|
|
152
152
|
candleMinWidth: 0.5,
|
|
153
153
|
candleWickWidth: 1,
|
|
154
|
+
candleColorMode: "openClose",
|
|
155
|
+
candleColorEpsilon: -1,
|
|
154
156
|
autoScaleSmoothing: 0.16,
|
|
155
157
|
autoScaleIgnoreLatestCandle: true,
|
|
156
158
|
doubleClickEnabled: true,
|
|
@@ -511,11 +513,11 @@ var BUILTIN_VOLUME_INDICATOR = {
|
|
|
511
513
|
const xCenter = xFromIndex(index);
|
|
512
514
|
const barX = Math.round(xCenter - barWidth / 2);
|
|
513
515
|
const barY = Math.round(paneBottom - volumeHeight);
|
|
514
|
-
const
|
|
515
|
-
const opacity =
|
|
516
|
+
const direction = renderContext.getCandleDirectionByIndex(index);
|
|
517
|
+
const opacity = direction === "up" ? upOpacity : downOpacity;
|
|
516
518
|
ctx.save();
|
|
517
519
|
ctx.globalAlpha = opacity;
|
|
518
|
-
ctx.fillStyle =
|
|
520
|
+
ctx.fillStyle = direction === "up" ? upColor : downColor;
|
|
519
521
|
ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
|
|
520
522
|
ctx.restore();
|
|
521
523
|
}
|
|
@@ -867,6 +869,24 @@ function createChart(element, options = {}) {
|
|
|
867
869
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
868
870
|
return price.toFixed(decimals);
|
|
869
871
|
};
|
|
872
|
+
const roundToPricePrecision = (price) => {
|
|
873
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
874
|
+
return Number(price.toFixed(decimals));
|
|
875
|
+
};
|
|
876
|
+
const getResolvedCandleColorEpsilon = () => {
|
|
877
|
+
const configured = mergedOptions.candleColorEpsilon;
|
|
878
|
+
if (configured >= 0) {
|
|
879
|
+
return configured;
|
|
880
|
+
}
|
|
881
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
882
|
+
return decimals > 0 ? 0.5 / 10 ** decimals : 0;
|
|
883
|
+
};
|
|
884
|
+
const getDirectionFromDelta = (delta) => {
|
|
885
|
+
const epsilon = Math.max(0, getResolvedCandleColorEpsilon());
|
|
886
|
+
if (delta > epsilon) return 1;
|
|
887
|
+
if (delta < -epsilon) return -1;
|
|
888
|
+
return 0;
|
|
889
|
+
};
|
|
870
890
|
const getStabilizedPriceTemplate = () => {
|
|
871
891
|
const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
|
|
872
892
|
if (explicitTemplate.length > 0) {
|
|
@@ -893,14 +913,33 @@ function createChart(element, options = {}) {
|
|
|
893
913
|
return Math.max(measured, templateWidth);
|
|
894
914
|
};
|
|
895
915
|
const parseData = (nextData) => {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
916
|
+
const dedupedByTime = /* @__PURE__ */ new Map();
|
|
917
|
+
for (const point of nextData) {
|
|
918
|
+
const time = new Date(point.t);
|
|
919
|
+
const timeMs = time.getTime();
|
|
920
|
+
if (!Number.isFinite(timeMs)) {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
const open = Number(point.o);
|
|
924
|
+
const close = Number(point.c);
|
|
925
|
+
const highInput = Number(point.h);
|
|
926
|
+
const lowInput = Number(point.l);
|
|
927
|
+
if (!Number.isFinite(open) || !Number.isFinite(close) || !Number.isFinite(highInput) || !Number.isFinite(lowInput)) {
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
const normalizedHigh = Math.max(highInput, open, close);
|
|
931
|
+
const normalizedLow = Math.min(lowInput, open, close);
|
|
932
|
+
const volumeValue = point.v === void 0 ? void 0 : Number(point.v);
|
|
933
|
+
dedupedByTime.set(timeMs, {
|
|
934
|
+
time,
|
|
935
|
+
o: open,
|
|
936
|
+
h: normalizedHigh,
|
|
937
|
+
l: normalizedLow,
|
|
938
|
+
c: close,
|
|
939
|
+
...volumeValue === void 0 || !Number.isFinite(volumeValue) ? {} : { v: volumeValue }
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
return Array.from(dedupedByTime.values()).sort((a, b) => a.time.getTime() - b.time.getTime());
|
|
904
943
|
};
|
|
905
944
|
const getTimeStepMs = () => {
|
|
906
945
|
if (data.length < 2) {
|
|
@@ -977,6 +1016,26 @@ function createChart(element, options = {}) {
|
|
|
977
1016
|
const upperDelta = Math.abs(upperPoint.time.getTime() - timeMs);
|
|
978
1017
|
return lowerDelta <= upperDelta ? lower : upper;
|
|
979
1018
|
};
|
|
1019
|
+
const getCandleDirectionByIndex = (index) => {
|
|
1020
|
+
const point = data[index];
|
|
1021
|
+
if (!point) {
|
|
1022
|
+
return "up";
|
|
1023
|
+
}
|
|
1024
|
+
const prevPoint = index > 0 ? data[index - 1] : void 0;
|
|
1025
|
+
const mode = mergedOptions.candleColorMode;
|
|
1026
|
+
const baseForMode = mode === "prevClose" && prevPoint ? prevPoint.c : point.o;
|
|
1027
|
+
let direction = getDirectionFromDelta(point.c - baseForMode);
|
|
1028
|
+
if (direction === 0 && mode === "prevClose") {
|
|
1029
|
+
direction = getDirectionFromDelta(point.c - point.o);
|
|
1030
|
+
}
|
|
1031
|
+
if (direction === 0 && prevPoint) {
|
|
1032
|
+
direction = getDirectionFromDelta(point.c - prevPoint.c);
|
|
1033
|
+
}
|
|
1034
|
+
if (direction === 0) {
|
|
1035
|
+
return point.c >= point.o ? "up" : "down";
|
|
1036
|
+
}
|
|
1037
|
+
return direction > 0 ? "up" : "down";
|
|
1038
|
+
};
|
|
980
1039
|
const formatHoverTimeLabel = (time, mode) => {
|
|
981
1040
|
if (mode === "time") {
|
|
982
1041
|
return time.toLocaleTimeString(void 0, {
|
|
@@ -1562,8 +1621,8 @@ function createChart(element, options = {}) {
|
|
|
1562
1621
|
const closeY = yFromPrice(point.c);
|
|
1563
1622
|
const highY = yFromPrice(point.h);
|
|
1564
1623
|
const lowY = yFromPrice(point.l);
|
|
1565
|
-
const
|
|
1566
|
-
const candleColor =
|
|
1624
|
+
const direction = getCandleDirectionByIndex(index);
|
|
1625
|
+
const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
|
|
1567
1626
|
ctx.strokeStyle = candleColor;
|
|
1568
1627
|
ctx.lineWidth = candleWickWidth;
|
|
1569
1628
|
ctx.beginPath();
|
|
@@ -1597,6 +1656,7 @@ function createChart(element, options = {}) {
|
|
|
1597
1656
|
chartHeight,
|
|
1598
1657
|
xFromIndex,
|
|
1599
1658
|
yFromPrice,
|
|
1659
|
+
getCandleDirectionByIndex,
|
|
1600
1660
|
candleSpacing,
|
|
1601
1661
|
upColor: mergedOptions.upColor,
|
|
1602
1662
|
downColor: mergedOptions.downColor
|
|
@@ -1656,6 +1716,7 @@ function createChart(element, options = {}) {
|
|
|
1656
1716
|
chartHeight: paneHeight,
|
|
1657
1717
|
xFromIndex,
|
|
1658
1718
|
yFromPrice: null,
|
|
1719
|
+
getCandleDirectionByIndex,
|
|
1659
1720
|
candleSpacing,
|
|
1660
1721
|
upColor: mergedOptions.upColor,
|
|
1661
1722
|
downColor: mergedOptions.downColor
|
|
@@ -1693,7 +1754,8 @@ function createChart(element, options = {}) {
|
|
|
1693
1754
|
const tickerPrice = lastPoint.c;
|
|
1694
1755
|
const tickerY = yFromPrice(tickerPrice);
|
|
1695
1756
|
const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
|
|
1696
|
-
const
|
|
1757
|
+
const lastDirection = getCandleDirectionByIndex(data.length - 1);
|
|
1758
|
+
const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
|
|
1697
1759
|
const tickerThickness = Math.max(1, ticker.thickness ?? 1);
|
|
1698
1760
|
const tickerStyle = ticker.style ?? "solid";
|
|
1699
1761
|
ctx.save();
|
|
@@ -2061,7 +2123,7 @@ function createChart(element, options = {}) {
|
|
|
2061
2123
|
x,
|
|
2062
2124
|
y,
|
|
2063
2125
|
region,
|
|
2064
|
-
...region === "plot" ? { price:
|
|
2126
|
+
...region === "plot" ? { price: roundToPricePrecision(priceFromCanvasY(y)) } : {},
|
|
2065
2127
|
...index === null ? {} : { index },
|
|
2066
2128
|
...hoverTime ? { time: hoverTime.toISOString() } : {},
|
|
2067
2129
|
...point ? { point } : {}
|
|
@@ -2107,7 +2169,7 @@ function createChart(element, options = {}) {
|
|
|
2107
2169
|
if (orderRegion) {
|
|
2108
2170
|
if (orderRegion.draggable) {
|
|
2109
2171
|
activePointerId = event.pointerId;
|
|
2110
|
-
const startPrice =
|
|
2172
|
+
const startPrice = roundToPricePrecision(orderRegion.line.price);
|
|
2111
2173
|
actionDragState = {
|
|
2112
2174
|
orderId: orderRegion.orderId,
|
|
2113
2175
|
action: orderRegion.action,
|
|
@@ -2166,7 +2228,7 @@ function createChart(element, options = {}) {
|
|
|
2166
2228
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
2167
2229
|
return;
|
|
2168
2230
|
}
|
|
2169
|
-
const nextPrice =
|
|
2231
|
+
const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
|
|
2170
2232
|
if (nextPrice !== orderDragState.lastPrice) {
|
|
2171
2233
|
orderDragState.lastPrice = nextPrice;
|
|
2172
2234
|
orderLines = orderLines.map((line) => {
|
|
@@ -2196,7 +2258,7 @@ function createChart(element, options = {}) {
|
|
|
2196
2258
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
2197
2259
|
return;
|
|
2198
2260
|
}
|
|
2199
|
-
const nextPrice =
|
|
2261
|
+
const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
|
|
2200
2262
|
if (nextPrice !== actionDragState.lastPrice) {
|
|
2201
2263
|
actionDragState.lastPrice = nextPrice;
|
|
2202
2264
|
actionDragState.moved = true;
|
|
@@ -2313,7 +2375,7 @@ function createChart(element, options = {}) {
|
|
|
2313
2375
|
canvas.style.cursor = "default";
|
|
2314
2376
|
if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
|
|
2315
2377
|
if (!pointerDownInfo.moved) {
|
|
2316
|
-
const clickPrice = pointerDownInfo.region === "plot" ?
|
|
2378
|
+
const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
|
|
2317
2379
|
chartClickHandler?.({
|
|
2318
2380
|
x: pointerDownInfo.x,
|
|
2319
2381
|
y: pointerDownInfo.y,
|
|
@@ -2373,7 +2435,7 @@ function createChart(element, options = {}) {
|
|
|
2373
2435
|
}
|
|
2374
2436
|
orderActionHandler?.({
|
|
2375
2437
|
action: "createLimit",
|
|
2376
|
-
price:
|
|
2438
|
+
price: roundToPricePrecision(priceFromCanvasY(point.y))
|
|
2377
2439
|
});
|
|
2378
2440
|
return;
|
|
2379
2441
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -22,6 +22,8 @@ interface ChartOptions {
|
|
|
22
22
|
candleBodyWidthRatio?: number;
|
|
23
23
|
candleMinWidth?: number;
|
|
24
24
|
candleWickWidth?: number;
|
|
25
|
+
candleColorMode?: "openClose" | "prevClose";
|
|
26
|
+
candleColorEpsilon?: number;
|
|
25
27
|
autoScaleSmoothing?: number;
|
|
26
28
|
autoScaleIgnoreLatestCandle?: boolean;
|
|
27
29
|
doubleClickEnabled?: boolean;
|
|
@@ -65,6 +67,7 @@ interface IndicatorRenderContext {
|
|
|
65
67
|
chartHeight: number;
|
|
66
68
|
xFromIndex: (index: number) => number;
|
|
67
69
|
yFromPrice: ((price: number) => number) | null;
|
|
70
|
+
getCandleDirectionByIndex: (index: number) => "up" | "down";
|
|
68
71
|
candleSpacing: number;
|
|
69
72
|
upColor: string;
|
|
70
73
|
downColor: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ interface ChartOptions {
|
|
|
22
22
|
candleBodyWidthRatio?: number;
|
|
23
23
|
candleMinWidth?: number;
|
|
24
24
|
candleWickWidth?: number;
|
|
25
|
+
candleColorMode?: "openClose" | "prevClose";
|
|
26
|
+
candleColorEpsilon?: number;
|
|
25
27
|
autoScaleSmoothing?: number;
|
|
26
28
|
autoScaleIgnoreLatestCandle?: boolean;
|
|
27
29
|
doubleClickEnabled?: boolean;
|
|
@@ -65,6 +67,7 @@ interface IndicatorRenderContext {
|
|
|
65
67
|
chartHeight: number;
|
|
66
68
|
xFromIndex: (index: number) => number;
|
|
67
69
|
yFromPrice: ((price: number) => number) | null;
|
|
70
|
+
getCandleDirectionByIndex: (index: number) => "up" | "down";
|
|
68
71
|
candleSpacing: number;
|
|
69
72
|
upColor: string;
|
|
70
73
|
downColor: string;
|
package/dist/index.js
CHANGED
|
@@ -127,6 +127,8 @@ var DEFAULT_OPTIONS = {
|
|
|
127
127
|
candleBodyWidthRatio: 0.7,
|
|
128
128
|
candleMinWidth: 0.5,
|
|
129
129
|
candleWickWidth: 1,
|
|
130
|
+
candleColorMode: "openClose",
|
|
131
|
+
candleColorEpsilon: -1,
|
|
130
132
|
autoScaleSmoothing: 0.16,
|
|
131
133
|
autoScaleIgnoreLatestCandle: true,
|
|
132
134
|
doubleClickEnabled: true,
|
|
@@ -487,11 +489,11 @@ var BUILTIN_VOLUME_INDICATOR = {
|
|
|
487
489
|
const xCenter = xFromIndex(index);
|
|
488
490
|
const barX = Math.round(xCenter - barWidth / 2);
|
|
489
491
|
const barY = Math.round(paneBottom - volumeHeight);
|
|
490
|
-
const
|
|
491
|
-
const opacity =
|
|
492
|
+
const direction = renderContext.getCandleDirectionByIndex(index);
|
|
493
|
+
const opacity = direction === "up" ? upOpacity : downOpacity;
|
|
492
494
|
ctx.save();
|
|
493
495
|
ctx.globalAlpha = opacity;
|
|
494
|
-
ctx.fillStyle =
|
|
496
|
+
ctx.fillStyle = direction === "up" ? upColor : downColor;
|
|
495
497
|
ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
|
|
496
498
|
ctx.restore();
|
|
497
499
|
}
|
|
@@ -843,6 +845,24 @@ function createChart(element, options = {}) {
|
|
|
843
845
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
844
846
|
return price.toFixed(decimals);
|
|
845
847
|
};
|
|
848
|
+
const roundToPricePrecision = (price) => {
|
|
849
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
850
|
+
return Number(price.toFixed(decimals));
|
|
851
|
+
};
|
|
852
|
+
const getResolvedCandleColorEpsilon = () => {
|
|
853
|
+
const configured = mergedOptions.candleColorEpsilon;
|
|
854
|
+
if (configured >= 0) {
|
|
855
|
+
return configured;
|
|
856
|
+
}
|
|
857
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
858
|
+
return decimals > 0 ? 0.5 / 10 ** decimals : 0;
|
|
859
|
+
};
|
|
860
|
+
const getDirectionFromDelta = (delta) => {
|
|
861
|
+
const epsilon = Math.max(0, getResolvedCandleColorEpsilon());
|
|
862
|
+
if (delta > epsilon) return 1;
|
|
863
|
+
if (delta < -epsilon) return -1;
|
|
864
|
+
return 0;
|
|
865
|
+
};
|
|
846
866
|
const getStabilizedPriceTemplate = () => {
|
|
847
867
|
const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
|
|
848
868
|
if (explicitTemplate.length > 0) {
|
|
@@ -869,14 +889,33 @@ function createChart(element, options = {}) {
|
|
|
869
889
|
return Math.max(measured, templateWidth);
|
|
870
890
|
};
|
|
871
891
|
const parseData = (nextData) => {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
892
|
+
const dedupedByTime = /* @__PURE__ */ new Map();
|
|
893
|
+
for (const point of nextData) {
|
|
894
|
+
const time = new Date(point.t);
|
|
895
|
+
const timeMs = time.getTime();
|
|
896
|
+
if (!Number.isFinite(timeMs)) {
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
const open = Number(point.o);
|
|
900
|
+
const close = Number(point.c);
|
|
901
|
+
const highInput = Number(point.h);
|
|
902
|
+
const lowInput = Number(point.l);
|
|
903
|
+
if (!Number.isFinite(open) || !Number.isFinite(close) || !Number.isFinite(highInput) || !Number.isFinite(lowInput)) {
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
const normalizedHigh = Math.max(highInput, open, close);
|
|
907
|
+
const normalizedLow = Math.min(lowInput, open, close);
|
|
908
|
+
const volumeValue = point.v === void 0 ? void 0 : Number(point.v);
|
|
909
|
+
dedupedByTime.set(timeMs, {
|
|
910
|
+
time,
|
|
911
|
+
o: open,
|
|
912
|
+
h: normalizedHigh,
|
|
913
|
+
l: normalizedLow,
|
|
914
|
+
c: close,
|
|
915
|
+
...volumeValue === void 0 || !Number.isFinite(volumeValue) ? {} : { v: volumeValue }
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
return Array.from(dedupedByTime.values()).sort((a, b) => a.time.getTime() - b.time.getTime());
|
|
880
919
|
};
|
|
881
920
|
const getTimeStepMs = () => {
|
|
882
921
|
if (data.length < 2) {
|
|
@@ -953,6 +992,26 @@ function createChart(element, options = {}) {
|
|
|
953
992
|
const upperDelta = Math.abs(upperPoint.time.getTime() - timeMs);
|
|
954
993
|
return lowerDelta <= upperDelta ? lower : upper;
|
|
955
994
|
};
|
|
995
|
+
const getCandleDirectionByIndex = (index) => {
|
|
996
|
+
const point = data[index];
|
|
997
|
+
if (!point) {
|
|
998
|
+
return "up";
|
|
999
|
+
}
|
|
1000
|
+
const prevPoint = index > 0 ? data[index - 1] : void 0;
|
|
1001
|
+
const mode = mergedOptions.candleColorMode;
|
|
1002
|
+
const baseForMode = mode === "prevClose" && prevPoint ? prevPoint.c : point.o;
|
|
1003
|
+
let direction = getDirectionFromDelta(point.c - baseForMode);
|
|
1004
|
+
if (direction === 0 && mode === "prevClose") {
|
|
1005
|
+
direction = getDirectionFromDelta(point.c - point.o);
|
|
1006
|
+
}
|
|
1007
|
+
if (direction === 0 && prevPoint) {
|
|
1008
|
+
direction = getDirectionFromDelta(point.c - prevPoint.c);
|
|
1009
|
+
}
|
|
1010
|
+
if (direction === 0) {
|
|
1011
|
+
return point.c >= point.o ? "up" : "down";
|
|
1012
|
+
}
|
|
1013
|
+
return direction > 0 ? "up" : "down";
|
|
1014
|
+
};
|
|
956
1015
|
const formatHoverTimeLabel = (time, mode) => {
|
|
957
1016
|
if (mode === "time") {
|
|
958
1017
|
return time.toLocaleTimeString(void 0, {
|
|
@@ -1538,8 +1597,8 @@ function createChart(element, options = {}) {
|
|
|
1538
1597
|
const closeY = yFromPrice(point.c);
|
|
1539
1598
|
const highY = yFromPrice(point.h);
|
|
1540
1599
|
const lowY = yFromPrice(point.l);
|
|
1541
|
-
const
|
|
1542
|
-
const candleColor =
|
|
1600
|
+
const direction = getCandleDirectionByIndex(index);
|
|
1601
|
+
const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
|
|
1543
1602
|
ctx.strokeStyle = candleColor;
|
|
1544
1603
|
ctx.lineWidth = candleWickWidth;
|
|
1545
1604
|
ctx.beginPath();
|
|
@@ -1573,6 +1632,7 @@ function createChart(element, options = {}) {
|
|
|
1573
1632
|
chartHeight,
|
|
1574
1633
|
xFromIndex,
|
|
1575
1634
|
yFromPrice,
|
|
1635
|
+
getCandleDirectionByIndex,
|
|
1576
1636
|
candleSpacing,
|
|
1577
1637
|
upColor: mergedOptions.upColor,
|
|
1578
1638
|
downColor: mergedOptions.downColor
|
|
@@ -1632,6 +1692,7 @@ function createChart(element, options = {}) {
|
|
|
1632
1692
|
chartHeight: paneHeight,
|
|
1633
1693
|
xFromIndex,
|
|
1634
1694
|
yFromPrice: null,
|
|
1695
|
+
getCandleDirectionByIndex,
|
|
1635
1696
|
candleSpacing,
|
|
1636
1697
|
upColor: mergedOptions.upColor,
|
|
1637
1698
|
downColor: mergedOptions.downColor
|
|
@@ -1669,7 +1730,8 @@ function createChart(element, options = {}) {
|
|
|
1669
1730
|
const tickerPrice = lastPoint.c;
|
|
1670
1731
|
const tickerY = yFromPrice(tickerPrice);
|
|
1671
1732
|
const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
|
|
1672
|
-
const
|
|
1733
|
+
const lastDirection = getCandleDirectionByIndex(data.length - 1);
|
|
1734
|
+
const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
|
|
1673
1735
|
const tickerThickness = Math.max(1, ticker.thickness ?? 1);
|
|
1674
1736
|
const tickerStyle = ticker.style ?? "solid";
|
|
1675
1737
|
ctx.save();
|
|
@@ -2037,7 +2099,7 @@ function createChart(element, options = {}) {
|
|
|
2037
2099
|
x,
|
|
2038
2100
|
y,
|
|
2039
2101
|
region,
|
|
2040
|
-
...region === "plot" ? { price:
|
|
2102
|
+
...region === "plot" ? { price: roundToPricePrecision(priceFromCanvasY(y)) } : {},
|
|
2041
2103
|
...index === null ? {} : { index },
|
|
2042
2104
|
...hoverTime ? { time: hoverTime.toISOString() } : {},
|
|
2043
2105
|
...point ? { point } : {}
|
|
@@ -2083,7 +2145,7 @@ function createChart(element, options = {}) {
|
|
|
2083
2145
|
if (orderRegion) {
|
|
2084
2146
|
if (orderRegion.draggable) {
|
|
2085
2147
|
activePointerId = event.pointerId;
|
|
2086
|
-
const startPrice =
|
|
2148
|
+
const startPrice = roundToPricePrecision(orderRegion.line.price);
|
|
2087
2149
|
actionDragState = {
|
|
2088
2150
|
orderId: orderRegion.orderId,
|
|
2089
2151
|
action: orderRegion.action,
|
|
@@ -2142,7 +2204,7 @@ function createChart(element, options = {}) {
|
|
|
2142
2204
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
2143
2205
|
return;
|
|
2144
2206
|
}
|
|
2145
|
-
const nextPrice =
|
|
2207
|
+
const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
|
|
2146
2208
|
if (nextPrice !== orderDragState.lastPrice) {
|
|
2147
2209
|
orderDragState.lastPrice = nextPrice;
|
|
2148
2210
|
orderLines = orderLines.map((line) => {
|
|
@@ -2172,7 +2234,7 @@ function createChart(element, options = {}) {
|
|
|
2172
2234
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
2173
2235
|
return;
|
|
2174
2236
|
}
|
|
2175
|
-
const nextPrice =
|
|
2237
|
+
const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
|
|
2176
2238
|
if (nextPrice !== actionDragState.lastPrice) {
|
|
2177
2239
|
actionDragState.lastPrice = nextPrice;
|
|
2178
2240
|
actionDragState.moved = true;
|
|
@@ -2289,7 +2351,7 @@ function createChart(element, options = {}) {
|
|
|
2289
2351
|
canvas.style.cursor = "default";
|
|
2290
2352
|
if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
|
|
2291
2353
|
if (!pointerDownInfo.moved) {
|
|
2292
|
-
const clickPrice = pointerDownInfo.region === "plot" ?
|
|
2354
|
+
const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
|
|
2293
2355
|
chartClickHandler?.({
|
|
2294
2356
|
x: pointerDownInfo.x,
|
|
2295
2357
|
y: pointerDownInfo.y,
|
|
@@ -2349,7 +2411,7 @@ function createChart(element, options = {}) {
|
|
|
2349
2411
|
}
|
|
2350
2412
|
orderActionHandler?.({
|
|
2351
2413
|
action: "createLimit",
|
|
2352
|
-
price:
|
|
2414
|
+
price: roundToPricePrecision(priceFromCanvasY(point.y))
|
|
2353
2415
|
});
|
|
2354
2416
|
return;
|
|
2355
2417
|
}
|
package/docs/API.md
CHANGED
|
@@ -50,6 +50,8 @@ Top-level options:
|
|
|
50
50
|
- `candleBodyWidthRatio` (default `0.7`)
|
|
51
51
|
- `candleMinWidth` (default `0.5`)
|
|
52
52
|
- `candleWickWidth` (default `1`)
|
|
53
|
+
- `candleColorMode` (`"openClose" | "prevClose"`, default `"openClose"`)
|
|
54
|
+
- `candleColorEpsilon` (default `-1` = auto from `priceDecimals`; set `0` to disable tolerance)
|
|
53
55
|
- `autoScaleSmoothing` (default `0.16`)
|
|
54
56
|
- `autoScaleIgnoreLatestCandle` (default `true`)
|
|
55
57
|
- `doubleClickEnabled` (default `true`)
|
package/docs/RECIPES.md
CHANGED
|
@@ -26,6 +26,26 @@ Use:
|
|
|
26
26
|
- `autoScaleSmoothing` for smoother scale transitions
|
|
27
27
|
- `autoScaleIgnoreLatestCandle` to reduce live-candle jitter
|
|
28
28
|
|
|
29
|
+
## Stabilize candle up/down coloring on tiny deltas
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const chart = createChart(rootEl, {
|
|
33
|
+
// default behavior
|
|
34
|
+
candleColorMode: "openClose",
|
|
35
|
+
// auto epsilon from priceDecimals (recommended)
|
|
36
|
+
candleColorEpsilon: -1
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use previous-close mode if your UX expects green/red by last-close change:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const chart = createChart(rootEl, {
|
|
44
|
+
candleColorMode: "prevClose",
|
|
45
|
+
candleColorEpsilon: -1
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
29
49
|
## Add a static alert/level line
|
|
30
50
|
|
|
31
51
|
```ts
|