hyperprop-charting-library 0.1.142 → 0.1.144

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
@@ -593,7 +593,7 @@ an up measurement, red for down.
593
593
  - `updateIndicator(id: string, patch: Partial<IndicatorInstanceOptions>): void`
594
594
  - `removeIndicator(id: string): void`
595
595
  - `setIndicators(indicators: IndicatorInstanceOptions[]): void`
596
- - `saveState(): ChartSavedState` — one JSON-serializable snapshot of the full user-visible setup: `{ version: 1, chartType, viewport, drawings, indicators, magnetMode, priceScale, drawingDefaults }`. Chart *data* is not included; keep feeding bars through `setData`/`upsertBar`.
596
+ - `saveState(): ChartSavedState` — one JSON-serializable snapshot of the full user-visible setup: `{ version: 1, chartType, viewport, drawings, indicators, magnetMode, priceScale, drawingDefaults, alerts, timezone, timeFormat }`. Chart *data* is not included; keep feeding bars through `setData`/`upsertBar`.
597
597
  - `loadState(state: Partial<ChartSavedState>): void` — restore a `saveState()` blob. Tolerant of partial blobs (missing sections are left untouched), so it doubles as a bulk setter. Register custom script indicator plugins with `registerIndicator` *before* calling it so their instances resolve. Viewport is applied last so pane changes don't clobber the restore.
598
598
  - `takeScreenshot(options?: { type?: "image/png" | "image/jpeg"; quality?: number }): string` — returns a data URL of the current frame (default PNG) at the chart's device pixel ratio. Everything on the canvas is included: series, indicators, panes, drawings, legends.
599
599
 
@@ -609,11 +609,239 @@ link.download = "chart.png";
609
609
  link.click();
