emberwick 0.1.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/LICENSE +21 -0
- package/README.md +530 -0
- package/index.d.ts +257 -0
- package/index.js +1310 -0
- package/index.js.map +1 -0
- package/package.json +56 -0
- package/react.d.ts +31 -0
- package/react.js +76 -0
- package/react.js.map +1 -0
- package/umd/emberwick.umd.js +2 -0
- package/umd/emberwick.umd.js.map +1 -0
- package/webcomponent.d.ts +16 -0
- package/webcomponent.js +75 -0
- package/webcomponent.js.map +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Emberwick contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
# Emberwick
|
|
2
|
+
|
|
3
|
+
A smooth-flowing candlestick chart for the web. Canvas-rendered, framework-free,
|
|
4
|
+
and driven by a pluggable data feed — drop it into any financial frontend and
|
|
5
|
+
point it at your own market data.
|
|
6
|
+
|
|
7
|
+
- **Zero dependencies.** The core imports nothing but DOM and Canvas APIs.
|
|
8
|
+
- **Framework-agnostic.** Works in React, Vue, Svelte, or a plain `<script type="module">`.
|
|
9
|
+
- **Actually smooth.** Ticks ease into the forming candle, the price axis glides
|
|
10
|
+
to new bounds, zoom is cursor-anchored and eased, panning has inertia.
|
|
11
|
+
- **Fast on big data.** One `requestAnimationFrame` loop, dirty-flag driven,
|
|
12
|
+
with visible-range culling — 500k bars loaded costs only the ~200 on screen.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installing
|
|
17
|
+
|
|
18
|
+
### From npm
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install emberwick
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { createChart, RandomFeed } from 'emberwick'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> **Not published yet.** The package builds and packs (`npm run release`), but
|
|
29
|
+
> it has not been pushed to the registry, and the name has not been confirmed
|
|
30
|
+
> available — run `npm view emberwick` before relying on it.
|
|
31
|
+
|
|
32
|
+
### From a CDN, no build step
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<div id="chart" style="height: 480px"></div>
|
|
36
|
+
<script src="https://unpkg.com/emberwick/umd/emberwick.umd.js"></script>
|
|
37
|
+
<script>
|
|
38
|
+
const chart = Emberwick.createChart(document.getElementById('chart'))
|
|
39
|
+
chart.setFeed(new Emberwick.RandomFeed({ timeframe: 60000, speed: 60 }))
|
|
40
|
+
</script>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The UMD build is core-only and exposes the global `Emberwick`.
|
|
44
|
+
|
|
45
|
+
### By vendoring the source
|
|
46
|
+
|
|
47
|
+
Nothing stops you copying the core in directly:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cp -r src/chart /path/to/your-project/src/emberwick
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
import { createChart, RandomFeed } from './emberwick/index.js'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The folder is self-contained — `src/chart/` has no imports that point outside
|
|
58
|
+
itself, and no framework dependency. Anything that can bundle ES modules
|
|
59
|
+
(Vite, webpack, Rollup, esbuild, or a browser with native ESM) can consume it
|
|
60
|
+
as-is.
|
|
61
|
+
|
|
62
|
+
### Entry points
|
|
63
|
+
|
|
64
|
+
| Import | Contents |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `emberwick` | The core: `createChart`, `Chart`, feeds, themes, motion primitives |
|
|
67
|
+
| `emberwick/react` | `<EmberwickChart />` React component |
|
|
68
|
+
| `emberwick/webcomponent` | Registers `<emberwick-chart>` (side-effecting import) |
|
|
69
|
+
| `emberwick/umd` | Single-file UMD build for `<script>` tags |
|
|
70
|
+
|
|
71
|
+
TypeScript declarations ship for all three entries.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Quick start
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
import { createChart, RandomFeed } from 'emberwick'
|
|
79
|
+
|
|
80
|
+
const chart = createChart(document.getElementById('chart'))
|
|
81
|
+
await chart.setFeed(new RandomFeed({ timeframe: 60_000 }))
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The container needs a real size — the chart fills it and follows resizes:
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<div id="chart" style="width: 100%; height: 480px"></div>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> The container's `position` is set to `relative` automatically if it is
|
|
91
|
+
> `static`, because the canvas layers are absolutely positioned inside it.
|
|
92
|
+
|
|
93
|
+
Without a feed, push bars in directly:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
const chart = createChart(el)
|
|
97
|
+
chart.setData(bars) // Bar[], ascending by time
|
|
98
|
+
chart.update(formingBar) // merge a tick into the last candle (animates)
|
|
99
|
+
chart.append(newBar) // open a new candle
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## The bar shape
|
|
105
|
+
|
|
106
|
+
One contract, used everywhere:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
{
|
|
110
|
+
time: 1737024000000, // ms epoch, start of the bar
|
|
111
|
+
open: 100.2,
|
|
112
|
+
high: 101.4,
|
|
113
|
+
low: 99.8,
|
|
114
|
+
close: 100.9,
|
|
115
|
+
volume: 1420,
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Bars must be **ascending by time** and **de-duplicated**. The chart infers the
|
|
120
|
+
timeframe from the gap between the first two bars (or from `feed.timeframe`).
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Plugging in your own data — the `DataFeed` interface
|
|
125
|
+
|
|
126
|
+
This is the seam the whole library is built around. The core never knows where
|
|
127
|
+
bars come from: implement two methods and it works.
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
import { DataFeed } from 'emberwick'
|
|
131
|
+
|
|
132
|
+
class MyApiFeed extends DataFeed {
|
|
133
|
+
constructor() {
|
|
134
|
+
super({ symbol: 'AAPL', timeframe: 60_000 })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Historical bars ENDING at `to` (exclusive). Return [] when exhausted.
|
|
138
|
+
async getBars({ symbol, timeframe, to, limit }) {
|
|
139
|
+
const qs = new URLSearchParams({ symbol, tf: timeframe, limit })
|
|
140
|
+
if (to) qs.set('to', to)
|
|
141
|
+
const res = await fetch(`/api/candles?${qs}`)
|
|
142
|
+
return res.json() // Bar[], ascending by time
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Live updates. Return an unsubscribe function.
|
|
146
|
+
subscribe(handler) {
|
|
147
|
+
const ws = new WebSocket('wss://example.com/stream')
|
|
148
|
+
ws.onmessage = (e) => {
|
|
149
|
+
const { bar, closed } = JSON.parse(e.data)
|
|
150
|
+
handler({ type: closed ? 'append' : 'update', bar })
|
|
151
|
+
}
|
|
152
|
+
return () => ws.close()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await chart.setFeed(new MyApiFeed())
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Feed contract
|
|
160
|
+
|
|
161
|
+
| Member | Required | Purpose |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `symbol` | yes | Passed back to you in `getBars` |
|
|
164
|
+
| `timeframe` | yes | Bar duration in ms; sets the chart's time axis |
|
|
165
|
+
| `getBars({ symbol, timeframe, to, limit })` | yes | `Promise<Bar[]>`. `to: null` means "most recent". Return `[]` to signal no more history |
|
|
166
|
+
| `subscribe(handler)` | for live data | Returns an unsubscribe function |
|
|
167
|
+
| `prime(lastBar)` | optional | Called once after history loads, so the feed can seed its forming candle |
|
|
168
|
+
| `destroy()` | optional | Your own cleanup |
|
|
169
|
+
|
|
170
|
+
### Update messages
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
handler({ type: 'update', bar }) // forming candle changed → animates
|
|
174
|
+
handler({ type: 'append', bar }) // a new candle opened
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`update` is the one that produces the flowing motion. Send it as often as your
|
|
178
|
+
feed ticks — 500 messages between two frames still cost exactly one repaint,
|
|
179
|
+
because rendering is decoupled from data arrival.
|
|
180
|
+
|
|
181
|
+
### Lazy history
|
|
182
|
+
|
|
183
|
+
When the user pans to within **80 bars** of the left edge, the chart calls
|
|
184
|
+
`getBars({ to: oldestLoadedTime })` and prepends the result, holding the view
|
|
185
|
+
on the same bars. Return an empty array and it stops asking permanently.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## API
|
|
190
|
+
|
|
191
|
+
### `createChart(container, options?)`
|
|
192
|
+
|
|
193
|
+
Returns a `Chart`. (`new Chart(container, options)` is equivalent.)
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
const chart = createChart(el, {
|
|
197
|
+
theme: { background: '#000', up: '#00d68f' },
|
|
198
|
+
volumeRatio: 0.18, // fraction of height for the volume strip
|
|
199
|
+
magnet: true, // crosshair snaps to nearest OHLC
|
|
200
|
+
animate: true, // live-candle easing
|
|
201
|
+
initialBars: 1500, // first getBars() page size
|
|
202
|
+
timeScale: { spacing: 9, minSpacing: 0.8, maxSpacing: 160, rightOffset: 12 },
|
|
203
|
+
priceScale: { mode: 'linear', tau: 120, marginTop: 0.12, marginBottom: 0.12 },
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Methods
|
|
208
|
+
|
|
209
|
+
| Method | Description |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `setData(bars)` | Replace all bars and snap to the right edge |
|
|
212
|
+
| `update(bar)` | Merge a tick into the forming candle (animated) |
|
|
213
|
+
| `append(bar)` | Open a new candle |
|
|
214
|
+
| `setFeed(feed)` | `async` — loads history, then subscribes. Detaches any previous feed |
|
|
215
|
+
| `detachFeed()` | Unsubscribe, keep the bars on screen |
|
|
216
|
+
| `subscribe('crosshair', fn)` | Returns an unsubscribe fn. See events below |
|
|
217
|
+
| `setTheme(partial)` | Merge theme keys and repaint |
|
|
218
|
+
| `setPriceMode(mode)` | `'linear'` or `'log'` |
|
|
219
|
+
| `setAnimate(bool)` | Toggle live-candle easing |
|
|
220
|
+
| `setMagnet(bool)` | Toggle crosshair OHLC snapping |
|
|
221
|
+
| `snapToRealtime()` | Jump back to the newest bar and re-enable autoscale |
|
|
222
|
+
| `toImage()` | PNG data URL of the composited layers |
|
|
223
|
+
| `destroy()` | Remove listeners, stop the loop, drop canvases |
|
|
224
|
+
| `chart.fps` | Getter — measured frames per second |
|
|
225
|
+
|
|
226
|
+
### Events
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
const off = chart.subscribe('crosshair', (payload) => {
|
|
230
|
+
if (!payload) return // pointer left the plot
|
|
231
|
+
const { index, bar, price } = payload
|
|
232
|
+
legend.textContent = `O ${bar.open} H ${bar.high} L ${bar.low} C ${bar.close}`
|
|
233
|
+
})
|
|
234
|
+
off() // unsubscribe
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`'crosshair'` is the only event that currently fires. See *Known gaps* below.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Interaction
|
|
242
|
+
|
|
243
|
+
| Input | Action |
|
|
244
|
+
|---|---|
|
|
245
|
+
| Drag in plot | Pan (with inertia on release) |
|
|
246
|
+
| Wheel | Zoom, anchored on the cursor |
|
|
247
|
+
| Two-finger pinch | Zoom |
|
|
248
|
+
| Drag price axis | Stretch the price scale |
|
|
249
|
+
| Drag time axis | Zoom the time scale |
|
|
250
|
+
| Double-click | Snap back to realtime |
|
|
251
|
+
| `←` / `→` | Pan (hold `Shift` for a bigger step) |
|
|
252
|
+
| `+` / `-` | Zoom |
|
|
253
|
+
|
|
254
|
+
The container gets `tabindex="0"` if it has none, so keyboard nav works
|
|
255
|
+
without extra markup.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Theming
|
|
260
|
+
|
|
261
|
+
Pass any subset of the theme keys; the rest fall back to `defaultTheme`.
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
import { defaultTheme, lightTheme } from 'emberwick'
|
|
265
|
+
|
|
266
|
+
chart.setTheme(lightTheme)
|
|
267
|
+
chart.setTheme({ up: '#00d68f', down: '#ff5c5c', background: '#0b0e14' })
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Available keys: `background`, `grid`, `axisLine`, `text`, `textStrong`, `up`,
|
|
271
|
+
`down`, `upFill`, `downFill`, `wickUp`, `wickDown`, `volumeUp`, `volumeDown`,
|
|
272
|
+
`crosshair`, `labelBg`, `labelText`, `tagText`, `font`, `priceAxisWidth`,
|
|
273
|
+
`timeAxisHeight`.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Framework integration
|
|
278
|
+
|
|
279
|
+
### React — the bundled adapter
|
|
280
|
+
|
|
281
|
+
```jsx
|
|
282
|
+
import { EmberwickChart } from 'emberwick/react'
|
|
283
|
+
import { RandomFeed } from 'emberwick'
|
|
284
|
+
import { useMemo, useRef } from 'react'
|
|
285
|
+
|
|
286
|
+
export function Chart() {
|
|
287
|
+
// Memoise the feed — a new instance re-loads history.
|
|
288
|
+
const feed = useMemo(() => new RandomFeed({ timeframe: 60_000, speed: 60 }), [])
|
|
289
|
+
const ref = useRef(null)
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div style={{ height: 480 }}>
|
|
293
|
+
<EmberwickChart
|
|
294
|
+
ref={ref}
|
|
295
|
+
feed={feed}
|
|
296
|
+
priceMode="linear"
|
|
297
|
+
theme={{ up: '#00d68f' }}
|
|
298
|
+
onCrosshair={(p) => p && console.log(p.bar)}
|
|
299
|
+
/>
|
|
300
|
+
<button onClick={() => ref.current.chart.snapToRealtime()}>Realtime</button>
|
|
301
|
+
</div>
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Props: `data`, `feed`, `options`, `theme`, `priceMode`, `animate`, `magnet`,
|
|
307
|
+
`onCrosshair`, `className`, `style`. Any other prop is spread onto the host
|
|
308
|
+
`<div>`. The ref exposes `{ chart }` for imperative calls.
|
|
309
|
+
|
|
310
|
+
The component creates the chart **once** and drives it through its methods on
|
|
311
|
+
prop changes — it never rebuilds the canvas, so pan/zoom position and animation
|
|
312
|
+
state survive re-renders. `destroy()` runs on unmount, so React 18 StrictMode
|
|
313
|
+
double-mounting is safe.
|
|
314
|
+
|
|
315
|
+
### React — by hand
|
|
316
|
+
|
|
317
|
+
The adapter is ~100 lines of `useEffect`; doing it yourself is fine:
|
|
318
|
+
|
|
319
|
+
```jsx
|
|
320
|
+
import { useEffect, useRef } from 'react'
|
|
321
|
+
import { createChart, RandomFeed } from 'emberwick'
|
|
322
|
+
|
|
323
|
+
export function Chart() {
|
|
324
|
+
const hostRef = useRef(null)
|
|
325
|
+
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
const chart = createChart(hostRef.current)
|
|
328
|
+
const feed = new RandomFeed({ timeframe: 60_000 })
|
|
329
|
+
chart.setFeed(feed)
|
|
330
|
+
return () => { feed.destroy(); chart.destroy() }
|
|
331
|
+
}, [])
|
|
332
|
+
|
|
333
|
+
return <div ref={hostRef} style={{ width: '100%', height: 480 }} />
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Web Component — works in any framework
|
|
338
|
+
|
|
339
|
+
```html
|
|
340
|
+
<script type="module">
|
|
341
|
+
import 'emberwick/webcomponent'
|
|
342
|
+
import { RandomFeed } from 'emberwick'
|
|
343
|
+
|
|
344
|
+
const el = document.querySelector('emberwick-chart')
|
|
345
|
+
el.feed = new RandomFeed({ timeframe: 60000, speed: 60 })
|
|
346
|
+
el.addEventListener('crosshair', (e) => console.log(e.detail))
|
|
347
|
+
</script>
|
|
348
|
+
|
|
349
|
+
<emberwick-chart theme="dark" style="height: 480px"></emberwick-chart>
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
| | |
|
|
353
|
+
|---|---|
|
|
354
|
+
| Attributes | `theme="dark\|light"`, `animate="false"`, `magnet="false"` |
|
|
355
|
+
| Properties | `feed`, `data`, `chart` (read-only) |
|
|
356
|
+
| Events | `crosshair` — `event.detail` is the payload or `null` |
|
|
357
|
+
|
|
358
|
+
The element renders into a shadow root, so host-page CSS can't reposition the
|
|
359
|
+
stacked canvases. Assigning `feed`/`data` before the element upgrades is safe.
|
|
360
|
+
This is the path for Vue, Svelte, Angular or server-rendered templates without
|
|
361
|
+
a framework-specific wrapper.
|
|
362
|
+
|
|
363
|
+
### Vue 3
|
|
364
|
+
|
|
365
|
+
```vue
|
|
366
|
+
<script setup>
|
|
367
|
+
import { onMounted, onBeforeUnmount, ref } from 'vue'
|
|
368
|
+
import { createChart, RandomFeed } from 'emberwick'
|
|
369
|
+
|
|
370
|
+
const host = ref(null)
|
|
371
|
+
let chart, feed
|
|
372
|
+
|
|
373
|
+
onMounted(() => {
|
|
374
|
+
chart = createChart(host.value)
|
|
375
|
+
feed = new RandomFeed({ timeframe: 60_000 })
|
|
376
|
+
chart.setFeed(feed)
|
|
377
|
+
})
|
|
378
|
+
onBeforeUnmount(() => { feed?.destroy(); chart?.destroy() })
|
|
379
|
+
</script>
|
|
380
|
+
|
|
381
|
+
<template><div ref="host" style="width: 100%; height: 480px" /></template>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Plain HTML
|
|
385
|
+
|
|
386
|
+
```html
|
|
387
|
+
<div id="chart" style="height: 480px"></div>
|
|
388
|
+
<script type="module">
|
|
389
|
+
import { createChart, RandomFeed } from 'emberwick'
|
|
390
|
+
const chart = createChart(document.getElementById('chart'))
|
|
391
|
+
chart.setFeed(new RandomFeed({ timeframe: 60_000 }))
|
|
392
|
+
</script>
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## `RandomFeed` — synthetic data for development
|
|
398
|
+
|
|
399
|
+
Bundled so you can build UI before your backend exists. It's an ordinary
|
|
400
|
+
`DataFeed` implementation with no special privileges.
|
|
401
|
+
|
|
402
|
+
```js
|
|
403
|
+
new RandomFeed({
|
|
404
|
+
symbol: 'EMBR',
|
|
405
|
+
timeframe: 60_000,
|
|
406
|
+
seed: 7, // deterministic: same seed → same chart
|
|
407
|
+
start: 100, // starting price
|
|
408
|
+
volatility: 0.0022,
|
|
409
|
+
drift: 0.00002,
|
|
410
|
+
ticksPerSecond: 8, // live update rate
|
|
411
|
+
speed: 1, // time multiplier; 60 = one candle per second
|
|
412
|
+
})
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Runtime controls: `setSpeed(n)`, `setTicksPerSecond(n)`, `setPaused(bool)`,
|
|
416
|
+
`paused` (getter), `stop()`, `destroy()`.
|
|
417
|
+
|
|
418
|
+
Set `speed: 60` to see the flowing motion immediately instead of waiting a
|
|
419
|
+
minute per candle.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Exports
|
|
424
|
+
|
|
425
|
+
```js
|
|
426
|
+
import {
|
|
427
|
+
createChart, Chart, // entry point
|
|
428
|
+
DataFeed, RandomFeed, // data layer
|
|
429
|
+
defaultTheme, lightTheme, // themes
|
|
430
|
+
TimeScale, PriceScale, // scales (advanced)
|
|
431
|
+
Smoothed, Tween, Inertia, // motion primitives
|
|
432
|
+
LiveCandle,
|
|
433
|
+
easeOutCubic, easeInOutCubic,
|
|
434
|
+
mulberry32, // seeded PRNG
|
|
435
|
+
version,
|
|
436
|
+
} from 'emberwick'
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## How the motion works
|
|
442
|
+
|
|
443
|
+
Three independent mechanisms, which is why it reads as smooth rather than
|
|
444
|
+
merely animated:
|
|
445
|
+
|
|
446
|
+
1. **Live candle** — each of O/H/L/C eases toward the incoming tick over
|
|
447
|
+
~55ms, with the wick clamped so it can't invert mid-chase. A brand new
|
|
448
|
+
candle plays a short grow-from-centre animation as it scrolls in.
|
|
449
|
+
2. **Price axis** — autoscale bounds are smoothed, so a spike glides the axis
|
|
450
|
+
instead of jolting it and losing the reader's place.
|
|
451
|
+
3. **Time axis** — `spacing` (zoom) and `right` (edge position) are smoothed,
|
|
452
|
+
so wheel zoom eases and new bars slide in. **Dragging deliberately does
|
|
453
|
+
not ease** — easing a drag feels like lag, not polish.
|
|
454
|
+
|
|
455
|
+
All of it runs through `Smoothed`, a frame-rate-independent exponential
|
|
456
|
+
smoother: the same visual speed at 30fps and 144fps. The render loop returns
|
|
457
|
+
whether anything is still animating, so a static chart idles at zero CPU.
|
|
458
|
+
|
|
459
|
+
Rendering is split across three stacked canvases — `base` (grid + axes),
|
|
460
|
+
`main` (candles + volume), `overlay` (crosshair) — so moving the pointer
|
|
461
|
+
repaints only the crosshair, never the candles underneath. All contexts are
|
|
462
|
+
pre-scaled by `devicePixelRatio`, so drawing code works in CSS pixels and
|
|
463
|
+
output stays crisp on retina.
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Building the package
|
|
468
|
+
|
|
469
|
+
The repo is both the library and its playground. The playground is an ordinary
|
|
470
|
+
Vite app (`npm run dev` / `npm run build`); the library has its own builds.
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
npm run build:lib # ESM, multi-entry -> dist-lib/{index,react,webcomponent}.js
|
|
474
|
+
npm run build:umd # minified UMD -> dist-lib/umd/emberwick.umd.js
|
|
475
|
+
npm run pack:lib # manifest + README + .d.ts into dist-lib/
|
|
476
|
+
npm run size # gzipped size budget check
|
|
477
|
+
npm run release # all four, in order
|
|
478
|
+
npm publish dist-lib # publish the assembled directory
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
Publishing from `dist-lib/` keeps the playground, the build configs and the
|
|
482
|
+
app's private `package.json` out of the artifact. `package.lib.json` is the
|
|
483
|
+
manifest that becomes the published `package.json`.
|
|
484
|
+
|
|
485
|
+
React is an **optional peer dependency**, external in every build — importing
|
|
486
|
+
`emberwick` never pulls React in.
|
|
487
|
+
|
|
488
|
+
Source layout:
|
|
489
|
+
|
|
490
|
+
```
|
|
491
|
+
src/chart/ the library core (zero deps, no framework)
|
|
492
|
+
core/ Chart, Layers, Loop, TimeScale, PriceScale, palette, formatters
|
|
493
|
+
render/ grid, candles, crosshair
|
|
494
|
+
motion/ Tween (Smoothed), Inertia, LiveCandle
|
|
495
|
+
data/ DataFeed (the seam), RandomFeed
|
|
496
|
+
index.js public API surface index.d.ts types
|
|
497
|
+
src/adapters/react/ <EmberwickChart />
|
|
498
|
+
src/adapters/webcomponent/ <emberwick-chart>
|
|
499
|
+
src/App.jsx, src/styles.css the playground (not published)
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Known gaps
|
|
505
|
+
|
|
506
|
+
Honest list of what isn't there yet:
|
|
507
|
+
|
|
508
|
+
- **`subscribe('visibleRange', fn)` never fires.** The event is accepted and
|
|
509
|
+
the handler is stored, but nothing emits it. Use `'crosshair'` for now.
|
|
510
|
+
- **Not published to npm.** The package builds and packs, but it has not been
|
|
511
|
+
pushed, and the name is unverified — check `npm view emberwick` first.
|
|
512
|
+
- **The library builds are unverified.** `npm run build:lib` / `build:umd`
|
|
513
|
+
have not been executed here; this environment only runs the app build. Run
|
|
514
|
+
`npm run release` locally before publishing.
|
|
515
|
+
- **No indicators or drawing tools.** SMA/EMA/RSI/MACD, multi-pane layout and
|
|
516
|
+
trendlines are planned but not implemented.
|
|
517
|
+
- **Candlesticks only.** No Heikin-Ashi, line, area or baseline series yet.
|
|
518
|
+
- **No session-gap collapsing.** Weekends and market closes render as ordinary
|
|
519
|
+
bar-index steps, not gaps.
|
|
520
|
+
- **`RandomFeed` deep history is per-page coherent, not one continuous walk** —
|
|
521
|
+
it regenerates backwards from a seed offset, so paging far left can show a
|
|
522
|
+
visible seam. Real feeds don't have this artifact.
|
|
523
|
+
- **Types are hand-written**, not generated from source, so they can drift
|
|
524
|
+
from the implementation.
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## License
|
|
529
|
+
|
|
530
|
+
MIT
|