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 +17 -1
- package/dist/hyperprop-charting-library.cjs +87 -22
- package/dist/hyperprop-charting-library.d.ts +17 -1
- package/dist/hyperprop-charting-library.js +87 -22
- package/dist/index.cjs +87 -22
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +87 -22
- package/docs/API.md +11 -0
- package/docs/EVENTS.md +27 -0
- package/docs/RECIPES.md +29 -0
- package/package.json +1 -1
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 &&
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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 &&
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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 &&
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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 &&
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|