hyperprop-charting-library 0.1.53 → 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.
@@ -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);
@@ -2517,8 +2606,14 @@ function createChart(element, options = {}) {
2517
2606
  drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
2518
2607
  }
2519
2608
  if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
2520
- const labelEntries = [...activeOverlayIndicators, ...activeSeparateIndicators].map(({ indicator, plugin }) => {
2521
- const inputValues = Object.entries(indicator.inputs).filter(([, value]) => typeof value === "number" || typeof value === "string" || typeof value === "boolean").slice(0, 2).map(([, value]) => String(value));
2609
+ const isLegendInputValue = (value) => {
2610
+ if (typeof value === "number" || typeof value === "boolean") {
2611
+ return true;
2612
+ }
2613
+ return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
2614
+ };
2615
+ const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
2616
+ const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
2522
2617
  if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
2523
2618
  return `${plugin.name} ${inputValues.join(" ")}`;
2524
2619
  }
@@ -2958,6 +3053,66 @@ function createChart(element, options = {}) {
2958
3053
  const ratio = clamp((drawState.chartBottom - y) / drawState.chartHeight, 0, 1);
2959
3054
  return drawState.yMin + ratio * (drawState.yMax - drawState.yMin);
2960
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
+ };
2961
3116
  const indexFromCanvasX = (x) => {
2962
3117
  if (!drawState) {
2963
3118
  return null;
@@ -3010,6 +3165,53 @@ function createChart(element, options = {}) {
3010
3165
  }
3011
3166
  return "outside";
3012
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
+ };
3013
3215
  let isDragging = false;
3014
3216
  let dragMode = null;
3015
3217
  let lastPointerX = 0;
@@ -3135,6 +3337,25 @@ function createChart(element, options = {}) {
3135
3337
  if (region === "outside") {
3136
3338
  return;
3137
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
+ }
3138
3359
  isDragging = true;
3139
3360
  dragMode = region;
3140
3361
  activePointerId = event.pointerId;
@@ -3166,6 +3387,18 @@ function createChart(element, options = {}) {
3166
3387
  pointerDownInfo.moved = true;
3167
3388
  }
3168
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
+ }
3169
3402
  if (orderDragState) {
3170
3403
  if (activePointerId !== null && event.pointerId !== activePointerId) {
3171
3404
  return;
@@ -3236,9 +3469,14 @@ function createChart(element, options = {}) {
3236
3469
  setCrosshairPoint(null);
3237
3470
  return;
3238
3471
  }
3472
+ if (!activeDrawingTool && getDrawingHit(point.x, point.y)) {
3473
+ canvas.style.cursor = "pointer";
3474
+ setCrosshairPoint(null);
3475
+ return;
3476
+ }
3239
3477
  const hoverRegion = getHitRegion(point.x, point.y);
3240
3478
  if (hoverRegion === "plot") {
3241
- canvas.style.cursor = doubleClickEnabled ? "default" : "crosshair";
3479
+ canvas.style.cursor = activeDrawingTool ? "crosshair" : doubleClickEnabled ? "default" : "crosshair";
3242
3480
  setCrosshairPoint(point);
3243
3481
  emitCrosshairMove(point.x, point.y, "plot");
3244
3482
  } else if (hoverRegion === "x-axis") {
@@ -3637,6 +3875,56 @@ function createChart(element, options = {}) {
3637
3875
  indicators = indicators.filter((indicator) => indicator.id !== id);
3638
3876
  draw();
3639
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
+ };
3640
3928
  const destroy = () => {
3641
3929
  if (smoothingRafId !== null) {
3642
3930
  cancelAnimationFrame(smoothingRafId);
@@ -3680,6 +3968,16 @@ function createChart(element, options = {}) {
3680
3968
  getViewport,
3681
3969
  setViewport,
3682
3970
  onViewportChange,
3971
+ setActiveDrawingTool,
3972
+ getActiveDrawingTool,
3973
+ setDrawings,
3974
+ getDrawings,
3975
+ addDrawing,
3976
+ updateDrawing,
3977
+ removeDrawing,
3978
+ clearDrawings,
3979
+ onDrawingsChange,
3980
+ onDrawingSelect,
3683
3981
  setDoubleClickEnabled,
3684
3982
  setDoubleClickAction,
3685
3983
  registerIndicator,