hyperprop-charting-library 0.1.54 → 0.1.55

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,41 @@ 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 drawingsChangeHandler = null;
939
+ let drawingSelectHandler = null;
940
+ const emitDrawingsChange = () => {
941
+ drawingsChangeHandler?.(drawings.map((drawing) => serializeDrawing(drawing)));
942
+ };
907
943
  const orderWidgetWidthById = /* @__PURE__ */ new Map();
908
944
  const orderPriceTagWidthById = /* @__PURE__ */ new Map();
909
945
  let xCenter = 0;
@@ -1957,6 +1993,55 @@ function createChart(element, options = {}) {
1957
1993
  const yFromPrice = (price) => {
1958
1994
  return chartBottom - (price - yMin) / yRange * chartHeight;
1959
1995
  };
1996
+ const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
1997
+ const drawDrawingHandle = (x, y, color) => {
1998
+ ctx.save();
1999
+ ctx.setLineDash([]);
2000
+ ctx.fillStyle = mergedOptions.backgroundColor;
2001
+ ctx.strokeStyle = color;
2002
+ ctx.lineWidth = 2;
2003
+ ctx.beginPath();
2004
+ ctx.arc(x, y, 5, 0, Math.PI * 2);
2005
+ ctx.fill();
2006
+ ctx.stroke();
2007
+ ctx.restore();
2008
+ };
2009
+ const drawDrawing = (drawing, draft = false) => {
2010
+ if (!drawing.visible) {
2011
+ return;
2012
+ }
2013
+ ctx.save();
2014
+ ctx.strokeStyle = drawing.color;
2015
+ ctx.lineWidth = drawing.width;
2016
+ ctx.globalAlpha = draft ? 0.72 : 1;
2017
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2018
+ if (drawing.type === "horizontal-line") {
2019
+ const point = drawing.points[0];
2020
+ if (point) {
2021
+ const y = clamp(yFromPrice(point.price), chartTop + 1, chartBottom - 1);
2022
+ ctx.beginPath();
2023
+ ctx.moveTo(crisp(chartLeft), crisp(y));
2024
+ ctx.lineTo(crisp(chartRight), crisp(y));
2025
+ ctx.stroke();
2026
+ }
2027
+ } else if (drawing.type === "trendline") {
2028
+ const first = drawing.points[0];
2029
+ const second = drawing.points[1];
2030
+ if (first && second) {
2031
+ const firstX = xFromDrawingPoint(first);
2032
+ const firstY = yFromPrice(first.price);
2033
+ const secondX = xFromDrawingPoint(second);
2034
+ const secondY = yFromPrice(second.price);
2035
+ ctx.beginPath();
2036
+ ctx.moveTo(firstX, firstY);
2037
+ ctx.lineTo(secondX, secondY);
2038
+ ctx.stroke();
2039
+ drawDrawingHandle(firstX, firstY, drawing.color);
2040
+ drawDrawingHandle(secondX, secondY, drawing.color);
2041
+ }
2042
+ }
2043
+ ctx.restore();
2044
+ };
1960
2045
  if (watermark.visible && watermark.imageSrc.trim().length > 0) {
1961
2046
  ensureWatermarkImage(watermark.imageSrc.trim());
1962
2047
  if (watermarkImageReady && watermarkImage) {
@@ -2128,6 +2213,10 @@ function createChart(element, options = {}) {
2128
2213
  );
2129
2214
  });
2130
2215
  }
2216
+ drawings.forEach((drawing) => drawDrawing(drawing));
2217
+ if (draftDrawing) {
2218
+ drawDrawing(draftDrawing, true);
2219
+ }
2131
2220
  const crosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
