hyperprop-charting-library 0.1.16 → 0.1.18

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 CHANGED
@@ -60,6 +60,22 @@ const chart = createChart(root, {
60
60
  });
61
61
  ```
62
62
 
63
+ ## Crosshair "+" Button
64
+
65
+ ```ts
66
+ const chart = createChart(root, {
67
+ crosshair: {
68
+ showPriceActionButton: true,
69
+ priceActionButtonRounded: false // square button
70
+ }
71
+ });
72
+
73
+ chart.onCrosshairPriceAction((event) => {
74
+ // Frontend decides what to do
75
+ console.log(event.price);
76
+ });
77
+ ```
78
+
63
79
  ## Full Documentation
64
80
 
65
81
  - API reference: `docs/API.md`
@@ -74,7 +90,7 @@ const chart = createChart(root, {
74
90
  - `chart.setData(data)`
75
91
  - `chart.setPriceLines(lines)` / `chart.addPriceLine(line)` / `chart.removePriceLine(id)`
76
92
  - `chart.setOrderLines(lines)` / `chart.addOrderLine(line)` / `chart.updateOrderLine(id, patch)` / `chart.removeOrderLine(id)`
77
- - `chart.onOrderAction(handler)` / `chart.onChartClick(handler)` / `chart.onCrosshairMove(handler)`
93
+ - `chart.onOrderAction(handler)` / `chart.onChartClick(handler)` / `chart.onCrosshairMove(handler)` / `chart.onCrosshairPriceAction(handler)`
78
94
  - `chart.setDoubleClickEnabled(enabled)` / `chart.setDoubleClickAction(action)`
79
95
  - `chart.zoomInX()` / `chart.zoomOutX()` / `chart.panX(bars)` / `chart.resetViewport()`
80
96
  - `chart.resize(width, height)` / `chart.destroy()`
@@ -51,7 +51,14 @@ var DEFAULT_CROSSHAIR_OPTIONS = {
51
51
  labelBorderRadius: 3,
52
52
  labelBorderColor: "#94a3b8",
53
53
  labelBorderWidth: 1,
54
- labelBorderStyle: "solid"
54
+ labelBorderStyle: "solid",
55
+ showPriceActionButton: false,
56
+ priceActionButtonIcon: "plusThin",
57
+ priceActionButtonText: "+",
58
+ priceActionButtonSize: 16,
59
+ priceActionButtonGap: 4,
60
+ priceActionButtonRounded: true,
61
+ priceActionButtonBorderRadius: 8
55
62
  };
56
63
  var DEFAULT_WATERMARK_OPTIONS = {
57
64
  visible: false,
@@ -163,10 +170,6 @@ var DEFAULT_OPTIONS = {
163
170
  },
164
171
  dashPatterns: DEFAULT_DASH_PATTERNS
165
172
  };
166
- var BRAND_LOGO_VIEWBOX_WIDTH = 190;
167
- var BRAND_LOGO_VIEWBOX_HEIGHT = 186;
168
- var BRAND_LOGO_PATH_A = "M0 93.0171V45.2271H48.9851V75.0332C48.9851 84.9545 57.0416 93.0001 66.9763 93.0001H94.9957V186H49.0531V110.984C49.0531 101.063 40.9965 93.0171 31.0619 93.0171H0Z";
169
- var BRAND_LOGO_PATH_B = "M190 92.9915V140.782H141.015V110.975C141.015 101.054 132.958 93.0085 123.023 93.0085H95.0039V0H140.955V75.0162C140.955 84.9374 149.012 92.9831 158.946 92.9831H190V92.9915Z";
170
173
  function createChart(element, options = {}) {
171
174
  const mergedOptions = {
172
175
  ...DEFAULT_OPTIONS,
@@ -211,6 +214,8 @@ function createChart(element, options = {}) {
211
214
  let orderActionHandler = null;
212
215
  let chartClickHandler = null;
213
216
  let crosshairMoveHandler = null;
217
+ let crosshairPriceActionHandler = null;
218
+ let crosshairPriceActionRegion = null;
214
219
  let orderActionRegions = [];
215
220
  let orderDragRegions = [];
216
221
  let generatedPriceLineId = 1;
@@ -223,8 +228,6 @@ function createChart(element, options = {}) {
223
228
  let yMaxOverride = null;
224
229
  let autoYMin = null;
225
230
  let autoYMax = null;
226
- const brandLogoPathA = new Path2D(BRAND_LOGO_PATH_A);
227
- const brandLogoPathB = new Path2D(BRAND_LOGO_PATH_B);
228
231
  let watermarkImageSrc = null;
229
232
  let watermarkImage = null;
230
233
  let watermarkImageReady = false;
@@ -802,6 +805,7 @@ function createChart(element, options = {}) {
802
805
  const draw = () => {
803
806
  orderActionRegions = [];
804
807
  orderDragRegions = [];
808
+ crosshairPriceActionRegion = null;
805
809
  const pixelRatio = getPixelRatio();
806
810
  canvas.style.width = `${width}px`;
807
811
  canvas.style.height = `${height}px`;
@@ -1107,17 +1111,6 @@ function createChart(element, options = {}) {
1107
1111
  });
1108
1112
  drawText(timeLabel, x, chartBottom + 8, "center", "top", axis.textColor);
1109
1113
  }
1110
- const brandLogoWidth = 34;
1111
- const brandLogoHeight = BRAND_LOGO_VIEWBOX_HEIGHT / BRAND_LOGO_VIEWBOX_WIDTH * brandLogoWidth;
1112
- const brandLogoX = chartLeft + 16;
1113
- const brandLogoY = chartBottom - brandLogoHeight - 10;
1114
- ctx.save();
1115
- ctx.translate(brandLogoX, brandLogoY);
1116
- ctx.scale(brandLogoWidth / BRAND_LOGO_VIEWBOX_WIDTH, brandLogoHeight / BRAND_LOGO_VIEWBOX_HEIGHT);
1117
- ctx.fillStyle = "#ffffff";
1118
- ctx.fill(brandLogoPathA);
1119
- ctx.fill(brandLogoPathB);
1120
- ctx.restore();
1121
1114
  if (crosshair.visible && crosshairPoint) {
1122
1115
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
1123
1116
  const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
@@ -1154,6 +1147,83 @@ function createChart(element, options = {}) {
1154
1147
  fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
1155
1148
  strokeCrosshairLabel(priceX, priceY, priceWidth);
1156
1149
  drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
1150
+ if (crosshair.showPriceActionButton) {
1151
+ const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4));
1152
+ const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
1153
+ const containerPaddingX = 5;
1154
+ const containerWidth = buttonSize + containerPaddingX * 2;
1155
+ const containerX = priceX - buttonGap - containerWidth;
1156
+ const containerY = priceY;
1157
+ const buttonX = containerX + containerPaddingX;
1158
+ const buttonY = priceY + (labelHeight - buttonSize) / 2;
1159
+ const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
1160
+ const buttonBorderWidth = Math.max(1, Math.round(labelBorderWidth || 1));
1161
+ const buttonCenterX = buttonX + buttonSize / 2;
1162
+ const buttonCenterY = buttonY + buttonSize / 2;
1163
+ const containerRadius = labelRadius;
1164
+ ctx.fillStyle = labelBackground;
1165
+ fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
1166
+ if (labelBorderWidth > 0) {
1167
+ ctx.save();
1168
+ ctx.strokeStyle = labelBorderColor;
1169
+ ctx.lineWidth = labelBorderWidth;
1170
+ ctx.setLineDash([]);
1171
+ strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
1172
+ ctx.restore();
1173
+ }
1174
+ if (buttonBorderWidth > 0) {
1175
+ ctx.save();
1176
+ ctx.strokeStyle = labelBorderColor;
1177
+ ctx.lineWidth = buttonBorderWidth;
1178
+ ctx.setLineDash([]);
1179
+ if (crosshair.priceActionButtonRounded) {
1180
+ ctx.beginPath();
1181
+ ctx.arc(
1182
+ buttonCenterX,
1183
+ buttonCenterY,
1184
+ Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
1185
+ 0,
1186
+ Math.PI * 2
1187
+ );
1188
+ ctx.stroke();
1189
+ } else {
1190
+ strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
1191
+ }
1192
+ ctx.restore();
1193
+ }
1194
+ if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
1195
+ const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
1196
+ const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
1197
+ ctx.save();
1198
+ ctx.strokeStyle = labelTextColor;
1199
+ ctx.lineWidth = plusThickness;
1200
+ ctx.lineCap = "round";
1201
+ ctx.setLineDash([]);
1202
+ ctx.beginPath();
1203
+ ctx.moveTo(buttonCenterX - plusHalf, buttonCenterY);
1204
+ ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
1205
+ ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
1206
+ ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
1207
+ ctx.stroke();
1208
+ ctx.restore();
1209
+ } else {
1210
+ drawText(
1211
+ crosshair.priceActionButtonText,
1212
+ buttonCenterX,
1213
+ buttonCenterY,
1214
+ "center",
1215
+ "middle",
1216
+ labelTextColor
1217
+ );
1218
+ }
1219
+ crosshairPriceActionRegion = {
1220
+ x: containerX,
1221
+ y: containerY,
1222
+ width: containerWidth,
1223
+ height: labelHeight,
1224
+ price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
1225
+ };
1226
+ }
1157
1227
  }
1158
1228
  if (crosshair.showTimeLabel) {
1159
1229
  const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
@@ -1316,6 +1386,16 @@ function createChart(element, options = {}) {
1316
1386
  (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
1317
1387
  );
1318
1388
  };
1389
+ const getCrosshairPriceActionRegion = (x, y) => {
1390
+ if (!crosshairPriceActionRegion) {
1391
+ return null;
1392
+ }
1393
+ const region = crosshairPriceActionRegion;
1394
+ if (x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height) {
1395
+ return region;
1396
+ }
1397
+ return null;
1398
+ };
1319
1399
  const priceFromCanvasY = (y) => {
1320
1400
  if (!drawState) {
1321
1401
  return 0;
@@ -1381,6 +1461,15 @@ function createChart(element, options = {}) {
1381
1461
  let activePointerId = null;
1382
1462
  const onPointerDown = (event) => {
1383
1463
  const point = getCanvasPoint(event);
1464
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1465
+ if (crosshairButtonRegion) {
1466
+ crosshairPriceActionHandler?.({
1467
+ x: point.x,
1468
+ y: point.y,
1469
+ price: crosshairButtonRegion.price
1470
+ });
1471
+ return;
1472
+ }
1384
1473
  const orderRegion = getOrderActionRegion(point.x, point.y);
1385
1474
  if (orderRegion) {
1386
1475
  if (orderRegion.draggable) {
@@ -1493,6 +1582,11 @@ function createChart(element, options = {}) {
1493
1582
  return;
1494
1583
  }
1495
1584
  if (!isDragging || !dragMode) {
1585
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1586
+ if (crosshairButtonRegion) {
1587
+ canvas.style.cursor = "pointer";
1588
+ return;
1589
+ }
1496
1590
  const orderRegion = getOrderActionRegion(point.x, point.y);
1497
1591
  if (orderRegion) {
1498
1592
  canvas.style.cursor = orderRegion.draggable ? "ns-resize" : "pointer";
@@ -1746,6 +1840,9 @@ function createChart(element, options = {}) {
1746
1840
  const onCrosshairMove = (handler) => {
1747
1841
  crosshairMoveHandler = handler;
1748
1842
  };
1843
+ const onCrosshairPriceAction = (handler) => {
1844
+ crosshairPriceActionHandler = handler;
1845
+ };
1749
1846
  const setDoubleClickEnabled = (enabled) => {
1750
1847
  doubleClickEnabled = enabled;
1751
1848
  };
@@ -1775,6 +1872,7 @@ function createChart(element, options = {}) {
1775
1872
  onOrderAction,
1776
1873
  onChartClick,
1777
1874
  onCrosshairMove,
1875
+ onCrosshairPriceAction,
1778
1876
  zoomInX,
1779
1877
  zoomOutX,
1780
1878
  zoomInY,
@@ -71,6 +71,13 @@ interface CrosshairOptions {
71
71
  labelBorderColor?: string;
72
72
  labelBorderWidth?: number;
73
73
  labelBorderStyle?: "solid" | "dotted" | "dashed";
74
+ showPriceActionButton?: boolean;
75
+ priceActionButtonIcon?: "plus" | "plusThin" | "text";
76
+ priceActionButtonText?: string;
77
+ priceActionButtonSize?: number;
78
+ priceActionButtonGap?: number;
79
+ priceActionButtonRounded?: boolean;
80
+ priceActionButtonBorderRadius?: number;
74
81
  }
75
82
  interface WatermarkOptions {
76
83
  visible?: boolean;
@@ -178,6 +185,11 @@ interface CrosshairMoveEvent {
178
185
  time?: string;
179
186
  point?: OhlcDataPoint;
180
187
  }
188
+ interface CrosshairPriceActionEvent {
189
+ x: number;
190
+ y: number;
191
+ price: number;
192
+ }
181
193
  interface TickerLineOptions {
182
194
  visible?: boolean;
183
195
  style?: "solid" | "dotted" | "dashed";
@@ -199,6 +211,7 @@ interface ChartInstance {
199
211
  onOrderAction: (handler: ((event: OrderActionEvent) => void) | null) => void;
200
212
  onChartClick: (handler: ((event: ChartClickEvent) => void) | null) => void;
201
213
  onCrosshairMove: (handler: ((event: CrosshairMoveEvent) => void) | null) => void;
214
+ onCrosshairPriceAction: (handler: ((event: CrosshairPriceActionEvent) => void) | null) => void;
202
215
  zoomInX: (factor?: number) => void;
203
216
  zoomOutX: (factor?: number) => void;
204
217
  zoomInY: (factor?: number) => void;
@@ -222,4 +235,4 @@ interface OhlcDataPoint {
222
235
  }
223
236
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
224
237
 
225
- export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type DashPatternOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
238
+ export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
@@ -27,7 +27,14 @@ var DEFAULT_CROSSHAIR_OPTIONS = {
27
27
  labelBorderRadius: 3,
28
28
  labelBorderColor: "#94a3b8",
29
29
  labelBorderWidth: 1,
30
- labelBorderStyle: "solid"
30
+ labelBorderStyle: "solid",
31
+ showPriceActionButton: false,
32
+ priceActionButtonIcon: "plusThin",
33
+ priceActionButtonText: "+",
34
+ priceActionButtonSize: 16,
35
+ priceActionButtonGap: 4,
36
+ priceActionButtonRounded: true,
37
+ priceActionButtonBorderRadius: 8
31
38
  };
32
39
  var DEFAULT_WATERMARK_OPTIONS = {
33
40
  visible: false,
@@ -139,10 +146,6 @@ var DEFAULT_OPTIONS = {
139
146
  },
140
147
  dashPatterns: DEFAULT_DASH_PATTERNS
141
148
  };
142
- var BRAND_LOGO_VIEWBOX_WIDTH = 190;
143
- var BRAND_LOGO_VIEWBOX_HEIGHT = 186;
144
- var BRAND_LOGO_PATH_A = "M0 93.0171V45.2271H48.9851V75.0332C48.9851 84.9545 57.0416 93.0001 66.9763 93.0001H94.9957V186H49.0531V110.984C49.0531 101.063 40.9965 93.0171 31.0619 93.0171H0Z";
145
- var BRAND_LOGO_PATH_B = "M190 92.9915V140.782H141.015V110.975C141.015 101.054 132.958 93.0085 123.023 93.0085H95.0039V0H140.955V75.0162C140.955 84.9374 149.012 92.9831 158.946 92.9831H190V92.9915Z";
146
149
  function createChart(element, options = {}) {
147
150
  const mergedOptions = {
148
151
  ...DEFAULT_OPTIONS,
@@ -187,6 +190,8 @@ function createChart(element, options = {}) {
187
190
  let orderActionHandler = null;
188
191
  let chartClickHandler = null;
189
192
  let crosshairMoveHandler = null;
193
+ let crosshairPriceActionHandler = null;
194
+ let crosshairPriceActionRegion = null;
190
195
  let orderActionRegions = [];
191
196
  let orderDragRegions = [];
192
197
  let generatedPriceLineId = 1;
@@ -199,8 +204,6 @@ function createChart(element, options = {}) {
199
204
  let yMaxOverride = null;
200
205
  let autoYMin = null;
201
206
  let autoYMax = null;
202
- const brandLogoPathA = new Path2D(BRAND_LOGO_PATH_A);
203
- const brandLogoPathB = new Path2D(BRAND_LOGO_PATH_B);
204
207
  let watermarkImageSrc = null;
205
208
  let watermarkImage = null;
206
209
  let watermarkImageReady = false;
@@ -778,6 +781,7 @@ function createChart(element, options = {}) {
778
781
  const draw = () => {
779
782
  orderActionRegions = [];
780
783
  orderDragRegions = [];
784
+ crosshairPriceActionRegion = null;
781
785
  const pixelRatio = getPixelRatio();
782
786
  canvas.style.width = `${width}px`;
783
787
  canvas.style.height = `${height}px`;
@@ -1083,17 +1087,6 @@ function createChart(element, options = {}) {
1083
1087
  });
1084
1088
  drawText(timeLabel, x, chartBottom + 8, "center", "top", axis.textColor);
1085
1089
  }
1086
- const brandLogoWidth = 34;
1087
- const brandLogoHeight = BRAND_LOGO_VIEWBOX_HEIGHT / BRAND_LOGO_VIEWBOX_WIDTH * brandLogoWidth;
1088
- const brandLogoX = chartLeft + 16;
1089
- const brandLogoY = chartBottom - brandLogoHeight - 10;
1090
- ctx.save();
1091
- ctx.translate(brandLogoX, brandLogoY);
1092
- ctx.scale(brandLogoWidth / BRAND_LOGO_VIEWBOX_WIDTH, brandLogoHeight / BRAND_LOGO_VIEWBOX_HEIGHT);
1093
- ctx.fillStyle = "#ffffff";
1094
- ctx.fill(brandLogoPathA);
1095
- ctx.fill(brandLogoPathB);
1096
- ctx.restore();
1097
1090
  if (crosshair.visible && crosshairPoint) {
1098
1091
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
1099
1092
  const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
@@ -1130,6 +1123,83 @@ function createChart(element, options = {}) {
1130
1123
  fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
1131
1124
  strokeCrosshairLabel(priceX, priceY, priceWidth);
1132
1125
  drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
1126
+ if (crosshair.showPriceActionButton) {
1127
+ const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4));
1128
+ const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
1129
+ const containerPaddingX = 5;
1130
+ const containerWidth = buttonSize + containerPaddingX * 2;
1131
+ const containerX = priceX - buttonGap - containerWidth;
1132
+ const containerY = priceY;
1133
+ const buttonX = containerX + containerPaddingX;
1134
+ const buttonY = priceY + (labelHeight - buttonSize) / 2;
1135
+ const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
1136
+ const buttonBorderWidth = Math.max(1, Math.round(labelBorderWidth || 1));
1137
+ const buttonCenterX = buttonX + buttonSize / 2;
1138
+ const buttonCenterY = buttonY + buttonSize / 2;
1139
+ const containerRadius = labelRadius;
1140
+ ctx.fillStyle = labelBackground;
1141
+ fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
1142
+ if (labelBorderWidth > 0) {
1143
+ ctx.save();
1144
+ ctx.strokeStyle = labelBorderColor;
1145
+ ctx.lineWidth = labelBorderWidth;
1146
+ ctx.setLineDash([]);
1147
+ strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
1148
+ ctx.restore();
1149
+ }
1150
+ if (buttonBorderWidth > 0) {
1151
+ ctx.save();
1152
+ ctx.strokeStyle = labelBorderColor;
1153
+ ctx.lineWidth = buttonBorderWidth;
1154
+ ctx.setLineDash([]);
1155
+ if (crosshair.priceActionButtonRounded) {
1156
+ ctx.beginPath();
1157
+ ctx.arc(
1158
+ buttonCenterX,
1159
+ buttonCenterY,
1160
+ Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
1161
+ 0,
1162
+ Math.PI * 2
1163
+ );
1164
+ ctx.stroke();
1165
+ } else {
1166
+ strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
1167
+ }
1168
+ ctx.restore();
1169
+ }
1170
+ if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
1171
+ const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
1172
+ const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
1173
+ ctx.save();
1174
+ ctx.strokeStyle = labelTextColor;
1175
+ ctx.lineWidth = plusThickness;
1176
+ ctx.lineCap = "round";
1177
+ ctx.setLineDash([]);
1178
+ ctx.beginPath();
1179
+ ctx.moveTo(buttonCenterX - plusHalf, buttonCenterY);
1180
+ ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
1181
+ ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
1182
+ ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
1183
+ ctx.stroke();
1184
+ ctx.restore();
1185
+ } else {
1186
+ drawText(
1187
+ crosshair.priceActionButtonText,
1188
+ buttonCenterX,
1189
+ buttonCenterY,
1190
+ "center",
1191
+ "middle",
1192
+ labelTextColor
1193
+ );
1194
+ }
1195
+ crosshairPriceActionRegion = {
1196
+ x: containerX,
1197
+ y: containerY,
1198
+ width: containerWidth,
1199
+ height: labelHeight,
1200
+ price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
1201
+ };
1202
+ }
1133
1203
  }
1134
1204
  if (crosshair.showTimeLabel) {
1135
1205
  const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
@@ -1292,6 +1362,16 @@ function createChart(element, options = {}) {
1292
1362
  (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
1293
1363
  );
1294
1364
  };
1365
+ const getCrosshairPriceActionRegion = (x, y) => {
1366
+ if (!crosshairPriceActionRegion) {
1367
+ return null;
1368
+ }
1369
+ const region = crosshairPriceActionRegion;
1370
+ if (x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height) {
1371
+ return region;
1372
+ }
1373
+ return null;
1374
+ };
1295
1375
  const priceFromCanvasY = (y) => {
1296
1376
  if (!drawState) {
1297
1377
  return 0;
@@ -1357,6 +1437,15 @@ function createChart(element, options = {}) {
1357
1437
  let activePointerId = null;
1358
1438
  const onPointerDown = (event) => {
1359
1439
  const point = getCanvasPoint(event);
1440
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1441
+ if (crosshairButtonRegion) {
1442
+ crosshairPriceActionHandler?.({
1443
+ x: point.x,
1444
+ y: point.y,
1445
+ price: crosshairButtonRegion.price
1446
+ });
1447
+ return;
1448
+ }
1360
1449
  const orderRegion = getOrderActionRegion(point.x, point.y);
1361
1450
  if (orderRegion) {
1362
1451
  if (orderRegion.draggable) {
@@ -1469,6 +1558,11 @@ function createChart(element, options = {}) {
1469
1558
  return;
1470
1559
  }
1471
1560
  if (!isDragging || !dragMode) {
1561
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1562
+ if (crosshairButtonRegion) {
1563
+ canvas.style.cursor = "pointer";
1564
+ return;
1565
+ }
1472
1566
  const orderRegion = getOrderActionRegion(point.x, point.y);
1473
1567
  if (orderRegion) {
1474
1568
  canvas.style.cursor = orderRegion.draggable ? "ns-resize" : "pointer";
@@ -1722,6 +1816,9 @@ function createChart(element, options = {}) {
1722
1816
  const onCrosshairMove = (handler) => {
1723
1817
  crosshairMoveHandler = handler;
1724
1818
  };
1819
+ const onCrosshairPriceAction = (handler) => {
1820
+ crosshairPriceActionHandler = handler;
1821
+ };
1725
1822
  const setDoubleClickEnabled = (enabled) => {
1726
1823
  doubleClickEnabled = enabled;
1727
1824
  };
@@ -1751,6 +1848,7 @@ function createChart(element, options = {}) {
1751
1848
  onOrderAction,
1752
1849
  onChartClick,
1753
1850
  onCrosshairMove,
1851
+ onCrosshairPriceAction,
1754
1852
  zoomInX,
1755
1853
  zoomOutX,
1756
1854
  zoomInY,
package/dist/index.cjs CHANGED
@@ -51,7 +51,14 @@ var DEFAULT_CROSSHAIR_OPTIONS = {
51
51
  labelBorderRadius: 3,
52
52
  labelBorderColor: "#94a3b8",
53
53
  labelBorderWidth: 1,
54
- labelBorderStyle: "solid"
54
+ labelBorderStyle: "solid",
55
+ showPriceActionButton: false,
56
+ priceActionButtonIcon: "plusThin",
57
+ priceActionButtonText: "+",
58
+ priceActionButtonSize: 16,
59
+ priceActionButtonGap: 4,
60
+ priceActionButtonRounded: true,
61
+ priceActionButtonBorderRadius: 8
55
62
  };
56
63
  var DEFAULT_WATERMARK_OPTIONS = {
57
64
  visible: false,
@@ -163,10 +170,6 @@ var DEFAULT_OPTIONS = {
163
170
  },
164
171
  dashPatterns: DEFAULT_DASH_PATTERNS
165
172
  };
166
- var BRAND_LOGO_VIEWBOX_WIDTH = 190;
167
- var BRAND_LOGO_VIEWBOX_HEIGHT = 186;
168
- var BRAND_LOGO_PATH_A = "M0 93.0171V45.2271H48.9851V75.0332C48.9851 84.9545 57.0416 93.0001 66.9763 93.0001H94.9957V186H49.0531V110.984C49.0531 101.063 40.9965 93.0171 31.0619 93.0171H0Z";
169
- var BRAND_LOGO_PATH_B = "M190 92.9915V140.782H141.015V110.975C141.015 101.054 132.958 93.0085 123.023 93.0085H95.0039V0H140.955V75.0162C140.955 84.9374 149.012 92.9831 158.946 92.9831H190V92.9915Z";
170
173
  function createChart(element, options = {}) {
171
174
  const mergedOptions = {
172
175
  ...DEFAULT_OPTIONS,
@@ -211,6 +214,8 @@ function createChart(element, options = {}) {
211
214
  let orderActionHandler = null;
212
215
  let chartClickHandler = null;
213
216
  let crosshairMoveHandler = null;
217
+ let crosshairPriceActionHandler = null;
218
+ let crosshairPriceActionRegion = null;
214
219
  let orderActionRegions = [];
215
220
  let orderDragRegions = [];
216
221
  let generatedPriceLineId = 1;
@@ -223,8 +228,6 @@ function createChart(element, options = {}) {
223
228
  let yMaxOverride = null;
224
229
  let autoYMin = null;
225
230
  let autoYMax = null;
226
- const brandLogoPathA = new Path2D(BRAND_LOGO_PATH_A);
227
- const brandLogoPathB = new Path2D(BRAND_LOGO_PATH_B);
228
231
  let watermarkImageSrc = null;
229
232
  let watermarkImage = null;
230
233
  let watermarkImageReady = false;
@@ -802,6 +805,7 @@ function createChart(element, options = {}) {
802
805
  const draw = () => {
803
806
  orderActionRegions = [];
804
807
  orderDragRegions = [];
808
+ crosshairPriceActionRegion = null;
805
809
  const pixelRatio = getPixelRatio();
806
810
  canvas.style.width = `${width}px`;
807
811
  canvas.style.height = `${height}px`;
@@ -1107,17 +1111,6 @@ function createChart(element, options = {}) {
1107
1111
  });
1108
1112
  drawText(timeLabel, x, chartBottom + 8, "center", "top", axis.textColor);
1109
1113
  }
1110
- const brandLogoWidth = 34;
1111
- const brandLogoHeight = BRAND_LOGO_VIEWBOX_HEIGHT / BRAND_LOGO_VIEWBOX_WIDTH * brandLogoWidth;
1112
- const brandLogoX = chartLeft + 16;
1113
- const brandLogoY = chartBottom - brandLogoHeight - 10;
1114
- ctx.save();
1115
- ctx.translate(brandLogoX, brandLogoY);
1116
- ctx.scale(brandLogoWidth / BRAND_LOGO_VIEWBOX_WIDTH, brandLogoHeight / BRAND_LOGO_VIEWBOX_HEIGHT);
1117
- ctx.fillStyle = "#ffffff";
1118
- ctx.fill(brandLogoPathA);
1119
- ctx.fill(brandLogoPathB);
1120
- ctx.restore();
1121
1114
  if (crosshair.visible && crosshairPoint) {
1122
1115
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
1123
1116
  const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
@@ -1154,6 +1147,83 @@ function createChart(element, options = {}) {
1154
1147
  fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
1155
1148
  strokeCrosshairLabel(priceX, priceY, priceWidth);
1156
1149
  drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
1150
+ if (crosshair.showPriceActionButton) {
1151
+ const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4));
1152
+ const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
1153
+ const containerPaddingX = 5;
1154
+ const containerWidth = buttonSize + containerPaddingX * 2;
1155
+ const containerX = priceX - buttonGap - containerWidth;
1156
+ const containerY = priceY;
1157
+ const buttonX = containerX + containerPaddingX;
1158
+ const buttonY = priceY + (labelHeight - buttonSize) / 2;
1159
+ const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
1160
+ const buttonBorderWidth = Math.max(1, Math.round(labelBorderWidth || 1));
1161
+ const buttonCenterX = buttonX + buttonSize / 2;
1162
+ const buttonCenterY = buttonY + buttonSize / 2;
1163
+ const containerRadius = labelRadius;
1164
+ ctx.fillStyle = labelBackground;
1165
+ fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
1166
+ if (labelBorderWidth > 0) {
1167
+ ctx.save();
1168
+ ctx.strokeStyle = labelBorderColor;
1169
+ ctx.lineWidth = labelBorderWidth;
1170
+ ctx.setLineDash([]);
1171
+ strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
1172
+ ctx.restore();
1173
+ }
1174
+ if (buttonBorderWidth > 0) {
1175
+ ctx.save();
1176
+ ctx.strokeStyle = labelBorderColor;
1177
+ ctx.lineWidth = buttonBorderWidth;
1178
+ ctx.setLineDash([]);
1179
+ if (crosshair.priceActionButtonRounded) {
1180
+ ctx.beginPath();
1181
+ ctx.arc(
1182
+ buttonCenterX,
1183
+ buttonCenterY,
1184
+ Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
1185
+ 0,
1186
+ Math.PI * 2
1187
+ );
1188
+ ctx.stroke();
1189
+ } else {
1190
+ strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
1191
+ }
1192
+ ctx.restore();
1193
+ }
1194
+ if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
1195
+ const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
1196
+ const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
1197
+ ctx.save();
1198
+ ctx.strokeStyle = labelTextColor;
1199
+ ctx.lineWidth = plusThickness;
1200
+ ctx.lineCap = "round";
1201
+ ctx.setLineDash([]);
1202
+ ctx.beginPath();
1203
+ ctx.moveTo(buttonCenterX - plusHalf, buttonCenterY);
1204
+ ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
1205
+ ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
1206
+ ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
1207
+ ctx.stroke();
1208
+ ctx.restore();
1209
+ } else {
1210
+ drawText(
1211
+ crosshair.priceActionButtonText,
1212
+ buttonCenterX,
1213
+ buttonCenterY,
1214
+ "center",
1215
+ "middle",
1216
+ labelTextColor
1217
+ );
1218
+ }
1219
+ crosshairPriceActionRegion = {
1220
+ x: containerX,
1221
+ y: containerY,
1222
+ width: containerWidth,
1223
+ height: labelHeight,
1224
+ price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
1225
+ };
1226
+ }
1157
1227
  }
1158
1228
  if (crosshair.showTimeLabel) {
1159
1229
  const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
@@ -1316,6 +1386,16 @@ function createChart(element, options = {}) {
1316
1386
  (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
1317
1387
  );
1318
1388
  };
1389
+ const getCrosshairPriceActionRegion = (x, y) => {
1390
+ if (!crosshairPriceActionRegion) {
1391
+ return null;
1392
+ }
1393
+ const region = crosshairPriceActionRegion;
1394
+ if (x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height) {
1395
+ return region;
1396
+ }
1397
+ return null;
1398
+ };
1319
1399
  const priceFromCanvasY = (y) => {
1320
1400
  if (!drawState) {
1321
1401
  return 0;
@@ -1381,6 +1461,15 @@ function createChart(element, options = {}) {
1381
1461
  let activePointerId = null;
1382
1462
  const onPointerDown = (event) => {
1383
1463
  const point = getCanvasPoint(event);
1464
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1465
+ if (crosshairButtonRegion) {
1466
+ crosshairPriceActionHandler?.({
1467
+ x: point.x,
1468
+ y: point.y,
1469
+ price: crosshairButtonRegion.price
1470
+ });
1471
+ return;
1472
+ }
1384
1473
  const orderRegion = getOrderActionRegion(point.x, point.y);
1385
1474
  if (orderRegion) {
1386
1475
  if (orderRegion.draggable) {
@@ -1493,6 +1582,11 @@ function createChart(element, options = {}) {
1493
1582
  return;
1494
1583
  }
1495
1584
  if (!isDragging || !dragMode) {
1585
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1586
+ if (crosshairButtonRegion) {
1587
+ canvas.style.cursor = "pointer";
1588
+ return;
1589
+ }
1496
1590
  const orderRegion = getOrderActionRegion(point.x, point.y);
1497
1591
  if (orderRegion) {
1498
1592
  canvas.style.cursor = orderRegion.draggable ? "ns-resize" : "pointer";
@@ -1746,6 +1840,9 @@ function createChart(element, options = {}) {
1746
1840
  const onCrosshairMove = (handler) => {
1747
1841
  crosshairMoveHandler = handler;
1748
1842
  };
1843
+ const onCrosshairPriceAction = (handler) => {
1844
+ crosshairPriceActionHandler = handler;
1845
+ };
1749
1846
  const setDoubleClickEnabled = (enabled) => {
1750
1847
  doubleClickEnabled = enabled;
1751
1848
  };
@@ -1775,6 +1872,7 @@ function createChart(element, options = {}) {
1775
1872
  onOrderAction,
1776
1873
  onChartClick,
1777
1874
  onCrosshairMove,
1875
+ onCrosshairPriceAction,
1778
1876
  zoomInX,
1779
1877
  zoomOutX,
1780
1878
  zoomInY,
package/dist/index.d.cts CHANGED
@@ -71,6 +71,13 @@ interface CrosshairOptions {
71
71
  labelBorderColor?: string;
72
72
  labelBorderWidth?: number;
73
73
  labelBorderStyle?: "solid" | "dotted" | "dashed";
74
+ showPriceActionButton?: boolean;
75
+ priceActionButtonIcon?: "plus" | "plusThin" | "text";
76
+ priceActionButtonText?: string;
77
+ priceActionButtonSize?: number;
78
+ priceActionButtonGap?: number;
79
+ priceActionButtonRounded?: boolean;
80
+ priceActionButtonBorderRadius?: number;
74
81
  }
75
82
  interface WatermarkOptions {
76
83
  visible?: boolean;
@@ -178,6 +185,11 @@ interface CrosshairMoveEvent {
178
185
  time?: string;
179
186
  point?: OhlcDataPoint;
180
187
  }
188
+ interface CrosshairPriceActionEvent {
189
+ x: number;
190
+ y: number;
191
+ price: number;
192
+ }
181
193
  interface TickerLineOptions {
182
194
  visible?: boolean;
183
195
  style?: "solid" | "dotted" | "dashed";
@@ -199,6 +211,7 @@ interface ChartInstance {
199
211
  onOrderAction: (handler: ((event: OrderActionEvent) => void) | null) => void;
200
212
  onChartClick: (handler: ((event: ChartClickEvent) => void) | null) => void;
201
213
  onCrosshairMove: (handler: ((event: CrosshairMoveEvent) => void) | null) => void;
214
+ onCrosshairPriceAction: (handler: ((event: CrosshairPriceActionEvent) => void) | null) => void;
202
215
  zoomInX: (factor?: number) => void;
203
216
  zoomOutX: (factor?: number) => void;
204
217
  zoomInY: (factor?: number) => void;
@@ -222,4 +235,4 @@ interface OhlcDataPoint {
222
235
  }
223
236
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
224
237
 
225
- export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type DashPatternOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
238
+ export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
package/dist/index.d.ts CHANGED
@@ -71,6 +71,13 @@ interface CrosshairOptions {
71
71
  labelBorderColor?: string;
72
72
  labelBorderWidth?: number;
73
73
  labelBorderStyle?: "solid" | "dotted" | "dashed";
74
+ showPriceActionButton?: boolean;
75
+ priceActionButtonIcon?: "plus" | "plusThin" | "text";
76
+ priceActionButtonText?: string;
77
+ priceActionButtonSize?: number;
78
+ priceActionButtonGap?: number;
79
+ priceActionButtonRounded?: boolean;
80
+ priceActionButtonBorderRadius?: number;
74
81
  }
75
82
  interface WatermarkOptions {
76
83
  visible?: boolean;
@@ -178,6 +185,11 @@ interface CrosshairMoveEvent {
178
185
  time?: string;
179
186
  point?: OhlcDataPoint;
180
187
  }
188
+ interface CrosshairPriceActionEvent {
189
+ x: number;
190
+ y: number;
191
+ price: number;
192
+ }
181
193
  interface TickerLineOptions {
182
194
  visible?: boolean;
183
195
  style?: "solid" | "dotted" | "dashed";
@@ -199,6 +211,7 @@ interface ChartInstance {
199
211
  onOrderAction: (handler: ((event: OrderActionEvent) => void) | null) => void;
200
212
  onChartClick: (handler: ((event: ChartClickEvent) => void) | null) => void;
201
213
  onCrosshairMove: (handler: ((event: CrosshairMoveEvent) => void) | null) => void;
214
+ onCrosshairPriceAction: (handler: ((event: CrosshairPriceActionEvent) => void) | null) => void;
202
215
  zoomInX: (factor?: number) => void;
203
216
  zoomOutX: (factor?: number) => void;
204
217
  zoomInY: (factor?: number) => void;
@@ -222,4 +235,4 @@ interface OhlcDataPoint {
222
235
  }
223
236
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
224
237
 
225
- export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type DashPatternOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
238
+ export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
package/dist/index.js CHANGED
@@ -27,7 +27,14 @@ var DEFAULT_CROSSHAIR_OPTIONS = {
27
27
  labelBorderRadius: 3,
28
28
  labelBorderColor: "#94a3b8",
29
29
  labelBorderWidth: 1,
30
- labelBorderStyle: "solid"
30
+ labelBorderStyle: "solid",
31
+ showPriceActionButton: false,
32
+ priceActionButtonIcon: "plusThin",
33
+ priceActionButtonText: "+",
34
+ priceActionButtonSize: 16,
35
+ priceActionButtonGap: 4,
36
+ priceActionButtonRounded: true,
37
+ priceActionButtonBorderRadius: 8
31
38
  };
32
39
  var DEFAULT_WATERMARK_OPTIONS = {
33
40
  visible: false,
@@ -139,10 +146,6 @@ var DEFAULT_OPTIONS = {
139
146
  },
140
147
  dashPatterns: DEFAULT_DASH_PATTERNS
141
148
  };
142
- var BRAND_LOGO_VIEWBOX_WIDTH = 190;
143
- var BRAND_LOGO_VIEWBOX_HEIGHT = 186;
144
- var BRAND_LOGO_PATH_A = "M0 93.0171V45.2271H48.9851V75.0332C48.9851 84.9545 57.0416 93.0001 66.9763 93.0001H94.9957V186H49.0531V110.984C49.0531 101.063 40.9965 93.0171 31.0619 93.0171H0Z";
145
- var BRAND_LOGO_PATH_B = "M190 92.9915V140.782H141.015V110.975C141.015 101.054 132.958 93.0085 123.023 93.0085H95.0039V0H140.955V75.0162C140.955 84.9374 149.012 92.9831 158.946 92.9831H190V92.9915Z";
146
149
  function createChart(element, options = {}) {
147
150
  const mergedOptions = {
148
151
  ...DEFAULT_OPTIONS,
@@ -187,6 +190,8 @@ function createChart(element, options = {}) {
187
190
  let orderActionHandler = null;
188
191
  let chartClickHandler = null;
189
192
  let crosshairMoveHandler = null;
193
+ let crosshairPriceActionHandler = null;
194
+ let crosshairPriceActionRegion = null;
190
195
  let orderActionRegions = [];
191
196
  let orderDragRegions = [];
192
197
  let generatedPriceLineId = 1;
@@ -199,8 +204,6 @@ function createChart(element, options = {}) {
199
204
  let yMaxOverride = null;
200
205
  let autoYMin = null;
201
206
  let autoYMax = null;
202
- const brandLogoPathA = new Path2D(BRAND_LOGO_PATH_A);
203
- const brandLogoPathB = new Path2D(BRAND_LOGO_PATH_B);
204
207
  let watermarkImageSrc = null;
205
208
  let watermarkImage = null;
206
209
  let watermarkImageReady = false;
@@ -778,6 +781,7 @@ function createChart(element, options = {}) {
778
781
  const draw = () => {
779
782
  orderActionRegions = [];
780
783
  orderDragRegions = [];
784
+ crosshairPriceActionRegion = null;
781
785
  const pixelRatio = getPixelRatio();
782
786
  canvas.style.width = `${width}px`;
783
787
  canvas.style.height = `${height}px`;
@@ -1083,17 +1087,6 @@ function createChart(element, options = {}) {
1083
1087
  });
1084
1088
  drawText(timeLabel, x, chartBottom + 8, "center", "top", axis.textColor);
1085
1089
  }
1086
- const brandLogoWidth = 34;
1087
- const brandLogoHeight = BRAND_LOGO_VIEWBOX_HEIGHT / BRAND_LOGO_VIEWBOX_WIDTH * brandLogoWidth;
1088
- const brandLogoX = chartLeft + 16;
1089
- const brandLogoY = chartBottom - brandLogoHeight - 10;
1090
- ctx.save();
1091
- ctx.translate(brandLogoX, brandLogoY);
1092
- ctx.scale(brandLogoWidth / BRAND_LOGO_VIEWBOX_WIDTH, brandLogoHeight / BRAND_LOGO_VIEWBOX_HEIGHT);
1093
- ctx.fillStyle = "#ffffff";
1094
- ctx.fill(brandLogoPathA);
1095
- ctx.fill(brandLogoPathB);
1096
- ctx.restore();
1097
1090
  if (crosshair.visible && crosshairPoint) {
1098
1091
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
1099
1092
  const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
@@ -1130,6 +1123,83 @@ function createChart(element, options = {}) {
1130
1123
  fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
1131
1124
  strokeCrosshairLabel(priceX, priceY, priceWidth);
1132
1125
  drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
1126
+ if (crosshair.showPriceActionButton) {
1127
+ const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4));
1128
+ const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
1129
+ const containerPaddingX = 5;
1130
+ const containerWidth = buttonSize + containerPaddingX * 2;
1131
+ const containerX = priceX - buttonGap - containerWidth;
1132
+ const containerY = priceY;
1133
+ const buttonX = containerX + containerPaddingX;
1134
+ const buttonY = priceY + (labelHeight - buttonSize) / 2;
1135
+ const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
1136
+ const buttonBorderWidth = Math.max(1, Math.round(labelBorderWidth || 1));
1137
+ const buttonCenterX = buttonX + buttonSize / 2;
1138
+ const buttonCenterY = buttonY + buttonSize / 2;
1139
+ const containerRadius = labelRadius;
1140
+ ctx.fillStyle = labelBackground;
1141
+ fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
1142
+ if (labelBorderWidth > 0) {
1143
+ ctx.save();
1144
+ ctx.strokeStyle = labelBorderColor;
1145
+ ctx.lineWidth = labelBorderWidth;
1146
+ ctx.setLineDash([]);
1147
+ strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
1148
+ ctx.restore();
1149
+ }
1150
+ if (buttonBorderWidth > 0) {
1151
+ ctx.save();
1152
+ ctx.strokeStyle = labelBorderColor;
1153
+ ctx.lineWidth = buttonBorderWidth;
1154
+ ctx.setLineDash([]);
1155
+ if (crosshair.priceActionButtonRounded) {
1156
+ ctx.beginPath();
1157
+ ctx.arc(
1158
+ buttonCenterX,
1159
+ buttonCenterY,
1160
+ Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
1161
+ 0,
1162
+ Math.PI * 2
1163
+ );
1164
+ ctx.stroke();
1165
+ } else {
1166
+ strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
1167
+ }
1168
+ ctx.restore();
1169
+ }
1170
+ if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
1171
+ const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
1172
+ const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
1173
+ ctx.save();
1174
+ ctx.strokeStyle = labelTextColor;
1175
+ ctx.lineWidth = plusThickness;
1176
+ ctx.lineCap = "round";
1177
+ ctx.setLineDash([]);
1178
+ ctx.beginPath();
1179
+ ctx.moveTo(buttonCenterX - plusHalf, buttonCenterY);
1180
+ ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
1181
+ ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
1182
+ ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
1183
+ ctx.stroke();
1184
+ ctx.restore();
1185
+ } else {
1186
+ drawText(
1187
+ crosshair.priceActionButtonText,
1188
+ buttonCenterX,
1189
+ buttonCenterY,
1190
+ "center",
1191
+ "middle",
1192
+ labelTextColor
1193
+ );
1194
+ }
1195
+ crosshairPriceActionRegion = {
1196
+ x: containerX,
1197
+ y: containerY,
1198
+ width: containerWidth,
1199
+ height: labelHeight,
1200
+ price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
1201
+ };
1202
+ }
1133
1203
  }
1134
1204
  if (crosshair.showTimeLabel) {
1135
1205
  const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
@@ -1292,6 +1362,16 @@ function createChart(element, options = {}) {
1292
1362
  (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
1293
1363
  );
1294
1364
  };
1365
+ const getCrosshairPriceActionRegion = (x, y) => {
1366
+ if (!crosshairPriceActionRegion) {
1367
+ return null;
1368
+ }
1369
+ const region = crosshairPriceActionRegion;
1370
+ if (x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height) {
1371
+ return region;
1372
+ }
1373
+ return null;
1374
+ };
1295
1375
  const priceFromCanvasY = (y) => {
1296
1376
  if (!drawState) {
1297
1377
  return 0;
@@ -1357,6 +1437,15 @@ function createChart(element, options = {}) {
1357
1437
  let activePointerId = null;
1358
1438
  const onPointerDown = (event) => {
1359
1439
  const point = getCanvasPoint(event);
1440
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1441
+ if (crosshairButtonRegion) {
1442
+ crosshairPriceActionHandler?.({
1443
+ x: point.x,
1444
+ y: point.y,
1445
+ price: crosshairButtonRegion.price
1446
+ });
1447
+ return;
1448
+ }
1360
1449
  const orderRegion = getOrderActionRegion(point.x, point.y);
1361
1450
  if (orderRegion) {
1362
1451
  if (orderRegion.draggable) {
@@ -1469,6 +1558,11 @@ function createChart(element, options = {}) {
1469
1558
  return;
1470
1559
  }
1471
1560
  if (!isDragging || !dragMode) {
1561
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1562
+ if (crosshairButtonRegion) {
1563
+ canvas.style.cursor = "pointer";
1564
+ return;
1565
+ }
1472
1566
  const orderRegion = getOrderActionRegion(point.x, point.y);
1473
1567
  if (orderRegion) {
1474
1568
  canvas.style.cursor = orderRegion.draggable ? "ns-resize" : "pointer";
@@ -1722,6 +1816,9 @@ function createChart(element, options = {}) {
1722
1816
  const onCrosshairMove = (handler) => {
1723
1817
  crosshairMoveHandler = handler;
1724
1818
  };
1819
+ const onCrosshairPriceAction = (handler) => {
1820
+ crosshairPriceActionHandler = handler;
1821
+ };
1725
1822
  const setDoubleClickEnabled = (enabled) => {
1726
1823
  doubleClickEnabled = enabled;
1727
1824
  };
@@ -1751,6 +1848,7 @@ function createChart(element, options = {}) {
1751
1848
  onOrderAction,
1752
1849
  onChartClick,
1753
1850
  onCrosshairMove,
1851
+ onCrosshairPriceAction,
1754
1852
  zoomInX,
1755
1853
  zoomOutX,
1756
1854
  zoomInY,
package/docs/API.md CHANGED
@@ -94,6 +94,15 @@ Top-level options:
94
94
  - `labelBorderColor` (default `#94a3b8`)
