hyperprop-charting-library 0.1.7 → 0.1.9
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/dist/hyperprop-charting-library.cjs +245 -38
- package/dist/hyperprop-charting-library.d.ts +31 -1
- package/dist/hyperprop-charting-library.js +245 -38
- package/dist/index.cjs +144 -3
- package/dist/index.d.cts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +144 -3
- package/docs/API.md +14 -4
- package/docs/EVENTS.md +33 -0
- package/package.json +1 -1
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
var DEFAULT_GRID_OPTIONS = {
|
|
3
|
-
color: "#
|
|
4
|
-
opacity: 0.
|
|
3
|
+
color: "#2b2f38",
|
|
4
|
+
opacity: 0.38,
|
|
5
5
|
horizontalLines: true,
|
|
6
6
|
verticalLines: true,
|
|
7
7
|
horizontalTickCount: 5
|
|
8
8
|
};
|
|
9
9
|
var DEFAULT_AXIS_OPTIONS = {
|
|
10
|
-
lineColor: "#
|
|
11
|
-
textColor: "#
|
|
10
|
+
lineColor: "#3b3f47",
|
|
11
|
+
textColor: "#a9adb6",
|
|
12
12
|
fontSize: 12,
|
|
13
13
|
lineWidth: 1
|
|
14
14
|
};
|
|
@@ -18,24 +18,39 @@ var DEFAULT_CROSSHAIR_OPTIONS = {
|
|
|
18
18
|
width: 1,
|
|
19
19
|
style: "dotted",
|
|
20
20
|
showHorizontal: true,
|
|
21
|
-
showVertical: true
|
|
21
|
+
showVertical: true,
|
|
22
|
+
showPriceLabel: true,
|
|
23
|
+
showTimeLabel: true,
|
|
24
|
+
timeLabelFormat: "auto",
|
|
25
|
+
labelBackgroundColor: "#0b1220",
|
|
26
|
+
labelTextColor: "#cbd5e1",
|
|
27
|
+
labelBorderRadius: 3,
|
|
28
|
+
labelBorderColor: "#94a3b8",
|
|
29
|
+
labelBorderWidth: 1,
|
|
30
|
+
labelBorderStyle: "solid"
|
|
22
31
|
};
|
|
23
32
|
var DEFAULT_WATERMARK_OPTIONS = {
|
|
24
33
|
visible: false,
|
|
25
34
|
text: "",
|
|
26
|
-
color: "#
|
|
27
|
-
opacity: 0.
|
|
35
|
+
color: "#81858d",
|
|
36
|
+
opacity: 0.14,
|
|
28
37
|
fontSize: 92,
|
|
29
38
|
fontWeight: 700,
|
|
30
|
-
thickness: 0
|
|
39
|
+
thickness: 0,
|
|
40
|
+
imageSrc: "",
|
|
41
|
+
imageScale: 1,
|
|
42
|
+
imageMaxWidthRatio: 0.42,
|
|
43
|
+
imageMaxHeightRatio: 0.3,
|
|
44
|
+
imageTintColor: "",
|
|
45
|
+
imageTintOpacity: 1
|
|
31
46
|
};
|
|
32
47
|
var DEFAULT_PRICE_LINE_OPTIONS = {
|
|
33
48
|
visible: true,
|
|
34
49
|
style: "solid",
|
|
35
50
|
thickness: 1,
|
|
36
|
-
color: "#
|
|
37
|
-
labelBackgroundColor: "#
|
|
38
|
-
labelTextColor: "#
|
|
51
|
+
color: "#38bdf8",
|
|
52
|
+
labelBackgroundColor: "#0b1220",
|
|
53
|
+
labelTextColor: "#60a5fa",
|
|
39
54
|
labelBorderRadius: 3,
|
|
40
55
|
showLabel: true
|
|
41
56
|
};
|
|
@@ -44,9 +59,9 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
|
|
|
44
59
|
behavior: "static",
|
|
45
60
|
style: "solid",
|
|
46
61
|
thickness: 1,
|
|
47
|
-
color: "
|
|
48
|
-
labelBackgroundColor: "#
|
|
49
|
-
labelTextColor: "#
|
|
62
|
+
color: "rgba(59,130,246,0.8)",
|
|
63
|
+
labelBackgroundColor: "#0b1220",
|
|
64
|
+
labelTextColor: "#60a5fa",
|
|
50
65
|
labelBorderRadius: 3,
|
|
51
66
|
showCloseButton: true,
|
|
52
67
|
widgetPosition: "left",
|
|
@@ -73,12 +88,17 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
|
|
|
73
88
|
var DEFAULT_OPTIONS = {
|
|
74
89
|
width: 720,
|
|
75
90
|
height: 360,
|
|
76
|
-
backgroundColor: "#
|
|
77
|
-
axisColor: "#
|
|
91
|
+
backgroundColor: "#101114",
|
|
92
|
+
axisColor: "#7f8289",
|
|
78
93
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
priceDecimals: 2,
|
|
95
|
+
initialViewport: "latest",
|
|
96
|
+
initialVisibleBars: 60,
|
|
97
|
+
rightEdgePaddingBars: 2,
|
|
98
|
+
preserveViewportOnDataUpdate: true,
|
|
99
|
+
upColor: "#2fb171",
|
|
100
|
+
downColor: "#d35a5a",
|
|
101
|
+
gridColor: "#252932",
|
|
82
102
|
fontFamily: "Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
|
|
83
103
|
candleBodyWidthRatio: 0.7,
|
|
84
104
|
candleMinWidth: 0.5,
|
|
@@ -94,12 +114,12 @@ var DEFAULT_OPTIONS = {
|
|
|
94
114
|
orderLines: [],
|
|
95
115
|
tickerLine: {
|
|
96
116
|
visible: true,
|
|
97
|
-
style: "
|
|
117
|
+
style: "dotted",
|
|
98
118
|
thickness: 1,
|
|
99
|
-
color: "#
|
|
100
|
-
labelBackgroundColor: "#
|
|
119
|
+
color: "#38bdf8",
|
|
120
|
+
labelBackgroundColor: "#38bdf8",
|
|
101
121
|
labelTextColor: "#0b1220",
|
|
102
|
-
labelBorderRadius:
|
|
122
|
+
labelBorderRadius: 3
|
|
103
123
|
}
|
|
104
124
|
};
|
|
105
125
|
function createChart(element, options = {}) {
|
|
@@ -141,6 +161,7 @@ function createChart(element, options = {}) {
|
|
|
141
161
|
}));
|
|
142
162
|
let orderActionHandler = null;
|
|
143
163
|
let chartClickHandler = null;
|
|
164
|
+
let crosshairMoveHandler = null;
|
|
144
165
|
let orderActionRegions = [];
|
|
145
166
|
let orderDragRegions = [];
|
|
146
167
|
let generatedPriceLineId = 1;
|
|
@@ -153,6 +174,9 @@ function createChart(element, options = {}) {
|
|
|
153
174
|
let yMaxOverride = null;
|
|
154
175
|
let autoYMin = null;
|
|
155
176
|
let autoYMax = null;
|
|
177
|
+
let watermarkImageSrc = null;
|
|
178
|
+
let watermarkImage = null;
|
|
179
|
+
let watermarkImageReady = false;
|
|
156
180
|
let drawState = null;
|
|
157
181
|
let orderDragState = null;
|
|
158
182
|
let actionDragState = null;
|
|
@@ -171,13 +195,36 @@ function createChart(element, options = {}) {
|
|
|
171
195
|
element.appendChild(canvas);
|
|
172
196
|
const margin = { top: 16, right: 72, bottom: 34, left: 12 };
|
|
173
197
|
const maxPanBars = 1e6;
|
|
174
|
-
const rightEdgePaddingBars =
|
|
198
|
+
const rightEdgePaddingBars = Math.max(0, mergedOptions.rightEdgePaddingBars);
|
|
175
199
|
const getPixelRatio = () => {
|
|
176
200
|
if (typeof window === "undefined") {
|
|
177
201
|
return 1;
|
|
178
202
|
}
|
|
179
203
|
return Math.max(1, window.devicePixelRatio || 1);
|
|
180
204
|
};
|
|
205
|
+
const ensureWatermarkImage = (src) => {
|
|
206
|
+
if (src === watermarkImageSrc) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
watermarkImageSrc = src;
|
|
210
|
+
watermarkImageReady = false;
|
|
211
|
+
watermarkImage = null;
|
|
212
|
+
if (!src) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const image = new Image();
|
|
216
|
+
image.crossOrigin = "anonymous";
|
|
217
|
+
image.onload = () => {
|
|
218
|
+
watermarkImage = image;
|
|
219
|
+
watermarkImageReady = true;
|
|
220
|
+
draw();
|
|
221
|
+
};
|
|
222
|
+
image.onerror = () => {
|
|
223
|
+
watermarkImage = null;
|
|
224
|
+
watermarkImageReady = false;
|
|
225
|
+
};
|
|
226
|
+
image.src = src;
|
|
227
|
+
};
|
|
181
228
|
const crisp = (value) => Math.round(value) + 0.5;
|
|
182
229
|
const clamp = (value, min, max) => {
|
|
183
230
|
return Math.min(max, Math.max(min, value));
|
|
@@ -201,8 +248,13 @@ function createChart(element, options = {}) {
|
|
|
201
248
|
xSpan = 60;
|
|
202
249
|
return;
|
|
203
250
|
}
|
|
204
|
-
|
|
205
|
-
|
|
251
|
+
const requestedVisibleBars = Math.max(5, Math.floor(mergedOptions.initialVisibleBars));
|
|
252
|
+
xSpan = Math.min(requestedVisibleBars, Math.max(5, count));
|
|
253
|
+
if (mergedOptions.initialViewport === "center") {
|
|
254
|
+
xCenter = count / 2;
|
|
255
|
+
} else {
|
|
256
|
+
xCenter = count - xSpan / 2 + rightEdgePaddingBars;
|
|
257
|
+
}
|
|
206
258
|
clampXViewport();
|
|
207
259
|
};
|
|
208
260
|
const getYBounds = () => {
|
|
@@ -242,13 +294,8 @@ function createChart(element, options = {}) {
|
|
|
242
294
|
return { min: nextMin, max: nextMax };
|
|
243
295
|
};
|
|
244
296
|
const formatPrice = (price) => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
if (price >= 100) {
|
|
249
|
-
return price.toFixed(1);
|
|
250
|
-
}
|
|
251
|
-
return price.toFixed(2);
|
|
297
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
298
|
+
return price.toFixed(decimals);
|
|
252
299
|
};
|
|
253
300
|
const parseData = (nextData) => {
|
|
254
301
|
return nextData.map((point) => ({
|
|
@@ -302,6 +349,42 @@ function createChart(element, options = {}) {
|
|
|
302
349
|
const extra = index - (data.length - 1);
|
|
303
350
|
return new Date(last.time.getTime() + extra * stepMs);
|
|
304
351
|
};
|
|
352
|
+
const formatHoverTimeLabel = (time, mode) => {
|
|
353
|
+
if (mode === "time") {
|
|
354
|
+
return time.toLocaleTimeString(void 0, {
|
|
355
|
+
hour: "2-digit",
|
|
356
|
+
minute: "2-digit",
|
|
357
|
+
hour12: false
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (mode === "datetime") {
|
|
361
|
+
return time.toLocaleString(void 0, {
|
|
362
|
+
month: "short",
|
|
363
|
+
day: "numeric",
|
|
364
|
+
hour: "2-digit",
|
|
365
|
+
minute: "2-digit",
|
|
366
|
+
hour12: false
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (mode === "date") {
|
|
370
|
+
return time.toLocaleDateString(void 0, {
|
|
371
|
+
month: "short",
|
|
372
|
+
day: "numeric"
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
const stepMs = getTimeStepMs();
|
|
376
|
+
if (stepMs < 24 * 60 * 60 * 1e3) {
|
|
377
|
+
return time.toLocaleTimeString(void 0, {
|
|
378
|
+
hour: "2-digit",
|
|
379
|
+
minute: "2-digit",
|
|
380
|
+
hour12: false
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return time.toLocaleDateString(void 0, {
|
|
384
|
+
month: "short",
|
|
385
|
+
day: "numeric"
|
|
386
|
+
});
|
|
387
|
+
};
|
|
305
388
|
const drawText = (text, x, y, align = "left", baseline = "alphabetic", color = mergedOptions.axis?.textColor ?? mergedOptions.axisColor) => {
|
|
306
389
|
ctx.fillStyle = color;
|
|
307
390
|
ctx.textAlign = align;
|
|
@@ -365,7 +448,7 @@ function createChart(element, options = {}) {
|
|
|
365
448
|
return;
|
|
366
449
|
}
|
|
367
450
|
const lineY = clamp(yFromPrice(renderPrice), chartTop + 1, chartBottom - 1);
|
|
368
|
-
const color =
|
|
451
|
+
const color = line.color ?? (mergedLine.type === "takeProfit" ? "rgba(45,212,191,0.86)" : mergedLine.type === "stop" ? "rgba(245,158,11,0.86)" : "rgba(59,130,246,0.8)");
|
|
369
452
|
if (Number.isFinite(mergedLine.fillToPrice)) {
|
|
370
453
|
const fillY = clamp(yFromPrice(mergedLine.fillToPrice), chartTop + 1, chartBottom - 1);
|
|
371
454
|
const topY = Math.min(lineY, fillY);
|
|
@@ -476,7 +559,7 @@ function createChart(element, options = {}) {
|
|
|
476
559
|
const borderRadius = Math.max(0, mergedLine.labelBorderRadius);
|
|
477
560
|
const widgetBackground = mergedOptions.backgroundColor;
|
|
478
561
|
const widgetBorder = color;
|
|
479
|
-
const textColor = mergedLine.labelTextColor;
|
|
562
|
+
const textColor = line.labelTextColor ?? (mergedLine.type === "takeProfit" ? "#5eead4" : mergedLine.type === "stop" ? "#fbbf24" : mergedLine.labelTextColor);
|
|
480
563
|
const mainWidgetX = leftWidgetX + actionButtonsTotalWidth + actionButtonsGap;
|
|
481
564
|
ctx.fillStyle = widgetBackground;
|
|
482
565
|
fillRoundedRect(Math.round(mainWidgetX), Math.round(labelY), mainWidgetWidth, labelHeight, borderRadius);
|
|
@@ -726,6 +809,31 @@ function createChart(element, options = {}) {
|
|
|
726
809
|
const yFromPrice = (price) => {
|
|
727
810
|
return chartBottom - (price - yMin) / yRange * chartHeight;
|
|
728
811
|
};
|
|
812
|
+
if (watermark.visible && watermark.imageSrc.trim().length > 0) {
|
|
813
|
+
ensureWatermarkImage(watermark.imageSrc.trim());
|
|
814
|
+
if (watermarkImageReady && watermarkImage) {
|
|
815
|
+
const maxW = chartWidth * clamp(watermark.imageMaxWidthRatio, 0.05, 1);
|
|
816
|
+
const maxH = chartHeight * clamp(watermark.imageMaxHeightRatio, 0.05, 1);
|
|
817
|
+
const naturalW = Math.max(1, watermarkImage.naturalWidth || watermarkImage.width);
|
|
818
|
+
const naturalH = Math.max(1, watermarkImage.naturalHeight || watermarkImage.height);
|
|
819
|
+
const containScale = Math.min(maxW / naturalW, maxH / naturalH);
|
|
820
|
+
const scale = Math.max(0.05, containScale * Math.max(0.05, watermark.imageScale));
|
|
821
|
+
const drawW = naturalW * scale;
|
|
822
|
+
const drawH = naturalH * scale;
|
|
823
|
+
const drawX = chartLeft + (chartWidth - drawW) / 2;
|
|
824
|
+
const drawY = chartTop + (chartHeight - drawH) / 2;
|
|
825
|
+
ctx.save();
|
|
826
|
+
ctx.globalAlpha = clamp(watermark.opacity, 0, 1);
|
|
827
|
+
ctx.drawImage(watermarkImage, drawX, drawY, drawW, drawH);
|
|
828
|
+
if (watermark.imageTintColor.trim().length > 0) {
|
|
829
|
+
ctx.globalCompositeOperation = "source-atop";
|
|
830
|
+
ctx.globalAlpha = clamp(watermark.opacity, 0, 1) * clamp(watermark.imageTintOpacity, 0, 1);
|
|
831
|
+
ctx.fillStyle = watermark.imageTintColor;
|
|
832
|
+
ctx.fillRect(drawX, drawY, drawW, drawH);
|
|
833
|
+
}
|
|
834
|
+
ctx.restore();
|
|
835
|
+
}
|
|
836
|
+
}
|
|
729
837
|
if (watermark.visible && watermark.text.trim().length > 0) {
|
|
730
838
|
ctx.save();
|
|
731
839
|
ctx.globalAlpha = clamp(watermark.opacity, 0, 1);
|
|
@@ -923,6 +1031,61 @@ function createChart(element, options = {}) {
|
|
|
923
1031
|
});
|
|
924
1032
|
drawText(timeLabel, x, chartBottom + 8, "center", "top", axis.textColor);
|
|
925
1033
|
}
|
|
1034
|
+
if (crosshair.visible && crosshairPoint) {
|
|
1035
|
+
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
1036
|
+
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|
|
1037
|
+
const labelPaddingX = 8;
|
|
1038
|
+
const labelHeight = 20;
|
|
1039
|
+
const labelRadius = Math.max(0, crosshair.labelBorderRadius);
|
|
1040
|
+
const labelBackground = crosshair.labelBackgroundColor;
|
|
1041
|
+
const labelTextColor = crosshair.labelTextColor;
|
|
1042
|
+
const labelBorderColor = crosshair.labelBorderColor;
|
|
1043
|
+
const labelBorderWidth = Math.max(0, crosshair.labelBorderWidth);
|
|
1044
|
+
const labelBorderStyle = crosshair.labelBorderStyle;
|
|
1045
|
+
const strokeCrosshairLabel = (x, y, widthValue) => {
|
|
1046
|
+
if (labelBorderWidth <= 0) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
ctx.save();
|
|
1050
|
+
ctx.strokeStyle = labelBorderColor;
|
|
1051
|
+
ctx.lineWidth = labelBorderWidth;
|
|
1052
|
+
if (labelBorderStyle === "dotted") {
|
|
1053
|
+
ctx.setLineDash([2, 3]);
|
|
1054
|
+
} else if (labelBorderStyle === "dashed") {
|
|
1055
|
+
ctx.setLineDash([6, 4]);
|
|
1056
|
+
} else {
|
|
1057
|
+
ctx.setLineDash([]);
|
|
1058
|
+
}
|
|
1059
|
+
strokeRoundedRect(Math.round(x), Math.round(y), widthValue, labelHeight, labelRadius);
|
|
1060
|
+
ctx.restore();
|
|
1061
|
+
};
|
|
1062
|
+
if (crosshair.showPriceLabel) {
|
|
1063
|
+
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
1064
|
+
const priceText = formatPrice(hoverPrice);
|
|
1065
|
+
const priceWidth = Math.ceil(ctx.measureText(priceText).width) + labelPaddingX * 2;
|
|
1066
|
+
const priceX = chartRight + 4;
|
|
1067
|
+
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1068
|
+
ctx.fillStyle = labelBackground;
|
|
1069
|
+
fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
|
|
1070
|
+
strokeCrosshairLabel(priceX, priceY, priceWidth);
|
|
1071
|
+
drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
|
|
1072
|
+
}
|
|
1073
|
+
if (crosshair.showTimeLabel) {
|
|
1074
|
+
const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
|
|
1075
|
+
const hoverIndex = Math.round(xStart + ratio * xSpan - 0.5);
|
|
1076
|
+
const hoverTime = getTimeForIndex(hoverIndex);
|
|
1077
|
+
if (hoverTime) {
|
|
1078
|
+
const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
|
|
1079
|
+
const timeWidth = Math.ceil(ctx.measureText(timeText).width) + labelPaddingX * 2;
|
|
1080
|
+
const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
|
|
1081
|
+
const timeY = chartBottom + 8;
|
|
1082
|
+
ctx.fillStyle = labelBackground;
|
|
1083
|
+
fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, labelHeight, labelRadius);
|
|
1084
|
+
strokeCrosshairLabel(timeX, timeY, timeWidth);
|
|
1085
|
+
drawText(timeText, timeX + labelPaddingX, timeY + labelHeight / 2, "left", "middle", labelTextColor);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
926
1089
|
};
|
|
927
1090
|
const zoomX = (factor, anchorX) => {
|
|
928
1091
|
if (!drawState || data.length === 0) {
|
|
@@ -1017,6 +1180,39 @@ function createChart(element, options = {}) {
|
|
|
1017
1180
|
const ratio = clamp((drawState.chartBottom - y) / drawState.chartHeight, 0, 1);
|
|
1018
1181
|
return drawState.yMin + ratio * (drawState.yMax - drawState.yMin);
|
|
1019
1182
|
};
|
|
1183
|
+
const indexFromCanvasX = (x) => {
|
|
1184
|
+
if (!drawState) {
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
const ratio = clamp((x - drawState.chartLeft) / drawState.chartWidth, 0, 1);
|
|
1188
|
+
return Math.round(drawState.xStart + ratio * drawState.xSpan - 0.5);
|
|
1189
|
+
};
|
|
1190
|
+
const emitCrosshairMove = (x, y, region) => {
|
|
1191
|
+
if (!crosshairMoveHandler) {
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
const index = indexFromCanvasX(x);
|
|
1195
|
+
const hoverTime = index === null ? null : getTimeForIndex(index);
|
|
1196
|
+
const parsedPoint = index === null ? void 0 : data[index];
|
|
1197
|
+
const point = parsedPoint === void 0 ? void 0 : {
|
|
1198
|
+
t: parsedPoint.time.toISOString(),
|
|
1199
|
+
o: parsedPoint.o,
|
|
1200
|
+
h: parsedPoint.h,
|
|
1201
|
+
l: parsedPoint.l,
|
|
1202
|
+
c: parsedPoint.c,
|
|
1203
|
+
...parsedPoint.v === void 0 ? {} : { v: parsedPoint.v }
|
|
1204
|
+
};
|
|
1205
|
+
const payload = {
|
|
1206
|
+
x,
|
|
1207
|
+
y,
|
|
1208
|
+
region,
|
|
1209
|
+
...region === "plot" ? { price: Number(priceFromCanvasY(y).toFixed(mergedOptions.priceDecimals)) } : {},
|
|
1210
|
+
...index === null ? {} : { index },
|
|
1211
|
+
...hoverTime ? { time: hoverTime.toISOString() } : {},
|
|
1212
|
+
...point ? { point } : {}
|
|
1213
|
+
};
|
|
1214
|
+
crosshairMoveHandler(payload);
|
|
1215
|
+
};
|
|
1020
1216
|
const getHitRegion = (x, y) => {
|
|
1021
1217
|
if (!drawState) {
|
|
1022
1218
|
return "outside";
|
|
@@ -1168,14 +1364,17 @@ function createChart(element, options = {}) {
|
|
|
1168
1364
|
}
|
|
1169
1365
|
const hoverRegion = getHitRegion(point.x, point.y);
|
|
1170
1366
|
if (hoverRegion === "plot") {
|
|
1171
|
-
canvas.style.cursor = "
|
|
1367
|
+
canvas.style.cursor = "default";
|
|
1172
1368
|
setCrosshairPoint(point);
|
|
1369
|
+
emitCrosshairMove(point.x, point.y, "plot");
|
|
1173
1370
|
} else if (hoverRegion === "x-axis") {
|
|
1174
1371
|
canvas.style.cursor = "ew-resize";
|
|
1175
1372
|
setCrosshairPoint(null);
|
|
1373
|
+
emitCrosshairMove(point.x, point.y, "x-axis");
|
|
1176
1374
|
} else if (hoverRegion === "y-axis") {
|
|
1177
1375
|
canvas.style.cursor = "ns-resize";
|
|
1178
1376
|
setCrosshairPoint(null);
|
|
1377
|
+
emitCrosshairMove(point.x, point.y, "y-axis");
|
|
1179
1378
|
} else {
|
|
1180
1379
|
canvas.style.cursor = "default";
|
|
1181
1380
|
setCrosshairPoint(null);
|
|
@@ -1185,7 +1384,7 @@ function createChart(element, options = {}) {
|
|
|
1185
1384
|
const deltaX = point.x - lastPointerX;
|
|
1186
1385
|
const deltaY = point.y - lastPointerY;
|
|
1187
1386
|
if (dragMode === "plot") {
|
|
1188
|
-
canvas.style.cursor = "
|
|
1387
|
+
canvas.style.cursor = "default";
|
|
1189
1388
|
pan(deltaX, deltaY, true, true);
|
|
1190
1389
|
setCrosshairPoint(null);
|
|
1191
1390
|
} else if (dragMode === "x-axis") {
|
|
@@ -1350,7 +1549,11 @@ function createChart(element, options = {}) {
|
|
|
1350
1549
|
autoYMin = null;
|
|
1351
1550
|
autoYMax = null;
|
|
1352
1551
|
} else {
|
|
1353
|
-
|
|
1552
|
+
if (mergedOptions.preserveViewportOnDataUpdate) {
|
|
1553
|
+
clampXViewport();
|
|
1554
|
+
} else {
|
|
1555
|
+
fitXViewport();
|
|
1556
|
+
}
|
|
1354
1557
|
}
|
|
1355
1558
|
draw();
|
|
1356
1559
|
};
|
|
@@ -1411,6 +1614,9 @@ function createChart(element, options = {}) {
|
|
|
1411
1614
|
const onChartClick = (handler) => {
|
|
1412
1615
|
chartClickHandler = handler;
|
|
1413
1616
|
};
|
|
1617
|
+
const onCrosshairMove = (handler) => {
|
|
1618
|
+
crosshairMoveHandler = handler;
|
|
1619
|
+
};
|
|
1414
1620
|
const setDoubleClickEnabled = (enabled) => {
|
|
1415
1621
|
doubleClickEnabled = enabled;
|
|
1416
1622
|
};
|
|
@@ -1439,6 +1645,7 @@ function createChart(element, options = {}) {
|
|
|
1439
1645
|
removeOrderLine,
|
|
1440
1646
|
onOrderAction,
|
|
1441
1647
|
onChartClick,
|
|
1648
|
+
onCrosshairMove,
|
|
1442
1649
|
setDoubleClickEnabled,
|
|
1443
1650
|
setDoubleClickAction,
|
|
1444
1651
|
resize,
|
package/dist/index.cjs
CHANGED
|
@@ -42,7 +42,16 @@ var DEFAULT_CROSSHAIR_OPTIONS = {
|
|
|
42
42
|
width: 1,
|
|
43
43
|
style: "dotted",
|
|
44
44
|
showHorizontal: true,
|
|
45
|
-
showVertical: true
|
|
45
|
+
showVertical: true,
|
|
46
|
+
showPriceLabel: true,
|
|
47
|
+
showTimeLabel: true,
|
|
48
|
+
timeLabelFormat: "auto",
|
|
49
|
+
labelBackgroundColor: "#0b1220",
|
|
50
|
+
labelTextColor: "#cbd5e1",
|
|
51
|
+
labelBorderRadius: 3,
|
|
52
|
+
labelBorderColor: "#94a3b8",
|
|
53
|
+
labelBorderWidth: 1,
|
|
54
|
+
labelBorderStyle: "solid"
|
|
46
55
|
};
|
|
47
56
|
var DEFAULT_WATERMARK_OPTIONS = {
|
|
48
57
|
visible: false,
|
|
@@ -176,6 +185,7 @@ function createChart(element, options = {}) {
|
|
|
176
185
|
}));
|
|
177
186
|
let orderActionHandler = null;
|
|
178
187
|
let chartClickHandler = null;
|
|
188
|
+
let crosshairMoveHandler = null;
|
|
179
189
|
let orderActionRegions = [];
|
|
180
190
|
let orderDragRegions = [];
|
|
181
191
|
let generatedPriceLineId = 1;
|
|
@@ -363,6 +373,42 @@ function createChart(element, options = {}) {
|
|
|
363
373
|
const extra = index - (data.length - 1);
|
|
364
374
|
return new Date(last.time.getTime() + extra * stepMs);
|
|
365
375
|
};
|
|
376
|
+
const formatHoverTimeLabel = (time, mode) => {
|
|
377
|
+
if (mode === "time") {
|
|
378
|
+
return time.toLocaleTimeString(void 0, {
|
|
379
|
+
hour: "2-digit",
|
|
380
|
+
minute: "2-digit",
|
|
381
|
+
hour12: false
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (mode === "datetime") {
|
|
385
|
+
return time.toLocaleString(void 0, {
|
|
386
|
+
month: "short",
|
|
387
|
+
day: "numeric",
|
|
388
|
+
hour: "2-digit",
|
|
389
|
+
minute: "2-digit",
|
|
390
|
+
hour12: false
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
if (mode === "date") {
|
|
394
|
+
return time.toLocaleDateString(void 0, {
|
|
395
|
+
month: "short",
|
|
396
|
+
day: "numeric"
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const stepMs = getTimeStepMs();
|
|
400
|
+
if (stepMs < 24 * 60 * 60 * 1e3) {
|
|
401
|
+
return time.toLocaleTimeString(void 0, {
|
|
402
|
+
hour: "2-digit",
|
|
403
|
+
minute: "2-digit",
|
|
404
|
+
hour12: false
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return time.toLocaleDateString(void 0, {
|
|
408
|
+
month: "short",
|
|
409
|
+
day: "numeric"
|
|
410
|
+
});
|
|
411
|
+
};
|
|
366
412
|
const drawText = (text, x, y, align = "left", baseline = "alphabetic", color = mergedOptions.axis?.textColor ?? mergedOptions.axisColor) => {
|
|
367
413
|
ctx.fillStyle = color;
|
|
368
414
|
ctx.textAlign = align;
|
|
@@ -1009,6 +1055,61 @@ function createChart(element, options = {}) {
|
|
|
1009
1055
|
});
|
|
1010
1056
|
drawText(timeLabel, x, chartBottom + 8, "center", "top", axis.textColor);
|
|
1011
1057
|
}
|
|
1058
|
+
if (crosshair.visible && crosshairPoint) {
|
|
1059
|
+
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
1060
|
+
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|
|
1061
|
+
const labelPaddingX = 8;
|
|
1062
|
+
const labelHeight = 20;
|
|
1063
|
+
const labelRadius = Math.max(0, crosshair.labelBorderRadius);
|
|
1064
|
+
const labelBackground = crosshair.labelBackgroundColor;
|
|
1065
|
+
const labelTextColor = crosshair.labelTextColor;
|
|
1066
|
+
const labelBorderColor = crosshair.labelBorderColor;
|
|
1067
|
+
const labelBorderWidth = Math.max(0, crosshair.labelBorderWidth);
|
|
1068
|
+
const labelBorderStyle = crosshair.labelBorderStyle;
|
|
1069
|
+
const strokeCrosshairLabel = (x, y, widthValue) => {
|
|
1070
|
+
if (labelBorderWidth <= 0) {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
ctx.save();
|
|
1074
|
+
ctx.strokeStyle = labelBorderColor;
|
|
1075
|
+
ctx.lineWidth = labelBorderWidth;
|
|
1076
|
+
if (labelBorderStyle === "dotted") {
|
|
1077
|
+
ctx.setLineDash([2, 3]);
|
|
1078
|
+
} else if (labelBorderStyle === "dashed") {
|
|
1079
|
+
ctx.setLineDash([6, 4]);
|
|
1080
|
+
} else {
|
|
1081
|
+
ctx.setLineDash([]);
|
|
1082
|
+
}
|
|
1083
|
+
strokeRoundedRect(Math.round(x), Math.round(y), widthValue, labelHeight, labelRadius);
|
|
1084
|
+
ctx.restore();
|
|
1085
|
+
};
|
|
1086
|
+
if (crosshair.showPriceLabel) {
|
|
1087
|
+
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
1088
|
+
const priceText = formatPrice(hoverPrice);
|
|
1089
|
+
const priceWidth = Math.ceil(ctx.measureText(priceText).width) + labelPaddingX * 2;
|
|
1090
|
+
const priceX = chartRight + 4;
|
|
1091
|
+
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1092
|
+
ctx.fillStyle = labelBackground;
|
|
1093
|
+
fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
|
|
1094
|
+
strokeCrosshairLabel(priceX, priceY, priceWidth);
|
|
1095
|
+
drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
|
|
1096
|
+
}
|
|
1097
|
+
if (crosshair.showTimeLabel) {
|
|
1098
|
+
const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
|
|
1099
|
+
const hoverIndex = Math.round(xStart + ratio * xSpan - 0.5);
|
|
1100
|
+
const hoverTime = getTimeForIndex(hoverIndex);
|
|
1101
|
+
if (hoverTime) {
|
|
1102
|
+
const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
|
|
1103
|
+
const timeWidth = Math.ceil(ctx.measureText(timeText).width) + labelPaddingX * 2;
|
|
1104
|
+
const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
|
|
1105
|
+
const timeY = chartBottom + 8;
|
|
1106
|
+
ctx.fillStyle = labelBackground;
|
|
1107
|
+
fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, labelHeight, labelRadius);
|
|
1108
|
+
strokeCrosshairLabel(timeX, timeY, timeWidth);
|
|
1109
|
+
drawText(timeText, timeX + labelPaddingX, timeY + labelHeight / 2, "left", "middle", labelTextColor);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1012
1113
|
};
|
|
1013
1114
|
const zoomX = (factor, anchorX) => {
|
|
1014
1115
|
if (!drawState || data.length === 0) {
|
|
@@ -1103,6 +1204,39 @@ function createChart(element, options = {}) {
|
|
|
1103
1204
|
const ratio = clamp((drawState.chartBottom - y) / drawState.chartHeight, 0, 1);
|
|
1104
1205
|
return drawState.yMin + ratio * (drawState.yMax - drawState.yMin);
|
|
1105
1206
|
};
|
|
1207
|
+
const indexFromCanvasX = (x) => {
|
|
1208
|
+
if (!drawState) {
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
const ratio = clamp((x - drawState.chartLeft) / drawState.chartWidth, 0, 1);
|
|
1212
|
+
return Math.round(drawState.xStart + ratio * drawState.xSpan - 0.5);
|
|
1213
|
+
};
|
|
1214
|
+
const emitCrosshairMove = (x, y, region) => {
|
|
1215
|
+
if (!crosshairMoveHandler) {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const index = indexFromCanvasX(x);
|
|
1219
|
+
const hoverTime = index === null ? null : getTimeForIndex(index);
|
|
1220
|
+
const parsedPoint = index === null ? void 0 : data[index];
|
|
1221
|
+
const point = parsedPoint === void 0 ? void 0 : {
|
|
1222
|
+
t: parsedPoint.time.toISOString(),
|
|
1223
|
+
o: parsedPoint.o,
|
|
1224
|
+
h: parsedPoint.h,
|
|
1225
|
+
l: parsedPoint.l,
|
|
1226
|
+
c: parsedPoint.c,
|
|
1227
|
+
...parsedPoint.v === void 0 ? {} : { v: parsedPoint.v }
|
|
1228
|
+
};
|
|
1229
|
+
const payload = {
|
|
1230
|
+
x,
|
|
1231
|
+
y,
|
|
1232
|
+
region,
|
|
1233
|
+
...region === "plot" ? { price: Number(priceFromCanvasY(y).toFixed(mergedOptions.priceDecimals)) } : {},
|
|
1234
|
+
...index === null ? {} : { index },
|
|
1235
|
+
...hoverTime ? { time: hoverTime.toISOString() } : {},
|
|
1236
|
+
...point ? { point } : {}
|
|
1237
|
+
};
|
|
1238
|
+
crosshairMoveHandler(payload);
|
|
1239
|
+
};
|
|
1106
1240
|
const getHitRegion = (x, y) => {
|
|
1107
1241
|
if (!drawState) {
|
|
1108
1242
|
return "outside";
|
|
@@ -1254,14 +1388,17 @@ function createChart(element, options = {}) {
|
|
|
1254
1388
|
}
|
|
1255
1389
|
const hoverRegion = getHitRegion(point.x, point.y);
|
|
1256
1390
|
if (hoverRegion === "plot") {
|
|
1257
|
-
canvas.style.cursor = "
|
|
1391
|
+
canvas.style.cursor = "default";
|
|
1258
1392
|
setCrosshairPoint(point);
|
|
1393
|
+
emitCrosshairMove(point.x, point.y, "plot");
|
|
1259
1394
|
} else if (hoverRegion === "x-axis") {
|
|
1260
1395
|
canvas.style.cursor = "ew-resize";
|
|
1261
1396
|
setCrosshairPoint(null);
|
|
1397
|
+
emitCrosshairMove(point.x, point.y, "x-axis");
|
|
1262
1398
|
} else if (hoverRegion === "y-axis") {
|
|
1263
1399
|
canvas.style.cursor = "ns-resize";
|
|
1264
1400
|
setCrosshairPoint(null);
|
|
1401
|
+
emitCrosshairMove(point.x, point.y, "y-axis");
|
|
1265
1402
|
} else {
|
|
1266
1403
|
canvas.style.cursor = "default";
|
|
1267
1404
|
setCrosshairPoint(null);
|
|
@@ -1271,7 +1408,7 @@ function createChart(element, options = {}) {
|
|
|
1271
1408
|
const deltaX = point.x - lastPointerX;
|
|
1272
1409
|
const deltaY = point.y - lastPointerY;
|
|
1273
1410
|
if (dragMode === "plot") {
|
|
1274
|
-
canvas.style.cursor = "
|
|
1411
|
+
canvas.style.cursor = "default";
|
|
1275
1412
|
pan(deltaX, deltaY, true, true);
|
|
1276
1413
|
setCrosshairPoint(null);
|
|
1277
1414
|
} else if (dragMode === "x-axis") {
|
|
@@ -1501,6 +1638,9 @@ function createChart(element, options = {}) {
|
|
|
1501
1638
|
const onChartClick = (handler) => {
|
|
1502
1639
|
chartClickHandler = handler;
|
|
1503
1640
|
};
|
|
1641
|
+
const onCrosshairMove = (handler) => {
|
|
1642
|
+
crosshairMoveHandler = handler;
|
|
1643
|
+
};
|
|
1504
1644
|
const setDoubleClickEnabled = (enabled) => {
|
|
1505
1645
|
doubleClickEnabled = enabled;
|
|
1506
1646
|
};
|
|
@@ -1529,6 +1669,7 @@ function createChart(element, options = {}) {
|
|
|
1529
1669
|
removeOrderLine,
|
|
1530
1670
|
onOrderAction,
|
|
1531
1671
|
onChartClick,
|
|
1672
|
+
onCrosshairMove,
|
|
1532
1673
|
setDoubleClickEnabled,
|
|
1533
1674
|
setDoubleClickAction,
|
|
1534
1675
|
resize,
|