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 +115 -31
- package/dist/draw/index.d.ts +198 -1
- package/dist/index.d.ts +2413 -68
- package/dist/indicators/index.d.ts +4 -3
- package/dist/openalgo-charts.draw.mjs +1 -1
- package/dist/openalgo-charts.draw.mjs.map +1 -1
- package/dist/openalgo-charts.indicators.mjs +1 -1
- package/dist/openalgo-charts.indicators.mjs.map +1 -1
- package/dist/openalgo-charts.mjs +1 -1
- package/dist/openalgo-charts.mjs.map +1 -1
- package/dist/openalgo-charts.profile.mjs +1 -1
- package/dist/openalgo-charts.profile.mjs.map +1 -1
- package/dist/openalgo-charts.standalone.js +1 -1
- package/dist/openalgo-charts.standalone.js.map +1 -1
- package/dist/profile/index.d.ts +64 -21
- package/package.json +1 -1
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
|
|
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
|
[](https://www.npmjs.com/package/openalgo-charts)
|
|
10
10
|
[](./LICENSE)
|
|
11
|
-
[](#size-budget)
|
|
12
|
+
[](#develop)
|
|
13
13
|
[](#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 |
|
|
64
|
-
| `openalgo-charts/indicators` | 91 built-in indicators + the Tier-2 (external-data) contract | 24.
|
|
65
|
-
| `openalgo-charts/draw` | 43 drawing tools + a headless drawing controller |
|
|
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.
|
|
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 **
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
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
|
|
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)
|
|
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 |
|
|
126
|
-
| Base + trade |
|
|
127
|
-
| Indicators tier |
|
|
128
|
-
| Draw tier | 14 KB | 11
|
|
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.
|
|
131
|
-
| **Everything** | **
|
|
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
|
|
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) -
|
|
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)
|
|
181
|
-
- **Gapless time axis by default
|
|
182
|
-
- **Registries, not switches
|
|
183
|
-
- **Zero runtime dependencies
|
|
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.
|
|
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
|
|
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
|
-
-
|
|
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.
|
package/dist/draw/index.d.ts
CHANGED
|
@@ -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 };
|