95
95
  - `labelBorderWidth` (default `1`)
96
96
  - `labelBorderStyle` (`"solid" | "dotted" | "dashed"`, default `"solid"`)
97
+ - `showPriceActionButton` (default `false`)
98
+ - `priceActionButtonIcon` (`"plus" | "plusThin" | "text"`, default `"plusThin"`)
99
+ - `priceActionButtonText` (default `"+"`)
100
+ - `priceActionButtonSize` (default `16`)
101
+ - `priceActionButtonGap` (default `4`)
102
+ - `priceActionButtonRounded` (default `true`; set `false` for square corners)
103
+ - `priceActionButtonBorderRadius` (default `8`)
104
+
105
+ Note: the button and its container automatically inherit crosshair label colors/border for a consistent `[button-box][label]` look.
97
106
 
98
107
  ### `WatermarkOptions`
99
108
 
@@ -243,6 +252,7 @@ Connector/fill visuals:
243
252
  - `onOrderAction(handler: ((event: OrderActionEvent) => void) | null): void`
244
253
  - `onChartClick(handler: ((event: ChartClickEvent) => void) | null): void`
245
254
  - `onCrosshairMove(handler: ((event: CrosshairMoveEvent) => void) | null): void`
255
+ - `onCrosshairPriceAction(handler: ((event: CrosshairPriceActionEvent) => void) | null): void`
246
256
  - `zoomInX(factor?: number): void` (default factor `1.25`)
