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/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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",