openalgo-charts 1.2.0 → 1.4.0

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/README.md CHANGED
@@ -4,12 +4,12 @@
4
4
 
5
5
  **A from-scratch, dependency-free HTML5-canvas charting engine for OpenAlgo.**
6
6
 
7
- Professional interactive charts, indicators, drawing tools, order flow, and on-chart trading six lazy-loaded tiers, zero runtime dependencies, ~37 KB Brotli for the base engine.
7
+ Professional interactive charts, indicators, drawing tools, order flow, market replay, linked chart grids, and on-chart trading. Six lazy-loaded tiers, zero runtime dependencies, ~53 KB Brotli for the base engine.
8
8
 
9
9
  [![npm version](https://img.shields.io/npm/v/openalgo-charts.svg?color=cb3837&label=npm)](https://www.npmjs.com/package/openalgo-charts)
10
10
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)
11
- [![bundle](https://img.shields.io/badge/brotli-37%20KB%20base%20%C2%B7%2088%20KB%20all%20tiers-brightgreen.svg)](#size-budget)
12
- [![tests](https://img.shields.io/badge/tests-1001%20passing-brightgreen.svg)](#develop)
11
+ [![bundle](https://img.shields.io/badge/brotli-53%20KB%20base%20%C2%B7%20111%20KB%20all%20tiers-brightgreen.svg)](#size-budget)
12
+ [![tests](https://img.shields.io/badge/tests-1719%20passing-brightgreen.svg)](#develop)
13
13
  [![dependencies](https://img.shields.io/badge/runtime%20deps-0-brightgreen.svg)](#principles)
14
14
 
15
15
  [**Documentation**](https://marketcalls.github.io/openalgo-charts/)  ·  [**Live examples**](https://marketcalls.github.io/openalgo-charts/examples)  ·  [**Getting started**](./docs/getting-started.md)  ·  [**Architecture**](./ARCHITECTURE.md)
@@ -60,19 +60,19 @@ Import only what you use. Each tier is a separate bundle that registers into the
60
60
 
61
61
  | Import | Contents | Brotli |
62
62
  |---|---|---|
63
- | `openalgo-charts` | Engine, 13 chart types, panes & scales, primitives, registries, chart state, trading overlay, OpenAlgo feeds | 37.0 KB |
64
- | `openalgo-charts/indicators` | 91 built-in indicators + the Tier-2 (external-data) contract | 24.7 KB |
65
- | `openalgo-charts/draw` | 43 drawing tools + a headless drawing controller | 11.7 KB |
63
+ | `openalgo-charts` | Engine, 13 chart types, panes & scales, primitives, registries, chart state, chart linking, bar cache, interval registry, trading overlay, OpenAlgo feeds | 52.9 KB |
64
+ | `openalgo-charts/indicators` | 91 built-in indicators + the Tier-2 (external-data) contract | 24.9 KB |
65
+ | `openalgo-charts/draw` | 43 drawing tools + a headless drawing controller and clipboard | 13.1 KB |
66
66
  | `openalgo-charts/transform` | Heikin Ashi, Renko, Range bars, Line Break, Point & Figure, Kagi | 2.7 KB |
67
- | `openalgo-charts/profile` | Volume Profile, Market Profile (TPO), Footprint, order flow | 10.1 KB |
67
+ | `openalgo-charts/profile` | Volume Profile, Market Profile (TPO), Footprint, order flow | 10.7 KB |
68
68
  | `openalgo-charts/trade` | Order / position / bracket tools + DOM ladder | 6.6 KB |
69
69
 
70
- Everything together is **88.2 KB Brotli**. Figures are the measured `size-limit` output; the trade tier is a delta over the base, which is why base + trade (43.6 KB) is less than their listed sum.
70
+ Everything together is **110.8 KB Brotli**. Figures are the measured `size-limit` output; the trade tier is a delta over the base, which is why base + trade (59.5 KB) is less than their listed sum.
71
71
 
72
72
  ## What's built
73
73
 
74
74
  ### Chart types & transforms
75
- Candles, hollow and volume candles, OHLC bars, high-low, line, line+markers, step, area, HLC-area, baseline, columns, histogram plus Heikin Ashi, Renko, Range bars, Line Break, **Point & Figure** (fixed / percent / ATR box sizing, high-low or close construction), and Kagi.
75
+ Candles, hollow and volume candles, OHLC bars, high-low, line, line+markers, step, area, HLC-area, baseline, columns, histogram, plus Heikin Ashi, Renko, Range bars, Line Break, **Point & Figure** (fixed / percent / ATR box sizing, high-low or close construction), and Kagi.
76
76
 
77
77
  ### Indicators
78
78
 
@@ -86,7 +86,7 @@ macd.setSettings({ 'macd:width': 2, 'macd:lineStyle': 'dashed' });
86
86
 
87
87
  91 built-ins across Trend, Momentum, Volatility and Volume, from the everyday (SMA, EMA, WMA, VWAP, Bollinger Bands, RSI, MACD, Stochastic, ADX/DMI, ATR) through Supertrend, HalfTrend, Ichimoku, Keltner, Donchian, Chandelier Exit and CPR with floor pivots to Connors RSI, Fisher Transform, Woodies CCI, Klinger, Vortex, WaveTrend Pro, Chop Zone and Williams Fractals. Twenty-five of them draw shaded bands, five emit named buy/sell markers, and Seasonality draws a monthly return heatmap as a table over the chart. The full catalogue with ids and defaults is in the docs.
88
88
 
89
- The chart owns the whole lifecycle series, pane placement, reference levels, fixed ranges (RSI 0..100), recompute on data change, teardown. Every plot gets colour, opacity, thickness, and line style for free, generated from the descriptor. Write your own with `registerIndicator`, or use the **Tier-2 contract** for indicators whose data isn't derived from OHLCV (open interest, CVD, any external feed).
89
+ The chart owns the whole lifecycle: series, pane placement, reference levels, fixed ranges (RSI 0..100), recompute on data change, teardown. Every plot gets colour, opacity, thickness, and line style for free, generated from the descriptor. Write your own with `registerIndicator`, or use the **Tier-2 contract** for indicators whose data isn't derived from OHLCV (open interest, CVD, any external feed).
90
90
 
91
91
  ### Drawing tools
92
92
 
@@ -99,10 +99,81 @@ draw.setTool('trend-line'); // the next two clicks place it
99
99
 
100
100
  43 tools. Lines: trend line, ray, extended line, arrow, horizontal line/ray, vertical line, cross line. Shapes: rectangle, rotated rectangle, ellipse, circle, triangle. Paths: path, polyline, arc, curve, double curve. Channels: parallel channel, fib channel. Fibonacci: retracement, extension, time zone, speed fan. Gann: fan, box. Cycles: cyclic lines, time cycles, sine line. Forecasting: long/short position (1:1 from one click, with risk/reward and risk-based sizing), forecast. Measurers: price range, date range, measure. Arrows: mark up, mark down. Text and notes: text, price label, callout, flag mark. Brushes: brush, highlighter (freehand).
101
101
 
102
- Headless by design no toolbar, no dialogs. Placement with live preview, selection, whole-shape and per-anchor dragging, magnet snap to O/H/L/C, undo/redo (a drag is one step), and persistence. Anchors are `{ time, price }`, never pixels, so they survive zoom and resolve inside collapsed session gaps and past the last bar.
102
+ Headless by design: no toolbar, no dialogs. Placement with live preview, selection, whole-shape and per-anchor dragging, magnet snap to O/H/L/C, undo/redo (a drag is one step), and persistence. Anchors are `{ time, price }`, never pixels, so they survive zoom and resolve inside collapsed session gaps and past the last bar.
103
103
 
104
- ### Panes & legends
105
- Draggable pane dividers, move / maximize / remove, and pane legends showing one reading per plot in that plot's own colour, with inline show-hide / settings / move / delete controls revealed on hover.
104
+ `draw.copy()`, `draw.cut()` and `draw.paste()` move drawings through the OS clipboard, including between two charts on the page. The payload is JSON under one namespaced key, so foreign text pastes nothing instead of throwing at your Ctrl+V handler, and every field is validated before it reaches the model. A refused clipboard permission does not lose the copy: every write also lands in a shared in-memory clipboard, and a cut deletes only after the write succeeds. A paste is one undo step of fresh objects, nudged two bars and 16 px so it is visibly a second shape. The key bindings stay yours; the engine installs no listeners.
105
+
106
+ ### Panes, scales & legends
107
+ Draggable pane dividers, move / maximize / remove, and pane legends showing one reading per plot in that plot's own colour, with inline show-hide / settings / move / delete controls revealed on hover. The status line is switchable field by field (logo, title, market status, OHLC, bar change, volume, last day change, last value) over a host-supplied data source.
108
+
109
+ Each pane carries a right, a left and a hidden overlay price scale, in four modes: linear, logarithmic, and the two rebasing modes **percentage** (`+3.42%`) and **indexed-to-100** (`103.42`), which quote every price against a baseline taken from the first visible bar, so panning re-bases the axis.
110
+
111
+ ### Reference levels and axis chrome
112
+
113
+ ```ts
114
+ import { PriceLevels } from 'openalgo-charts';
115
+
116
+ const levels = new PriceLevels({
117
+ levels: { previousClose: { line: true, label: true }, sessionHigh: { line: true, label: false } },
118
+ });
119
+ chart.addPrimitive(levels, 0);
120
+ levels.available('bid'); // false until a quote is fed: render that control disabled, not hidden
121
+ ```
122
+
123
+ One primitive over ten levels: previous close, session high and low, last price, the four extended-hours opens and closes, and bid and ask. Each level's line across the plot and its tag on the price axis are two flags in the same options group, so they cannot drift apart. The session comes from the gaps in the bars rather than from a calendar midnight, and the session in view follows the viewport's right edge, so scrolling back through history moves the previous close back with it. A level with no data is `null`, never `0`: nothing draws at zero, and `available(kind)` is the signal to render that control disabled with its state visible instead of hiding it.
124
+
125
+ Axis chrome is off until a chart asks for it. `createChart(el, { axisChrome: { sessionClock: true, barCountdown: true } })` puts a live clock in the corner where the two axis strips meet, in the chart's own timezone with the zone's UTC offset under it, and a countdown to the current bar's close as a second row inside the last-price tag, with the interval read back off the bars so a timeframe switch is followed. Tick labels that the last-price tag would cover are dropped rather than drawn through it, on a priority order that puts the crosshair above the last price, above a price line, above a session level.
126
+
127
+ ### Timezones
128
+
129
+ ```ts
130
+ const chart = createChart(el, { timezone: 'America/New_York' });
131
+ chart.setTimezone('Europe/London'); // relabels and recomputes on the next frame
132
+ ```
133
+
134
+ An IANA name, never a fixed offset, so daylight saving is followed rather than approximated. The zone drives the time axis (including which ticks escalate to a day, month or year label), the crosshair time tag, and every calendar-anchored study (VWAP and TWAP anchors, CPR's weekly and monthly frames, the month a Seasonality bar counts in), and it rides along in `getState()`. Profile session windows carry their own zone, so `TRADING_HOURS['us-regular']` reads as 09:30-16:00 `America/New_York` whatever the chart is displayed in. The default is `Asia/Kolkata` on the same fixed-offset arithmetic it always used, so a chart that names no zone labels and computes exactly as before.
135
+
136
+ ### Market replay
137
+
138
+ ```ts
139
+ import { ReplayController } from 'openalgo-charts';
140
+
141
+ const replay = new ReplayController(chart, { bars, startIndex: 200, barMs: 500 });
142
+ replay.play({ speed: 2 }); // emits replay:frame per bar
143
+ ```
144
+
145
+ Headless: the controller owns the playhead and ships no DOM, so the transport bar is yours to draw from `state()` and the `replay:*` events. Each step hands the series a prefix of the session through the ordinary `setData` path, which is what makes every indicator, level, fill, marker and legend row reconstruct itself as it stood at that bar. `stop()` puts the full history and the exact viewport back.
146
+
147
+ ### Symbol comparison
148
+
149
+ ```ts
150
+ import { addComparison } from 'openalgo-charts';
151
+
152
+ const bn = addComparison(chart, { symbol: 'BANKNIFTY', bars });
153
+ ```
154
+
155
+ The comparison rides the pane's hidden overlay scale in its own real prices, the pane rebases to percentage (or indexed-to-100), and the overlay's range is mirrored from the primary's, so equal percentage moves land on equal pixels instead of each line filling the pane. Alignment is by timestamp: a comparison print with no primary bar is dropped, a primary bar with no print becomes a gap.
156
+
157
+ ### Linked chart grids
158
+
159
+ ```ts
160
+ import { createLinkGroup } from 'openalgo-charts';
161
+
162
+ const group = createLinkGroup({ crosshair: true, viewport: true, symbol: false });
163
+ group.add(daily);
164
+ group.add(hourly, { symbol: 'RELIANCE', onSymbol: (s, c) => loadBars(s, c) });
165
+ ```
166
+
167
+ Hover one chart and the same **instant** is marked on the others; pan or zoom one and the others move to the same wall-clock window. Each channel switches on its own, because mirroring the cursor across four timeframes while keeping each zoom is a different thing from slaving every chart's instrument.
168
+
169
+ Nothing crosses a chart boundary as a logical index. The x axis is a gapless index over each chart's own bars, so index 300 is a different instant on every chart: every value is converted index to time on the sender and time back to index on the receiver, against that chart's own data. A daily chart and an hourly chart with different history depth therefore stay on the same instant, which the naive index copy gets right only when both charts hold the same bars. An instant outside a follower's first or last bar is an absence, not a gap, so it draws nothing; inside its range with no bar there it snaps to the nearest bar in time, or draws nothing under `whenMissing: 'hide'`. The linked crosshair is a vertical line only, at reduced opacity: a mirrored horizontal line would assert a price that belongs to another instrument.
170
+
171
+ Headless like the rest, and the engine has no instrument concept, so symbol sync is a partnership: the host emits `'symbol'` on the chart's bus (or calls `group.setSymbol`) and supplies the per-member `onSymbol` that loads the bars. A member with no `onSymbol` broadcasts but never follows, which is how you pin one chart of a grid.
172
+
173
+ ### Settings & context menu
174
+ `chartSettingsSchema(chart)` describes a full settings dialog as tabs of controls, in the same descriptor vocabulary the indicator settings form already uses; `readChartSettings` and `applyChartSettings` are its round trip over flat, JSON-safe keys. Five tabs (Price, Readout, Axes, Appearance, Trading), and a bullish/bearish pair is **one** `colorPair` row carrying its switch and both swatches instead of two stacked rows. Grid, crosshair, scale text, plot margins, status-line fields, the chart timezone, trading colours and the primary series' own style are all real options behind it, so no control in the schema is inert.
175
+
176
+ `chart.on('contextmenu', ...)` reports the pane, price, time, logical index and what sits under the pointer: a drawing, an indicator instance, a legend, a primitive, a series, a price scale (with the side and the scale id it names), the time scale, or empty plot. For a menu raised on a price axis, `chart.priceAxisState(pane, scaleId)` reads back every item that menu draws (auto-fit, invert, scale mode, price-per-bar lock, whether the axis is movable) and `setPriceAxisOptions`, `setPriceAxisAutoFit`, `setPriceAxisLockRatio` and `movePriceAxis` act on it, so no row is ticked with nothing behind it.
106
177
 
107
178
  ### Trading
108
179
  Order, position, and bracket lines with live P&L, one-click and drag-to-modify, OCO, validation, an order state machine, analyzer (sandbox) mode, and a depth-of-market ladder (5 to 200 levels).
@@ -110,25 +181,38 @@ Order, position, and bracket lines with live P&L, one-click and drag-to-modi
110
181
  ### Profiles & order flow
111
182
  Volume Profile, Market Profile (TPO), Footprint, and cumulative delta.
112
183
 
184
+ ### Warm-load cache & interval registry
185
+
186
+ ```ts
187
+ import { withBarCache, registerInterval } from 'openalgo-charts';
188
+
189
+ const feed = withBarCache(new OpenAlgoDataFeed(cfg), { ttlMs: 60_000 });
190
+ registerInterval({ code: '1MO', bucketing: { mode: 'calendar', unit: 'month' } });
191
+ ```
192
+
193
+ `withBarCache` wraps **any** `DataFeed`, so a custom feed warms up too. One entry per `symbol|exchange|interval` holds the widest range fetched so far and a narrower request is sliced out of it, because keying on the range would miss on every pan. **The forming bar is never stored**: a frozen snapshot of a live candle reaching the last-price line, the header LTP and every indicator computed off that close is worse than no cache at all, so coverage ends at the last closed bar and a hit is short by the one bar a live subscription re-supplies. Freshness is two gates, a TTL and "nothing new can have closed", the second measured on the feed's own bar grid rather than UTC midnight. Bounded LRU on entries and on total bars, in-memory by default with an injectable store if you want localStorage or IndexedDB, and `noCache`, `invalidate()`, `clear()` and `stats()` for the rest.
194
+
195
+ An interval code resolves through a registry whose entry is a **bucketing rule, not a duration**: fixed seconds, a calendar month/quarter/year that opens at local midnight in a named zone, N ticks, or N traded quantity. That is the vocabulary the tick aggregator already used, widened by one case. An unrecognised code now throws `UnknownIntervalError` instead of quietly meaning 60 seconds, so a subscription fails at subscribe time rather than drawing minute bars under someone else's label; `tryResolveInterval` and `isKnownInterval` are the non-throwing probes for validating a picker.
196
+
113
197
  ### State
114
- `chart.getState()` / `chart.restoreState()` capture the viewport, grid, panes, price scales, indicator instances, and drawings as one JSON payload saved layouts and templates with no extra storage plumbing.
198
+ `chart.getState()` / `chart.restoreState()` capture the viewport, grid, panes, price scales, indicator instances, drawings, and the whole settings block (canvas, status line, trading colours, event filters) as one JSON payload: saved layouts and templates with no extra storage plumbing.
115
199
 
116
200
  ### Data
117
- OpenAlgo REST history + WebSocket ticks with auto-reconnect and resubscribe, live candle aggregation, tick/volume bars, a unified `chart.on(...)` event bus, markers and signals, earnings/dividend/expiry event markers, and custom price/time formatters.
201
+ OpenAlgo REST history + WebSocket ticks with auto-reconnect and resubscribe, live candle aggregation, tick/volume bars, a unified `chart.on(...)` event bus, markers and signals, earnings/dividend/expiry event markers, an IANA chart timezone, and custom price/time formatters.
118
202
 
119
203
  ## Size budget
120
204
 
121
- Enforced in CI by [`size-limit`](./.size-limit.json) nothing is excluded, because there are no runtime dependencies to exclude.
205
+ Enforced in CI by [`size-limit`](./.size-limit.json). Nothing is excluded, because there are no runtime dependencies to exclude.
122
206
 
123
207
  | Bundle | Limit | Actual |
124
208
  |---|---|---|
125
- | Base engine | 40 KB | 37.04 KB |
126
- | Base + trade | 47 KB | 43.62 KB |
127
- | Indicators tier | 21 KB | 20.09 KB |
128
- | Draw tier | 14 KB | 11.73 KB |
209
+ | Base engine | 55 KB | 52.88 KB |
210
+ | Base + trade | 62 KB | 59.46 KB |
211
+ | Indicators tier | 27 KB | 24.88 KB |
212
+ | Draw tier | 14 KB | 13.11 KB |
129
213
  | Transform tier | 5 KB | 2.66 KB |
130
- | Profile tier | 11 KB | 10.12 KB |
131
- | **Everything** | **90 KB** | **88.21 KB** |
214
+ | Profile tier | 11 KB | 10.66 KB |
215
+ | **Everything** | **120 KB** | **110.76 KB** |
132
216
 
133
217
  ## Documentation
134
218
 
@@ -151,7 +235,7 @@ Teach your AI coding assistant this library:
151
235
  npx skills add https://github.com/marketcalls/openalgo-charts
152
236
  ```
153
237
 
154
- Installs six skills from [`.github/skills/`](./.github/skills) - a reference hub with 18 deep-dive files covering the whole API surface and its foot-guns, plus task skills for scaffolding a chart, adding indicators, building a terminal, writing a plugin, and debugging. Works with Claude Code, Cursor, Codex, Copilot, Gemini CLI and the rest of the `skills` CLI's supported agents.
238
+ Installs six skills from [`.github/skills/`](./.github/skills) - a reference hub with 21 deep-dive files covering the whole API surface and its foot-guns, plus task skills for scaffolding a chart, adding indicators, building a terminal, writing a plugin, and debugging. Works with Claude Code, Cursor, Codex, Copilot, Gemini CLI and the rest of the `skills` CLI's supported agents.
155
239
 
156
240
  ## Examples
157
241
 
@@ -168,7 +252,7 @@ cd examples/yfinance && pip install -r requirements.txt && python server.py
168
252
  ```bash
169
253
  npm install # install dev toolchain
170
254
  npm run typecheck # strict TypeScript check
171
- npm test # unit tests (vitest) - 1001 across 61 files
255
+ npm test # unit tests (vitest) - 1719 across 91 files
172
256
  npm run build # Rollup -> dist/ (minified ESM per tier + types)
173
257
  npm run size # size-limit (Brotli) against the budget
174
258
  npm run e2e # Playwright Chromium smoke tests
@@ -177,22 +261,22 @@ npm run verify # typecheck + test + build + size
177
261
 
178
262
  ## Principles
179
263
 
180
- - **Single canvas pipeline** (no SVG, no DOM-per-bar) small and fast.
181
- - **Gapless time axis by default** weekends, holidays, and session breaks collapse.
182
- - **Registries, not switches** chart types, indicators, and drawing tools are all descriptors. Adding one is a registration, never a core change.
183
- - **Zero runtime dependencies** nothing is excluded from the size budget.
264
+ - **Single canvas pipeline** (no SVG, no DOM-per-bar): small and fast.
265
+ - **Gapless time axis by default**: weekends, holidays, and session breaks collapse.
266
+ - **Registries, not switches**: chart types, indicators, and drawing tools are all descriptors. Adding one is a registration, never a core change.
267
+ - **Zero runtime dependencies**: nothing is excluded from the size budget.
184
268
  - **Apache-2.0**, original code.
185
269
 
186
270
  ## Status & limitations
187
271
 
188
- Version **1.1.0**. All engine build phases are implemented with 1001 unit tests across 61 files.
272
+ Version **1.4.0**. All engine build phases are implemented with 1727 unit tests across 91 files.
189
273
 
190
274
  Known gaps, stated plainly:
191
275
 
192
- - **Footprint and order flow need trade-by-trade data classified bid/ask.** OpenAlgo does not store this by default, so it is live-session-only unless you add a tick recorder `FootprintAggregator` is the live path. See [`ARCHITECTURE.md`](./ARCHITECTURE.md) §6A.
276
+ - **Footprint and order flow need trade-by-trade data classified bid/ask.** OpenAlgo does not store this by default, so it is live-session-only unless you add a tick recorder: `FootprintAggregator` is the live path. See [`ARCHITECTURE.md`](./ARCHITECTURE.md) §6A.
193
277
  - **Only `Footprint` is theme-aware among the profile primitives.** `VolumeProfile`, `MarketProfile` and `HorizontalProfile` never read `rc.theme`; their defaults are dark-tuned, so a light theme needs explicit colours. `HorizontalProfile` also hardcodes its POC / value-area line colours and has no `setOptions`.
194
278
  - The OpenAlgo **WS/trade adapter wire schemas** ship with injectable transports and offline tests, but the exact field names should be verified against your running OpenAlgo build.
195
- - Price-scale **`percentage`** and **`indexed-to-100`** modes are not implemented. (Hidden overlay scales *are* add a series with `priceScaleId: ''`.)
279
+ - **A pane has exactly one hidden overlay scale**, so every symbol comparison on a pane shares one baseline. That is right for a single comparison, the common case, but a second one on the same pane is quoted against the first instrument's price; put further instruments on their own pane with `paneIndex` until the overlay scales are keyed.
196
280
  - An optional **DOM chrome package** (toolbar, dialogs, command palette, objects panel) is the next planned piece; today that UI lives in the examples.
197
281
 
198
282
  See [`ARCHITECTURE.md`](./ARCHITECTURE.md) §13a for the full deferred list.
@@ -254,6 +254,115 @@ declare class DrawingLayer implements IPrimitive {
254
254
  hitTest(x: number, y: number, rc: PrimitiveRenderContext): PrimitiveHit | null;
255
255
  }
256
256
 
257
+ /**
258
+ * Clipboard transfer for drawings: the layer between the drawing model and the
259
+ * OS clipboard.
260
+ *
261
+ * Two things make this more than `JSON.stringify`:
262
+ *
263
+ * 1. **The OS clipboard is shared with everything else on the machine.** A
264
+ * paste can arrive from a spreadsheet, another charting product, or a
265
+ * hand-edited copy of our own payload. So the payload is written under a
266
+ * namespaced key and everything read back is validated field by field before
267
+ * it can reach the model. Foreign text is *not ours* and is ignored, never an
268
+ * exception the host has to catch on every Ctrl+V.
269
+ * 2. **Clipboard access is async and permission-gated.** `navigator.clipboard`
270
+ * rejects when the document is not focused, the page is not secure, or the
271
+ * user denied the permission. Copy must still work inside the tab, so every
272
+ * write also lands in a module-level in-memory clipboard which is shared by
273
+ * every controller in the page. That is what makes chart-to-chart paste work
274
+ * with the permission denied, and it is why the memory store is a singleton
275
+ * rather than per-controller state.
276
+ */
277
+
278
+ /**
279
+ * Top-level key of the JSON payload. Namespaced so a paste of arbitrary text,
280
+ * or of JSON belonging to some other application, is recognisable as not ours
281
+ * by looking at one property.
282
+ */
283
+ declare const DRAWING_CLIPBOARD_KEY = "openalgo-charts/drawings";
284
+ /** Payload format version. Bumped only when the on-clipboard shape changes. */
285
+ declare const DRAWING_CLIPBOARD_VERSION = 1;
286
+ /** The async slice of `navigator.clipboard` this module uses. */
287
+ interface ClipboardPort {
288
+ writeText(text: string): Promise<void>;
289
+ readText(): Promise<string>;
290
+ }
291
+ /** Test seam: drop whatever the in-memory clipboard is holding. */
292
+ declare function clearMemoryClipboard(): void;
293
+ /** `navigator.clipboard` when the browser exposes it, else null. */
294
+ declare function systemClipboard(): ClipboardPort | null;
295
+ /** Serialise drawings into the namespaced payload written to the clipboard. */
296
+ declare function encodeClipboardPayload(drawings: readonly Drawing[]): string;
297
+ /**
298
+ * Validate one entry into a drawing with no id. Returns null for anything that
299
+ * cannot be rendered: an unknown tool would throw inside the controller, and a
300
+ * NaN anchor produces a drawing that can never be drawn or hit-tested again.
301
+ */
302
+ declare function sanitizeDrawing(value: unknown): Omit<Drawing, 'id'> | null;
303
+ /**
304
+ * Parse clipboard text into drawings, or null when the text is not ours or is
305
+ * not usable. All-or-nothing on purpose: a payload with one corrupt entry is a
306
+ * corrupt payload, and pasting the other nine silently would be worse than
307
+ * pasting none.
308
+ */
309
+ declare function decodeClipboardPayload(text: unknown): Omit<Drawing, 'id'>[] | null;
310
+ interface DrawingClipboardOptions {
311
+ /** Where to read and write. Defaults to `navigator.clipboard` when present. */
312
+ port?: ClipboardPort | null;
313
+ /**
314
+ * Keep a copy in the shared in-memory clipboard so copy and paste keep
315
+ * working when the OS clipboard is unavailable or the permission is refused.
316
+ * Default true. Set false to make a failed system write a failed copy, which
317
+ * a host that must not silently lose cross-tab transfer may prefer.
318
+ */
319
+ fallbackToMemory?: boolean;
320
+ }
321
+ /**
322
+ * The clipboard as the controller sees it: drawings in, drawings out, no
323
+ * exceptions escaping. Every failure mode (no clipboard API, refused
324
+ * permission, foreign text, corrupt payload) reduces to `false` or `null`.
325
+ */
326
+ declare class DrawingClipboard {
327
+ private _port;
328
+ private _fallback;
329
+ /** Why the last operation did not reach the OS clipboard, if it did not. */
330
+ private _lastError;
331
+ constructor(options?: DrawingClipboardOptions);
332
+ /** Swap the port at runtime (a host granting permission later, or a test). */
333
+ setPort(port: ClipboardPort | null): void;
334
+ /**
335
+ * Turn the in-memory backstop off or on. With it off, a refused system write
336
+ * is a failed copy, which is what a host wants when a cut that cannot reach
337
+ * the OS clipboard must not delete the drawing.
338
+ */
339
+ setFallbackToMemory(enabled: boolean): void;
340
+ /**
341
+ * Why the OS clipboard was not used by the last call, or null when it was.
342
+ * Set on a successful copy too, when the payload reached memory but not the
343
+ * system clipboard: that copy works in this tab and will not appear in
344
+ * another, which is exactly what a host wants to be able to tell the user.
345
+ */
346
+ lastError(): string | null;
347
+ /**
348
+ * Write drawings out. Resolves true when the payload is retrievable by a
349
+ * later `read()` (through the system clipboard, or through memory when the
350
+ * fallback is on), false when it is not, so a caller doing a cut knows
351
+ * whether it is safe to delete the original.
352
+ */
353
+ write(drawings: readonly Drawing[]): Promise<boolean>;
354
+ /**
355
+ * Read drawings back, or null when there is nothing of ours to paste. The
356
+ * system clipboard wins when it holds our payload, so a copy made in another
357
+ * tab beats a stale in-tab one; memory is consulted when the read fails or
358
+ * returns something that is not ours. That last case can paste an older copy
359
+ * after the user has copied unrelated text elsewhere, which is the deliberate
360
+ * trade: losing the copy whenever a browser refuses the read half of the
361
+ * permission would be the worse surprise.
362
+ */
363
+ read(): Promise<Omit<Drawing, 'id'>[] | null>;
364
+ }
365
+
257
366
  /**
258
367
  * Drawing controller — the interaction and persistence layer over
259
368
  * `DrawingLayer`. It is **headless**: no DOM, no toolbar. A host sets the
@@ -289,6 +398,21 @@ interface DrawingChartHost {
289
398
  drawingState(): unknown;
290
399
  setDrawingState(state: unknown): void;
291
400
  setPlacementMode?(active: boolean): void;
401
+ /**
402
+ * Optional, and used only to offset a paste by a fixed screen distance. Going
403
+ * through pixels rather than adding a price delta keeps the offset the same
404
+ * visible nudge on a log scale as on a linear one. A host without them still
405
+ * gets the time half of the offset.
406
+ */
407
+ priceToCoordinate?(price: number, paneIndex?: number): number | null;
408
+ coordinateToPrice?(y: number, paneIndex?: number): number | null;
409
+ /**
410
+ * Optional, and used only to keep a paste from a chart with more panes than
411
+ * this one landing on a pane the user cannot see. Adding a primitive creates
412
+ * the pane it names, so without this a drawing copied out of an indicator
413
+ * pane would conjure an empty pane in a single-pane chart.
414
+ */
415
+ panes?(): readonly unknown[];
292
416
  }
293
417
  interface DrawingControllerOptions {
294
418
  /**
@@ -302,10 +426,34 @@ interface DrawingControllerOptions {
302
426
  stayInDrawingMode?: boolean;
303
427
  /** Undo depth. Default 50. */
304
428
  historyLimit?: number;
429
+ /**
430
+ * Where copy and paste move text. Defaults to `navigator.clipboard`; pass a
431
+ * port to route through a host's own transfer, or `null` to stay in the
432
+ * process-local clipboard entirely.
433
+ */
434
+ clipboard?: ClipboardPort | null;
435
+ /**
436
+ * Whether a refused or failing clipboard write still lands in the in-process
437
+ * clipboard. Defaults to true, which is what makes copy and paste work between
438
+ * two charts on a page where the browser has denied clipboard permission.
439
+ *
440
+ * Pass false for a host that would rather a failed copy be a failed copy: with
441
+ * it off, `cut` leaves the drawing alone when the write does not land, so a
442
+ * shape is never destroyed for a transfer that did not happen.
443
+ */
444
+ clipboardFallbackToMemory?: boolean;
445
+ /**
446
+ * How far a pasted copy lands from its original, in bars along time and in
447
+ * screen pixels down the price axis. A paste that lands exactly on top of the
448
+ * original reads as nothing having happened. Defaults: 2 bars, 16 px.
449
+ */
450
+ pasteOffsetBars?: number;
451
+ pasteOffsetPixels?: number;
305
452
  }
306
453
  declare class DrawingController {
307
454
  private readonly _chart;
308
455
  private _opts;
456
+ private readonly _clipboard;
309
457
  private readonly _layers;
310
458
  private _drawings;
311
459
  private _tool;
@@ -330,6 +478,8 @@ declare class DrawingController {
330
478
  private _setPlacementMode;
331
479
  activeTool(): string | null;
332
480
  setOptions(patch: DrawingControllerOptions): void;
481
+ /** The clipboard behind copy / cut / paste, for a host reporting failures. */
482
+ clipboard(): DrawingClipboard;
333
483
  /** Every drawing, in creation order. */
334
484
  drawings(): readonly Drawing[];
335
485
  get(id: string): Drawing | undefined;
@@ -337,11 +487,52 @@ declare class DrawingController {
337
487
  add(drawing: Omit<Drawing, 'id'> & {
338
488
  id?: string;
339
489
  }): Drawing;
490
+ /**
491
+ * Append one drawing without touching history or the layers. Split out so a
492
+ * paste of several drawings is a single undo step rather than one per shape.
493
+ */
494
+ private _insert;
340
495
  update(id: string, patch: Partial<Pick<Drawing, 'points' | 'style' | 'locked' | 'visible'>>): boolean;
341
496
  remove(id: string): boolean;
342
497
  clear(): void;
343
498
  select(id: string | null): void;
344
499
  selected(): string | null;
500
+ /**
501
+ * Put drawings on the clipboard. Defaults to the selection; pass an id or a
502
+ * list of ids to copy something else. Resolves false when there was nothing
503
+ * to copy, or when the payload could not be stored anywhere.
504
+ */
505
+ copy(target?: string | string[] | null): Promise<boolean>;
506
+ /**
507
+ * Copy, then delete. The delete happens **only** after the clipboard write
508
+ * resolves successfully, so a refused write leaves the model exactly as it
509
+ * was rather than destroying a drawing that went nowhere.
510
+ */
511
+ cut(target?: string | string[] | null): Promise<boolean>;
512
+ /**
513
+ * Paste whatever is on the clipboard into this chart, offset from the
514
+ * original so the copy is visibly a second object. Each pasted drawing is a
515
+ * fresh object with a fresh id, never a second reference to the one copied,
516
+ * so editing the paste cannot alter its source (or the clipboard).
517
+ *
518
+ * Anything that is not our payload (foreign text, a truncated or hand-edited
519
+ * copy, a newer format) pastes nothing and resolves to an empty array: a
520
+ * paste shortcut must not throw at the host because the user last copied a
521
+ * spreadsheet cell.
522
+ */
523
+ paste(): Promise<Drawing[]>;
524
+ /** Resolve a copy/cut target to live drawings; defaults to the selection. */
525
+ private _targets;
526
+ /** Fold a pane index from another chart onto a pane this one actually has. */
527
+ private _clampPane;
528
+ /** Nudge every anchor so a pasted copy is not hidden under its original. */
529
+ private _offsetPoints;
530
+ /**
531
+ * Move a price down the screen by `px`. Done per anchor rather than as one
532
+ * price delta so the paste is a rigid *screen* translation, which is what the
533
+ * eye expects and what keeps a shape's proportions on a log scale.
534
+ */
535
+ private _offsetPrice;
345
536
  undo(): boolean;
346
537
  redo(): boolean;
347
538
  canUndo(): boolean;
@@ -356,6 +547,12 @@ declare class DrawingController {
356
547
  * build a 1:1 box off one click). Identity for tools without the hook.
357
548
  */
358
549
  private _expand;
550
+ /**
551
+ * Bar spacing in seconds, read from the last gap in the data. A one-bar chart
552
+ * has no gap to read, so fall back to a minute rather than answering zero and
553
+ * producing zero-width defaults and invisible paste offsets.
554
+ */
555
+ private _barSeconds;
359
556
  private _isFreehand;
360
557
  /**
361
558
  * Append one sample to the stroke in progress. Points arriving closer than a
@@ -426,4 +623,4 @@ declare function extendSegment(a: ScreenPoint, b: ScreenPoint, width: number, le
426
623
 
427
624
  declare const DRAW_TIER: "draw";
428
625
 
429
- export { ARROW, BUILTIN_DRAWING_TOOLS, CROSS_LINE, DRAW_TIER, type DrawContext, type Drawing, DrawingController, type DrawingControllerOptions, DrawingLayer, type DrawingPoint, type DrawingStyle, type DrawingTool, ELLIPSE, EXTENDED_LINE, FIB_EXTENSION, FIB_RETRACEMENT, HORIZONTAL_LINE, HORIZONTAL_RAY, type HitContext, LONG_POSITION, MEASURE, PARALLEL_CHANNEL, PATH, RAY, RECTANGLE, SHORT_POSITION, type ScreenPoint, type ShortcutEvent, TEXT, TREND_LINE, VERTICAL_LINE, distToEllipse, distToLine, distToPolyline, distToRect, distToSegment, drawingShortcuts, extendSegment, getDrawingTool, hasDrawingTool, matchDrawingShortcut, rectOf, registerBuiltinDrawingTools, registerDrawingTool, registeredDrawingTools };
626
+ export { ARROW, BUILTIN_DRAWING_TOOLS, CROSS_LINE, type ClipboardPort, DRAWING_CLIPBOARD_KEY, DRAWING_CLIPBOARD_VERSION, DRAW_TIER, type DrawContext, type Drawing, DrawingClipboard, type DrawingClipboardOptions, DrawingController, type DrawingControllerOptions, DrawingLayer, type DrawingPoint, type DrawingStyle, type DrawingTool, ELLIPSE, EXTENDED_LINE, FIB_EXTENSION, FIB_RETRACEMENT, HORIZONTAL_LINE, HORIZONTAL_RAY, type HitContext, LONG_POSITION, MEASURE, PARALLEL_CHANNEL, PATH, RAY, RECTANGLE, SHORT_POSITION, type ScreenPoint, type ShortcutEvent, TEXT, TREND_LINE, VERTICAL_LINE, clearMemoryClipboard, decodeClipboardPayload, distToEllipse, distToLine, distToPolyline, distToRect, distToSegment, drawingShortcuts, encodeClipboardPayload, extendSegment, getDrawingTool, hasDrawingTool, matchDrawingShortcut, rectOf, registerBuiltinDrawingTools, registerDrawingTool, registeredDrawingTools, sanitizeDrawing, systemClipboard };