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.
@@ -214,7 +214,8 @@ var DEFAULT_OPTIONS = {
214
214
  },
215
215
  labels: DEFAULT_LABELS_OPTIONS,
216
216
  dashPatterns: DEFAULT_DASH_PATTERNS,
217
- indicators: []
217
+ indicators: [],
218
+ drawings: []
218
219
  };
219
220
  var mergeChartOptions = (baseOptions, options = {}) => ({
220
221
  ...baseOptions,
@@ -905,6 +906,7 @@ function createChart(element, options = {}) {
905
906
  let generatedPriceLineId = 1;
906
907
  let generatedOrderLineId = 1;
907
908
  let generatedIndicatorId = 1;
909
+ let generatedDrawingId = 1;
908
910
  const indicatorRegistry = /* @__PURE__ */ new Map();
909
911
  for (const indicator of BUILTIN_INDICATORS) {
910
912
  indicatorRegistry.set(indicator.id, indicator);
@@ -927,7 +929,41 @@ function createChart(element, options = {}) {
927
929
  }
928
930
  };
929
931
  };
932
+ const normalizeDrawingState = (drawing) => ({
933
+ id: drawing.id ?? `drawing-${generatedDrawingId++}`,
934
+ type: drawing.type,
935
+ points: drawing.points.map((point) => ({
936
+ index: Number(point.index) || 0,
937
+ price: Number(point.price) || 0,
938
+ ...point.time ? { time: point.time } : {}
939
+ })),
940
+ visible: drawing.visible ?? true,
941
+ color: drawing.color ?? "#94a3b8",
942
+ style: drawing.style ?? "dotted",
943
+ width: Math.max(1, Number(drawing.width) || 1),
944
+ locked: drawing.locked ?? false,
945
+ ...drawing.label === void 0 ? {} : { label: drawing.label }
946
+ });
947
+ const serializeDrawing = (drawing) => ({
948
+ id: drawing.id,
949
+ type: drawing.type,
950
+ points: drawing.points.map((point) => ({ ...point })),
951
+ visible: drawing.visible,
952
+ color: drawing.color,
953
+ style: drawing.style,
954
+ width: drawing.width,
955
+ locked: drawing.locked,
956
+ ...drawing.label === void 0 ? {} : { label: drawing.label }
957
+ });
930
958
  let indicators = (options.indicators ?? []).map((indicator) => normalizeIndicatorState(indicator));
959
+ let drawings = (options.drawings ?? []).map((drawing) => normalizeDrawingState(drawing));
960
+ let activeDrawingTool = null;
961
+ let draftDrawing = null;
962
+ let drawingsChangeHandler = null;
963
+ let drawingSelectHandler = null;
964
+ const emitDrawingsChange = () => {
965
+ drawingsChangeHandler?.(drawings.map((drawing) => serializeDrawing(drawing)));
966
+ };
931
967
  const orderWidgetWidthById = /* @__PURE__ */ new Map();
932
968
  const orderPriceTagWidthById = /* @__PURE__ */ new Map();
933
969
  let xCenter = 0;
@@ -1981,6 +2017,55 @@ function createChart(element, options = {}) {
1981
2017
  const yFromPrice = (price) => {
1982
2018
  return chartBottom - (price - yMin) / yRange * chartHeight;
1983
2019
  };
2020
+ const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
2021
+ const drawDrawingHandle = (x, y, color) => {
2022
+ ctx.save();
2023
+ ctx.setLineDash([]);
2024
+ ctx.fillStyle = mergedOptions.backgroundColor;
2025
+ ctx.strokeStyle = color;
2026
+ ctx.lineWidth = 2;
2027
+ ctx.beginPath();
2028
+ ctx.arc(x, y, 5, 0, Math.PI * 2);
2029
+ ctx.fill();
2030
+ ctx.stroke();
2031
+ ctx.restore();
2032
+ };
2033
+ const drawDrawing = (drawing, draft = false) => {
2034
+ if (!drawing.visible) {
2035
+ return;
2036
+ }
2037
+ ctx.save();
2038
+ ctx.strokeStyle = drawing.color;
2039
+ ctx.lineWidth = drawing.width;
2040
+ ctx.globalAlpha = draft ? 0.72 : 1;
2041
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2042
+ if (drawing.type === "horizontal-line") {
2043
+ const point = drawing.points[0];
2044
+ if (point) {
2045
+ const y = clamp(yFromPrice(point.price), chartTop + 1, chartBottom - 1);
2046
+ ctx.beginPath();
2047
+ ctx.moveTo(crisp(chartLeft), crisp(y));
2048
+ ctx.lineTo(crisp(chartRight), crisp(y));
2049
+ ctx.stroke();
2050
+ }
2051
+ } else if (drawing.type === "trendline") {
2052
+ const first = drawing.points[0];
2053
+ const second = drawing.points[1];
2054
+ if (first && second) {
2055
+ const firstX = xFromDrawingPoint(first);
2056
+ const firstY = yFromPrice(first.price);
2057
+ const secondX = xFromDrawingPoint(second);
2058
+ const secondY = yFromPrice(second.price);
2059
+ ctx.beginPath();
2060
+ ctx.moveTo(firstX, firstY);
2061
+ ctx.lineTo(secondX, secondY);
2062
+ ctx.stroke();
2063
+ drawDrawingHandle(firstX, firstY, drawing.color);
2064
+ drawDrawingHandle(secondX, secondY, drawing.color);
2065
+ }
2066
+ }
2067
+ ctx.restore();
2068
+ };
1984
2069
  if (watermark.visible && watermark.imageSrc.trim().length > 0) {
1985
2070
  ensureWatermarkImage(watermark.imageSrc.trim());
1986
2071
  if (watermarkImageReady && watermarkImage) {
@@ -2152,6 +2237,10 @@ function createChart(element, options = {}) {
2152
2237
  );
2153
2238
  });
2154
2239
  }
2240
+ drawings.forEach((drawing) => drawDrawing(drawing));
2241
+ if (draftDrawing) {
2242
+ drawDrawing(draftDrawing, true);
2243
+ }
2155
2244
  const crosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
2156
2245
  if (crosshair.visible && crosshairPoint) {
2157
2246
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
@@ -2541,8 +2630,14 @@ function createChart(element, options = {}) {
2541
2630
  drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
2542
2631
  }
2543
2632
  if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
2544
- const labelEntries = [...activeOverlayIndicators, ...activeSeparateIndicators].map(({ indicator, plugin }) => {
2545
- const inputValues = Object.entries(indicator.inputs).filter(([, value]) => typeof value === "number" || typeof value === "string" || typeof value === "boolean").slice(0, 2).map(([, value]) => String(value));
2633
+ const isLegendInputValue = (value) => {
2634
+ if (typeof value === "number" || typeof value === "boolean") {
2635
+ return true;
2636
+ }
2637
+ return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
2638
+ };
2639
+ const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
2640
+ const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
2546
2641
  if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
2547
2642
  return `${plugin.name} ${inputValues.join(" ")}`;
2548
2643
  }
@@ -2982,6 +3077,66 @@ function createChart(element, options = {}) {
2982
3077
  const ratio = clamp((drawState.chartBottom - y) / drawState.chartHeight, 0, 1);
2983
3078
  return drawState.yMin + ratio * (drawState.yMax - drawState.yMin);
2984
3079
  };
3080
+ const drawingPointFromCanvas = (x, y) => {
3081
+ if (!drawState) {
3082
+ return null;
3083
+ }
3084
+ const ratio = clamp((x - drawState.chartLeft) / drawState.chartWidth, 0, 1);
3085
+ const index = drawState.xStart + ratio * drawState.xSpan - 0.5;
3086
+ const nearestIndex = Math.round(index);
3087
+ const time = getTimeForIndex(nearestIndex);
3088
+ return {
3089
+ index,
3090
+ price: roundToPricePrecision(priceFromCanvasY(y)),
3091
+ ...time ? { time: time.toISOString() } : {}
3092
+ };
3093
+ };
3094
+ const canvasXFromDrawingPoint = (point) => {
3095
+ if (!drawState) return 0;
3096
+ return drawState.chartLeft + (point.index + 0.5 - drawState.xStart) / drawState.xSpan * drawState.chartWidth;
3097
+ };
3098
+ const canvasYFromDrawingPrice = (price) => {
3099
+ if (!drawState) return 0;
3100
+ const range = drawState.yMax - drawState.yMin || 1;
3101
+ return drawState.chartBottom - (price - drawState.yMin) / range * drawState.chartHeight;
3102
+ };
3103
+ const distanceToSegment = (x, y, x1, y1, x2, y2) => {
3104
+ const dx = x2 - x1;
3105
+ const dy = y2 - y1;
3106
+ const lengthSq = dx * dx + dy * dy;
3107
+ if (lengthSq === 0) return Math.hypot(x - x1, y - y1);
3108
+ const t = clamp(((x - x1) * dx + (y - y1) * dy) / lengthSq, 0, 1);
3109
+ return Math.hypot(x - (x1 + t * dx), y - (y1 + t * dy));
3110
+ };
3111
+ const getDrawingHit = (x, y) => {
3112
+ for (let index = drawings.length - 1; index >= 0; index -= 1) {
3113
+ const drawing = drawings[index];
3114
+ if (!drawing?.visible) continue;
3115
+ if (drawing.type === "horizontal-line") {
3116
+ const point = drawing.points[0];
3117
+ if (!point) continue;
3118
+ const lineY = canvasYFromDrawingPrice(point.price);
3119
+ if (Math.abs(y - lineY) <= 7) {
3120
+ return { drawing, target: "line" };
3121
+ }
3122
+ } else if (drawing.type === "trendline") {
3123
+ const first = drawing.points[0];
3124
+ const second = drawing.points[1];
3125
+ if (!first || !second) continue;
3126
+ const x1 = canvasXFromDrawingPoint(first);
3127
+ const y1 = canvasYFromDrawingPrice(first.price);
3128
+ const x2 = canvasXFromDrawingPoint(second);
3129
+ const y2 = canvasYFromDrawingPrice(second.price);
3130
+ if (Math.hypot(x - x1, y - y1) <= 8 || Math.hypot(x - x2, y - y2) <= 8) {
3131
+ return { drawing, target: "handle" };
3132
+ }
3133
+ if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
3134
+ return { drawing, target: "line" };
3135
+ }
3136
+ }
3137
+ }
3138
+ return null;
3139
+ };
2985
3140
  const indexFromCanvasX = (x) => {
2986
3141
  if (!drawState) {
2987
3142
  return null;
@@ -3034,6 +3189,53 @@ function createChart(element, options = {}) {
3034
3189
  }
3035
3190
  return "outside";
3036
3191
  };
3192
+ const handleDrawingToolPointerDown = (x, y) => {
3193
+ if (!activeDrawingTool || !drawState) {
3194
+ return false;
3195
+ }
3196
+ const point = drawingPointFromCanvas(x, y);
3197
+ if (!point) {
3198
+ return false;
3199
+ }
3200
+ if (activeDrawingTool === "horizontal-line") {
3201
+ drawings.push(
3202
+ normalizeDrawingState({
3203
+ type: "horizontal-line",
3204
+ points: [point],
3205
+ color: "#38bdf8",
3206
+ style: "dotted",
3207
+ width: 1
3208
+ })
3209
+ );
3210
+ emitDrawingsChange();
3211
+ draw();
3212
+ return true;
3213
+ }
3214
+ if (activeDrawingTool === "trendline") {
3215
+ if (draftDrawing?.type === "trendline") {
3216
+ const completed = normalizeDrawingState({
3217
+ ...serializeDrawing(draftDrawing),
3218
+ points: [draftDrawing.points[0], point]
3219
+ });
3220
+ drawings.push(completed);
3221
+ draftDrawing = null;
3222
+ activeDrawingTool = null;
3223
+ emitDrawingsChange();
3224
+ draw();
3225
+ return true;
3226
+ }
3227
+ draftDrawing = normalizeDrawingState({
3228
+ type: "trendline",
3229
+ points: [point, point],
3230
+ color: "#2563eb",
3231
+ style: "solid",
3232
+ width: 2
3233
+ });
3234
+ draw();
3235
+ return true;
3236
+ }
3237
+ return false;
3238
+ };
3037
3239
  let isDragging = false;
3038
3240
  let dragMode = null;
3039
3241
  let lastPointerX = 0;
@@ -3159,6 +3361,25 @@ function createChart(element, options = {}) {
3159
3361
  if (region === "outside") {
3160
3362
  return;
3161
3363
  }
3364
+ if (region === "plot" && !activeDrawingTool) {
3365
+ const drawingHit = getDrawingHit(point.x, point.y);
3366
+ if (drawingHit) {
3367
+ drawingSelectHandler?.({
3368
+ drawing: serializeDrawing(drawingHit.drawing),
3369
+ target: drawingHit.target,
3370
+ x: point.x,
3371
+ y: point.y
3372
+ });
3373
+ setCrosshairPoint(null);
3374
+ canvas.style.cursor = "pointer";
3375
+ return;
3376
+ }
3377
+ }
3378
+ if (region === "plot" && handleDrawingToolPointerDown(point.x, point.y)) {
3379
+ setCrosshairPoint(null);
3380
+ canvas.style.cursor = "crosshair";
3381
+ return;
3382
+ }
3162
3383
  isDragging = true;
3163
3384
  dragMode = region;
3164
3385
  activePointerId = event.pointerId;
@@ -3190,6 +3411,18 @@ function createChart(element, options = {}) {
3190
3411
  pointerDownInfo.moved = true;
3191
3412
  }
3192
3413
  }
3414
+ if (draftDrawing && activeDrawingTool === "trendline") {
3415
+ const nextPoint = drawingPointFromCanvas(point.x, point.y);
3416
+ if (nextPoint) {
3417
+ draftDrawing = {
3418
+ ...draftDrawing,
3419
+ points: [draftDrawing.points[0], nextPoint]
3420
+ };
3421
+ canvas.style.cursor = "crosshair";
3422
+ draw();
3423
+ return;
3424
+ }
3425
+ }
3193
3426
  if (orderDragState) {
3194
3427
  if (activePointerId !== null && event.pointerId !== activePointerId) {
3195
3428
  return;
@@ -3260,9 +3493,14 @@ function createChart(element, options = {}) {
3260
3493
  setCrosshairPoint(null);
3261
3494
  return;
3262
3495
  }
3496
+ if (!activeDrawingTool && getDrawingHit(point.x, point.y)) {
3497
+ canvas.style.cursor = "pointer";
3498
+ setCrosshairPoint(null);
3499
+ return;
3500
+ }
3263
3501
  const hoverRegion = getHitRegion(point.x, point.y);
3264
3502
  if (hoverRegion === "plot") {
3265
- canvas.style.cursor = doubleClickEnabled ? "default" : "crosshair";
3503
+ canvas.style.cursor = activeDrawingTool ? "crosshair" : doubleClickEnabled ? "default" : "crosshair";
3266
3504
  setCrosshairPoint(point);
3267
3505
  emitCrosshairMove(point.x, point.y, "plot");
3268
3506
  } else if (hoverRegion === "x-axis") {
@@ -3661,6 +3899,56 @@ function createChart(element, options = {}) {
3661
3899
  indicators = indicators.filter((indicator) => indicator.id !== id);
3662
3900
  draw();
3663
3901
  };
3902
+ const setActiveDrawingTool = (tool) => {
3903
+ activeDrawingTool = tool;
3904
+ draftDrawing = null;
3905
+ canvas.style.cursor = tool ? "crosshair" : "default";
3906
+ draw();
3907
+ };
3908
+ const getActiveDrawingTool = () => activeDrawingTool;
3909
+ const getDrawings = () => drawings.map((drawing) => serializeDrawing(drawing));
3910
+ const setDrawings = (nextDrawings) => {
3911
+ drawings = nextDrawings.map((drawing) => normalizeDrawingState(drawing));
3912
+ draftDrawing = null;
3913
+ emitDrawingsChange();
3914
+ draw();
3915
+ };
3916
+ const addDrawing = (drawing) => {
3917
+ const next = normalizeDrawingState(drawing);
3918
+ drawings.push(next);
3919
+ emitDrawingsChange();
3920
+ draw();
3921
+ return next.id;
3922
+ };
3923
+ const updateDrawing = (id, patch) => {
3924
+ drawings = drawings.map(
3925
+ (drawing) => drawing.id === id ? normalizeDrawingState({
3926
+ ...serializeDrawing(drawing),
3927
+ ...patch,
3928
+ id,
3929
+ points: patch.points ?? drawing.points
3930
+ }) : drawing
3931
+ );
3932
+ emitDrawingsChange();
3933
+ draw();
3934
+ };
3935
+ const removeDrawing = (id) => {
3936
+ drawings = drawings.filter((drawing) => drawing.id !== id);
3937
+ emitDrawingsChange();
3938
+ draw();
3939
+ };
3940
+ const clearDrawings = () => {
3941
+ drawings = [];
3942
+ draftDrawing = null;
3943
+ emitDrawingsChange();
3944
+ draw();
3945
+ };
3946
+ const onDrawingsChange = (handler) => {
3947
+ drawingsChangeHandler = handler;
3948
+ };
3949
+ const onDrawingSelect = (handler) => {
3950
+ drawingSelectHandler = handler;
3951
+ };
3664
3952
  const destroy = () => {
3665
3953
  if (smoothingRafId !== null) {
3666
3954
  cancelAnimationFrame(smoothingRafId);
@@ -3704,6 +3992,16 @@ function createChart(element, options = {}) {
3704
3992
  getViewport,
3705
3993
  setViewport,
3706
3994
  onViewportChange,
3995
+ setActiveDrawingTool,
3996
+ getActiveDrawingTool,
3997
+ setDrawings,
3998
+ getDrawings,
3999
+ addDrawing,
4000
+ updateDrawing,
4001
+ removeDrawing,
4002
+ clearDrawings,
4003
+ onDrawingsChange,
4004
+ onDrawingSelect,
3707
4005
  setDoubleClickEnabled,
3708
4006
  setDoubleClickAction,
3709
4007
  registerIndicator,
@@ -40,8 +40,32 @@ interface ChartOptions {
40
40
  labels?: LabelsOptions;
41
41
  dashPatterns?: Partial<DashPatternOptions>;
42
42
  indicators?: IndicatorInstanceOptions[];
43
+ drawings?: DrawingObjectOptions[];
43
44
  }
44
45
  type IndicatorPane = "overlay" | "separate";
46
+ type DrawingToolType = "horizontal-line" | "trendline";
47
+ interface DrawingPoint {
48
+ index: number;
49
+ price: number;
50
+ time?: string;
51
+ }
52
+ interface DrawingObjectOptions {
53
+ id?: string;
54
+ type: DrawingToolType;
55
+ points: DrawingPoint[];
56
+ visible?: boolean;
57
+ color?: string;
58
+ style?: "solid" | "dotted" | "dashed";
59
+ width?: number;
60
+ label?: string;
61
+ locked?: boolean;
62
+ }
63
+ interface DrawingSelectEvent {
64
+ drawing: DrawingObjectOptions;
65
+ target: "line" | "handle";
66
+ x: number;
67
+ y: number;
68
+ }
45
69
  interface IndicatorInstanceOptions<TInputs extends Record<string, unknown> = Record<string, unknown>> {
46
70
  id?: string;
47
71
  type: string;
@@ -363,6 +387,16 @@ interface ChartInstance {
363
387
  getViewport: () => ViewportState;
364
388
  setViewport: (viewport: Partial<ViewportState>) => void;
365
389
  onViewportChange: (handler: ((viewport: ViewportState) => void) | null) => void;
390
+ setActiveDrawingTool: (tool: DrawingToolType | null) => void;
391
+ getActiveDrawingTool: () => DrawingToolType | null;
392
+ setDrawings: (nextDrawings: DrawingObjectOptions[]) => void;
393
+ getDrawings: () => DrawingObjectOptions[];
394
+ addDrawing: (drawing: DrawingObjectOptions) => string;
395
+ updateDrawing: (id: string, patch: Partial<DrawingObjectOptions>) => void;
396
+ removeDrawing: (id: string) => void;
397
+ clearDrawings: () => void;
398
+ onDrawingsChange: (handler: ((drawings: DrawingObjectOptions[]) => void) | null) => void;
399
+ onDrawingSelect: (handler: ((event: DrawingSelectEvent) => void) | null) => void;
366
400
  setDoubleClickEnabled: (enabled: boolean) => void;
367
401
  setDoubleClickAction: (action: "reset" | "placeLimitOrder") => void;
368
402
  registerIndicator: (plugin: IndicatorPlugin<any>) => void;
@@ -398,4 +432,4 @@ interface ViewportState {
398
432
  }
399
433
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
400
434
 
401
- export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type ViewportState, type WatermarkOptions, createChart };
435
+ export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type DrawingObjectOptions, type DrawingPoint, type DrawingSelectEvent, type DrawingToolType, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type ViewportState, type WatermarkOptions, createChart };