hyperprop-charting-library 0.1.137 → 0.1.138

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.d.ts CHANGED
@@ -78,6 +78,8 @@ interface ChartOptions {
78
78
  pinOutOfRangeLines?: boolean;
79
79
  doubleClickEnabled?: boolean;
80
80
  doubleClickAction?: "reset" | "placeLimitOrder";
81
+ keyboard?: KeyboardOptions;
82
+ accessibility?: AccessibilityOptions;
81
83
  crosshair?: CrosshairOptions;
82
84
  grid?: GridOptions;
83
85
  watermark?: WatermarkOptions;
@@ -164,6 +166,36 @@ interface DrawingSelectEvent {
164
166
  pointIndex?: number;
165
167
  x: number;
166
168
  y: number;
169
+ /** On-screen box of the drawing, for anchoring a host toolbar to it. */
170
+ bounds?: DrawingBounds;
171
+ }
172
+ /** Canvas-pixel bounding box of a drawing, relative to the chart canvas. */
173
+ interface DrawingBounds {
174
+ left: number;
175
+ top: number;
176
+ right: number;
177
+ bottom: number;
178
+ centerX: number;
179
+ centerY: number;
180
+ }
181
+ /**
182
+ * Emitted whenever the selected drawing's on-screen box moves — selection,
183
+ * deselection (payload `null`), drag, edit, pan, zoom or resize. Hosts render
184
+ * their own floating toolbar and position it from `bounds`, keeping it inside
185
+ * `plot`; the chart never has to draw toolbar chrome itself.
186
+ */
187
+ interface DrawingSelectionChangeEvent {
188
+ drawing: DrawingObjectOptions;
189
+ bounds: DrawingBounds;
190
+ /** Plot rectangle, so a toolbar can be clamped inside the chart area. */
191
+ plot: {
192
+ left: number;
193
+ top: number;
194
+ right: number;
195
+ bottom: number;
196
+ };
197
+ /** True while the drawing is actively being dragged or resized. */
198
+ dragging: boolean;
167
199
  }
168
200
  interface DrawingHoverEvent {
169
201
  drawing: DrawingObjectOptions | null;
@@ -470,6 +502,67 @@ interface CrosshairPriceActionEvent {
470
502
  y: number;
471
503
  price: number;
472
504
  }
505
+ /**
506
+ * What the pointer was over when the context menu was requested. Hosts switch
507
+ * on this to show the right menu instead of re-deriving it from coordinates.
508
+ */
509
+ type ChartContextMenuRegion = "plot" | "drawing" | "indicator-pane" | "x-axis" | "y-axis";
510
+ interface ChartContextMenuEvent {
511
+ region: ChartContextMenuRegion;
512
+ /** Canvas-relative position. */
513
+ x: number;
514
+ y: number;
515
+ /** Viewport position, ready to place a DOM menu without re-measuring. */
516
+ clientX: number;
517
+ clientY: number;
518
+ /** Price under the pointer (main pane regions only). */
519
+ price?: number;
520
+ index?: number;
521
+ time?: string;
522
+ point?: OhlcDataPoint;
523
+ /** Set for region "drawing"; the drawing is also selected by the chart. */
524
+ drawing?: DrawingObjectOptions;
525
+ drawingTarget?: "line" | "handle";
526
+ pointIndex?: number;
527
+ /** Set for region "indicator-pane". */
528
+ indicator?: {
529
+ id: string;
530
+ type: string;
531
+ };
532
+ }
533
+ /** Built-in keyboard actions, reported after the chart applies them. */
534
+ type ChartKeyboardAction = "delete-drawing" | "cancel" | "pan-left" | "pan-right" | "pan-up" | "pan-down" | "zoom-in" | "zoom-out" | "reset-viewport" | "scroll-to-realtime";
535
+ interface ChartKeyboardShortcutEvent {
536
+ action: ChartKeyboardAction;
537
+ /** The originating `KeyboardEvent.key`. */
538
+ key: string;
539
+ shiftKey: boolean;
540
+ /** The drawing that was removed, for "delete-drawing" (so hosts can undo). */
541
+ drawing?: DrawingObjectOptions;
542
+ }
543
+ interface KeyboardOptions {
544
+ /**
545
+ * Master switch for the built-in shortcuts. Shortcuts only fire while the
546
+ * chart canvas has focus, so they never swallow typing in host inputs.
547
+ * Default true.
548
+ */
549
+ enabled?: boolean;
550
+ /** Bars panned per arrow-key press (Shift multiplies by 10). Default 1. */
551
+ panBars?: number;
552
+ /** Delete/Backspace removes the selected drawing. Default true. */
553
+ deleteSelectedDrawing?: boolean;
554
+ }
555
+ interface AccessibilityOptions {
556
+ /** aria-label on the canvas. Default "Price chart". */
557
+ label?: string;
558
+ /**
559
+ * Longer description exposed via aria-description, e.g. the symbol and
560
+ * interval currently shown.
561
+ */
562
+ description?: string;
563
+ /** Make the canvas focusable so keyboard users can reach it. Default true. */
564
+ focusable?: boolean;
565
+ }
473
566
  interface TickerLineOptions {
474
567
  visible?: boolean;
475
568
  style?: "solid" | "dotted" | "dashed";
@@ -581,6 +674,29 @@ interface ChartInstance {
581
674
  onDrawingSelect: (handler: ((event: DrawingSelectEvent) => void) | null) => void;
582
675
  onDrawingDoubleClick: (handler: ((event: DrawingSelectEvent) => void) | null) => void;
583
676
  onDrawingEditText: (handler: ((event: DrawingSelectEvent) => void) | null) => void;
677
+ /**
678
+ * Selection geometry stream for a host-rendered floating toolbar. Fires on
679
+ * select/deselect and on every frame where the selected drawing's box moves
680
+ * (drag, pan, zoom, resize), so the toolbar can track the shape instead of
681
+ * sitting in a fixed corner. Payload is `null` when nothing is selected.
682
+ */
683
+ onSelectionChange: (handler: ((event: DrawingSelectionChangeEvent | null) => void) | null) => void;
684
+ /** Current selection and its on-screen box, for imperative reads. */
685
+ getSelectedDrawing: () => {
686
+ drawing: DrawingObjectOptions;
687
+ bounds: DrawingBounds;
688
+ } | null;
689
+ /**
690
+ * Right-click (or long-press equivalent) with everything the host needs to
691
+ * open the correct menu: which region was hit, the price/bar under the
692
+ * pointer, and the drawing or indicator pane it landed on. The chart always
693
+ * suppresses the browser's native menu.
694
+ */
695
+ onContextMenu: (handler: ((event: ChartContextMenuEvent) => void) | null) => void;
696
+ /** Fires after a built-in keyboard shortcut is applied. */
697
+ onKeyboardShortcut: (handler: ((event: ChartKeyboardShortcutEvent) => void) | null) => void;
698
+ /** Focus the chart canvas so keyboard shortcuts apply to it. */
699
+ focus: () => void;
584
700
  setMagnetMode: (mode: "none" | "weak" | "strong") => void;
585
701
  getMagnetMode: () => "none" | "weak" | "strong";
586
702
  /**
@@ -781,4 +897,4 @@ declare const compileScriptIndicator: (definition: ScriptIndicatorDefinition) =>
781
897
 
782
898
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
783
899
 
784
- export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type ChartSavedState, type ChartType, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type DrawingDefaults, type DrawingHoverEvent, type DrawingObjectOptions, type DrawingPoint, type DrawingSelectEvent, type DrawingToolType, FIB_DEFAULT_PALETTE, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneActionEvent, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneHeightChangeEvent, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, POSITION_DEFAULT_COLORS, type PriceLineOptions, type PriceScaleMode, type PriceScaleOptions, SCRIPT_SOURCE_INPUT_VALUES, type ScriptComputeResult, type ScriptIndicatorDefinition, type ScriptIndicatorInputDef, type ScriptInputHelper, type ScriptInputOptions, type ScriptPlotSpec, type TickerLineOptions, type TradeMarkerOptions, type ViewportState, type WatermarkOptions, compileScriptIndicator, createChart, extractScriptInputs, validateScriptSource };
900
+ export { type AccessibilityOptions, type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartContextMenuEvent, type ChartContextMenuRegion, type ChartInstance, type ChartKeyboardAction, type ChartKeyboardShortcutEvent, type ChartOptions, type ChartSavedState, type ChartType, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type DrawingBounds, type DrawingDefaults, type DrawingHoverEvent, type DrawingObjectOptions, type DrawingPoint, type DrawingSelectEvent, type DrawingSelectionChangeEvent, type DrawingToolType, FIB_DEFAULT_PALETTE, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneActionEvent, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneHeightChangeEvent, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type KeyboardOptions, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, POSITION_DEFAULT_COLORS, type PriceLineOptions, type PriceScaleMode, type PriceScaleOptions, SCRIPT_SOURCE_INPUT_VALUES, type ScriptComputeResult, type ScriptIndicatorDefinition, type ScriptIndicatorInputDef, type ScriptInputHelper, type ScriptInputOptions, type ScriptPlotSpec, type TickerLineOptions, type TradeMarkerOptions, type ViewportState, type WatermarkOptions, compileScriptIndicator, createChart, extractScriptInputs, validateScriptSource };
package/dist/index.js CHANGED
@@ -2819,6 +2819,8 @@ var DEFAULT_OPTIONS = {
2819
2819
  pinOutOfRangeLines: false,
2820
2820
  doubleClickEnabled: true,
2821
2821
  doubleClickAction: "reset",
2822
+ keyboard: { enabled: true, panBars: 1, deleteSelectedDrawing: true },
2823
+ accessibility: { label: "Price chart", description: "", focusable: true },
2822
2824
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
2823
2825
  grid: DEFAULT_GRID_OPTIONS,
2824
2826
  watermark: DEFAULT_WATERMARK_OPTIONS,
@@ -2861,6 +2863,14 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
2861
2863
  ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
2862
2864
  ...options.yAxis ?? {}
2863
2865
  },
2866
+ keyboard: {
2867
+ ...baseOptions.keyboard,
2868
+ ...options.keyboard ?? {}
2869
+ },
2870
+ accessibility: {
2871
+ ...baseOptions.accessibility,
2872
+ ...options.accessibility ?? {}
2873
+ },
2864
2874
  crosshair: {
2865
2875
  ...baseOptions.crosshair,
2866
2876
  ...options.crosshair ?? {}
@@ -3034,6 +3044,10 @@ function createChart(element, options = {}) {
3034
3044
  let drawingSelectHandler = null;
3035
3045
  let drawingDoubleClickHandler = null;
3036
3046
  let drawingEditTextHandler = null;
3047
+ let selectionChangeHandler = null;
3048
+ let contextMenuHandler = null;
3049
+ let keyboardShortcutHandler = null;
3050
+ let lastSelectionSignature = null;
3037
3051
  let magnetMode = "none";
3038
3052
  let priceScaleMode = "linear";
3039
3053
  let priceScaleInverted = false;
@@ -3196,6 +3210,39 @@ function createChart(element, options = {}) {
3196
3210
  canvas.style.setProperty("-webkit-user-select", "none");
3197
3211
  canvas.style.setProperty("-webkit-touch-callout", "none");
3198
3212
  canvas.setAttribute("draggable", "false");
3213
+ const applyAccessibilityAttributes = () => {
3214
+ const a11y = mergedOptions.accessibility ?? {};
3215
+ const focusable = a11y.focusable !== false;
3216
+ if (focusable) {
3217
+ canvas.tabIndex = 0;
3218
+ } else {
3219
+ canvas.removeAttribute("tabindex");
3220
+ }
3221
+ canvas.setAttribute("role", "application");
3222
+ canvas.setAttribute("aria-label", a11y.label ?? "Price chart");
3223
+ if (a11y.description) {
3224
+ canvas.setAttribute("aria-description", a11y.description);
3225
+ } else {
3226
+ canvas.removeAttribute("aria-description");
3227
+ }
3228
+ if (focusable && (mergedOptions.keyboard?.enabled ?? true)) {
3229
+ canvas.setAttribute(
3230
+ "aria-keyshortcuts",
3231
+ "ArrowLeft ArrowRight ArrowUp ArrowDown Plus Minus Home End Delete Escape"
3232
+ );
3233
+ } else {
3234
+ canvas.removeAttribute("aria-keyshortcuts");
3235
+ }
3236
+ };
3237
+ applyAccessibilityAttributes();
3238
+ let focusFromPointer = false;
3239
+ canvas.addEventListener("focus", () => {
3240
+ canvas.style.outline = focusFromPointer ? "none" : "";
3241
+ focusFromPointer = false;
3242
+ });
3243
+ canvas.addEventListener("blur", () => {
3244
+ canvas.style.outline = "none";
3245
+ });
3199
3246
  element.innerHTML = "";
3200
3247
  element.appendChild(canvas);
3201
3248
  const baseCanvas = document.createElement("canvas");
@@ -6423,6 +6470,7 @@ function createChart(element, options = {}) {
6423
6470
  xSpan
6424
6471
  };
6425
6472
  paintCrosshairOverlay();
6473
+ emitSelectionChangeIfMoved();
6426
6474
  };
6427
6475
  const paintCrosshairOverlay = () => {
6428
6476
  crosshairPriceActionRegion = null;
@@ -7023,6 +7071,98 @@ function createChart(element, options = {}) {
7023
7071
  const ratio = (priceScaleTransform(price) - scaledMin) / scaledRange;
7024
7072
  return priceScaleInverted ? drawState.chartTop + ratio * drawState.chartHeight : drawState.chartBottom - ratio * drawState.chartHeight;
7025
7073
  };
7074
+ const getDrawingBounds = (drawing) => {
7075
+ if (!drawState || drawing.points.length === 0) {
7076
+ return null;
7077
+ }
7078
+ const { chartLeft, chartRight, chartTop, chartBottom } = drawState;
7079
+ let left;
7080
+ let right;
7081
+ let top;
7082
+ let bottom;
7083
+ if (drawing.type === "horizontal-line") {
7084
+ const y = canvasYFromDrawingPrice(drawing.points[0].price);
7085
+ left = chartLeft;
7086
+ right = chartRight;
7087
+ top = y;
7088
+ bottom = y;
7089
+ } else if (drawing.type === "vertical-line") {
7090
+ const x = canvasXFromDrawingPoint(drawing.points[0]);
7091
+ left = x;
7092
+ right = x;
7093
+ top = chartTop;
7094
+ bottom = chartBottom;
7095
+ } else {
7096
+ const xs = drawing.points.map((point) => canvasXFromDrawingPoint(point));
7097
+ const ys = drawing.points.map((point) => canvasYFromDrawingPrice(point.price));
7098
+ left = Math.min(...xs);
7099
+ right = Math.max(...xs);
7100
+ top = Math.min(...ys);
7101
+ bottom = Math.max(...ys);
7102
+ if (drawing.type === "ray") {
7103
+ right = chartRight;
7104
+ }
7105
+ }
7106
+ const clampX = (value) => clamp(value, chartLeft, chartRight);
7107
+ const clampY = (value) => clamp(value, chartTop, chartBottom);
7108
+ left = clampX(left);
7109
+ right = clampX(right);
7110
+ top = clampY(top);
7111
+ bottom = clampY(bottom);
7112
+ return {
7113
+ left,
7114
+ top,
7115
+ right,
7116
+ bottom,
7117
+ centerX: (left + right) / 2,
7118
+ centerY: (top + bottom) / 2
7119
+ };
7120
+ };
7121
+ const getSelectedDrawingWithBounds = () => {
7122
+ if (!selectedDrawingId) {
7123
+ return null;
7124
+ }
7125
+ const drawing = drawings.find((entry) => entry.id === selectedDrawingId);
7126
+ if (!drawing || !drawing.visible) {
7127
+ return null;
7128
+ }
7129
+ const bounds = getDrawingBounds(drawing);
7130
+ return bounds ? { drawing: serializeDrawing(drawing), bounds } : null;
7131
+ };
7132
+ const emitSelectionChangeIfMoved = () => {
7133
+ if (!selectionChangeHandler) {
7134
+ lastSelectionSignature = null;
7135
+ return;
7136
+ }
7137
+ const selection = getSelectedDrawingWithBounds();
7138
+ if (!selection || !drawState) {
7139
+ if (lastSelectionSignature !== null) {
7140
+ lastSelectionSignature = null;
7141
+ selectionChangeHandler(null);
7142
+ }
7143
+ return;
7144
+ }
7145
+ const dragging = drawingDragState !== null;
7146
+ const { bounds } = selection;
7147
+ const signature = `${selection.drawing.id}:${Math.round(bounds.left)}:${Math.round(bounds.top)}:${Math.round(
7148
+ bounds.right
7149
+ )}:${Math.round(bounds.bottom)}:${dragging ? 1 : 0}`;
7150
+ if (signature === lastSelectionSignature) {
7151
+ return;
7152
+ }
7153
+ lastSelectionSignature = signature;
7154
+ selectionChangeHandler({
7155
+ drawing: selection.drawing,
7156
+ bounds,
7157
+ plot: {
7158
+ left: drawState.chartLeft,
7159
+ top: drawState.chartTop,
7160
+ right: drawState.chartRight,
7161
+ bottom: drawState.chartBottom
7162
+ },
7163
+ dragging
7164
+ });
7165
+ };
7026
7166
  const distanceToSegment = (x, y, x1, y1, x2, y2) => {
7027
7167
  const dx = x2 - x1;
7028
7168
  const dy = y2 - y1;
@@ -7989,6 +8129,10 @@ function createChart(element, options = {}) {
7989
8129
  if (event.pointerType === "touch" || event.pointerType === "pen") {
7990
8130
  event.preventDefault();
7991
8131
  }
8132
+ if (mergedOptions.accessibility?.focusable !== false && document.activeElement !== canvas) {
8133
+ focusFromPointer = true;
8134
+ canvas.focus({ preventScroll: true });
8135
+ }
7992
8136
  cancelKineticPan();
7993
8137
  panVelocitySamples.length = 0;
7994
8138
  magnetModifierActive = event.metaKey || event.ctrlKey;
@@ -8106,12 +8250,14 @@ function createChart(element, options = {}) {
8106
8250
  const drawingHit = getDrawingHit(point.x, point.y);
8107
8251
  if (drawingHit) {
8108
8252
  selectedDrawingId = drawingHit.drawing.id;
8253
+ const selectBounds = getDrawingBounds(drawingHit.drawing);
8109
8254
  drawingSelectHandler?.({
8110
8255
  drawing: serializeDrawing(drawingHit.drawing),
8111
8256
  target: drawingHit.target,
8112
8257
  ...drawingHit.pointIndex === void 0 ? {} : { pointIndex: drawingHit.pointIndex },
8113
8258
  x: point.x,
8114
- y: point.y
8259
+ y: point.y,
8260
+ ...selectBounds ? { bounds: selectBounds } : {}
8115
8261
  });
8116
8262
  if (!drawingHit.drawing.locked) {
8117
8263
  const startCanvasPoint = drawingPointFromCanvas(point.x, point.y, false);
@@ -8672,7 +8818,152 @@ function createChart(element, options = {}) {
8672
8818
  };
8673
8819
  const onContextMenu = (event) => {
8674
8820
  event.preventDefault();
8821
+ if (!contextMenuHandler || !drawState) {
8822
+ return;
8823
+ }
8824
+ const point = getCanvasPoint(event);
8825
+ const hitRegion = getHitRegion(point.x, point.y);
8826
+ if (hitRegion === "outside") {
8827
+ return;
8828
+ }
8829
+ const pane = hitRegion === "plot" ? getPaneAt(point.x, point.y) : null;
8830
+ const drawingHit = hitRegion === "plot" && !pane ? getDrawingHit(point.x, point.y) : null;
8831
+ if (drawingHit) {
8832
+ if (selectedDrawingId !== drawingHit.drawing.id) {
8833
+ selectedDrawingId = drawingHit.drawing.id;
8834
+ scheduleDraw();
8835
+ }
8836
+ }
8837
+ const region = drawingHit ? "drawing" : pane ? "indicator-pane" : hitRegion;
8838
+ const index = indexFromCanvasX(point.x);
8839
+ const barTime = index === null ? null : getTimeForIndex(index);
8840
+ const bar = index === null ? void 0 : data[index];
8841
+ const inMainPane = !pane && (hitRegion === "plot" || hitRegion === "y-axis");
8842
+ contextMenuHandler({
8843
+ region,
8844
+ x: point.x,
8845
+ y: point.y,
8846
+ clientX: event.clientX,
8847
+ clientY: event.clientY,
8848
+ ...inMainPane ? { price: roundToPricePrecision(priceFromCanvasY(point.y)) } : {},
8849
+ ...index === null ? {} : { index },
8850
+ ...barTime ? { time: barTime.toISOString() } : {},
8851
+ ...bar ? {
8852
+ point: {
8853
+ t: bar.time.toISOString(),
8854
+ o: bar.o,
8855
+ h: bar.h,
8856
+ l: bar.l,
8857
+ c: bar.c,
8858
+ ...bar.v === void 0 ? {} : { v: bar.v }
8859
+ }
8860
+ } : {},
8861
+ ...drawingHit ? {
8862
+ drawing: serializeDrawing(drawingHit.drawing),
8863
+ drawingTarget: drawingHit.target,
8864
+ ...drawingHit.pointIndex === void 0 ? {} : { pointIndex: drawingHit.pointIndex }
8865
+ } : {},
8866
+ ...pane ? { indicator: { id: pane.id, type: pane.type } } : {}
8867
+ });
8868
+ };
8869
+ const onCanvasKeyDown = (event) => {
8870
+ const keyboard = mergedOptions.keyboard ?? {};
8871
+ if (keyboard.enabled === false) {
8872
+ return;
8873
+ }
8874
+ if (event.altKey || event.ctrlKey || event.metaKey) {
8875
+ return;
8876
+ }
8877
+ const panStep = Math.max(1, keyboard.panBars ?? 1) * (event.shiftKey ? 10 : 1);
8878
+ const emit = (action, drawing) => {
8879
+ keyboardShortcutHandler?.({
8880
+ action,
8881
+ key: event.key,
8882
+ shiftKey: event.shiftKey,
8883
+ ...drawing ? { drawing } : {}
8884
+ });
8885
+ };
8886
+ switch (event.key) {
8887
+ case "Delete":
8888
+ case "Backspace": {
8889
+ if (keyboard.deleteSelectedDrawing === false || !selectedDrawingId) {
8890
+ return;
8891
+ }
8892
+ const target = drawings.find((entry) => entry.id === selectedDrawingId);
8893
+ if (!target) {
8894
+ return;
8895
+ }
8896
+ const removed = serializeDrawing(target);
8897
+ removeDrawing(selectedDrawingId);
8898
+ selectedDrawingId = null;
8899
+ scheduleDraw();
8900
+ emit("delete-drawing", removed);
8901
+ break;
8902
+ }
8903
+ case "Escape": {
8904
+ if (cancelDrawing()) {
8905
+ emit("cancel");
8906
+ break;
8907
+ }
8908
+ if (activeDrawingTool) {
8909
+ setActiveDrawingTool(null);
8910
+ emit("cancel");
8911
+ break;
8912
+ }
8913
+ if (selectedDrawingId) {
8914
+ setSelectedDrawing(null);
8915
+ emit("cancel");
8916
+ break;
8917
+ }
8918
+ return;
8919
+ }
8920
+ case "ArrowLeft":
8921
+ panX(-panStep);
8922
+ emit("pan-left");
8923
+ break;
8924
+ case "ArrowRight":
8925
+ panX(panStep);
8926
+ emit("pan-right");
8927
+ break;
8928
+ case "ArrowUp":
8929
+ case "ArrowDown": {
8930
+ if (!drawState) {
8931
+ return;
8932
+ }
8933
+ const step = (drawState.yMax - drawState.yMin) / 20 * (event.shiftKey ? 5 : 1);
8934
+ panY(event.key === "ArrowUp" ? step : -step);
8935
+ emit(event.key === "ArrowUp" ? "pan-up" : "pan-down");
8936
+ break;
8937
+ }
8938
+ case "+":
8939
+ case "=":
8940
+ zoomInX();
8941
+ emit("zoom-in");
8942
+ break;
8943
+ case "-":
8944
+ case "_":
8945
+ zoomOutX();
8946
+ emit("zoom-out");
8947
+ break;
8948
+ case "Home":
8949
+ fitContent();
8950
+ emit("reset-viewport");
8951
+ break;
8952
+ case "End":
8953
+ setFollowingLatest(true);
8954
+ emit("scroll-to-realtime");
8955
+ break;
8956
+ case "r":
8957
+ case "R":
8958
+ resetViewport();
8959
+ emit("reset-viewport");
8960
+ break;
8961
+ default:
8962
+ return;
8963
+ }
8964
+ event.preventDefault();
8675
8965
  };
8966
+ canvas.addEventListener("keydown", onCanvasKeyDown);
8676
8967
  canvas.addEventListener("pointerdown", onPointerDown);
8677
8968
  canvas.addEventListener("pointermove", onPointerMove);
8678
8969
  canvas.addEventListener("pointerup", endPointerDrag);
@@ -8719,6 +9010,7 @@ function createChart(element, options = {}) {
8719
9010
  resetRightMarginCache();
8720
9011
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
8721
9012
  doubleClickAction = mergedOptions.doubleClickAction;
9013
+ applyAccessibilityAttributes();
8722
9014
  const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
8723
9015
  if (!isTickerSmoothingEnabled) {
8724
9016
  smoothedTickerPrice = null;
@@ -9102,6 +9394,23 @@ function createChart(element, options = {}) {
9102
9394
  const onDrawingHover = (handler) => {
9103
9395
  drawingHoverHandler = handler;
9104
9396
  };
9397
+ const onSelectionChange = (handler) => {
9398
+ selectionChangeHandler = handler;
9399
+ lastSelectionSignature = null;
9400
+ if (handler) {
9401
+ emitSelectionChangeIfMoved();
9402
+ }
9403
+ };
9404
+ const getSelectedDrawing = () => getSelectedDrawingWithBounds();
9405
+ const onContextMenuHandler = (handler) => {
9406
+ contextMenuHandler = handler;
9407
+ };
9408
+ const onKeyboardShortcut = (handler) => {
9409
+ keyboardShortcutHandler = handler;
9410
+ };
9411
+ const focus = () => {
9412
+ canvas.focus({ preventScroll: true });
9413
+ };
9105
9414
  const saveState = () => {
9106
9415
  const drawingDefaults = {};
9107
9416
  for (const [tool, defaults] of drawingToolDefaults) {
@@ -9166,6 +9475,7 @@ function createChart(element, options = {}) {
9166
9475
  drawRafId = null;
9167
9476
  pendingDrawUpdateAutoScale = false;
9168
9477
  }
9478
+ canvas.removeEventListener("keydown", onCanvasKeyDown);
9169
9479
  canvas.removeEventListener("pointerdown", onPointerDown);
9170
9480
  canvas.removeEventListener("pointermove", onPointerMove);
9171
9481
  canvas.removeEventListener("pointerup", endPointerDrag);
@@ -9229,6 +9539,11 @@ function createChart(element, options = {}) {
9229
9539
  onDrawingSelect,
9230
9540
  onDrawingDoubleClick,
9231
9541
  onDrawingEditText,
9542
+ onSelectionChange,
9543
+ getSelectedDrawing,
9544
+ onContextMenu: onContextMenuHandler,
9545
+ onKeyboardShortcut,
9546
+ focus,
9232
9547
  setMagnetMode,
9233
9548
  getMagnetMode,
9234
9549
  setPriceScale,
package/docs/API.md CHANGED
@@ -557,6 +557,11 @@ an up measurement, red for down.
557
557
  - `fitContent(): void` (x-only fit, keeps y zoom)
558
558
  - `resetViewport(): void` (fit x + reset y auto-scale)
559
559
  - `setSelectedDrawing(id: string | null): void` (marks a drawing selected; only the selected/drafted drawing renders its handles, and position tools render their lines/labels only while selected)
560
+ - `onSelectionChange(handler: ((event: DrawingSelectionChangeEvent | null) => void) | null): void` — selection geometry stream for a floating toolbar; see "Context menus, selection toolbars and keyboard" below
561
+ - `getSelectedDrawing(): { drawing: DrawingObjectOptions; bounds: DrawingBounds } | null`
562
+ - `onContextMenu(handler: ((event: ChartContextMenuEvent) => void) | null): void` — typed right-click with region + hit info
563
+ - `onKeyboardShortcut(handler: ((event: ChartKeyboardShortcutEvent) => void) | null): void`
564
+ - `focus(): void` — focus the canvas so keyboard shortcuts apply
560
565
  - `onDrawingDoubleClick(handler): void` (fires when a drawing is double-clicked; use it to open a settings dialog, e.g. for position tools)
561
566
  - `onDrawingEditText(handler): void` (fires when a `text`/`note`/`callout` tool is placed or double-clicked; use it to show an inline text editor at `{x, y}` and write the result back via `updateDrawing(id, { label })`). These drawings store their content in `label` and size in `fontSize`.
562
567
  - `setTradeMarkers(markers: TradeMarkerOptions[]): void` — draw trade execution markers (arrow + `qty @ price` label) on the candle of each fill's `time`. Buy = arrow below the low pointing up; sell = arrow above the high pointing down. Each marker: `{ time (ms|ISO), price, side: "buy"|"sell", qty?, text?, color?, textColor? }` (defaults: buy `#2962ff`, sell `#f23645`, text `#d1d4dc`). Markers resolve to bars by time, so they persist across timeframe switches and scrolling. Pass `[]` to clear.
@@ -605,6 +610,101 @@ link.click();
605
610
 
606
611
  ---
607
612
 
613
+ ## Context menus, selection toolbars and keyboard
614
+
615
+ Three pieces of chrome that every host previously rebuilt by hand. The chart
616
+ does the hit-testing and geometry; the host still renders its own menus and
617
+ toolbar, so it keeps full control of styling and framework.
618
+
619
+ ### `onContextMenu(handler)`
620
+
621
+ Right-click anywhere on the chart. The browser's native menu is always
622
+ suppressed. The event says *what* was under the pointer, so a host switches on
623
+ `region` instead of re-deriving it from coordinates:
624
+
625
+ ```ts
626
+ chart.onContextMenu((event) => {
627
+ switch (event.region) {
628
+ case "drawing":
629
+ // event.drawing / drawingTarget / pointIndex — the chart has already
630
+ // selected this drawing, exactly like TradingView.
631
+ showDrawingMenu(event.clientX, event.clientY, event.drawing!);
632
+ break;
633
+ case "indicator-pane":
634
+ showIndicatorMenu(event.clientX, event.clientY, event.indicator!); // { id, type }
635
+ break;
636
+ case "y-axis":
637
+ showPriceScaleMenu(event.clientX, event.clientY); // log / percent / invert
638
+ break;
639
+ case "x-axis":
640
+ showTimeScaleMenu(event.clientX, event.clientY);
641
+ break;
642
+ case "plot":
643
+ showChartMenu(event.clientX, event.clientY, event.price); // + index/time/point
644
+ break;
645
+ }
646
+ });
647
+ ```
648
+
649
+ `clientX`/`clientY` are viewport coordinates, ready to position a DOM menu;
650
+ `x`/`y` are canvas-relative. `price`, `index`, `time` and `point` describe the
651
+ bar under the pointer (`price` only in the main pane).
652
+
653
+ ### `onSelectionChange(handler)` — floating drawing toolbar
654
+
655
+ Fires when a drawing is selected or deselected (payload `null`) **and on every
656
+ frame where the selected drawing's box moves** — drag, resize, pan, zoom. That
657
+ is what lets a host toolbar sit above the shape and track it, instead of
658
+ floating at a fixed corner:
659
+
660
+ ```ts
661
+ chart.onSelectionChange((event) => {
662
+ if (!event) return hideToolbar();
663
+ const rect = container.getBoundingClientRect();
664
+ // Above the shape, flipping below when there's no room.
665
+ const above = rect.top + event.bounds.top - toolbarHeight - 10;
666
+ positionToolbar({
667
+ left: rect.left + event.bounds.centerX - toolbarWidth / 2,
668
+ top: above > rect.top + 8 ? above : rect.top + event.bounds.bottom + 10,
669
+ });
670
+ showToolbarFor(event.drawing);
671
+ });
672
+ ```
673
+
674
+ `bounds` is `{ left, top, right, bottom, centerX, centerY }` in canvas pixels,
675
+ clamped to the plot; `plot` gives the plot rectangle for keeping the toolbar
676
+ inside the chart; `dragging` is true mid-drag (dim or lock the toolbar if you
677
+ prefer it out of the way). `getSelectedDrawing()` returns the same
678
+ `{ drawing, bounds }` on demand, and `DrawingSelectEvent` now carries `bounds`
679
+ too.
680
+
681
+ ### Keyboard shortcuts and accessibility
682
+
683
+ The canvas is focusable (`tabIndex 0`, `role="application"`, `aria-label`,
684
+ `aria-keyshortcuts`) and takes focus on pointer-down, so shortcuts apply to the
685
+ chart the user is actually using and **never swallow typing in host inputs**.
686
+ The focus ring only shows for keyboard focus.
687
+
688
+ | Key | Action |
689
+ | --- | --- |
690
+ | `←` / `→` | Pan one bar (`Shift` = 10) |
691
+ | `↑` / `↓` | Pan price by 5% of the visible range (`Shift` = 5×) |
692
+ | `+` / `-` | Zoom in / out |
693
+ | `Delete` / `Backspace` | Delete the selected drawing |
694
+ | `Escape` | Cancel a half-drawn shape, then the armed tool, then the selection |
695
+ | `Home` | Fit all bars |
696
+ | `End` | Scroll to realtime |
697
+ | `R` | Reset the viewport |
698
+
699
+ Chords with Cmd/Ctrl/Alt are ignored so host and browser shortcuts still work.
700
+ Configure with `keyboard: { enabled, panBars, deleteSelectedDrawing }` and
701
+ `accessibility: { label, description, focusable }` in `ChartOptions`, and
702
+ subscribe to `onKeyboardShortcut(handler)` to mirror actions in your own UI
703
+ (the event includes the removed `drawing` for `"delete-drawing"`, so hosts can
704
+ offer undo). `focus()` puts keyboard focus on the chart programmatically.
705
+
706
+ ---
707
+
608
708
  ## Script Indicators (user-authored)
609
709
 
610
710
  `compileScriptIndicator(definition: ScriptIndicatorDefinition): IndicatorPlugin` — compiles a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.137",
3
+ "version": "0.1.138",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",