247
257
  - `zoomOutX(factor?: number): void` (default factor `1.25`)
248
258
  - `zoomInY(factor?: number): void` (default factor `1.25`)
package/docs/EVENTS.md CHANGED
@@ -109,6 +109,33 @@ type CrosshairMoveEvent = {
109
109
 
110
110
  ---
111
111
 
112
+ ## `onCrosshairPriceAction`
113
+
114
+ Register:
115
+
116
+ ```ts
117
+ chart.onCrosshairPriceAction((event) => {
118
+ // frontend action (open order ticket, quick action, etc)
119
+ });
120
+ ```
121
+
122
+ Payload type:
123
+
124
+ ```ts
125
+ type CrosshairPriceActionEvent = {
126
+ x: number;
127
+ y: number;
128
+ price: number;
129
+ };
130
+ ```
131
+
132
+ ### Notes
133
+
134
+ - Fires when the optional crosshair price action button (default text `"+"`) is clicked.
135
+ - Enable button rendering via `crosshair.showPriceActionButton: true`.
136
+
137
+ ---
138
+
112
139
  ## Double-click integration
113
140
 
114
141
  Set behavior:
package/docs/RECIPES.md CHANGED
@@ -63,6 +63,34 @@ const chart = createChart(root, {
63
63
  });
64
64
  ```
65
65
 
66
+ ## Add crosshair "+" action button
67
+
68
+ ```ts
69
+ const chart = createChart(root, {
70
+ crosshair: {
71
+ showPriceActionButton: true,
72
+ priceActionButtonText: "+",
73
+ priceActionButtonGap: 4
74
+ }
75
+ });
76
+
77
+ chart.onCrosshairPriceAction((event) => {
78
+ // Handle in app/frontend (place order modal, quick ticket, etc.)
79
+ console.log("crosshair + clicked at price", event.price);
80
+ });
81
+ ```
82
+
83
+ Square style example:
84
+
85
+ ```ts
86
+ const chart = createChart(root, {
87
+ crosshair: {
88
+ showPriceActionButton: true,
89
+ priceActionButtonRounded: false
90
+ }
91
+ });
92
+ ```
93
+
66
94
  ## Add a draggable pending limit line
67
95
 
68
96
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",