hyperprop-charting-library 0.1.8 → 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.
@@ -1,14 +1,14 @@
1
1
  // src/index.ts
2
2
  var DEFAULT_GRID_OPTIONS = {
3
- color: "#e2e8f0",
4
- opacity: 0.9,
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: "#94a3b8",
11
- textColor: "#94a3b8",
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: "#94a3b8",
27
- opacity: 0.2,
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: "#f59e0b",
37
- labelBackgroundColor: "#f59e0b",
38
- labelTextColor: "#0f172a",
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: "#f59e0b",
48
- labelBackgroundColor: "#f59e0b",
49
- labelTextColor: "#0f172a",
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: "#ffffff",
77
- axisColor: "#94a3b8",
91
+ backgroundColor: "#101114",
92
+ axisColor: "#7f8289",
78
93
  axis: DEFAULT_AXIS_OPTIONS,
79
- upColor: "#16a34a",
80
- downColor: "#dc2626",
81
- gridColor: "#e2e8f0",
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: "dashed",
117
+ style: "dotted",
98
118
  thickness: 1,
99
- color: "#22c55e",
100
- labelBackgroundColor: "#22c55e",
119
+ color: "#38bdf8",
120
+ labelBackgroundColor: "#38bdf8",
101
121
  labelTextColor: "#0b1220",
102
- labelBorderRadius: 6
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 = 2;
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
- xSpan = Math.min(60, count);
205
- xCenter = count - xSpan / 2;
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
- if (price >= 1e3) {
246
- return price.toFixed(0);
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 = mergedLine.color ?? (mergedLine.side === "buy" ? mergedOptions.upColor : mergedLine.side === "sell" ? mergedOptions.downColor : "#f59e0b");
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 = "grab";
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 = "grabbing";
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
- clampXViewport();
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
@@ -45,9 +45,13 @@ var DEFAULT_CROSSHAIR_OPTIONS = {
45
45
  showVertical: true,
46
46
  showPriceLabel: true,
47
47
  showTimeLabel: true,
48
+ timeLabelFormat: "auto",
48
49
  labelBackgroundColor: "#0b1220",
49
50
  labelTextColor: "#cbd5e1",
50
- labelBorderRadius: 3
51
+ labelBorderRadius: 3,
52
+ labelBorderColor: "#94a3b8",
53
+ labelBorderWidth: 1,
54
+ labelBorderStyle: "solid"
51
55
  };
52
56
  var DEFAULT_WATERMARK_OPTIONS = {
53
57
  visible: false,
@@ -181,6 +185,7 @@ function createChart(element, options = {}) {
181
185
  }));
182
186
  let orderActionHandler = null;
183
187
  let chartClickHandler = null;
188
+ let crosshairMoveHandler = null;
184
189
  let orderActionRegions = [];
185
190
  let orderDragRegions = [];
186
191
  let generatedPriceLineId = 1;
@@ -368,6 +373,42 @@ function createChart(element, options = {}) {
368
373
  const extra = index - (data.length - 1);
369
374
  return new Date(last.time.getTime() + extra * stepMs);
370
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
+ };
371
412
  const drawText = (text, x, y, align = "left", baseline = "alphabetic", color = mergedOptions.axis?.textColor ?? mergedOptions.axisColor) => {
372
413
  ctx.fillStyle = color;
373
414
  ctx.textAlign = align;
@@ -1022,6 +1063,26 @@ function createChart(element, options = {}) {
1022
1063
  const labelRadius = Math.max(0, crosshair.labelBorderRadius);
1023
1064
  const labelBackground = crosshair.labelBackgroundColor;
1024
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
+ };
1025
1086
  if (crosshair.showPriceLabel) {
1026
1087
  const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
1027
1088
  const priceText = formatPrice(hoverPrice);
@@ -1030,6 +1091,7 @@ function createChart(element, options = {}) {
1030
1091
  const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
1031
1092
  ctx.fillStyle = labelBackground;
1032
1093
  fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
1094
+ strokeCrosshairLabel(priceX, priceY, priceWidth);
1033
1095
  drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
1034
1096
  }
1035
1097
  if (crosshair.showTimeLabel) {
@@ -1037,15 +1099,13 @@ function createChart(element, options = {}) {
1037
1099
  const hoverIndex = Math.round(xStart + ratio * xSpan - 0.5);
1038
1100
  const hoverTime = getTimeForIndex(hoverIndex);
1039
1101
  if (hoverTime) {
1040
- const timeText = hoverTime.toLocaleDateString(void 0, {
1041
- month: "short",
1042
- day: "numeric"
1043
- });
1102
+ const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
1044
1103
  const timeWidth = Math.ceil(ctx.measureText(timeText).width) + labelPaddingX * 2;
1045
1104
  const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
1046
1105
  const timeY = chartBottom + 8;
1047
1106
  ctx.fillStyle = labelBackground;
1048
1107
  fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, labelHeight, labelRadius);
1108
+ strokeCrosshairLabel(timeX, timeY, timeWidth);
1049
1109
  drawText(timeText, timeX + labelPaddingX, timeY + labelHeight / 2, "left", "middle", labelTextColor);
1050
1110
  }
1051
1111
  }
@@ -1144,6 +1204,39 @@ function createChart(element, options = {}) {
1144
1204
  const ratio = clamp((drawState.chartBottom - y) / drawState.chartHeight, 0, 1);
1145
1205
  return drawState.yMin + ratio * (drawState.yMax - drawState.yMin);
1146
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
+ };
1147
1240
  const getHitRegion = (x, y) => {
1148
1241
  if (!drawState) {
1149
1242
  return "outside";
@@ -1297,12 +1390,15 @@ function createChart(element, options = {}) {
1297
1390
  if (hoverRegion === "plot") {
1298
1391
  canvas.style.cursor = "default";
1299
1392
  setCrosshairPoint(point);
1393
+ emitCrosshairMove(point.x, point.y, "plot");
1300
1394
  } else if (hoverRegion === "x-axis") {
1301
1395
  canvas.style.cursor = "ew-resize";
1302
1396
  setCrosshairPoint(null);
1397
+ emitCrosshairMove(point.x, point.y, "x-axis");
1303
1398
  } else if (hoverRegion === "y-axis") {
1304
1399
  canvas.style.cursor = "ns-resize";
1305
1400
  setCrosshairPoint(null);
1401
+ emitCrosshairMove(point.x, point.y, "y-axis");
1306
1402
  } else {
1307
1403
  canvas.style.cursor = "default";
1308
1404
  setCrosshairPoint(null);
@@ -1312,7 +1408,7 @@ function createChart(element, options = {}) {
1312
1408
  const deltaX = point.x - lastPointerX;
1313
1409
  const deltaY = point.y - lastPointerY;
1314
1410
  if (dragMode === "plot") {
1315
- canvas.style.cursor = "grabbing";
1411
+ canvas.style.cursor = "default";
1316
1412
  pan(deltaX, deltaY, true, true);
1317
1413
  setCrosshairPoint(null);
1318
1414
  } else if (dragMode === "x-axis") {
@@ -1542,6 +1638,9 @@ function createChart(element, options = {}) {
1542
1638
  const onChartClick = (handler) => {
1543
1639
  chartClickHandler = handler;
1544
1640
  };
1641
+ const onCrosshairMove = (handler) => {
1642
+ crosshairMoveHandler = handler;
1643
+ };
1545
1644
  const setDoubleClickEnabled = (enabled) => {
1546
1645
  doubleClickEnabled = enabled;
1547
1646
  };
@@ -1570,6 +1669,7 @@ function createChart(element, options = {}) {
1570
1669
  removeOrderLine,
1571
1670
  onOrderAction,
1572
1671
  onChartClick,
1672
+ onCrosshairMove,
1573
1673
  setDoubleClickEnabled,
1574
1674
  setDoubleClickAction,
1575
1675
  resize,
package/dist/index.d.cts CHANGED
@@ -49,9 +49,13 @@ interface CrosshairOptions {
49
49
  showVertical?: boolean;
50
50
  showPriceLabel?: boolean;
51
51
  showTimeLabel?: boolean;
52
+ timeLabelFormat?: "auto" | "date" | "time" | "datetime";
52
53
  labelBackgroundColor?: string;
53
54
  labelTextColor?: string;
54
55
  labelBorderRadius?: number;
56
+ labelBorderColor?: string;
57
+ labelBorderWidth?: number;
58
+ labelBorderStyle?: "solid" | "dotted" | "dashed";
55
59
  }
56
60
  interface WatermarkOptions {
57
61
  visible?: boolean;
@@ -148,6 +152,15 @@ interface ChartClickEvent {
148
152
  price?: number;
149
153
  region: "plot" | "x-axis" | "y-axis";
150
154
  }
155
+ interface CrosshairMoveEvent {
156
+ x: number;
157
+ y: number;
158
+ region: "plot" | "x-axis" | "y-axis";
159
+ price?: number;
160
+ index?: number;
161
+ time?: string;
162
+ point?: OhlcDataPoint;
163
+ }
151
164
  interface TickerLineOptions {
152
165
  visible?: boolean;
153
166
  style?: "solid" | "dotted" | "dashed";
@@ -168,6 +181,7 @@ interface ChartInstance {
168
181
  removeOrderLine: (id: string) => void;
169
182
  onOrderAction: (handler: ((event: OrderActionEvent) => void) | null) => void;
170
183
  onChartClick: (handler: ((event: ChartClickEvent) => void) | null) => void;
184
+ onCrosshairMove: (handler: ((event: CrosshairMoveEvent) => void) | null) => void;
171
185
  setDoubleClickEnabled: (enabled: boolean) => void;
172
186
  setDoubleClickAction: (action: "reset" | "placeLimitOrder") => void;
173
187
  resize: (width?: number, height?: number) => void;
@@ -183,4 +197,4 @@ interface OhlcDataPoint {
183
197
  }
184
198
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
185
199
 
186
- export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
200
+ export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };