hyperprop-charting-library 0.1.15 → 0.1.17

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,17 @@ 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
+ priceActionButtonText: "+",
57
+ priceActionButtonSize: 20,
58
+ priceActionButtonGap: 6,
59
+ priceActionButtonBackgroundColor: "#1f2937",
60
+ priceActionButtonTextColor: "#e2e8f0",
61
+ priceActionButtonBorderColor: "#475569",
62
+ priceActionButtonBorderWidth: 1,
63
+ priceActionButtonRounded: true,
64
+ priceActionButtonBorderRadius: 3
55
65
  };
56
66
  var DEFAULT_WATERMARK_OPTIONS = {
57
67
  visible: false,
@@ -163,10 +173,6 @@ var DEFAULT_OPTIONS = {
163
173
  },
164
174
  dashPatterns: DEFAULT_DASH_PATTERNS
165
175
  };
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
176
  function createChart(element, options = {}) {
171
177
  const mergedOptions = {
172
178
  ...DEFAULT_OPTIONS,
@@ -211,6 +217,8 @@ function createChart(element, options = {}) {
211
217
  let orderActionHandler = null;
212
218
  let chartClickHandler = null;
213
219
  let crosshairMoveHandler = null;
220
+ let crosshairPriceActionHandler = null;
221
+ let crosshairPriceActionRegion = null;
214
222
  let orderActionRegions = [];
215
223
  let orderDragRegions = [];
216
224
  let generatedPriceLineId = 1;
@@ -223,8 +231,6 @@ function createChart(element, options = {}) {
223
231
  let yMaxOverride = null;
224
232
  let autoYMin = null;
225
233
  let autoYMax = null;
226
- const brandLogoPathA = new Path2D(BRAND_LOGO_PATH_A);
227
- const brandLogoPathB = new Path2D(BRAND_LOGO_PATH_B);
228
234
  let watermarkImageSrc = null;
229
235
  let watermarkImage = null;
230
236
  let watermarkImageReady = false;
@@ -802,6 +808,7 @@ function createChart(element, options = {}) {
802
808
  const draw = () => {
803
809
  orderActionRegions = [];
804
810
  orderDragRegions = [];
811
+ crosshairPriceActionRegion = null;
805
812
  const pixelRatio = getPixelRatio();
806
813
  canvas.style.width = `${width}px`;
807
814
  canvas.style.height = `${height}px`;
@@ -842,10 +849,18 @@ function createChart(element, options = {}) {
842
849
  const endIndex = Math.min(data.length - 1, Math.ceil(xEnd) - 1);
843
850
  const visibleData = data.slice(startIndex, endIndex + 1);
844
851
  let priceSource = visibleData.length > 0 ? visibleData : data;
845
- if (mergedOptions.autoScaleIgnoreLatestCandle && priceSource.length > 1) {
846
- priceSource = priceSource.slice(0, -1);
847
- if (priceSource.length === 0) {
848
- priceSource = visibleData.length > 0 ? visibleData : data;
852
+ if (mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1) {
853
+ const latestIndex = data.length - 1;
854
+ const filtered = priceSource.filter((_, offset) => startIndex + offset !== latestIndex);
855
+ if (filtered.length > 0) {
856
+ priceSource = filtered;
857
+ } else {
858
+ const fallbackWindow = 120;
859
+ const fallbackStart = Math.max(0, latestIndex - fallbackWindow);
860
+ const fallback = data.slice(fallbackStart, latestIndex);
861
+ if (fallback.length > 0) {
862
+ priceSource = fallback;
863
+ }
849
864
  }
850
865
  }
851
866
  const minPrice = Math.min(...priceSource.map((point) => point.l));
@@ -1099,17 +1114,6 @@ function createChart(element, options = {}) {
1099
1114
  });
1100
1115
  drawText(timeLabel, x, chartBottom + 8, "center", "top", axis.textColor);
1101
1116
  }
1102
- const brandLogoWidth = 34;
1103
- const brandLogoHeight = BRAND_LOGO_VIEWBOX_HEIGHT / BRAND_LOGO_VIEWBOX_WIDTH * brandLogoWidth;
1104
- const brandLogoX = chartLeft + 16;
1105
- const brandLogoY = chartBottom - brandLogoHeight - 10;
1106
- ctx.save();
1107
- ctx.translate(brandLogoX, brandLogoY);
1108
- ctx.scale(brandLogoWidth / BRAND_LOGO_VIEWBOX_WIDTH, brandLogoHeight / BRAND_LOGO_VIEWBOX_HEIGHT);
1109
- ctx.fillStyle = "#ffffff";
1110
- ctx.fill(brandLogoPathA);
1111
- ctx.fill(brandLogoPathB);
1112
- ctx.restore();
1113
1117
  if (crosshair.visible && crosshairPoint) {
1114
1118
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
1115
1119
  const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
@@ -1146,6 +1150,39 @@ function createChart(element, options = {}) {
1146
1150
  fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
1147
1151
  strokeCrosshairLabel(priceX, priceY, priceWidth);
1148
1152
  drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
1153
+ if (crosshair.showPriceActionButton) {
1154
+ const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, 30);
1155
+ const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
1156
+ const buttonX = priceX - buttonGap - buttonSize;
1157
+ const buttonY = priceY + (labelHeight - buttonSize) / 2;
1158
+ const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
1159
+ const buttonBorderWidth = Math.max(0, Math.round(crosshair.priceActionButtonBorderWidth));
1160
+ ctx.fillStyle = crosshair.priceActionButtonBackgroundColor;
1161
+ fillRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
1162
+ if (buttonBorderWidth > 0) {
1163
+ ctx.save();
1164
+ ctx.strokeStyle = crosshair.priceActionButtonBorderColor;
1165
+ ctx.lineWidth = buttonBorderWidth;
1166
+ ctx.setLineDash([]);
1167
+ strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
1168
+ ctx.restore();
1169
+ }
1170
+ drawText(
1171
+ crosshair.priceActionButtonText,
1172
+ buttonX + buttonSize / 2,
1173
+ buttonY + buttonSize / 2,
1174
+ "center",
1175
+ "middle",
1176
+ crosshair.priceActionButtonTextColor
1177
+ );
1178
+ crosshairPriceActionRegion = {
1179
+ x: buttonX,
1180
+ y: buttonY,
1181
+ width: buttonSize,
1182
+ height: buttonSize,
1183
+ price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
1184
+ };
1185
+ }
1149
1186
  }
1150
1187
  if (crosshair.showTimeLabel) {
1151
1188
  const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
@@ -1308,6 +1345,16 @@ function createChart(element, options = {}) {
1308
1345
  (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
1309
1346
  );
1310
1347
  };
1348
+ const getCrosshairPriceActionRegion = (x, y) => {
1349
+ if (!crosshairPriceActionRegion) {
1350
+ return null;
1351
+ }
1352
+ const region = crosshairPriceActionRegion;
1353
+ if (x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height) {
1354
+ return region;
1355
+ }
1356
+ return null;
1357
+ };
1311
1358
  const priceFromCanvasY = (y) => {
1312
1359
  if (!drawState) {
1313
1360
  return 0;
@@ -1373,6 +1420,15 @@ function createChart(element, options = {}) {
1373
1420
  let activePointerId = null;
1374
1421
  const onPointerDown = (event) => {
1375
1422
  const point = getCanvasPoint(event);
1423
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1424
+ if (crosshairButtonRegion) {
1425
+ crosshairPriceActionHandler?.({
1426
+ x: point.x,
1427
+ y: point.y,
1428
+ price: crosshairButtonRegion.price
1429
+ });
1430
+ return;
1431
+ }
1376
1432
  const orderRegion = getOrderActionRegion(point.x, point.y);
1377
1433
  if (orderRegion) {
1378
1434
  if (orderRegion.draggable) {
@@ -1485,6 +1541,11 @@ function createChart(element, options = {}) {
1485
1541
  return;
1486
1542
  }
1487
1543
  if (!isDragging || !dragMode) {
1544
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1545
+ if (crosshairButtonRegion) {
1546
+ canvas.style.cursor = "pointer";
1547
+ return;
1548
+ }
1488
1549
  const orderRegion = getOrderActionRegion(point.x, point.y);
1489
1550
  if (orderRegion) {
1490
1551
  canvas.style.cursor = orderRegion.draggable ? "ns-resize" : "pointer";
@@ -1738,6 +1799,9 @@ function createChart(element, options = {}) {
1738
1799
  const onCrosshairMove = (handler) => {
1739
1800
  crosshairMoveHandler = handler;
1740
1801
  };
1802
+ const onCrosshairPriceAction = (handler) => {
1803
+ crosshairPriceActionHandler = handler;
1804
+ };
1741
1805
  const setDoubleClickEnabled = (enabled) => {
1742
1806
  doubleClickEnabled = enabled;
1743
1807
  };
@@ -1767,6 +1831,7 @@ function createChart(element, options = {}) {
1767
1831
  onOrderAction,
1768
1832
  onChartClick,
1769
1833
  onCrosshairMove,
1834
+ onCrosshairPriceAction,
1770
1835
  zoomInX,
1771
1836
  zoomOutX,
1772
1837
  zoomInY,
@@ -71,6 +71,16 @@ interface CrosshairOptions {
71
71
  labelBorderColor?: string;
72
72
  labelBorderWidth?: number;
73
73
  labelBorderStyle?: "solid" | "dotted" | "dashed";
74
+ showPriceActionButton?: boolean;
75
+ priceActionButtonText?: string;
76
+ priceActionButtonSize?: number;
77
+ priceActionButtonGap?: number;
78
+ priceActionButtonBackgroundColor?: string;
79
+ priceActionButtonTextColor?: string;
80
+ priceActionButtonBorderColor?: string;
81
+ priceActionButtonBorderWidth?: number;
82
+ priceActionButtonRounded?: boolean;
83
+ priceActionButtonBorderRadius?: number;
74
84
  }
75
85
  interface WatermarkOptions {
76
86
  visible?: boolean;
@@ -178,6 +188,11 @@ interface CrosshairMoveEvent {
178
188
  time?: string;
179
189
  point?: OhlcDataPoint;
180
190
  }
191
+ interface CrosshairPriceActionEvent {
192
+ x: number;
193
+ y: number;
194
+ price: number;
195
+ }
181
196
  interface TickerLineOptions {
182
197
  visible?: boolean;
183
198
  style?: "solid" | "dotted" | "dashed";
@@ -199,6 +214,7 @@ interface ChartInstance {
199
214
  onOrderAction: (handler: ((event: OrderActionEvent) => void) | null) => void;
200
215
  onChartClick: (handler: ((event: ChartClickEvent) => void) | null) => void;
201
216
  onCrosshairMove: (handler: ((event: CrosshairMoveEvent) => void) | null) => void;
217
+ onCrosshairPriceAction: (handler: ((event: CrosshairPriceActionEvent) => void) | null) => void;
202
218
  zoomInX: (factor?: number) => void;
203
219
  zoomOutX: (factor?: number) => void;
204
220
  zoomInY: (factor?: number) => void;
@@ -222,4 +238,4 @@ interface OhlcDataPoint {
222
238
  }
223
239
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
224
240
 
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 };
241
+ 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,17 @@ 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
+ priceActionButtonText: "+",
33
+ priceActionButtonSize: 20,
34
+ priceActionButtonGap: 6,
35
+ priceActionButtonBackgroundColor: "#1f2937",
36
+ priceActionButtonTextColor: "#e2e8f0",
37
+ priceActionButtonBorderColor: "#475569",
38
+ priceActionButtonBorderWidth: 1,
39
+ priceActionButtonRounded: true,
40
+ priceActionButtonBorderRadius: 3
31
41
  };
32
42
  var DEFAULT_WATERMARK_OPTIONS = {
33
43
  visible: false,
@@ -139,10 +149,6 @@ var DEFAULT_OPTIONS = {
139
149
  },
140
150
  dashPatterns: DEFAULT_DASH_PATTERNS
141
151
  };
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
152
  function createChart(element, options = {}) {
147
153
  const mergedOptions = {
148
154
  ...DEFAULT_OPTIONS,
@@ -187,6 +193,8 @@ function createChart(element, options = {}) {
187
193
  let orderActionHandler = null;
188
194
  let chartClickHandler = null;
189
195
  let crosshairMoveHandler = null;
196
+ let crosshairPriceActionHandler = null;
197
+ let crosshairPriceActionRegion = null;
190
198
  let orderActionRegions = [];
191
199
  let orderDragRegions = [];
192
200
  let generatedPriceLineId = 1;
@@ -199,8 +207,6 @@ function createChart(element, options = {}) {
199
207
  let yMaxOverride = null;
200
208
  let autoYMin = null;
201
209
  let autoYMax = null;
202
- const brandLogoPathA = new Path2D(BRAND_LOGO_PATH_A);
203
- const brandLogoPathB = new Path2D(BRAND_LOGO_PATH_B);
204
210
  let watermarkImageSrc = null;
205
211
  let watermarkImage = null;
206
212
  let watermarkImageReady = false;
@@ -778,6 +784,7 @@ function createChart(element, options = {}) {
778
784
  const draw = () => {
779
785
  orderActionRegions = [];
780
786
  orderDragRegions = [];
787
+ crosshairPriceActionRegion = null;
781
788
  const pixelRatio = getPixelRatio();
782
789
  canvas.style.width = `${width}px`;
783
790
  canvas.style.height = `${height}px`;
@@ -818,10 +825,18 @@ function createChart(element, options = {}) {
818
825
  const endIndex = Math.min(data.length - 1, Math.ceil(xEnd) - 1);
819
826
  const visibleData = data.slice(startIndex, endIndex + 1);
820
827
  let priceSource = visibleData.length > 0 ? visibleData : data;
821
- if (mergedOptions.autoScaleIgnoreLatestCandle && priceSource.length > 1) {
822
- priceSource = priceSource.slice(0, -1);
823
- if (priceSource.length === 0) {
824
- priceSource = visibleData.length > 0 ? visibleData : data;
828
+ if (mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1) {
829
+ const latestIndex = data.length - 1;
830
+ const filtered = priceSource.filter((_, offset) => startIndex + offset !== latestIndex);
831
+ if (filtered.length > 0) {
832
+ priceSource = filtered;
833
+ } else {
834
+ const fallbackWindow = 120;
835
+ const fallbackStart = Math.max(0, latestIndex - fallbackWindow);
836
+ const fallback = data.slice(fallbackStart, latestIndex);
837
+ if (fallback.length > 0) {
838
+ priceSource = fallback;
839
+ }
825
840
  }
826
841
  }
827
842
  const minPrice = Math.min(...priceSource.map((point) => point.l));
@@ -1075,17 +1090,6 @@ function createChart(element, options = {}) {
1075
1090
  });
1076
1091
  drawText(timeLabel, x, chartBottom + 8, "center", "top", axis.textColor);
1077
1092
  }
1078
- const brandLogoWidth = 34;
1079
- const brandLogoHeight = BRAND_LOGO_VIEWBOX_HEIGHT / BRAND_LOGO_VIEWBOX_WIDTH * brandLogoWidth;
1080
- const brandLogoX = chartLeft + 16;
1081
- const brandLogoY = chartBottom - brandLogoHeight - 10;
1082
- ctx.save();
1083
- ctx.translate(brandLogoX, brandLogoY);
1084
- ctx.scale(brandLogoWidth / BRAND_LOGO_VIEWBOX_WIDTH, brandLogoHeight / BRAND_LOGO_VIEWBOX_HEIGHT);
1085
- ctx.fillStyle = "#ffffff";
1086
- ctx.fill(brandLogoPathA);
1087
- ctx.fill(brandLogoPathB);
1088
- ctx.restore();
1089
1093
  if (crosshair.visible && crosshairPoint) {
1090
1094
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
1091
1095
  const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
@@ -1122,6 +1126,39 @@ function createChart(element, options = {}) {
1122
1126
  fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
1123
1127
  strokeCrosshairLabel(priceX, priceY, priceWidth);
1124
1128
  drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
1129
+ if (crosshair.showPriceActionButton) {
1130
+ const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, 30);
1131
+ const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
1132
+ const buttonX = priceX - buttonGap - buttonSize;
1133
+ const buttonY = priceY + (labelHeight - buttonSize) / 2;
1134
+ const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
1135
+ const buttonBorderWidth = Math.max(0, Math.round(crosshair.priceActionButtonBorderWidth));
1136
+ ctx.fillStyle = crosshair.priceActionButtonBackgroundColor;
1137
+ fillRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
1138
+ if (buttonBorderWidth > 0) {
1139
+ ctx.save();
1140
+ ctx.strokeStyle = crosshair.priceActionButtonBorderColor;
1141
+ ctx.lineWidth = buttonBorderWidth;
1142
+ ctx.setLineDash([]);
1143
+ strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
1144
+ ctx.restore();
1145
+ }
1146
+ drawText(
1147
+ crosshair.priceActionButtonText,
1148
+ buttonX + buttonSize / 2,
1149
+ buttonY + buttonSize / 2,
1150
+ "center",
1151
+ "middle",
1152
+ crosshair.priceActionButtonTextColor
1153
+ );
1154
+ crosshairPriceActionRegion = {
1155
+ x: buttonX,
1156
+ y: buttonY,
1157
+ width: buttonSize,
1158
+ height: buttonSize,
1159
+ price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
1160
+ };
1161
+ }
1125
1162
  }
1126
1163
  if (crosshair.showTimeLabel) {
1127
1164
  const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
@@ -1284,6 +1321,16 @@ function createChart(element, options = {}) {
1284
1321
  (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
1285
1322
  );
1286
1323
  };
1324
+ const getCrosshairPriceActionRegion = (x, y) => {
1325
+ if (!crosshairPriceActionRegion) {
1326
+ return null;
1327
+ }
1328
+ const region = crosshairPriceActionRegion;
1329
+ if (x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height) {
1330
+ return region;
1331
+ }
1332
+ return null;
1333
+ };
1287
1334
  const priceFromCanvasY = (y) => {
1288
1335
  if (!drawState) {
1289
1336
  return 0;
@@ -1349,6 +1396,15 @@ function createChart(element, options = {}) {
1349
1396
  let activePointerId = null;
1350
1397
  const onPointerDown = (event) => {
1351
1398
  const point = getCanvasPoint(event);
1399
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1400
+ if (crosshairButtonRegion) {
1401
+ crosshairPriceActionHandler?.({
1402
+ x: point.x,
1403
+ y: point.y,
1404
+ price: crosshairButtonRegion.price
1405
+ });
1406
+ return;
1407
+ }
1352
1408
  const orderRegion = getOrderActionRegion(point.x, point.y);
1353
1409
  if (orderRegion) {
1354
1410
  if (orderRegion.draggable) {
@@ -1461,6 +1517,11 @@ function createChart(element, options = {}) {
1461
1517
  return;
1462
1518
  }
1463
1519
  if (!isDragging || !dragMode) {
1520
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1521
+ if (crosshairButtonRegion) {
1522
+ canvas.style.cursor = "pointer";
1523
+ return;
1524
+ }
1464
1525
  const orderRegion = getOrderActionRegion(point.x, point.y);
1465
1526
  if (orderRegion) {
1466
1527
  canvas.style.cursor = orderRegion.draggable ? "ns-resize" : "pointer";
@@ -1714,6 +1775,9 @@ function createChart(element, options = {}) {
1714
1775
  const onCrosshairMove = (handler) => {
1715
1776
  crosshairMoveHandler = handler;
1716
1777
  };
1778
+ const onCrosshairPriceAction = (handler) => {
1779
+ crosshairPriceActionHandler = handler;
1780
+ };
1717
1781
  const setDoubleClickEnabled = (enabled) => {
1718
1782
  doubleClickEnabled = enabled;
1719
1783
  };
@@ -1743,6 +1807,7 @@ function createChart(element, options = {}) {
1743
1807
  onOrderAction,
1744
1808
  onChartClick,
1745
1809
  onCrosshairMove,
1810
+ onCrosshairPriceAction,
1746
1811
  zoomInX,
1747
1812
  zoomOutX,
1748
1813
  zoomInY,
package/dist/index.cjs CHANGED
@@ -51,7 +51,17 @@ 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
+ priceActionButtonText: "+",
57
+ priceActionButtonSize: 20,
58
+ priceActionButtonGap: 6,
59
+ priceActionButtonBackgroundColor: "#1f2937",
60
+ priceActionButtonTextColor: "#e2e8f0",
61
+ priceActionButtonBorderColor: "#475569",
62
+ priceActionButtonBorderWidth: 1,
63
+ priceActionButtonRounded: true,
64
+ priceActionButtonBorderRadius: 3
55
65
  };
56
66
  var DEFAULT_WATERMARK_OPTIONS = {
57
67
  visible: false,
@@ -163,10 +173,6 @@ var DEFAULT_OPTIONS = {
163
173
  },
164
174
  dashPatterns: DEFAULT_DASH_PATTERNS
165
175
  };
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
176
  function createChart(element, options = {}) {
171
177
  const mergedOptions = {
172
178
  ...DEFAULT_OPTIONS,
@@ -211,6 +217,8 @@ function createChart(element, options = {}) {
211
217
  let orderActionHandler = null;
212
218
  let chartClickHandler = null;
213
219
  let crosshairMoveHandler = null;
220
+ let crosshairPriceActionHandler = null;
221
+ let crosshairPriceActionRegion = null;
214
222
  let orderActionRegions = [];
215
223
  let orderDragRegions = [];
216
224
  let generatedPriceLineId = 1;
@@ -223,8 +231,6 @@ function createChart(element, options = {}) {
223
231
  let yMaxOverride = null;
224
232
  let autoYMin = null;
225
233
  let autoYMax = null;
226
- const brandLogoPathA = new Path2D(BRAND_LOGO_PATH_A);
227
- const brandLogoPathB = new Path2D(BRAND_LOGO_PATH_B);
228
234
  let watermarkImageSrc = null;
229
235
  let watermarkImage = null;
230
236
  let watermarkImageReady = false;
@@ -802,6 +808,7 @@ function createChart(element, options = {}) {
802
808
  const draw = () => {
803
809
  orderActionRegions = [];
804
810
  orderDragRegions = [];
811
+ crosshairPriceActionRegion = null;
805
812
  const pixelRatio = getPixelRatio();
806
813
  canvas.style.width = `${width}px`;
807
814
  canvas.style.height = `${height}px`;
@@ -842,10 +849,18 @@ function createChart(element, options = {}) {
842
849
  const endIndex = Math.min(data.length - 1, Math.ceil(xEnd) - 1);
843
850
  const visibleData = data.slice(startIndex, endIndex + 1);
844
851
  let priceSource = visibleData.length > 0 ? visibleData : data;
845
- if (mergedOptions.autoScaleIgnoreLatestCandle && priceSource.length > 1) {
846
- priceSource = priceSource.slice(0, -1);
847
- if (priceSource.length === 0) {
848
- priceSource = visibleData.length > 0 ? visibleData : data;
852
+ if (mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1) {
853
+ const latestIndex = data.length - 1;
854
+ const filtered = priceSource.filter((_, offset) => startIndex + offset !== latestIndex);
855
+ if (filtered.length > 0) {
856
+ priceSource = filtered;
857
+ } else {
858
+ const fallbackWindow = 120;
859
+ const fallbackStart = Math.max(0, latestIndex - fallbackWindow);
860
+ const fallback = data.slice(fallbackStart, latestIndex);
861
+ if (fallback.length > 0) {
862
+ priceSource = fallback;
863
+ }
849
864
  }
850
865
  }
851
866
  const minPrice = Math.min(...priceSource.map((point) => point.l));
@@ -1099,17 +1114,6 @@ function createChart(element, options = {}) {
1099
1114
  });
1100
1115
  drawText(timeLabel, x, chartBottom + 8, "center", "top", axis.textColor);
1101
1116
  }
1102
- const brandLogoWidth = 34;
1103
- const brandLogoHeight = BRAND_LOGO_VIEWBOX_HEIGHT / BRAND_LOGO_VIEWBOX_WIDTH * brandLogoWidth;
1104
- const brandLogoX = chartLeft + 16;
1105
- const brandLogoY = chartBottom - brandLogoHeight - 10;
1106
- ctx.save();
1107
- ctx.translate(brandLogoX, brandLogoY);
1108
- ctx.scale(brandLogoWidth / BRAND_LOGO_VIEWBOX_WIDTH, brandLogoHeight / BRAND_LOGO_VIEWBOX_HEIGHT);
1109
- ctx.fillStyle = "#ffffff";
1110
- ctx.fill(brandLogoPathA);
1111
- ctx.fill(brandLogoPathB);
1112
- ctx.restore();
1113
1117
  if (crosshair.visible && crosshairPoint) {
1114
1118
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
1115
1119
  const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
@@ -1146,6 +1150,39 @@ function createChart(element, options = {}) {
1146
1150
  fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
1147
1151
  strokeCrosshairLabel(priceX, priceY, priceWidth);
1148
1152
  drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
1153
+ if (crosshair.showPriceActionButton) {
1154
+ const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, 30);
1155
+ const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
1156
+ const buttonX = priceX - buttonGap - buttonSize;
1157
+ const buttonY = priceY + (labelHeight - buttonSize) / 2;
1158
+ const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
1159
+ const buttonBorderWidth = Math.max(0, Math.round(crosshair.priceActionButtonBorderWidth));
1160
+ ctx.fillStyle = crosshair.priceActionButtonBackgroundColor;
1161
+ fillRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
1162
+ if (buttonBorderWidth > 0) {
1163
+ ctx.save();
1164
+ ctx.strokeStyle = crosshair.priceActionButtonBorderColor;
1165
+ ctx.lineWidth = buttonBorderWidth;
1166
+ ctx.setLineDash([]);
1167
+ strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
1168
+ ctx.restore();
1169
+ }
1170
+ drawText(
1171
+ crosshair.priceActionButtonText,
1172
+ buttonX + buttonSize / 2,
1173
+ buttonY + buttonSize / 2,
1174
+ "center",
1175
+ "middle",
1176
+ crosshair.priceActionButtonTextColor
1177
+ );
1178
+ crosshairPriceActionRegion = {
1179
+ x: buttonX,
1180
+ y: buttonY,
1181
+ width: buttonSize,
1182
+ height: buttonSize,
1183
+ price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
1184
+ };
1185
+ }
1149
1186
  }
1150
1187
  if (crosshair.showTimeLabel) {
1151
1188
  const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
@@ -1308,6 +1345,16 @@ function createChart(element, options = {}) {
1308
1345
  (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
1309
1346
  );
1310
1347
  };
1348
+ const getCrosshairPriceActionRegion = (x, y) => {
1349
+ if (!crosshairPriceActionRegion) {
1350
+ return null;
1351
+ }
1352
+ const region = crosshairPriceActionRegion;
1353
+ if (x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height) {
1354
+ return region;
1355
+ }
1356
+ return null;
1357
+ };
1311
1358
  const priceFromCanvasY = (y) => {
1312
1359
  if (!drawState) {
1313
1360
  return 0;
@@ -1373,6 +1420,15 @@ function createChart(element, options = {}) {
1373
1420
  let activePointerId = null;
1374
1421
  const onPointerDown = (event) => {
1375
1422
  const point = getCanvasPoint(event);
1423
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1424
+ if (crosshairButtonRegion) {
1425
+ crosshairPriceActionHandler?.({
1426
+ x: point.x,
1427
+ y: point.y,
1428
+ price: crosshairButtonRegion.price
1429
+ });
1430
+ return;
1431
+ }
1376
1432
  const orderRegion = getOrderActionRegion(point.x, point.y);
1377
1433
  if (orderRegion) {
1378
1434
  if (orderRegion.draggable) {
@@ -1485,6 +1541,11 @@ function createChart(element, options = {}) {
1485
1541
  return;
1486
1542
  }
1487
1543
  if (!isDragging || !dragMode) {
1544
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1545
+ if (crosshairButtonRegion) {
1546
+ canvas.style.cursor = "pointer";
1547
+ return;
1548
+ }
1488
1549
  const orderRegion = getOrderActionRegion(point.x, point.y);
1489
1550
  if (orderRegion) {
1490
1551
  canvas.style.cursor = orderRegion.draggable ? "ns-resize" : "pointer";
@@ -1738,6 +1799,9 @@ function createChart(element, options = {}) {
1738
1799
  const onCrosshairMove = (handler) => {
1739
1800
  crosshairMoveHandler = handler;
1740
1801
  };
1802
+ const onCrosshairPriceAction = (handler) => {
1803
+ crosshairPriceActionHandler = handler;
1804
+ };
1741
1805
  const setDoubleClickEnabled = (enabled) => {
1742
1806
  doubleClickEnabled = enabled;
1743
1807
  };
@@ -1767,6 +1831,7 @@ function createChart(element, options = {}) {
1767
1831
  onOrderAction,
1768
1832
  onChartClick,
1769
1833
  onCrosshairMove,
1834
+ onCrosshairPriceAction,
1770
1835
  zoomInX,
1771
1836
  zoomOutX,
1772
1837
  zoomInY,
package/dist/index.d.cts CHANGED
@@ -71,6 +71,16 @@ interface CrosshairOptions {
71
71
  labelBorderColor?: string;
72
72
  labelBorderWidth?: number;
73
73
  labelBorderStyle?: "solid" | "dotted" | "dashed";
74
+ showPriceActionButton?: boolean;
75
+ priceActionButtonText?: string;
76
+ priceActionButtonSize?: number;
77
+ priceActionButtonGap?: number;
78
+ priceActionButtonBackgroundColor?: string;
79
+ priceActionButtonTextColor?: string;
80
+ priceActionButtonBorderColor?: string;
81
+ priceActionButtonBorderWidth?: number;
82
+ priceActionButtonRounded?: boolean;
83
+ priceActionButtonBorderRadius?: number;
74
84
  }
75
85
  interface WatermarkOptions {
76
86
  visible?: boolean;
@@ -178,6 +188,11 @@ interface CrosshairMoveEvent {
178
188
  time?: string;
179
189
  point?: OhlcDataPoint;
180
190
  }
191
+ interface CrosshairPriceActionEvent {
192
+ x: number;
193
+ y: number;
194
+ price: number;
195
+ }
181
196
  interface TickerLineOptions {
182
197
  visible?: boolean;
183
198
  style?: "solid" | "dotted" | "dashed";
@@ -199,6 +214,7 @@ interface ChartInstance {
199
214
  onOrderAction: (handler: ((event: OrderActionEvent) => void) | null) => void;
200
215
  onChartClick: (handler: ((event: ChartClickEvent) => void) | null) => void;
201
216
  onCrosshairMove: (handler: ((event: CrosshairMoveEvent) => void) | null) => void;
217
+ onCrosshairPriceAction: (handler: ((event: CrosshairPriceActionEvent) => void) | null) => void;
202
218
  zoomInX: (factor?: number) => void;
203
219
  zoomOutX: (factor?: number) => void;
204
220
  zoomInY: (factor?: number) => void;
@@ -222,4 +238,4 @@ interface OhlcDataPoint {
222
238
  }
223
239
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
224
240
 
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 };
241
+ 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,16 @@ interface CrosshairOptions {
71
71
  labelBorderColor?: string;
72
72
  labelBorderWidth?: number;
73
73
  labelBorderStyle?: "solid" | "dotted" | "dashed";
74
+ showPriceActionButton?: boolean;
75
+ priceActionButtonText?: string;
76
+ priceActionButtonSize?: number;
77
+ priceActionButtonGap?: number;
78
+ priceActionButtonBackgroundColor?: string;
79
+ priceActionButtonTextColor?: string;
80
+ priceActionButtonBorderColor?: string;
81
+ priceActionButtonBorderWidth?: number;
82
+ priceActionButtonRounded?: boolean;
83
+ priceActionButtonBorderRadius?: number;
74
84
  }
75
85
  interface WatermarkOptions {
76
86
  visible?: boolean;
@@ -178,6 +188,11 @@ interface CrosshairMoveEvent {
178
188
  time?: string;
179
189
  point?: OhlcDataPoint;
180
190
  }
191
+ interface CrosshairPriceActionEvent {
192
+ x: number;
193
+ y: number;
194
+ price: number;
195
+ }
181
196
  interface TickerLineOptions {
182
197
  visible?: boolean;
183
198
  style?: "solid" | "dotted" | "dashed";
@@ -199,6 +214,7 @@ interface ChartInstance {
199
214
  onOrderAction: (handler: ((event: OrderActionEvent) => void) | null) => void;
200
215
  onChartClick: (handler: ((event: ChartClickEvent) => void) | null) => void;
201
216
  onCrosshairMove: (handler: ((event: CrosshairMoveEvent) => void) | null) => void;
217
+ onCrosshairPriceAction: (handler: ((event: CrosshairPriceActionEvent) => void) | null) => void;
202
218
  zoomInX: (factor?: number) => void;
203
219
  zoomOutX: (factor?: number) => void;
204
220
  zoomInY: (factor?: number) => void;
@@ -222,4 +238,4 @@ interface OhlcDataPoint {
222
238
  }
223
239
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
224
240
 
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 };
241
+ 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,17 @@ 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
+ priceActionButtonText: "+",
33
+ priceActionButtonSize: 20,
34
+ priceActionButtonGap: 6,
35
+ priceActionButtonBackgroundColor: "#1f2937",
36
+ priceActionButtonTextColor: "#e2e8f0",
37
+ priceActionButtonBorderColor: "#475569",
38
+ priceActionButtonBorderWidth: 1,
39
+ priceActionButtonRounded: true,
40
+ priceActionButtonBorderRadius: 3
31
41
  };
32
42
  var DEFAULT_WATERMARK_OPTIONS = {
33
43
  visible: false,
@@ -139,10 +149,6 @@ var DEFAULT_OPTIONS = {
139
149
  },
140
150
  dashPatterns: DEFAULT_DASH_PATTERNS
141
151
  };
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
152
  function createChart(element, options = {}) {
147
153
  const mergedOptions = {
148
154
  ...DEFAULT_OPTIONS,
@@ -187,6 +193,8 @@ function createChart(element, options = {}) {
187
193
  let orderActionHandler = null;
188
194
  let chartClickHandler = null;
189
195
  let crosshairMoveHandler = null;
196
+ let crosshairPriceActionHandler = null;
197
+ let crosshairPriceActionRegion = null;
190
198
  let orderActionRegions = [];
191
199
  let orderDragRegions = [];
192
200
  let generatedPriceLineId = 1;
@@ -199,8 +207,6 @@ function createChart(element, options = {}) {
199
207
  let yMaxOverride = null;
200
208
  let autoYMin = null;
201
209
  let autoYMax = null;
202
- const brandLogoPathA = new Path2D(BRAND_LOGO_PATH_A);
203
- const brandLogoPathB = new Path2D(BRAND_LOGO_PATH_B);
204
210
  let watermarkImageSrc = null;
205
211
  let watermarkImage = null;
206
212
  let watermarkImageReady = false;
@@ -778,6 +784,7 @@ function createChart(element, options = {}) {
778
784
  const draw = () => {
779
785
  orderActionRegions = [];
780
786
  orderDragRegions = [];
787
+ crosshairPriceActionRegion = null;
781
788
  const pixelRatio = getPixelRatio();
782
789
  canvas.style.width = `${width}px`;
783
790
  canvas.style.height = `${height}px`;
@@ -818,10 +825,18 @@ function createChart(element, options = {}) {
818
825
  const endIndex = Math.min(data.length - 1, Math.ceil(xEnd) - 1);
819
826
  const visibleData = data.slice(startIndex, endIndex + 1);
820
827
  let priceSource = visibleData.length > 0 ? visibleData : data;
821
- if (mergedOptions.autoScaleIgnoreLatestCandle && priceSource.length > 1) {
822
- priceSource = priceSource.slice(0, -1);
823
- if (priceSource.length === 0) {
824
- priceSource = visibleData.length > 0 ? visibleData : data;
828
+ if (mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1) {
829
+ const latestIndex = data.length - 1;
830
+ const filtered = priceSource.filter((_, offset) => startIndex + offset !== latestIndex);
831
+ if (filtered.length > 0) {
832
+ priceSource = filtered;
833
+ } else {
834
+ const fallbackWindow = 120;
835
+ const fallbackStart = Math.max(0, latestIndex - fallbackWindow);
836
+ const fallback = data.slice(fallbackStart, latestIndex);
837
+ if (fallback.length > 0) {
838
+ priceSource = fallback;
839
+ }
825
840
  }
826
841
  }
827
842
  const minPrice = Math.min(...priceSource.map((point) => point.l));
@@ -1075,17 +1090,6 @@ function createChart(element, options = {}) {
1075
1090
  });
1076
1091
  drawText(timeLabel, x, chartBottom + 8, "center", "top", axis.textColor);
1077
1092
  }
1078
- const brandLogoWidth = 34;
1079
- const brandLogoHeight = BRAND_LOGO_VIEWBOX_HEIGHT / BRAND_LOGO_VIEWBOX_WIDTH * brandLogoWidth;
1080
- const brandLogoX = chartLeft + 16;
1081
- const brandLogoY = chartBottom - brandLogoHeight - 10;
1082
- ctx.save();
1083
- ctx.translate(brandLogoX, brandLogoY);
1084
- ctx.scale(brandLogoWidth / BRAND_LOGO_VIEWBOX_WIDTH, brandLogoHeight / BRAND_LOGO_VIEWBOX_HEIGHT);
1085
- ctx.fillStyle = "#ffffff";
1086
- ctx.fill(brandLogoPathA);
1087
- ctx.fill(brandLogoPathB);
1088
- ctx.restore();
1089
1093
  if (crosshair.visible && crosshairPoint) {
1090
1094
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
1091
1095
  const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
@@ -1122,6 +1126,39 @@ function createChart(element, options = {}) {
1122
1126
  fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
1123
1127
  strokeCrosshairLabel(priceX, priceY, priceWidth);
1124
1128
  drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
1129
+ if (crosshair.showPriceActionButton) {
1130
+ const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, 30);
1131
+ const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
1132
+ const buttonX = priceX - buttonGap - buttonSize;
1133
+ const buttonY = priceY + (labelHeight - buttonSize) / 2;
1134
+ const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
1135
+ const buttonBorderWidth = Math.max(0, Math.round(crosshair.priceActionButtonBorderWidth));
1136
+ ctx.fillStyle = crosshair.priceActionButtonBackgroundColor;
1137
+ fillRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
1138
+ if (buttonBorderWidth > 0) {
1139
+ ctx.save();
1140
+ ctx.strokeStyle = crosshair.priceActionButtonBorderColor;
1141
+ ctx.lineWidth = buttonBorderWidth;
1142
+ ctx.setLineDash([]);
1143
+ strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
1144
+ ctx.restore();
1145
+ }
1146
+ drawText(
1147
+ crosshair.priceActionButtonText,
1148
+ buttonX + buttonSize / 2,
1149
+ buttonY + buttonSize / 2,
1150
+ "center",
1151
+ "middle",
1152
+ crosshair.priceActionButtonTextColor
1153
+ );
1154
+ crosshairPriceActionRegion = {
1155
+ x: buttonX,
1156
+ y: buttonY,
1157
+ width: buttonSize,
1158
+ height: buttonSize,
1159
+ price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
1160
+ };
1161
+ }
1125
1162
  }
1126
1163
  if (crosshair.showTimeLabel) {
1127
1164
  const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
@@ -1284,6 +1321,16 @@ function createChart(element, options = {}) {
1284
1321
  (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
1285
1322
  );
1286
1323
  };
1324
+ const getCrosshairPriceActionRegion = (x, y) => {
1325
+ if (!crosshairPriceActionRegion) {
1326
+ return null;
1327
+ }
1328
+ const region = crosshairPriceActionRegion;
1329
+ if (x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height) {
1330
+ return region;
1331
+ }
1332
+ return null;
1333
+ };
1287
1334
  const priceFromCanvasY = (y) => {
1288
1335
  if (!drawState) {
1289
1336
  return 0;
@@ -1349,6 +1396,15 @@ function createChart(element, options = {}) {
1349
1396
  let activePointerId = null;
1350
1397
  const onPointerDown = (event) => {
1351
1398
  const point = getCanvasPoint(event);
1399
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1400
+ if (crosshairButtonRegion) {
1401
+ crosshairPriceActionHandler?.({
1402
+ x: point.x,
1403
+ y: point.y,
1404
+ price: crosshairButtonRegion.price
1405
+ });
1406
+ return;
1407
+ }
1352
1408
  const orderRegion = getOrderActionRegion(point.x, point.y);
1353
1409
  if (orderRegion) {
1354
1410
  if (orderRegion.draggable) {
@@ -1461,6 +1517,11 @@ function createChart(element, options = {}) {
1461
1517
  return;
1462
1518
  }
1463
1519
  if (!isDragging || !dragMode) {
1520
+ const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
1521
+ if (crosshairButtonRegion) {
1522
+ canvas.style.cursor = "pointer";
1523
+ return;
1524
+ }
1464
1525
  const orderRegion = getOrderActionRegion(point.x, point.y);
1465
1526
  if (orderRegion) {
1466
1527
  canvas.style.cursor = orderRegion.draggable ? "ns-resize" : "pointer";
@@ -1714,6 +1775,9 @@ function createChart(element, options = {}) {
1714
1775
  const onCrosshairMove = (handler) => {
1715
1776
  crosshairMoveHandler = handler;
1716
1777
  };
1778
+ const onCrosshairPriceAction = (handler) => {
1779
+ crosshairPriceActionHandler = handler;
1780
+ };
1717
1781
  const setDoubleClickEnabled = (enabled) => {
1718
1782
  doubleClickEnabled = enabled;
1719
1783
  };
@@ -1743,6 +1807,7 @@ function createChart(element, options = {}) {
1743
1807
  onOrderAction,
1744
1808
  onChartClick,
1745
1809
  onCrosshairMove,
1810
+ onCrosshairPriceAction,
1746
1811
  zoomInX,
1747
1812
  zoomOutX,
1748
1813
  zoomInY,
package/docs/API.md CHANGED
@@ -94,6 +94,16 @@ 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
+ - `priceActionButtonText` (default `"+"`)
99
+ - `priceActionButtonSize` (default `20`)
100
+ - `priceActionButtonGap` (default `6`)
101
+ - `priceActionButtonBackgroundColor` (default `#1f2937`)
102
+ - `priceActionButtonTextColor` (default `#e2e8f0`)
103
+ - `priceActionButtonBorderColor` (default `#475569`)
104
+ - `priceActionButtonBorderWidth` (default `1`)
105
+ - `priceActionButtonRounded` (default `true`; set `false` for square corners)
106
+ - `priceActionButtonBorderRadius` (default `3`)
97
107
 
98
108
  ### `WatermarkOptions`
99
109
 
@@ -243,6 +253,7 @@ Connector/fill visuals:
243
253
  - `onOrderAction(handler: ((event: OrderActionEvent) => void) | null): void`
244
254
  - `onChartClick(handler: ((event: ChartClickEvent) => void) | null): void`
245
255
  - `onCrosshairMove(handler: ((event: CrosshairMoveEvent) => void) | null): void`
256
+ - `onCrosshairPriceAction(handler: ((event: CrosshairPriceActionEvent) => void) | null): void`
246
257
  - `zoomInX(factor?: number): void` (default factor `1.25`)
247
258
  - `zoomOutX(factor?: number): void` (default factor `1.25`)
248
259
  - `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,35 @@ 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: 6
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
+ priceActionButtonBorderRadius: 0
91
+ }
92
+ });
93
+ ```
94
+
66
95
  ## Add a draggable pending limit line
67
96
 
68
97
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",