hyperprop-charting-library 0.1.137 → 0.1.139

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/docs/API.md CHANGED
@@ -557,12 +557,18 @@ 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.
563
568
  - `cancelDrawing(): boolean` — abort an in-progress multi-click drawing (between the first click and the final point, e.g. a half-drawn trendline/ray/fib). Returns `true` if a draft was cancelled. Wire it to Escape.
564
569
  - Hold **Shift** while drawing or dragging an endpoint of a `trendline`/`ray`/`arrow` to constrain it to the nearest 0/45/90° angle (e.g. a perfectly horizontal line).
565
570
  - `setMagnetMode(mode): void` / `getMagnetMode()` — magnet/snap for all drawing tools. `"none"` off, `"weak"` snaps to a candle's OHLC only when the cursor is near a value, `"strong"` always snaps to the nearest OHLC of the bar under the cursor. Holding Cmd/Ctrl while drawing/dragging forces strong snapping regardless of the mode.
571
+ - `setCompareSeries(series: CompareSeriesOptions[]): void` / `addCompareSeries(series)` / `removeCompareSeries(id)` / `getCompareSeries()` — overlay other instruments on the main pane; see "Compare overlays" below
566
572
  - `setPriceScale(options: { mode?: "linear" | "log" | "percent"; inverted?: boolean }): void` / `getPriceScale()` — price-scale modes for the main pane (TradingView-style). `"log"` maps prices through log10 (wide ranges get 1/2/5-per-decade axis marks). `"percent"` keeps the linear mapping but relabels the axis (ticks, last-price/PDC/H/L/bid/ask tags and the crosshair) as % change from the first visible bar's close, updating as you pan. `inverted: true` flips the axis top-to-bottom. All viewport state stays in plain price space, so switching modes never moves your zoom, and drawings/orders/indicators follow automatically. Indicator panes keep their own scales. Included in `saveState()` as `priceScale`.
567
573
  - `setActiveDrawingTool(tool: DrawingToolType | null): void` (`DrawingToolType` = `"horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range" | "rectangle" | "text" | "note" | "parallel-channel" | "ellipse" | "arrow" | "brush" | "callout" | "measure"`)
568
574
  - `getActiveDrawingTool(): DrawingToolType | null`
@@ -605,6 +611,151 @@ link.click();
605
611
 
606
612
  ---
607
613
 
