hyperprop-charting-library 0.1.54 → 0.1.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hyperprop-charting-library.cjs +376 -2
- package/dist/hyperprop-charting-library.d.ts +36 -1
- package/dist/hyperprop-charting-library.js +376 -2
- package/dist/index.cjs +376 -2
- package/dist/index.d.cts +36 -1
- package/dist/index.d.ts +36 -1
- package/dist/index.js +376 -2
- package/docs/API.md +43 -0
- package/docs/RECIPES.md +28 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -190,7 +190,8 @@ var DEFAULT_OPTIONS = {
|
|
|
190
190
|
},
|
|
191
191
|
labels: DEFAULT_LABELS_OPTIONS,
|
|
192
192
|
dashPatterns: DEFAULT_DASH_PATTERNS,
|
|
193
|
-
indicators: []
|
|
193
|
+
indicators: [],
|
|
194
|
+
drawings: []
|
|
194
195
|
};
|
|
195
196
|
var mergeChartOptions = (baseOptions, options = {}) => ({
|
|
196
197
|
...baseOptions,
|
|
@@ -881,6 +882,7 @@ function createChart(element, options = {}) {
|
|
|
881
882
|
let generatedPriceLineId = 1;
|
|
882
883
|
let generatedOrderLineId = 1;
|
|
883
884
|
let generatedIndicatorId = 1;
|
|
885
|
+
let generatedDrawingId = 1;
|
|
884
886
|
const indicatorRegistry = /* @__PURE__ */ new Map();
|
|
885
887
|
for (const indicator of BUILTIN_INDICATORS) {
|
|
886
888
|
indicatorRegistry.set(indicator.id, indicator);
|
|
@@ -903,7 +905,42 @@ function createChart(element, options = {}) {
|
|
|
903
905
|
}
|
|
904
906
|
};
|
|
905
907
|
};
|
|
908
|
+
const normalizeDrawingState = (drawing) => ({
|
|
909
|
+
id: drawing.id ?? `drawing-${generatedDrawingId++}`,
|
|
910
|
+
type: drawing.type,
|
|
911
|
+
points: drawing.points.map((point) => ({
|
|
912
|
+
index: Number(point.index) || 0,
|
|
913
|
+
price: Number(point.price) || 0,
|
|
914
|
+
...point.time ? { time: point.time } : {}
|
|
915
|
+
})),
|
|
916
|
+
visible: drawing.visible ?? true,
|
|
917
|
+
color: drawing.color ?? "#94a3b8",
|
|
918
|
+
style: drawing.style ?? "dotted",
|
|
919
|
+
width: Math.max(1, Number(drawing.width) || 1),
|
|
920
|
+
locked: drawing.locked ?? false,
|
|
921
|
+
...drawing.label === void 0 ? {} : { label: drawing.label }
|
|
922
|
+
});
|
|
923
|
+
const serializeDrawing = (drawing) => ({
|
|
924
|
+
id: drawing.id,
|
|
925
|
+
type: drawing.type,
|
|
926
|
+
points: drawing.points.map((point) => ({ ...point })),
|
|
927
|
+
visible: drawing.visible,
|
|
928
|
+
color: drawing.color,
|
|
929
|
+
style: drawing.style,
|
|
930
|
+
width: drawing.width,
|
|
931
|
+
locked: drawing.locked,
|
|
932
|
+
...drawing.label === void 0 ? {} : { label: drawing.label }
|
|
933
|
+
});
|
|
906
934
|
let indicators = (options.indicators ?? []).map((indicator) => normalizeIndicatorState(indicator));
|
|
935
|
+
let drawings = (options.drawings ?? []).map((drawing) => normalizeDrawingState(drawing));
|
|
936
|
+
let activeDrawingTool = null;
|
|
937
|
+
let draftDrawing = null;
|
|
938
|
+
let drawingDragState = null;
|
|
939
|
+
let drawingsChangeHandler = null;
|
|
940
|
+
let drawingSelectHandler = null;
|
|
941
|
+
const emitDrawingsChange = () => {
|
|
942
|
+
drawingsChangeHandler?.(drawings.map((drawing) => serializeDrawing(drawing)));
|
|
943
|
+
};
|
|
907
944
|
const orderWidgetWidthById = /* @__PURE__ */ new Map();
|
|
908
945
|
const orderPriceTagWidthById = /* @__PURE__ */ new Map();
|
|
909
946
|
let xCenter = 0;
|
|
@@ -1957,6 +1994,57 @@ function createChart(element, options = {}) {
|
|
|
1957
1994
|
const yFromPrice = (price) => {
|
|
1958
1995
|
return chartBottom - (price - yMin) / yRange * chartHeight;
|
|
1959
1996
|
};
|
|
1997
|
+
const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
|
|
1998
|
+
const drawDrawingHandle = (x, y, color) => {
|
|
1999
|
+
ctx.save();
|
|
2000
|
+
ctx.setLineDash([]);
|
|
2001
|
+
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2002
|
+
ctx.strokeStyle = color;
|
|
2003
|
+
ctx.lineWidth = 2;
|
|
2004
|
+
ctx.beginPath();
|
|
2005
|
+
ctx.arc(x, y, 5, 0, Math.PI * 2);
|
|
2006
|
+
ctx.fill();
|
|
2007
|
+
ctx.stroke();
|
|
2008
|
+
ctx.restore();
|
|
2009
|
+
};
|
|
2010
|
+
const drawDrawing = (drawing, draft = false) => {
|
|
2011
|
+
if (!drawing.visible) {
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
ctx.save();
|
|
2015
|
+
ctx.strokeStyle = drawing.color;
|
|
2016
|
+
ctx.lineWidth = drawing.width;
|
|
2017
|
+
ctx.globalAlpha = draft ? 0.72 : 1;
|
|
2018
|
+
applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
2019
|
+
if (drawing.type === "horizontal-line") {
|
|
2020
|
+
const point = drawing.points[0];
|
|
2021
|
+
if (point) {
|
|
2022
|
+
const y = clamp(yFromPrice(point.price), chartTop + 1, chartBottom - 1);
|
|
2023
|
+
const handleX = chartRight - 14;
|
|
2024
|
+
ctx.beginPath();
|
|
2025
|
+
ctx.moveTo(crisp(chartLeft), crisp(y));
|
|
2026
|
+
ctx.lineTo(crisp(chartRight), crisp(y));
|
|
2027
|
+
ctx.stroke();
|
|
2028
|
+
drawDrawingHandle(handleX, y, drawing.color);
|
|
2029
|
+
}
|
|
2030
|
+
} else if (drawing.type === "trendline") {
|
|
2031
|
+
const first = drawing.points[0];
|
|
2032
|
+
const second = drawing.points[1];
|
|
2033
|
+
if (first && second) {
|
|
2034
|
+
const firstX = xFromDrawingPoint(first);
|
|
2035
|
+
const firstY = yFromPrice(first.price);
|
|
2036
|
+
const secondX = xFromDrawingPoint(second);
|
|
2037
|
+
const secondY = yFromPrice(second.price);
|
|
2038
|
+
ctx.beginPath();
|
|
2039
|
+
ctx.moveTo(firstX, firstY);
|
|
2040
|
+
ctx.lineTo(secondX, secondY);
|
|
2041
|
+
ctx.stroke();
|
|
2042
|
+
drawDrawingHandle(firstX, firstY, drawing.color);
|
|
2043
|
+
drawDrawingHandle(secondX, secondY, drawing.color);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
ctx.restore();
|
|
2047
|
+
};
|
|
1960
2048
|
if (watermark.visible && watermark.imageSrc.trim().length > 0) {
|
|
1961
2049
|
ensureWatermarkImage(watermark.imageSrc.trim());
|
|
1962
2050
|
if (watermarkImageReady && watermarkImage) {
|
|
@@ -2128,6 +2216,10 @@ function createChart(element, options = {}) {
|
|
|
2128
2216
|
);
|
|
2129
2217
|
});
|
|
2130
2218
|
}
|
|
2219
|
+
drawings.forEach((drawing) => drawDrawing(drawing));
|
|
2220
|
+
if (draftDrawing) {
|
|
2221
|
+
drawDrawing(draftDrawing, true);
|
|
2222
|
+
}
|
|
2131
2223
|
const crosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
2132
2224
|
if (crosshair.visible && crosshairPoint) {
|
|
2133
2225
|
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
@@ -2964,6 +3056,82 @@ function createChart(element, options = {}) {
|
|
|
2964
3056
|
const ratio = clamp((drawState.chartBottom - y) / drawState.chartHeight, 0, 1);
|
|
2965
3057
|
return drawState.yMin + ratio * (drawState.yMax - drawState.yMin);
|
|
2966
3058
|
};
|
|
3059
|
+
const drawingPointFromCanvas = (x, y) => {
|
|
3060
|
+
if (!drawState) {
|
|
3061
|
+
return null;
|
|
3062
|
+
}
|
|
3063
|
+
const ratio = clamp((x - drawState.chartLeft) / drawState.chartWidth, 0, 1);
|
|
3064
|
+
const index = drawState.xStart + ratio * drawState.xSpan - 0.5;
|
|
3065
|
+
const nearestIndex = Math.round(index);
|
|
3066
|
+
const time = getTimeForIndex(nearestIndex);
|
|
3067
|
+
return {
|
|
3068
|
+
index,
|
|
3069
|
+
price: roundToPricePrecision(priceFromCanvasY(y)),
|
|
3070
|
+
...time ? { time: time.toISOString() } : {}
|
|
3071
|
+
};
|
|
3072
|
+
};
|
|
3073
|
+
const normalizeDrawingPoint = (index, price) => {
|
|
3074
|
+
const roundedIndex = Math.round(index);
|
|
3075
|
+
const time = getTimeForIndex(roundedIndex);
|
|
3076
|
+
return {
|
|
3077
|
+
index,
|
|
3078
|
+
price: roundToPricePrecision(price),
|
|
3079
|
+
...time ? { time: time.toISOString() } : {}
|
|
3080
|
+
};
|
|
3081
|
+
};
|
|
3082
|
+
const canvasXFromDrawingPoint = (point) => {
|
|
3083
|
+
if (!drawState) return 0;
|
|
3084
|
+
return drawState.chartLeft + (point.index + 0.5 - drawState.xStart) / drawState.xSpan * drawState.chartWidth;
|
|
3085
|
+
};
|
|
3086
|
+
const canvasYFromDrawingPrice = (price) => {
|
|
3087
|
+
if (!drawState) return 0;
|
|
3088
|
+
const range = drawState.yMax - drawState.yMin || 1;
|
|
3089
|
+
return drawState.chartBottom - (price - drawState.yMin) / range * drawState.chartHeight;
|
|
3090
|
+
};
|
|
3091
|
+
const distanceToSegment = (x, y, x1, y1, x2, y2) => {
|
|
3092
|
+
const dx = x2 - x1;
|
|
3093
|
+
const dy = y2 - y1;
|
|
3094
|
+
const lengthSq = dx * dx + dy * dy;
|
|
3095
|
+
if (lengthSq === 0) return Math.hypot(x - x1, y - y1);
|
|
3096
|
+
const t = clamp(((x - x1) * dx + (y - y1) * dy) / lengthSq, 0, 1);
|
|
3097
|
+
return Math.hypot(x - (x1 + t * dx), y - (y1 + t * dy));
|
|
3098
|
+
};
|
|
3099
|
+
const getDrawingHit = (x, y) => {
|
|
3100
|
+
for (let index = drawings.length - 1; index >= 0; index -= 1) {
|
|
3101
|
+
const drawing = drawings[index];
|
|
3102
|
+
if (!drawing?.visible) continue;
|
|
3103
|
+
if (drawing.type === "horizontal-line") {
|
|
3104
|
+
const point = drawing.points[0];
|
|
3105
|
+
if (!point) continue;
|
|
3106
|
+
const lineY = canvasYFromDrawingPrice(point.price);
|
|
3107
|
+
const handleX = drawState ? drawState.chartRight - 14 : 0;
|
|
3108
|
+
if (Math.hypot(x - handleX, y - lineY) <= 8) {
|
|
3109
|
+
return { drawing, target: "handle", pointIndex: 0 };
|
|
3110
|
+
}
|
|
3111
|
+
if (Math.abs(y - lineY) <= 7) {
|
|
3112
|
+
return { drawing, target: "line" };
|
|
3113
|
+
}
|
|
3114
|
+
} else if (drawing.type === "trendline") {
|
|
3115
|
+
const first = drawing.points[0];
|
|
3116
|
+
const second = drawing.points[1];
|
|
3117
|
+
if (!first || !second) continue;
|
|
3118
|
+
const x1 = canvasXFromDrawingPoint(first);
|
|
3119
|
+
const y1 = canvasYFromDrawingPrice(first.price);
|
|
3120
|
+
const x2 = canvasXFromDrawingPoint(second);
|
|
3121
|
+
const y2 = canvasYFromDrawingPrice(second.price);
|
|
3122
|
+
if (Math.hypot(x - x1, y - y1) <= 8) {
|
|
3123
|
+
return { drawing, target: "handle", pointIndex: 0 };
|
|
3124
|
+
}
|
|
3125
|
+
if (Math.hypot(x - x2, y - y2) <= 8) {
|
|
3126
|
+
return { drawing, target: "handle", pointIndex: 1 };
|
|
3127
|
+
}
|
|
3128
|
+
if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
|
|
3129
|
+
return { drawing, target: "line" };
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
return null;
|
|
3134
|
+
};
|
|
2967
3135
|
const indexFromCanvasX = (x) => {
|
|
2968
3136
|
if (!drawState) {
|
|
2969
3137
|
return null;
|
|
@@ -3016,6 +3184,86 @@ function createChart(element, options = {}) {
|
|
|
3016
3184
|
}
|
|
3017
3185
|
return "outside";
|
|
3018
3186
|
};
|
|
3187
|
+
const handleDrawingToolPointerDown = (x, y) => {
|
|
3188
|
+
if (!activeDrawingTool || !drawState) {
|
|
3189
|
+
return false;
|
|
3190
|
+
}
|
|
3191
|
+
const point = drawingPointFromCanvas(x, y);
|
|
3192
|
+
if (!point) {
|
|
3193
|
+
return false;
|
|
3194
|
+
}
|
|
3195
|
+
if (activeDrawingTool === "horizontal-line") {
|
|
3196
|
+
drawings.push(
|
|
3197
|
+
normalizeDrawingState({
|
|
3198
|
+
type: "horizontal-line",
|
|
3199
|
+
points: [point],
|
|
3200
|
+
color: "#38bdf8",
|
|
3201
|
+
style: "dotted",
|
|
3202
|
+
width: 1
|
|
3203
|
+
})
|
|
3204
|
+
);
|
|
3205
|
+
emitDrawingsChange();
|
|
3206
|
+
draw();
|
|
3207
|
+
return true;
|
|
3208
|
+
}
|
|
3209
|
+
if (activeDrawingTool === "trendline") {
|
|
3210
|
+
if (draftDrawing?.type === "trendline") {
|
|
3211
|
+
const completed = normalizeDrawingState({
|
|
3212
|
+
...serializeDrawing(draftDrawing),
|
|
3213
|
+
points: [draftDrawing.points[0], point]
|
|
3214
|
+
});
|
|
3215
|
+
drawings.push(completed);
|
|
3216
|
+
draftDrawing = null;
|
|
3217
|
+
activeDrawingTool = null;
|
|
3218
|
+
emitDrawingsChange();
|
|
3219
|
+
draw();
|
|
3220
|
+
return true;
|
|
3221
|
+
}
|
|
3222
|
+
draftDrawing = normalizeDrawingState({
|
|
3223
|
+
type: "trendline",
|
|
3224
|
+
points: [point, point],
|
|
3225
|
+
color: "#2563eb",
|
|
3226
|
+
style: "solid",
|
|
3227
|
+
width: 2
|
|
3228
|
+
});
|
|
3229
|
+
draw();
|
|
3230
|
+
return true;
|
|
3231
|
+
}
|
|
3232
|
+
return false;
|
|
3233
|
+
};
|
|
3234
|
+
const updateDrawingDrag = (x, y) => {
|
|
3235
|
+
if (!drawingDragState) {
|
|
3236
|
+
return false;
|
|
3237
|
+
}
|
|
3238
|
+
const currentPoint = drawingPointFromCanvas(x, y);
|
|
3239
|
+
if (!currentPoint) {
|
|
3240
|
+
return true;
|
|
3241
|
+
}
|
|
3242
|
+
const deltaIndex = currentPoint.index - drawingDragState.startCanvasPoint.index;
|
|
3243
|
+
const deltaPrice = currentPoint.price - drawingDragState.startCanvasPoint.price;
|
|
3244
|
+
drawings = drawings.map((drawing) => {
|
|
3245
|
+
if (drawing.id !== drawingDragState?.drawingId || drawing.locked) {
|
|
3246
|
+
return drawing;
|
|
3247
|
+
}
|
|
3248
|
+
if (drawingDragState.target === "handle") {
|
|
3249
|
+
return {
|
|
3250
|
+
...drawing,
|
|
3251
|
+
points: drawing.points.map(
|
|
3252
|
+
(point, index) => index === (drawingDragState?.pointIndex ?? 0) ? normalizeDrawingPoint(currentPoint.index, currentPoint.price) : point
|
|
3253
|
+
)
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
return {
|
|
3257
|
+
...drawing,
|
|
3258
|
+
points: drawingDragState.startPoints.map(
|
|
3259
|
+
(point) => normalizeDrawingPoint(point.index + deltaIndex, point.price + deltaPrice)
|
|
3260
|
+
)
|
|
3261
|
+
};
|
|
3262
|
+
});
|
|
3263
|
+
emitDrawingsChange();
|
|
3264
|
+
draw();
|
|
3265
|
+
return true;
|
|
3266
|
+
};
|
|
3019
3267
|
let isDragging = false;
|
|
3020
3268
|
let dragMode = null;
|
|
3021
3269
|
let lastPointerX = 0;
|
|
@@ -3055,6 +3303,7 @@ function createChart(element, options = {}) {
|
|
|
3055
3303
|
pointerDownInfo = null;
|
|
3056
3304
|
orderDragState = null;
|
|
3057
3305
|
actionDragState = null;
|
|
3306
|
+
drawingDragState = null;
|
|
3058
3307
|
canvas.style.cursor = "default";
|
|
3059
3308
|
setCrosshairPoint(null);
|
|
3060
3309
|
};
|
|
@@ -3141,6 +3390,40 @@ function createChart(element, options = {}) {
|
|
|
3141
3390
|
if (region === "outside") {
|
|
3142
3391
|
return;
|
|
3143
3392
|
}
|
|
3393
|
+
if (region === "plot" && !activeDrawingTool) {
|
|
3394
|
+
const drawingHit = getDrawingHit(point.x, point.y);
|
|
3395
|
+
if (drawingHit) {
|
|
3396
|
+
drawingSelectHandler?.({
|
|
3397
|
+
drawing: serializeDrawing(drawingHit.drawing),
|
|
3398
|
+
target: drawingHit.target,
|
|
3399
|
+
...drawingHit.pointIndex === void 0 ? {} : { pointIndex: drawingHit.pointIndex },
|
|
3400
|
+
x: point.x,
|
|
3401
|
+
y: point.y
|
|
3402
|
+
});
|
|
3403
|
+
if (!drawingHit.drawing.locked) {
|
|
3404
|
+
const startCanvasPoint = drawingPointFromCanvas(point.x, point.y);
|
|
3405
|
+
if (startCanvasPoint) {
|
|
3406
|
+
drawingDragState = {
|
|
3407
|
+
drawingId: drawingHit.drawing.id,
|
|
3408
|
+
target: drawingHit.target,
|
|
3409
|
+
...drawingHit.pointIndex === void 0 ? {} : { pointIndex: drawingHit.pointIndex },
|
|
3410
|
+
startCanvasPoint,
|
|
3411
|
+
startPoints: drawingHit.drawing.points.map((drawingPoint) => ({ ...drawingPoint }))
|
|
3412
|
+
};
|
|
3413
|
+
activePointerId = event.pointerId;
|
|
3414
|
+
canvas.setPointerCapture(event.pointerId);
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
setCrosshairPoint(null);
|
|
3418
|
+
canvas.style.cursor = drawingHit.drawing.locked ? "pointer" : drawingHit.target === "handle" ? "grab" : "move";
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
if (region === "plot" && handleDrawingToolPointerDown(point.x, point.y)) {
|
|
3423
|
+
setCrosshairPoint(null);
|
|
3424
|
+
canvas.style.cursor = "crosshair";
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3144
3427
|
isDragging = true;
|
|
3145
3428
|
dragMode = region;
|
|
3146
3429
|
activePointerId = event.pointerId;
|
|
@@ -3172,6 +3455,27 @@ function createChart(element, options = {}) {
|
|
|
3172
3455
|
pointerDownInfo.moved = true;
|
|
3173
3456
|
}
|
|
3174
3457
|
}
|
|
3458
|
+
if (drawingDragState) {
|
|
3459
|
+
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
updateDrawingDrag(point.x, point.y);
|
|
3463
|
+
canvas.style.cursor = drawingDragState.target === "handle" ? "grabbing" : "move";
|
|
3464
|
+
setCrosshairPoint(null);
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
if (draftDrawing && activeDrawingTool === "trendline") {
|
|
3468
|
+
const nextPoint = drawingPointFromCanvas(point.x, point.y);
|
|
3469
|
+
if (nextPoint) {
|
|
3470
|
+
draftDrawing = {
|
|
3471
|
+
...draftDrawing,
|
|
3472
|
+
points: [draftDrawing.points[0], nextPoint]
|
|
3473
|
+
};
|
|
3474
|
+
canvas.style.cursor = "crosshair";
|
|
3475
|
+
draw();
|
|
3476
|
+
return;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3175
3479
|
if (orderDragState) {
|
|
3176
3480
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
3177
3481
|
return;
|
|
@@ -3242,9 +3546,15 @@ function createChart(element, options = {}) {
|
|
|
3242
3546
|
setCrosshairPoint(null);
|
|
3243
3547
|
return;
|
|
3244
3548
|
}
|
|
3549
|
+
if (!activeDrawingTool && getDrawingHit(point.x, point.y)) {
|
|
3550
|
+
const drawingHit = getDrawingHit(point.x, point.y);
|
|
3551
|
+
canvas.style.cursor = drawingHit?.drawing.locked ? "pointer" : drawingHit?.target === "handle" ? "grab" : "move";
|
|
3552
|
+
setCrosshairPoint(null);
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3245
3555
|
const hoverRegion = getHitRegion(point.x, point.y);
|
|
3246
3556
|
if (hoverRegion === "plot") {
|
|
3247
|
-
canvas.style.cursor = doubleClickEnabled ? "default" : "crosshair";
|
|
3557
|
+
canvas.style.cursor = activeDrawingTool ? "crosshair" : doubleClickEnabled ? "default" : "crosshair";
|
|
3248
3558
|
setCrosshairPoint(point);
|
|
3249
3559
|
emitCrosshairMove(point.x, point.y, "plot");
|
|
3250
3560
|
} else if (hoverRegion === "x-axis") {
|
|
@@ -3334,6 +3644,10 @@ function createChart(element, options = {}) {
|
|
|
3334
3644
|
}
|
|
3335
3645
|
actionDragState = null;
|
|
3336
3646
|
}
|
|
3647
|
+
if (drawingDragState) {
|
|
3648
|
+
drawingDragState = null;
|
|
3649
|
+
emitDrawingsChange();
|
|
3650
|
+
}
|
|
3337
3651
|
isDragging = false;
|
|
3338
3652
|
dragMode = null;
|
|
3339
3653
|
activePointerId = null;
|
|
@@ -3643,6 +3957,56 @@ function createChart(element, options = {}) {
|
|
|
3643
3957
|
indicators = indicators.filter((indicator) => indicator.id !== id);
|
|
3644
3958
|
draw();
|
|
3645
3959
|
};
|
|
3960
|
+
const setActiveDrawingTool = (tool) => {
|
|
3961
|
+
activeDrawingTool = tool;
|
|
3962
|
+
draftDrawing = null;
|
|
3963
|
+
canvas.style.cursor = tool ? "crosshair" : "default";
|
|
3964
|
+
draw();
|
|
3965
|
+
};
|
|
3966
|
+
const getActiveDrawingTool = () => activeDrawingTool;
|
|
3967
|
+
const getDrawings = () => drawings.map((drawing) => serializeDrawing(drawing));
|
|
3968
|
+
const setDrawings = (nextDrawings) => {
|
|
3969
|
+
drawings = nextDrawings.map((drawing) => normalizeDrawingState(drawing));
|
|
3970
|
+
draftDrawing = null;
|
|
3971
|
+
emitDrawingsChange();
|
|
3972
|
+
draw();
|
|
3973
|
+
};
|
|
3974
|
+
const addDrawing = (drawing) => {
|
|
3975
|
+
const next = normalizeDrawingState(drawing);
|
|
3976
|
+
drawings.push(next);
|
|
3977
|
+
emitDrawingsChange();
|
|
3978
|
+
draw();
|
|
3979
|
+
return next.id;
|
|
3980
|
+
};
|
|
3981
|
+
const updateDrawing = (id, patch) => {
|
|
3982
|
+
drawings = drawings.map(
|
|
3983
|
+
(drawing) => drawing.id === id ? normalizeDrawingState({
|
|
3984
|
+
...serializeDrawing(drawing),
|
|
3985
|
+
...patch,
|
|
3986
|
+
id,
|
|
3987
|
+
points: patch.points ?? drawing.points
|
|
3988
|
+
}) : drawing
|
|
3989
|
+
);
|
|
3990
|
+
emitDrawingsChange();
|
|
3991
|
+
draw();
|
|
3992
|
+
};
|
|
3993
|
+
const removeDrawing = (id) => {
|
|
3994
|
+
drawings = drawings.filter((drawing) => drawing.id !== id);
|
|
3995
|
+
emitDrawingsChange();
|
|
3996
|
+
draw();
|
|
3997
|
+
};
|
|
3998
|
+
const clearDrawings = () => {
|
|
3999
|
+
drawings = [];
|
|
4000
|
+
draftDrawing = null;
|
|
4001
|
+
emitDrawingsChange();
|
|
4002
|
+
draw();
|
|
4003
|
+
};
|
|
4004
|
+
const onDrawingsChange = (handler) => {
|
|
4005
|
+
drawingsChangeHandler = handler;
|
|
4006
|
+
};
|
|
4007
|
+
const onDrawingSelect = (handler) => {
|
|
4008
|
+
drawingSelectHandler = handler;
|
|
4009
|
+
};
|
|
3646
4010
|
const destroy = () => {
|
|
3647
4011
|
if (smoothingRafId !== null) {
|
|
3648
4012
|
cancelAnimationFrame(smoothingRafId);
|
|
@@ -3686,6 +4050,16 @@ function createChart(element, options = {}) {
|
|
|
3686
4050
|
getViewport,
|
|
3687
4051
|
setViewport,
|
|
3688
4052
|
onViewportChange,
|
|
4053
|
+
setActiveDrawingTool,
|
|
4054
|
+
getActiveDrawingTool,
|
|
4055
|
+
setDrawings,
|
|
4056
|
+
getDrawings,
|
|
4057
|
+
addDrawing,
|
|
4058
|
+
updateDrawing,
|
|
4059
|
+
removeDrawing,
|
|
4060
|
+
clearDrawings,
|
|
4061
|
+
onDrawingsChange,
|
|
4062
|
+
onDrawingSelect,
|
|
3689
4063
|
setDoubleClickEnabled,
|
|
3690
4064
|
setDoubleClickAction,
|
|
3691
4065
|
registerIndicator,
|
package/docs/API.md
CHANGED
|
@@ -68,6 +68,7 @@ Top-level options:
|
|
|
68
68
|
- `labels?: LabelsOptions` (TradingView-style price-scale/indicator label controls)
|
|
69
69
|
- `dashPatterns?: Partial<DashPatternOptions>` (controls dotted/dashed spacing)
|
|
70
70
|
- `indicators?: IndicatorInstanceOptions[]` (initial indicator instances, built-in includes `"volume"`)
|
|
71
|
+
- `drawings?: DrawingObjectOptions[]` (initial user drawings/tool objects)
|
|
71
72
|
|
|
72
73
|
### `AxisOptions`
|
|
73
74
|
|
|
@@ -405,6 +406,39 @@ Volume style inputs:
|
|
|
405
406
|
- `scaleType` (`"sqrt"` default, or `"log"` / `"linear"`)
|
|
406
407
|
- `clampPercentile` (`0..1`, default `1`; e.g. `0.95` to reduce outlier crush)
|
|
407
408
|
|
|
409
|
+
### `DrawingObjectOptions`
|
|
410
|
+
|
|
411
|
+
Drawings are user-created chart tools, separate from indicators. They are interactive chart objects like horizontal lines and trendlines.
|
|
412
|
+
|
|
413
|
+
- `id?: string`
|
|
414
|
+
- `type: "horizontal-line" | "trendline"`
|
|
415
|
+
- `points: DrawingPoint[]`
|
|
416
|
+
- `visible?: boolean`
|
|
417
|
+
- `color?: string`
|
|
418
|
+
- `style?: "solid" | "dotted" | "dashed"`
|
|
419
|
+
- `width?: number`
|
|
420
|
+
- `label?: string`
|
|
421
|
+
|
|
422
|
+
`DrawingPoint`:
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
type DrawingPoint = {
|
|
426
|
+
index: number; // fractional candle index
|
|
427
|
+
price: number;
|
|
428
|
+
time?: string; // ISO timestamp for reference/persistence
|
|
429
|
+
};
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Tool workflow:
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
chart.setActiveDrawingTool("horizontal-line"); // next plot click creates a line
|
|
436
|
+
chart.setActiveDrawingTool("trendline"); // first click starts, second click commits
|
|
437
|
+
chart.setActiveDrawingTool(null); // back to normal cursor/pan
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Use `getDrawings()` / `setDrawings()` for persistence.
|
|
441
|
+
|
|
408
442
|
---
|
|
409
443
|
|
|
410
444
|
## `ChartInstance` Methods
|
|
@@ -429,6 +463,15 @@ Volume style inputs:
|
|
|
429
463
|
- `panY(priceDelta: number): void` (positive = move viewport up)
|
|
430
464
|
- `fitContent(): void` (x-only fit, keeps y zoom)
|
|
431
465
|
- `resetViewport(): void` (fit x + reset y auto-scale)
|
|
466
|
+
- `setActiveDrawingTool(tool: "horizontal-line" | "trendline" | null): void`
|
|
467
|
+
- `getActiveDrawingTool(): "horizontal-line" | "trendline" | null`
|
|
468
|
+
- `setDrawings(drawings: DrawingObjectOptions[]): void`
|
|
469
|
+
- `getDrawings(): DrawingObjectOptions[]`
|
|
470
|
+
- `addDrawing(drawing: DrawingObjectOptions): string`
|
|
471
|
+
- `updateDrawing(id: string, patch: Partial<DrawingObjectOptions>): void`
|
|
472
|
+
- `removeDrawing(id: string): void`
|
|
473
|
+
- `clearDrawings(): void`
|
|
474
|
+
- `onDrawingsChange(handler: ((drawings: DrawingObjectOptions[]) => void) | null): void`
|
|
432
475
|
- `setDoubleClickEnabled(enabled: boolean): void`
|
|
433
476
|
- `setDoubleClickAction(action: "reset" | "placeLimitOrder"): void`
|
|
434
477
|
- `registerIndicator(plugin: IndicatorPlugin): void`
|
package/docs/RECIPES.md
CHANGED
|
@@ -85,6 +85,34 @@ chart.addPriceLine({
|
|
|
85
85
|
});
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
## Add chart drawing tools
|
|
89
|
+
|
|
90
|
+
Drawing tools are separate from indicators. Indicators compute/render data series; drawings are user-created objects that can be persisted.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// Let the next click create a horizontal line.
|
|
94
|
+
chart.setActiveDrawingTool("horizontal-line");
|
|
95
|
+
|
|
96
|
+
// First click starts a trendline, second click commits it.
|
|
97
|
+
chart.setActiveDrawingTool("trendline");
|
|
98
|
+
|
|
99
|
+
// Back to normal cursor/pan mode.
|
|
100
|
+
chart.setActiveDrawingTool(null);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Persist drawings:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
chart.onDrawingsChange((drawings) => {
|
|
107
|
+
localStorage.setItem("chart-drawings", JSON.stringify(drawings));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const saved = localStorage.getItem("chart-drawings");
|
|
111
|
+
if (saved) {
|
|
112
|
+
chart.setDrawings(JSON.parse(saved));
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
88
116
|
## Tighten dotted spacing globally
|
|
89
117
|
|
|
90
118
|
```ts
|