2132
2221
  if (crosshair.visible && crosshairPoint) {
2133
2222
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
@@ -2964,6 +3053,66 @@ function createChart(element, options = {}) {
2964
3053
  const ratio = clamp((drawState.chartBottom - y) / drawState.chartHeight, 0, 1);
2965
3054
  return drawState.yMin + ratio * (drawState.yMax - drawState.yMin);
2966
3055
  };
3056
+ const drawingPointFromCanvas = (x, y) => {
3057
+ if (!drawState) {
3058
+ return null;
3059
+ }
3060
+ const ratio = clamp((x - drawState.chartLeft) / drawState.chartWidth, 0, 1);
3061
+ const index = drawState.xStart + ratio * drawState.xSpan - 0.5;
3062
+ const nearestIndex = Math.round(index);
3063
+ const time = getTimeForIndex(nearestIndex);
3064
+ return {
3065
+ index,
3066
+ price: roundToPricePrecision(priceFromCanvasY(y)),
3067
+ ...time ? { time: time.toISOString() } : {}
3068
+ };
3069
+ };
3070
+ const canvasXFromDrawingPoint = (point) => {
3071
+ if (!drawState) return 0;
3072
+ return drawState.chartLeft + (point.index + 0.5 - drawState.xStart) / drawState.xSpan * drawState.chartWidth;
3073
+ };
3074
+ const canvasYFromDrawingPrice = (price) => {
3075
+ if (!drawState) return 0;
3076
+ const range = drawState.yMax - drawState.yMin || 1;
3077
+ return drawState.chartBottom - (price - drawState.yMin) / range * drawState.chartHeight;
3078
+ };
3079
+ const distanceToSegment = (x, y, x1, y1, x2, y2) => {
3080
+ const dx = x2 - x1;
3081
+ const dy = y2 - y1;
3082
+ const lengthSq = dx * dx + dy * dy;
3083
+ if (lengthSq === 0) return Math.hypot(x - x1, y - y1);
3084
+ const t = clamp(((x - x1) * dx + (y - y1) * dy) / lengthSq, 0, 1);
3085
+ return Math.hypot(x - (x1 + t * dx), y - (y1 + t * dy));
3086
+ };
3087
+ const getDrawingHit = (x, y) => {
3088
+ for (let index = drawings.length - 1; index >= 0; index -= 1) {
3089
+ const drawing = drawings[index];
3090
+ if (!drawing?.visible) continue;
3091
+ if (drawing.type === "horizontal-line") {
3092
+ const point = drawing.points[0];
3093
+ if (!point) continue;
3094
+ const lineY = canvasYFromDrawingPrice(point.price);
3095
+ if (Math.abs(y - lineY) <= 7) {
3096
+ return { drawing, target: "line" };
3097
+ }
3098
+ } else if (drawing.type === "trendline") {
3099
+ const first = drawing.points[0];
3100
+ const second = drawing.points[1];
3101
+ if (!first || !second) continue;
3102
+ const x1 = canvasXFromDrawingPoint(first);
3103
+ const y1 = canvasYFromDrawingPrice(first.price);
3104
+ const x2 = canvasXFromDrawingPoint(second);
3105
+ const y2 = canvasYFromDrawingPrice(second.price);
3106
+ if (Math.hypot(x - x1, y - y1) <= 8 || Math.hypot(x - x2, y - y2) <= 8) {
3107
+ return { drawing, target: "handle" };
3108
+ }
3109
+ if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
3110
+ return { drawing, target: "line" };
3111
+ }
3112
+ }
3113
+ }
3114
+ return null;
3115
+ };
2967
3116
  const indexFromCanvasX = (x) => {
2968
3117
  if (!drawState) {
2969
3118
  return null;
@@ -3016,6 +3165,53 @@ function createChart(element, options = {}) {
3016
3165
  }
3017
3166
  return "outside";
3018
3167
  };
3168
+ const handleDrawingToolPointerDown = (x, y) => {
3169
+ if (!activeDrawingTool || !drawState) {
3170
+ return false;
3171
+ }
3172
+ const point = drawingPointFromCanvas(x, y);
3173
+ if (!point) {
3174
+ return false;
3175
+ }
3176
+ if (activeDrawingTool === "horizontal-line") {
3177
+ drawings.push(
3178
+ normalizeDrawingState({
3179
+ type: "horizontal-line",
3180
+ points: [point],
3181
+ color: "#38bdf8",
3182
+ style: "dotted",
3183
+ width: 1
3184
+ })
3185
+ );
3186
+ emitDrawingsChange();
3187
+ draw();
3188
+ return true;
3189
+ }
3190
+ if (activeDrawingTool === "trendline") {
3191
+ if (draftDrawing?.type === "trendline") {
3192
+ const completed = normalizeDrawingState({
3193
+ ...serializeDrawing(draftDrawing),
3194
+ points: [draftDrawing.points[0], point]
3195
+ });
3196
+ drawings.push(completed);
3197
+ draftDrawing = null;
3198
+ activeDrawingTool = null;
3199
+ emitDrawingsChange();
3200
+ draw();
3201
+ return true;
3202
+ }
3203
+ draftDrawing = normalizeDrawingState({
3204
+ type: "trendline",
3205
+ points: [point, point],
3206
+ color: "#2563eb",
3207
+ style: "solid",
3208
+ width: 2
3209
+ });
3210
+ draw();
3211
+ return true;
3212
+ }
3213
+ return false;
3214
+ };
3019
3215
  let isDragging = false;
3020
3216
  let dragMode = null;
3021
3217
  let lastPointerX = 0;
@@ -3141,6 +3337,25 @@ function createChart(element, options = {}) {
3141
3337
  if (region === "outside") {
3142
3338
  return;
3143
3339
  }
3340
+ if (region === "plot" && !activeDrawingTool) {
3341
+ const drawingHit = getDrawingHit(point.x, point.y);
3342
+ if (drawingHit) {
3343
+ drawingSelectHandler?.({
3344
+ drawing: serializeDrawing(drawingHit.drawing),
3345
+ target: drawingHit.target,
3346
+ x: point.x,
3347
+ y: point.y
3348
+ });
3349
+ setCrosshairPoint(null);
3350
+ canvas.style.cursor = "pointer";
3351
+ return;
3352
+ }
3353
+ }
3354
+ if (region === "plot" && handleDrawingToolPointerDown(point.x, point.y)) {
3355
+ setCrosshairPoint(null);
3356
+ canvas.style.cursor = "crosshair";
3357
+ return;
3358
+ }
3144
3359
  isDragging = true;
3145
3360
  dragMode = region;
3146
3361
  activePointerId = event.pointerId;
@@ -3172,6 +3387,18 @@ function createChart(element, options = {}) {
3172
3387
  pointerDownInfo.moved = true;
3173
3388
  }
3174
3389
  }
3390
+ if (draftDrawing && activeDrawingTool === "trendline") {
3391
+ const nextPoint = drawingPointFromCanvas(point.x, point.y);
3392
+ if (nextPoint) {
3393
+ draftDrawing = {
3394
+ ...draftDrawing,
3395
+ points: [draftDrawing.points[0], nextPoint]
3396
+ };
3397
+ canvas.style.cursor = "crosshair";
3398
+ draw();
3399
+ return;
3400
+ }
3401
+ }
3175
3402
  if (orderDragState) {
3176
3403
  if (activePointerId !== null && event.pointerId !== activePointerId) {
3177
3404
  return;
@@ -3242,9 +3469,14 @@ function createChart(element, options = {}) {
3242
3469
  setCrosshairPoint(null);
3243
3470
  return;
3244
3471
  }
3472
+ if (!activeDrawingTool && getDrawingHit(point.x, point.y)) {
3473
+ canvas.style.cursor = "pointer";
3474
+ setCrosshairPoint(null);
3475
+ return;
3476
+ }
3245
3477
  const hoverRegion = getHitRegion(point.x, point.y);
3246
3478
  if (hoverRegion === "plot") {
3247
- canvas.style.cursor = doubleClickEnabled ? "default" : "crosshair";
3479
+ canvas.style.cursor = activeDrawingTool ? "crosshair" : doubleClickEnabled ? "default" : "crosshair";
3248
3480
  setCrosshairPoint(point);
3249
3481
  emitCrosshairMove(point.x, point.y, "plot");
3250
3482
  } else if (hoverRegion === "x-axis") {
@@ -3643,6 +3875,56 @@ function createChart(element, options = {}) {
3643
3875
  indicators = indicators.filter((indicator) => indicator.id !== id);
3644
3876
  draw();
3645
3877
  };
3878
+ const setActiveDrawingTool = (tool) => {
3879
+ activeDrawingTool = tool;
3880
+ draftDrawing = null;
3881
+ canvas.style.cursor = tool ? "crosshair" : "default";
3882
+ draw();
3883
+ };
3884
+ const getActiveDrawingTool = () => activeDrawingTool;
3885
+ const getDrawings = () => drawings.map((drawing) => serializeDrawing(drawing));
3886
+ const setDrawings = (nextDrawings) => {
3887
+ drawings = nextDrawings.map((drawing) => normalizeDrawingState(drawing));
3888
+ draftDrawing = null;
3889
+ emitDrawingsChange();
3890
+ draw();
3891
+ };
3892
+ const addDrawing = (drawing) => {
3893
+ const next = normalizeDrawingState(drawing);
3894
+ drawings.push(next);
3895
+ emitDrawingsChange();
3896
+ draw();
3897
+ return next.id;
3898
+ };
3899
+ const updateDrawing = (id, patch) => {
3900
+ drawings = drawings.map(
3901
+ (drawing) => drawing.id === id ? normalizeDrawingState({
3902
+ ...serializeDrawing(drawing),
3903
+ ...patch,
3904
+ id,
3905
+ points: patch.points ?? drawing.points
3906
+ }) : drawing
3907
+ );
3908
+ emitDrawingsChange();
3909
+ draw();
3910
+ };
3911
+ const removeDrawing = (id) => {
3912
+ drawings = drawings.filter((drawing) => drawing.id !== id);
3913
+ emitDrawingsChange();
3914
+ draw();
3915
+ };
3916
+ const clearDrawings = () => {
3917
+ drawings = [];
3918
+ draftDrawing = null;
3919
+ emitDrawingsChange();
3920
+ draw();
3921
+ };
3922
+ const onDrawingsChange = (handler) => {
3923
+ drawingsChangeHandler = handler;
3924
+ };
3925
+ const onDrawingSelect = (handler) => {
3926
+ drawingSelectHandler = handler;
3927
+ };
3646
3928
  const destroy = () => {
3647
3929
  if (smoothingRafId !== null) {
3648
3930
  cancelAnimationFrame(smoothingRafId);
@@ -3686,6 +3968,16 @@ function createChart(element, options = {}) {
3686
3968
  getViewport,
3687
3969
  setViewport,
3688
3970
  onViewportChange,
3971
+ setActiveDrawingTool,
3972
+ getActiveDrawingTool,
3973
+ setDrawings,
3974
+ getDrawings,
3975
+ addDrawing,
3976
+ updateDrawing,
3977
+ removeDrawing,
3978
+ clearDrawings,
3979
+ onDrawingsChange,
3980
+ onDrawingSelect,
3689
3981
  setDoubleClickEnabled,
3690
3982
  setDoubleClickAction,
3691
3983
  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.55",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",