emberwick 0.1.1 → 0.2.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
@@ -10,6 +10,8 @@ point it at your own market data.
10
10
  to new bounds, zoom is cursor-anchored and eased, panning has inertia.
11
11
  - **Fast on big data.** One `requestAnimationFrame` loop, dirty-flag driven,
12
12
  with visible-range culling — 500k bars loaded costs only the ~200 on screen.
13
+ - **Annotated.** Nine marker shapes, price lines and shaded zones, with
14
+ collision-aware stacking and hit-testing for hover and click.
13
15
 
14
16
  ---
15
17
 
@@ -25,10 +27,6 @@ npm install emberwick
25
27
  import { createChart, RandomFeed } from 'emberwick'
26
28
  ```
27
29
 
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
30
  ### From a CDN, no build step
33
31
 
34
32
  ```html
@@ -213,7 +211,15 @@ const chart = createChart(el, {
213
211
  | `append(bar)` | Open a new candle |
214
212
  | `setFeed(feed)` | `async` — loads history, then subscribes. Detaches any previous feed |
215
213
  | `detachFeed()` | Unsubscribe, keep the bars on screen |
216
- | `subscribe('crosshair', fn)` | Returns an unsubscribe fn. See events below |
214
+ | `subscribe(event, fn)` | Returns an unsubscribe fn. See events below |
215
+ | `setMarkers(markers)` | Replace every marker |
216
+ | `getMarkers()` | Current markers, normalised, each with its resolved bar index |
217
+ | `addMarker(marker)` | Append one marker |
218
+ | `removeMarker(id)` | Remove by id |
219
+ | `clearMarkers()` | Remove all markers |
220
+ | `setPriceLines(lines)` | Replace every horizontal price line |
221
+ | `setZones(zones)` | Replace every shaded region |
222
+ | `markerAt(x, y)` | Hit-test plot coordinates, returns a marker or `null` |
217
223
  | `setTheme(partial)` | Merge theme keys and repaint |
218
224
  | `setPriceMode(mode)` | `'linear'` or `'log'` |
219
225
  | `setAnimate(bool)` | Toggle live-candle easing |
@@ -234,7 +240,100 @@ const off = chart.subscribe('crosshair', (payload) => {
234
240
  off() // unsubscribe
235
241
  ```
236
242
 
237
- `'crosshair'` is the only event that currently fires. See *Known gaps* below.
243
+ | Event | Payload |
244
+ |---|---|
245
+ | `'crosshair'` | `{ index, bar, price }`, or `null` when the pointer leaves the plot |
246
+ | `'markerHover'` | The marker under the pointer, or `null` when none is |
247
+ | `'markerClick'` | The clicked marker. Only fires on a hit, never with `null` |
248
+ | `'visibleRange'` | **Accepted but never emitted.** See *Known gaps* |
249
+
250
+ A drag that happens to end on top of a marker does not fire `'markerClick'` —
251
+ panning and clicking stay distinct.
252
+
253
+ ---
254
+
255
+ ## Annotations
256
+
257
+ Three independent collections, each replaced wholesale. Zones paint behind the
258
+ candles; price lines and markers paint in front.
259
+
260
+ ```js
261
+ chart.setMarkers([
262
+ { time: 1717070400000, shape: 'arrowUp', text: 'BUY 120',
263
+ data: { orderId: 'A-7741' } },
264
+ { time: 1717074000000, shape: 'flag', text: 'Earnings', color: '#c084fc' },
265
+ { time: 1717077600000, shape: 'arrowDown', text: 'SELL 160' },
266
+ ])
267
+
268
+ chart.setPriceLines([
269
+ { price: 148.20, title: 'target', color: '#26a69a' },
270
+ { price: 141.05, title: 'stop', lineStyle: 'dotted' },
271
+ ])
272
+
273
+ chart.setZones([
274
+ { from: 143.5, to: 145.9, label: 'value area' },
275
+ ])
276
+
277
+ chart.subscribe('markerClick', (m) => openTicket(m.data.orderId))
278
+ ```
279
+
280
+ ### Markers
281
+
282
+ A marker is pinned to a **timestamp**, not a bar index, and resolves to the
283
+ nearest bar. Load an older page of history and every marker re-resolves, so
284
+ nothing drifts off its candle.
285
+
286
+ | Field | Default | Notes |
287
+ |---|---|---|
288
+ | `time` | *required* | ms since epoch, snapped to the closest bar |
289
+ | `id` | generated | Needed for `removeMarker(id)` |
290
+ | `shape` | `'circle'` | See the list below |
291
+ | `position` | shape-dependent | `'aboveBar'`, `'belowBar'`, `'inBar'`, `'atPrice'` |
292
+ | `price` | — | Only used when `position` is `'atPrice'` |
293
+ | `color` | `theme.up` / `theme.down` | Down-pointing shapes default to the down colour |
294
+ | `text` | — | Short caption. For `'label'` it is drawn inside the pill |
295
+ | `textColor` | theme | Caption colour |
296
+ | `size` | `1` | Scale factor |
297
+ | `data` | — | Anything. Handed straight back on hover and click |
298
+
299
+ Shapes: `arrowUp`, `arrowDown`, `triangleUp`, `triangleDown`, `circle`,
300
+ `square`, `diamond`, `flag`, `label`.
301
+
302
+ Position defaults follow the trading convention: up-pointing shapes sit
303
+ *below* the bar, down-pointing shapes *above* it, everything else above.
304
+
305
+ **Overlap and density.** Markers sharing a bar are stacked rather than drawn on
306
+ top of each other. Below about 3px per bar, dense runs thin to roughly one
307
+ marker per 4px — a thousand trades stay legible and stay fast. A marker on the
308
+ forming candle anchors to the animated values, so it flows with the live bar.
309
+
310
+ ### Price lines
311
+
312
+ | Field | Default | Notes |
313
+ |---|---|---|
314
+ | `price` | *required* | |
315
+ | `color` | `theme.textStrong` | |
316
+ | `lineStyle` | `'dashed'` | `'solid'`, `'dashed'`, `'dotted'` |
317
+ | `lineWidth` | `1` | |
318
+ | `title` | — | Pill drawn at the left end |
319
+ | `axisLabel` | `true` | Price tag on the axis |
320
+
321
+ ### Zones
322
+
323
+ Supply `from`/`to` for a **price band** spanning the full width, or
324
+ `fromTime`/`toTime` for a **time band** spanning the full height.
325
+
326
+ ```js
327
+ chart.setZones([
328
+ { from: 143.5, to: 145.9, label: 'value area' },
329
+ { fromTime: 1717070400000, toTime: 1717074000000,
330
+ color: 'rgba(239,83,80,0.07)', border: 'rgba(239,83,80,0.3)' },
331
+ ])
332
+ ```
333
+
334
+ Autoscale fits the **bars**, not the annotations — a price line far outside the
335
+ data range is simply off-screen. Derive extreme values from the visible range
336
+ if you need them guaranteed visible.
238
337
 
239
338
  ---
240
339
 
@@ -475,9 +574,12 @@ npm run build:umd # minified UMD -> dist-lib/umd/emberwick.umd.js
475
574
  npm run pack:lib # manifest + README + .d.ts into dist-lib/
476
575
  npm run size # gzipped size budget check
477
576
  npm run release # all four, in order
478
- npm publish dist-lib # publish the assembled directory
577
+ npm publish ./dist-lib # publish the assembled directory — note the ./
479
578
  ```
480
579
 
580
+ > The leading `./` is required. `npm publish dist-lib` makes npm look for a
581
+ > *registry package* named `dist-lib` and fail with `E404`.
582
+
481
583
  Publishing from `dist-lib/` keeps the playground, the build configs and the
482
584
  app's private `package.json` out of the artifact. `package.lib.json` is the
483
585
  manifest that becomes the published `package.json`.
@@ -507,11 +609,10 @@ Honest list of what isn't there yet:
507
609
 
508
610
  - **`subscribe('visibleRange', fn)` never fires.** The event is accepted and
509
611
  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.
612
+ - **No OHLCV legend in the package.** `subscribe('crosshair', fn)` gives you
613
+ the hovered bar; rendering the readout is still yours to do.
614
+ - **Markers are not draggable.** They are hit-tested for hover and click, but
615
+ there is no drag-to-move or editing interaction.
515
616
  - **No indicators or drawing tools.** SMA/EMA/RSI/MACD, multi-pane layout and
516
617
  trendlines are planned but not implemented.
517
618
  - **Candlesticks only.** No Heikin-Ashi, line, area or baseline series yet.
package/index.d.ts CHANGED
@@ -40,6 +40,97 @@ export interface Theme {
40
40
  timeAxisHeight: number
41
41
  }
42
42
 
43
+ /* --------------------------------------------------------------- markers -- */
44
+
45
+ export type MarkerShape =
46
+ | 'arrowUp'
47
+ | 'arrowDown'
48
+ | 'triangleUp'
49
+ | 'triangleDown'
50
+ | 'circle'
51
+ | 'square'
52
+ | 'diamond'
53
+ | 'flag'
54
+ | 'label'
55
+
56
+ export type MarkerPosition = 'aboveBar' | 'belowBar' | 'inBar' | 'atPrice'
57
+
58
+ /** A point annotation pinned to the bar nearest `time`. */
59
+ export interface Marker<T = unknown> {
60
+ /** Stable id. Generated when omitted; required for removeMarker(). */
61
+ id?: string
62
+ /** ms since epoch — snapped to the closest bar. */
63
+ time: number
64
+ /** Only used when position is 'atPrice'. */
65
+ price?: number
66
+ /** Default 'circle'. */
67
+ shape?: MarkerShape
68
+ /**
69
+ * Default depends on the shape: up shapes sit below the bar, down shapes
70
+ * above it, everything else above.
71
+ */
72
+ position?: MarkerPosition
73
+ /** Defaults to theme.up, or theme.down for the down-pointing shapes. */
74
+ color?: string
75
+ textColor?: string
76
+ /** Short caption. For shape 'label' it is drawn inside the pill. */
77
+ text?: string
78
+ /** Scale factor. Default 1. */
79
+ size?: number
80
+ /** Passed straight back to you on hover/click. */
81
+ data?: T
82
+ }
83
+
84
+ /** A marker after normalisation, as returned by getMarkers(). */
85
+ export interface ResolvedMarker<T = unknown> extends Marker<T> {
86
+ id: string
87
+ shape: MarkerShape
88
+ position: MarkerPosition
89
+ size: number
90
+ /** Bar index the marker resolved to, or -1 when there are no bars. */
91
+ index: number
92
+ }
93
+
94
+ export type LineStyle = 'solid' | 'dashed' | 'dotted'
95
+
96
+ /** A horizontal line at a fixed price. */
97
+ export interface PriceLine {
98
+ id?: string
99
+ price: number
100
+ /** Defaults to theme.textStrong. */
101
+ color?: string
102
+ /** Default 'dashed'. */
103
+ lineStyle?: LineStyle
104
+ /** Default 1. */
105
+ lineWidth?: number
106
+ /** Optional pill drawn at the left end. */
107
+ title?: string
108
+ titleColor?: string
109
+ /** Price tag on the axis. Default true. */
110
+ axisLabel?: boolean
111
+ tagTextColor?: string
112
+ }
113
+
114
+ /**
115
+ * A shaded region: supply `from`/`to` for a price band spanning the full
116
+ * width, or `fromTime`/`toTime` for a time band spanning the full height.
117
+ */
118
+ export interface Zone {
119
+ id?: string
120
+ from?: number
121
+ to?: number
122
+ fromTime?: number
123
+ toTime?: number
124
+ /** Fill. Use rgba(). Default a faint theme-green wash. */
125
+ color?: string
126
+ /** Optional 1px outline. */
127
+ border?: string
128
+ label?: string
129
+ labelColor?: string
130
+ }
131
+
132
+ /* --------------------------------------------------------------- options -- */
133
+
43
134
  export interface TimeScaleOptions {
44
135
  /** Pixels per bar. Default 9. */
45
136
  spacing?: number
@@ -73,6 +164,10 @@ export interface ChartOptions {
73
164
  animate?: boolean
74
165
  /** Bars requested from the feed on setFeed(). Default 1500. */
75
166
  initialBars?: number
167
+ /** Initial markers; equivalent to calling setMarkers() after construction. */
168
+ markers?: Marker[]
169
+ priceLines?: PriceLine[]
170
+ zones?: Zone[]
76
171
  timeScale?: TimeScaleOptions
77
172
  priceScale?: PriceScaleOptions
78
173
  }
@@ -84,6 +179,8 @@ export interface CrosshairPayload {
84
179
  price: number
85
180
  }
86
181
 
182
+ /* ------------------------------------------------------------------ feed -- */
183
+
87
184
  export interface GetBarsRequest {
88
185
  symbol: string
89
186
  timeframe: number
@@ -147,6 +244,8 @@ export declare class RandomFeed extends DataFeed {
147
244
  stop(): void
148
245
  }
149
246
 
247
+ /* ---------------------------------------------------------------- scales -- */
248
+
150
249
  export declare class TimeScale {
151
250
  constructor(opts?: TimeScaleOptions)
152
251
  visibleRange(): { from: number; to: number }
@@ -168,6 +267,8 @@ export declare class PriceScale {
168
267
  readonly hi: number
169
268
  }
170
269
 
270
+ /* ----------------------------------------------------------------- chart -- */
271
+
171
272
  export declare class Chart {
172
273
  constructor(container: HTMLElement, options?: ChartOptions)
173
274
 
@@ -181,6 +282,19 @@ export declare class Chart {
181
282
  setFeed(feed: Feed): Promise<void>
182
283
  detachFeed(): void
183
284
 
285
+ /** Replace every marker. */
286
+ setMarkers(markers: Marker[]): void
287
+ /** Current markers, normalised, each with its resolved bar index. */
288
+ getMarkers(): ResolvedMarker[]
289
+ addMarker(marker: Marker): void
290
+ /** Removes by id — including ids generated for you. */
291
+ removeMarker(id: string): void
292
+ clearMarkers(): void
293
+ setPriceLines(lines: PriceLine[]): void
294
+ setZones(zones: Zone[]): void
295
+ /** Topmost marker under a plot-relative point, else null. */
296
+ markerAt(x: number, y: number): ResolvedMarker | null
297
+
184
298
  setTheme(theme: Partial<Theme>): void
185
299
  setPriceMode(mode: PriceMode): void
186
300
  setAnimate(on: boolean): void
@@ -197,12 +311,16 @@ export declare class Chart {
197
311
  * NOTE: 'visibleRange' is accepted but is not currently emitted.
198
312
  */
199
313
  subscribe(event: 'crosshair', fn: (payload: CrosshairPayload | null) => void): () => void
314
+ subscribe(event: 'markerClick', fn: (marker: ResolvedMarker) => void): () => void
315
+ subscribe(event: 'markerHover', fn: (marker: ResolvedMarker | null) => void): () => void
200
316
  subscribe(event: 'visibleRange', fn: (range: { from: number; to: number }) => void): () => void
201
317
 
202
318
  /** Removes listeners, canvases and the render loop. */
203
319
  destroy(): void
204
320
 
205
321
  readonly bars: Bar[]
322
+ readonly priceLines: PriceLine[]
323
+ readonly zones: Zone[]
206
324
  readonly ts: TimeScale
207
325
  readonly ps: PriceScale
208
326
  }
@@ -214,6 +332,8 @@ export declare const defaultTheme: Theme
214
332
  export declare const lightTheme: Theme
215
333
  export declare const version: string
216
334
 
335
+ /* ------------------------------------------------------------- utilities -- */
336
+
217
337
  /** Seeded PRNG used by RandomFeed. */
218
338
  export declare function mulberry32(seed: number): () => number
219
339