614
+ ## Compare overlays (multi-series)
615
+
616
+ Overlay other instruments on the main pane. The host owns the data (it already
617
+ has a datafeed); the chart aligns each series to the main bars by timestamp and
618
+ draws it.
619
+
620
+ ```ts
621
+ chart.setCompareSeries([
622
+ { id: "NQ", label: "NQ1!", data: nqBars, color: "#22d3ee" },
623
+ { id: "BTC", label: "BTCUSD", data: btcBars, color: "#a855f7", style: "area" },
624
+ ]);
625
+ ```
626
+
627
+ - **Alignment** is by timestamp, computed once per data change (a merge walk,
628
+ not a search per bar), so different session hours and holidays are fine — the
629
+ last known value carries forward across gaps, and bars before a compared
630
+ instrument starts are simply not drawn.
631
+ - **`scale`** defaults to `"percent"`: both instruments are normalised to their
632
+ first visible bar, so a $180 index and a $60,000 coin can be compared by
633
+ shape. It re-anchors as you pan, exactly like TradingView. Use `"price"` to
634
+ plot raw values on the main scale (only sensible for related instruments).
635
+ - **Autoscale** includes compare series unless `includeInAutoScale: false`.
636
+ - Each series gets a colored tag in the price gutter showing its own % move
637
+ (or value in price mode); tags stack instead of overlapping, and drop the
638
+ label when the gutter is too narrow.
639
+ - Compare data is **not** in `saveState()` — it can be megabytes and the host
640
+ re-fetches it anyway. Persist the symbol list yourself and re-supply data on
641
+ load.
642
+
643
+ ## Deep zoom-out (viewport downsampling)
644
+
645
+ Once bars are narrower than `downsampling.thresholdPx` (default 1.5px), several
646
+ bars share a pixel column, and drawing each one separately is wasted work. The
647
+ chart aggregates each column into one min/max bucket, so path operations scale
648
+ with the chart's width rather than the number of bars. Extremes are preserved
649
+ exactly — a one-bar spike still reaches full height — because the bucket keeps
650
+ the true high and low rather than sampling. Candle and bar styles converge to a
651
+ single colored high-low column (the body is sub-pixel at that density anyway);
652
+ line/area/baseline keep a min/max envelope so the shape of the move survives.
653
+
654
+ Measured on the playground benchmark (50,000 bars, 120Hz display):
655
+
656
+ | Scenario | Per-bar (before) | Downsampled |
657
+ | --- | --- | --- |
658
+ | zoom 100 ↔ 20k bars | 21.3ms avg, 49.8ms p95, 47fps, 53 slow frames | 8.3ms avg, 10.3ms p95, 120fps, 0 slow frames |
659
+ | deep zoom-out, 50k bars | 52.7ms avg, 58.8ms p95, 19fps, 240 slow frames | 8.3ms avg, 10.2ms p95, 120fps, 0 slow frames |
660
+
661
+ Turn it off with `downsampling: { enabled: false }`, or tune when it kicks in
662
+ with `thresholdPx`.
663
+
664
+ ## Context menus, selection toolbars and keyboard
665
+
666
+ Three pieces of chrome that every host previously rebuilt by hand. The chart
667
+ does the hit-testing and geometry; the host still renders its own menus and
668
+ toolbar, so it keeps full control of styling and framework.
669
+
670
+ ### `onContextMenu(handler)`
671
+
672
+ Right-click anywhere on the chart. The browser's native menu is always
673
+ suppressed. The event says *what* was under the pointer, so a host switches on
674
+ `region` instead of re-deriving it from coordinates:
675
+
676
+ ```ts
677
+ chart.onContextMenu((event) => {
678
+ switch (event.region) {
679
+ case "drawing":
680
+ // event.drawing / drawingTarget / pointIndex — the chart has already
681
+ // selected this drawing, exactly like TradingView.
682
+ showDrawingMenu(event.clientX, event.clientY, event.drawing!);
683
+ break;
684
+ case "indicator-pane":
685
+ showIndicatorMenu(event.clientX, event.clientY, event.indicator!); // { id, type }
686
+ break;
687
+ case "y-axis":
688
+ showPriceScaleMenu(event.clientX, event.clientY); // log / percent / invert
689
+ break;
690
+ case "x-axis":
691
+ showTimeScaleMenu(event.clientX, event.clientY);
692
+ break;
693
+ case "plot":
694
+ showChartMenu(event.clientX, event.clientY, event.price); // + index/time/point
695
+ break;
696
+ }
697
+ });
698
+ ```
699
+
700
+ `clientX`/`clientY` are viewport coordinates, ready to position a DOM menu;
701
+ `x`/`y` are canvas-relative. `price`, `index`, `time` and `point` describe the
702
+ bar under the pointer (`price` only in the main pane).
703
+
704
+ ### `onSelectionChange(handler)` — floating drawing toolbar
705
+
706
+ Fires when a drawing is selected or deselected (payload `null`) **and on every
707
+ frame where the selected drawing's box moves** — drag, resize, pan, zoom. That
708
+ is what lets a host toolbar sit above the shape and track it, instead of
709
+ floating at a fixed corner:
710
+
711
+ ```ts
712
+ chart.onSelectionChange((event) => {
713
+ if (!event) return hideToolbar();
714
+ const rect = container.getBoundingClientRect();
715
+ // Above the shape, flipping below when there's no room.
716
+ const above = rect.top + event.bounds.top - toolbarHeight - 10;
717
+ positionToolbar({
718
+ left: rect.left + event.bounds.centerX - toolbarWidth / 2,
719
+ top: above > rect.top + 8 ? above : rect.top + event.bounds.bottom + 10,
720
+ });
721
+ showToolbarFor(event.drawing);
722
+ });
723
+ ```
724
+
725
+ `bounds` is `{ left, top, right, bottom, centerX, centerY }` in canvas pixels,
726
+ clamped to the plot; `plot` gives the plot rectangle for keeping the toolbar
727
+ inside the chart; `dragging` is true mid-drag (dim or lock the toolbar if you
728
+ prefer it out of the way). `getSelectedDrawing()` returns the same
729
+ `{ drawing, bounds }` on demand, and `DrawingSelectEvent` now carries `bounds`
730
+ too.
731
+
732
+ ### Keyboard shortcuts and accessibility
733
+
734
+ The canvas is focusable (`tabIndex 0`, `role="application"`, `aria-label`,
735
+ `aria-keyshortcuts`) and takes focus on pointer-down, so shortcuts apply to the
736
+ chart the user is actually using and **never swallow typing in host inputs**.
737
+ The focus ring only shows for keyboard focus.
738
+
739
+ | Key | Action |
740
+ | --- | --- |
741
+ | `←` / `→` | Pan one bar (`Shift` = 10) |
742
+ | `↑` / `↓` | Pan price by 5% of the visible range (`Shift` = 5×) |
743
+ | `+` / `-` | Zoom in / out |
744
+ | `Delete` / `Backspace` | Delete the selected drawing |
745
+ | `Escape` | Cancel a half-drawn shape, then the armed tool, then the selection |
746
+ | `Home` | Fit all bars |
747
+ | `End` | Scroll to realtime |
748
+ | `R` | Reset the viewport |
749
+
750
+ Chords with Cmd/Ctrl/Alt are ignored so host and browser shortcuts still work.
751
+ Configure with `keyboard: { enabled, panBars, deleteSelectedDrawing }` and
752
+ `accessibility: { label, description, focusable }` in `ChartOptions`, and
753
+ subscribe to `onKeyboardShortcut(handler)` to mirror actions in your own UI
754
+ (the event includes the removed `drawing` for `"delete-drawing"`, so hosts can
755
+ offer undo). `focus()` puts keyboard focus on the chart programmatically.
756
+
757
+ ---
758
+
608
759
  ## Script Indicators (user-authored)
609
760
 
610
761
  `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.139",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",