610
610
  ```
611
611
 
612
+ ### Time zone and sessions
613
+
614
+ - `setTimezone(timezone: ChartTimezone): void` / `getTimezone(): ChartTimezone` — an IANA id, `"utc"`, or `"local"` (default). Unknown ids fall back to local instead of throwing.
615
+ - `setTimeFormat(format: "24h" | "12h"): void` / `getTimeFormat()`.
616
+ - `setSession(session: SessionOptions | SessionSpec | SessionPresetName | null): void` / `getSession(): SessionOptions`.
617
+
618
+ ### Undo / redo
619
+
620
+ - `undo(): boolean` / `redo(): boolean` — return false when the stack is empty.
621
+ - `canUndo(): boolean` / `canRedo(): boolean` / `getUndoRedoState(): UndoRedoState`.
622
+ - `onUndoRedoStateChange(handler)` / `clearHistory(): void`.
623
+
624
+ ### Bar replay
625
+
626
+ - `startReplay(options?: ReplayStartOptions): void` / `stopReplay(): void`
627
+ - `replayStep(bars?: number): void` — negative rewinds.
628
+ - `replayPlay(speed?: number): void` / `replayPause(): void` / `setReplaySpeed(speed: number): void`
629
+ - `getReplayState(): ReplayState` / `onReplayStateChange(handler)`
630
+
631
+ ### Price alerts
632
+
633
+ - `setAlerts(alerts: AlertOptions[]): void` / `addAlert(alert): string` / `updateAlert(id, patch)` / `removeAlert(id)` / `getAlerts()`
634
+ - `onAlertTrigger(handler)` / `onAlertAction(handler)`
635
+
636
+ ### Marks
637
+
638
+ - `setBarMarks(marks: BarMarkOptions[]): void` / `getBarMarks()`
639
+ - `setTimescaleMarks(marks: TimescaleMarkOptions[]): void` / `getTimescaleMarks()`
640
+ - `onMarkClick(handler)` / `onMarkHover(handler)`
641
+
612
642
  - `resize(width?: number, height?: number): void`
613
643
  - `destroy(): void`
614
644
 
615
645
  ---
616
646
 
647
+ ## Time zones and trading sessions
648
+
649
+ Timestamps are rendered in whatever zone you choose, and the chart can be told
650
+ the instrument's actual trading hours. Both matter most for futures: a CME day
651
+ opens at 18:00 ET the *previous* calendar evening, so calendar-day boundaries
652
+ draw separators in the wrong places.
653
+
654
+ ```ts
655
+ const chart = createChart(el, {
656
+ timezone: "America/New_York", // IANA id, "utc", or "local" (default)
657
+ timeFormat: "24h", // or "12h"
658
+ session: {
659
+ spec: "cme-futures", // preset name, your own SessionSpec, or null
660
+ separators: true, // vertical rule at each session open
661
+ highlightOutOfSession: true // tint overnight/extended-hours bars
662
+ }
663
+ });
664
+
665
+ chart.setTimezone("Asia/Tokyo");
666
+ chart.setSession({ spec: "cme-rth" }); // 09:30–16:00 ET
667
+ ```
668
+
669
+ Presets (`SESSION_PRESETS`): `cme-futures` (Sun–Thu 18:00 → 17:00 ET, including
670
+ the daily maintenance break), `cme-rth`, `us-equities`,
671
+ `us-equities-extended`, `24x7`. A custom spec is three fields:
672
+
673
+ ```ts
674
+ chart.setSession({
675
+ spec: {
676
+ timezone: "America/New_York",
677
+ // Days the session OPENS. For a window that runs past midnight this is the
678
+ // evening side, so Sun–Thu means Friday evening is closed for the weekend.
679
+ days: [0, 1, 2, 3, 4],
680
+ segments: [{ start: "18:00", end: "17:00" }]
681
+ },
682
+ highlightOutOfSession: true
683
+ });
684
+ ```
685
+
686
+ Session separators only apply to intraday series (on daily bars every bar is
687
+ its own day). Zone conversion is offset-cached per UTC hour, so DST transitions
688
+ are exact and per-bar lookups stay arithmetic rather than `Intl` calls.
689
+
690
+ ---
691
+
692
+ ## Undo / redo
693
+
694
+ Covers the drawing document: create, move, restyle, delete, clear. A whole drag
695
+ collapses into one step, and `Ctrl/Cmd+Z` (redo: `Ctrl/Cmd+Shift+Z` or
696
+ `Ctrl+Y`) works whenever the chart canvas has focus.
697
+
698
+ ```ts
699
+ chart.onUndoRedoStateChange((state) => {
700
+ undoButton.disabled = !state.canUndo;
701
+ undoButton.title = state.undoLabel ?? ""; // e.g. "Move trendline"
702
+ });
703
+ chart.undo();
704
+ ```
705
+
706
+ Deliberately **not** recorded: viewport changes (panning is navigation, not an
707
+ edit — TradingView made the same call in v30) and bulk `setDrawings` syncs, so a
708
+ host that echoes `onDrawingsChange` straight back never fills the stack with its
709
+ own round-trips. Indicators are excluded too: hosts usually own that state, and
710
+ undoing it here would be overwritten on the next sync. `loadState()` clears the
711
+ history, since a restored layout is a starting point rather than a step.
712
+
713
+ ---
714
+
715
+ ## Bar replay
716
+
717
+ Hide everything after a chosen bar and step forward through it — the same
718
+ practice loop TradingView's replay gives you, which the Advanced Charts package
719
+ itself does not ship. Indicators, drawings and autoscale all see only the
720
+ revealed bars, so there is no lookahead.
721
+
722
+ ```ts
723
+ chart.startReplay({ fromTimeMs: Date.parse("2026-07-14T13:30:00Z"), speed: 4 });
724
+ chart.replayPlay(); // 4 bars per second
725
+ chart.replayStep(1); // or step by hand
726
+ chart.replayStep(-1); // rewind
727
+ chart.onReplayStateChange((s) => {
728
+ label.textContent = `${s.index + 1}/${s.total}${s.atEnd ? " (end)" : ""}`;
729
+ });
730
+ chart.stopReplay(); // restores the full series
731
+ ```
732
+
733
+ Live bars arriving during replay are buffered rather than shown, and are all
734
+ present when replay stops. Without `fromIndex`/`fromTimeMs` replay starts 70%
735
+ of the way through the series.
736
+
737
+ ---
738
+
739
+ ## Price alerts
740
+
741
+ Watch levels drawn as a dashed line plus a bell tag on the price axis. The
742
+ chart renders them, lets the user drag them and remove them, and tells you when
743
+ price satisfies the condition; delivery (push, sound, email) stays with the
744
+ host.
745
+
746
+ ```ts
747
+ chart.addAlert({ price: 5120.25, condition: "crossing-up", label: "breakout" });
748
+
749
+ chart.onAlertTrigger(({ alert, price, timeMs }) => {
750
+ notify(`${alert.label} hit ${price}`);
751
+ });
752
+ chart.onAlertAction(({ alert, action, price, dragging }) => {
753
+ if (action === "move" && !dragging) persist(alert.id, price);
754
+ if (action === "remove") forget(alert.id);
755
+ });
756
+ ```
757
+
758
+ Conditions: `crossing` (either direction), `crossing-up`, `crossing-down`,
759
+ `greater`, `less`. Crossings compare against the last price the alert saw, so an
760
+ alert armed mid-bar fires on the tick that crosses it instead of waiting for a
761
+ close. `once` (default true) disarms after firing; the line stays visible but
762
+ dimmed until you re-arm it with `updateAlert(id, { triggered: false })`.
763
+ Dragging a level re-arms it against the current market.
764
+
765
+ ---
766
+
767
+ ## Marks (events on bars and on the time axis)
768
+
769
+ ```ts
770
+ chart.setBarMarks([
771
+ { id: "cpi", time: "2026-07-15T12:30:00Z", label: "C", text: "CPI 0.2% m/m", color: "#f7a600" }
772
+ ]);
773
+ chart.setTimescaleMarks([
774
+ { id: "fomc", time: "2026-07-29T18:00:00Z", label: "F", text: "FOMC statement", shape: "diamond" }
775
+ ]);
776
+ chart.onMarkClick(({ mark, clientX, clientY }) => openPopover(mark, clientX, clientY));
777
+ chart.onMarkHover((event) => (event ? showTooltip(event) : hideTooltip()));
778
+ ```
779
+
780
+ Bar marks pin under (or above) the bar their timestamp falls on, or at an exact
781
+ `price`. Time-axis marks sit at the foot of the plot. Both accept
782
+ `circle | square | diamond | flag`, expose hover and click, and are host data —
783
+ they are not part of `saveState()`.
784
+
785
+ ---
786
+
787
+ ## In-chart data line
788
+
789
+ The symbol/OHLC line TradingView draws in the top-left of the plot. Hosts have
790
+ been building this in DOM and re-rendering it on every crosshair move; drawn
791
+ in-canvas it just follows the frame, inherits the theme's colors, and costs
792
+ nothing per pointer move.
793
+
794
+ ```ts
795
+ createChart(el, {
796
+ dataLine: {
797
+ visible: true,
798
+ symbol: "NQU6",
799
+ interval: "1h",
800
+ exchange: "CME",
801
+ showVolume: true,
802
+ statusColor: marketOpen ? "#22c55e" : "#ef4444",
803
+ details: [
804
+ { label: "Session", value: "RTH" },
805
+ { value: "Closed", color: "#fff", background: "rgba(242,54,69,0.85)" },
806
+ ],
807
+ },
808
+ });
809
+ ```
810
+
811
+ Values track the hovered bar and fall back to the latest one, the change is
812
+ colored by direction, and the indicator legend moves down to sit under it.
813
+ Detail chips are dropped rather than clipped when the row runs out of width.
814
+ Off by default so hosts with their own overlay don't suddenly render two.
815
+
816
+ ## Viewport transitions
817
+
818
+ The discrete jumps — `resetViewport()`, `fitContent()`,
819
+ `setFollowingLatest(true)` and the `Home`/`End`/`R` shortcuts — ease into place
820
+ over `animation.durationMs` (default 260) instead of teleporting, so it stays
821
+ obvious which way the chart moved. Zoom interpolates geometrically, so each
822
+ frame changes scale by the same ratio rather than appearing to accelerate.
823
+
824
+ Continuous gestures (drag, wheel, pinch) are never animated — they already
825
+ track the input — and any pointer interaction cancels a transition in flight.
826
+ The whole thing is skipped when the OS asks for reduced motion, or with
827
+ `animation: { viewportTransitions: false }`.
828
+
829
+ ## Indicator recomputation
830
+
831
+ Indicator series are memoised per (indicator, inputs, series fingerprint). A
832
+ live tick changes the fingerprint, which used to mean every indicator
833
+ recomputed its whole series on every animation frame — measured at 50k bars
834
+ with 10 indicators, that was ~11ms of the frame budget and dropped the chart to
835
+ 51fps.
836
+
837
+ While *only the forming bar* is ticking, recomputation is now throttled to
838
+ `indicatorUpdate.liveThrottleMs` (default 80ms). The values are always exact —
839
+ this changes how often the live bar's output refreshes, never how it is
840
+ computed — and anything structural (a bar closing, history prepending, inputs
841
+ or length changing) recomputes immediately, so closed bars are never stale.
842
+ Same benchmark after: 8.4ms and 118fps. Set `liveThrottleMs: 0` to recompute on
843
+ every frame.
844
+
617
845
  ## Themes
618
846
 
619
847
  A theme is the chart's whole palette as one object, so hosts stop setting a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.142",
3
+ "version": "0.1.144",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",
@@ -21,7 +21,8 @@
21
21
  "scripts": {
22
22
  "build": "tsup src/index.ts --format esm,cjs --dts --clean && cp \"./dist/index.js\" \"./dist/hyperprop-charting-library.js\" && cp \"./dist/index.cjs\" \"./dist/hyperprop-charting-library.cjs\" && cp \"./dist/index.d.ts\" \"./dist/hyperprop-charting-library.d.ts\"",
23
23
  "dev": "tsup src/index.ts --format esm,cjs --dts --watch --onSuccess \"cp ./dist/index.js ./dist/hyperprop-charting-library.js && cp ./dist/index.cjs ./dist/hyperprop-charting-library.cjs && cp ./dist/index.d.ts ./dist/hyperprop-charting-library.d.ts\"",
24
- "typecheck": "tsc -p tsconfig.json --noEmit"
24
+ "typecheck": "tsc -p tsconfig.json --noEmit",
25
+ "test": "npx tsx src/time.test.mjs"
25
26
  },
26
27
  "devDependencies": {
27
28
  "tsup": "^8.5.0"