@livo-build/charts 0.2.1 → 0.2.2

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
@@ -1,5 +1,9 @@
1
1
  # @livo-build/charts (livo-charts)
2
2
 
3
+ > **Live demos + docs: [charts.livo.build](https://charts.livo.build)** — interactive
4
+ > examples with copy-paste code, plus agent-friendly [llms.txt](https://charts.livo.build/llms.txt)
5
+ > / [llms-full.txt](https://charts.livo.build/llms-full.txt).
6
+
3
7
  A lightweight, **dependency-free** canvas charting library. The core renders to a
4
8
  `<canvas>` with zero runtime dependencies; a thin, self-styled React wrapper is
5
9
  shipped under a separate entry so non-React consumers never pull React into their
@@ -7,8 +11,8 @@ bundle.
7
11
 
8
12
  It ships a **TradingView-style trading chart**: candlesticks **or** a line series, a
9
13
  **volume panel**, a **crosshair with price + time axis labels**, an **OHLCV legend**,
10
- independent **X and Y zoom** (+ pan), and a toolbar for **intervals, USD/ETH,
11
- price/market-cap and fullscreen**. The architecture (a framework-agnostic `Chart`
14
+ independent **X and Y zoom** (+ pan, plus touch/pinch), and a toolbar for **intervals,
15
+ USD/ETH, price/market-cap, log scale and fullscreen**. The architecture (a framework-agnostic `Chart`
12
16
  controller + a pure `draw()` pass) is built to keep growing — indicators, overlays,
13
17
  multiple panes — without touching consumers.
14
18
 
@@ -28,8 +32,7 @@ import { PriceChart } from "@livo-build/charts/react";
28
32
  <PriceChart
29
33
  swaps={trades} // [{ t: unixSeconds, p: priceUsd, v: usdVolume }, …]
30
34
  ethUsd={1711.81} // enables the USD↔ETH toggle
31
- tokenAddress="0x…" // enables the Price↔MCap toggle (reads totalSupply())
32
- decimals={18}
35
+ supply={1_000_000_000} // enables the Price↔MCap toggle (or tokenAddress to read it)
33
36
  symbol="PEPE"
34
37
  quote="WETH"
35
38
  height={420}
@@ -37,14 +40,15 @@ import { PriceChart } from "@livo-build/charts/react";
37
40
  ```
38
41
 
39
42
  `swaps` are aggregated into OHLC+volume candles client-side. The toolbar
40
- (`1s 1m 5m 15m 1h 4h 1d`, candle/line, Price/MCap, USD/ETH, flip, fullscreen) and the
43
+ (`1s 1m 5m 15m 1h 4h 1d`, candle/line, Price/MCap, USD/ETH, flip, log, fullscreen) and the
41
44
  OHLCV legend are built in. The canvas is long-lived, so zoom/pan survive prop updates
42
45
  (streaming trades won't reset the user's view). The wrapper is styled inline — no CSS
43
46
  framework required.
44
47
 
45
- Market cap = `price × totalSupply()`, fetched once via `rpcUrl` (default
46
- `https://cloudflare-eth.com`) and cached; it silently falls back to price if the RPC
47
- is unavailable.
48
+ Market cap = `price × supply`. Pass `supply` directly, or pass `tokenAddress` (+`decimals`)
49
+ to read `totalSupply()` once via `rpcUrl` (default `https://cloudflare-eth.com`, cached).
50
+ The **Price/MCap toggle only appears when one of those is set** — otherwise it's hidden
51
+ (no dead button), and it falls back to price if the RPC read fails.
48
52
 
49
53
  ### Live Hyperliquid chart
50
54
 
@@ -71,12 +75,82 @@ API key. Zoom/pan survive every update.
71
75
  ### Indicators
72
76
 
73
77
  `PriceChart` and `HyperliquidChart` accept `indicators` — overlays drawn on the price
74
- pane. Each is `{ type: "sma" | "ema" | "wma" | "vwap", period, color?, source? }`
75
- (`source` defaults to `close`; `vwap` is cumulative and ignores period/source).
78
+ pane. Each is `{ type: "sma" | "ema" | "wma" | "vwap" | "bollinger", period, color?, source?, mult? }`
79
+ (`source` defaults to `close`; `vwap` is cumulative; `bollinger` draws a 3-line band at
80
+ `mult` std-devs, default 2).
76
81
  Colors fall back to a built-in palette by position. The pure
77
82
  `sma`/`ema`/`wma`/`vwap`/`bollingerBands`/`computeIndicator` helpers are exported from
78
83
  the core (e.g. feed `bollingerBands(values).{upper,mid,lower}` as three overlays).
79
84
 
85
+ **Oscillators (RSI / MACD)** render in their own **stacked sub-panes** below the volume
86
+ panel — pass `oscillators`:
87
+
88
+ ```tsx
89
+ <PriceChart
90
+ swaps={trades}
91
+ indicators={[{ type: "ema", period: 21 }]} // on the price pane
92
+ oscillators={[
93
+ { type: "rsi", period: 14 }, // 0–100 band with 70/30 rails
94
+ { type: "macd", fast: 12, slow: 26, signal: 9 }, // line + signal + histogram
95
+ ]}
96
+ />
97
+ ```
98
+
99
+ Each pane defaults to 84px (`height` to override). The pure `rsi(values, period)` and
100
+ `macd(values, fast, slow, signal) → { macd, signal, hist }` helpers are exported too.
101
+
102
+ ### Fitting the container & axis styling
103
+
104
+ By default `PriceChart` / `HyperliquidChart` **fit the container** (`fitContent`) — candles
105
+ spread across the full plot width instead of clustering at a capped slot. Set
106
+ `fitContent={false}` for the tight, right-anchored look (the low-level `Chart` defaults to
107
+ tight; the React wrappers default to fill).
108
+
109
+ The ticks are easy to style — every axis knob is a prop (and a `chart.setAxis({…})` call):
110
+
111
+ ```tsx
112
+ <PriceChart
113
+ swaps={trades}
114
+ priceFormat={(v) => `$${v.toLocaleString()}`} // y-axis labels
115
+ timeFormat={(t) => new Date(t * 1000).toLocaleTimeString()}
116
+ priceTicks={6} // horizontal gridlines (default 5)
117
+ timeTicks={8} // x-axis labels (default 7)
118
+ axisFont="11px Inter, sans-serif" // label font (color = theme.axis)
119
+ />
120
+ ```
121
+
122
+ The **default** price formatter is range-aware: it keeps just enough decimals to tell
123
+ adjacent ticks apart, so a narrow high-value axis (e.g. WETH 1.71K–1.75K) renders
124
+ `1.7350K / 1.7430K` instead of duplicate `1.73K`s. Tick text color comes from `theme.axis`.
125
+
126
+ ### Log scale & themes
127
+
128
+ The price axis can be **logarithmic** — equal vertical distance = equal % move, the
129
+ right default for assets that span orders of magnitude. `PriceChart`/`HyperliquidChart`
130
+ expose a **Log** toolbar toggle (or pass `options={{ logScale: true }}`); the core has
131
+ `chart.setLogScale(true)`. Log mode auto-falls back to linear if any visible low is ≤ 0.
132
+
133
+ Two built-in themes ship as `DEFAULT_THEME` (dark) and `LIGHT_THEME`, also addressable
134
+ via `THEME_PRESETS.dark` / `THEME_PRESETS.light`. Pass either (or a partial override) as
135
+ `theme`, or call `chart.setTheme(LIGHT_THEME)`.
136
+
137
+ ### Drawing tools (trendlines & horizontal lines)
138
+
139
+ The toolbar's **↗** (trendline) and **—** (horizontal line) buttons arm a one-shot draw:
140
+ drag for a trendline, click for a horizontal price line. Drawings are anchored in **data
141
+ space**, so they stay pinned to their price/time across pan, zoom and live updates. In the
142
+ default cursor mode, click a line to select it, drag to move it, and **double-click a line
143
+ to delete it**; **⌫** clears all.
144
+
145
+ ```tsx
146
+ <PriceChart swaps={trades} onDrawingsChange={(d) => save(d)} drawings={restored} />
147
+ ```
148
+
149
+ Drive it from the core too: `chart.setDrawMode("trendline" | "hline" | "none")`,
150
+ `getDrawings()` / `setDrawings()` / `removeDrawing(id)` / `clearDrawings()`, and the
151
+ `onDrawingsChange` callback. Pass `hideDrawingTools` to drop the buttons. The
152
+ `computeProjection(input)` helper exposes the same pixel↔data mapping for custom tools.
153
+
80
154
  ### Realtime feeds + lazy history (any data source)
81
155
 
82
156
  Connect a chart to a **feed** — a source that serves paged OHLCV history *and* live
@@ -133,7 +207,13 @@ chart.destroy();
133
207
  | `setCandles(candles)` | Replace the series (each candle has `vol`) and redraw. |
134
208
  | `setInterval(seconds)` | Bucket interval — drives time-axis label granularity. |
135
209
  | `setChartType("candle" \| "line")` | Switch series style. |
210
+ | `setShowVolume(on)` | Show / hide the volume panel (the price pane reclaims the space). |
211
+ | `setLogScale(on)` | Toggle the logarithmic price axis (auto-falls back to linear if any low ≤ 0). |
212
+ | `setAxis({ fitContent, priceFormat, timeFormat, priceTicks, timeTicks, axisFont })` | Style the axes — only the provided keys change. |
136
213
  | `setIndicators(indicators)` | Set the moving-average overlays and redraw. |
214
+ | `setOscillators(oscillators)` | Set the RSI / MACD sub-panes and redraw. |
215
+ | `setDrawMode(mode)` | Arm a drawing tool (`"trendline"` / `"hline"`) or `"none"`. |
216
+ | `setDrawings` / `getDrawings` / `removeDrawing(id)` / `clearDrawings()` | Manage drawings. |
137
217
  | `setNeedHistory(cb)` | Callback fired when the view nears the start of loaded data (lazy history). |
138
218
  | `setHeight(px)` / `setTheme(partial)` | Resize / merge theme overrides. |
139
219
  | `resetView()` | Reset zoom/pan (same as double-click). |
@@ -142,15 +222,17 @@ chart.destroy();
142
222
  `ChartOptions`: `height`, `theme`, `initialBars` (120), `minBars` (20),
143
223
  `maxBarWidth` (18 — caps candle spacing so sparse series stay tight, right-anchored),
144
224
  `volumeRatio` (0.18), `rightPad`/`bottomPad`/`topPad`, `onCrosshair`, `indicators`,
145
- and `onNeedHistory`.
225
+ `oscillators`, `drawings`, `showVolume` (true), `logScale` (false), `fitContent` (false),
226
+ `maxBarWidth` (18) / `maxBodyWidth` (40), `priceFormat`/`timeFormat`, `priceTicks` (5) /
227
+ `timeTicks` (7), `axisFont`, and `onNeedHistory`.
146
228
 
147
229
  ### Helpers
148
230
 
149
231
  - `buildOHLC(points, interval, transform?)` — bucket priced trades into OHLC+volume
150
232
  candles. `transform` = `{ denom, ethUsd, flip, supply }` (`supply` → market cap).
151
233
  - `transformPrice(p, transform?)` — apply the transform to one price.
152
- - `sma` / `ema` / `wma` / `vwap` / `bollingerBands` / `computeIndicator` pure
153
- indicator math (aligned to input, null until the window fills).
234
+ - `sma` / `ema` / `wma` / `vwap` / `bollingerBands` / `rsi` / `macd` / `computeIndicator`
235
+ — pure indicator math (aligned to input, null until the window fills).
154
236
  - `connectFeed(chart, feed, opts)` — wire a `ChartFeed` (paged history + live stream)
155
237
  to a chart: latest page, lazy older history, live merge. Returns `{ disconnect }`.
156
238
  - `hyperliquidFeed({ coin, interval, testnet })` — a ready `ChartFeed` (REST history +
@@ -158,14 +240,24 @@ and `onNeedHistory`.
158
240
  `HL_INTERVAL_SECONDS` are exported for custom fetching.
159
241
  - `draw(input)` — the pure render pass (exported for custom hosts/tests); returns the
160
242
  crosshair candle.
161
- - `DEFAULT_THEME`, `formatValue`, `formatVolume`, `formatTime`.
243
+ - `DEFAULT_THEME` / `LIGHT_THEME` / `THEME_PRESETS`, `formatValue`, `formatAxisValue`
244
+ (range-aware tick labels), `formatVolume`, `formatTime`.
245
+ - `slotWidth(plotW, count, maxBarWidth, fitContent?)` — candle slot-width math (exported
246
+ for custom hosts).
247
+ - `computeProjection(input)` / `priceScale` / `timeScale` / `windowOf` — the pixel↔data
248
+ mapping the renderer and drawing tools share (returns `{ xOfTime, timeOfX, yOfPrice,
249
+ priceOfY, … }`).
162
250
 
163
251
  ## Interaction
164
252
 
165
253
  - **Scroll** over the plot → zoom time; over the price axis → zoom price.
166
254
  - **Drag** the plot → pan; drag the time axis → zoom X; drag the price axis → zoom Y.
167
255
  - **Hover** → crosshair + price/time axis labels + the OHLCV legend.
168
- - **Double-click** → reset zoom/pan.
256
+ - **Double-click** → reset zoom/pan (or delete a drawing when one is under the cursor).
257
+ - **Touch** → one finger pans (or zooms an axis from its gutter); two-finger pinch
258
+ zooms time (horizontal spread) and price (vertical spread) together.
259
+ - **Drawing tools** → arm trendline/h-line from the toolbar; select + drag to move,
260
+ double-click to delete.
169
261
 
170
262
  ## Design notes
171
263
 
@@ -1,4 +1,4 @@
1
- import type { Candle, ChartOptions, ChartTheme, ChartType, Indicator } from "./types";
1
+ import type { AxisOptions, Candle, ChartOptions, ChartTheme, ChartType, DrawMode, Drawing, Indicator, Oscillator } from "./types";
2
2
  /**
3
3
  * Framework-agnostic candlestick/line chart with a volume panel. Creates and owns a
4
4
  * `<canvas>` inside the given container and wires the interactions:
@@ -23,17 +23,36 @@ export declare class Chart {
23
23
  private readonly ro;
24
24
  private readonly pads;
25
25
  private readonly minBars;
26
- private readonly maxBarWidth;
26
+ private maxBarWidth;
27
+ private maxBodyWidth?;
27
28
  private readonly volumeRatio;
28
29
  private readonly onCrosshair?;
29
30
  private theme;
30
31
  private height;
31
32
  private candles;
32
33
  private indicators;
34
+ private oscillators;
35
+ private drawings;
36
+ private drawMode;
37
+ private selectedDrawing;
38
+ /** in-progress trendline (drag from anchor A to B). */
39
+ private draftDrawing;
40
+ /** active move of an existing drawing: its id + the data-space grab anchor + originals. */
41
+ private drawingDrag;
42
+ private onDrawingsChange?;
43
+ private onDrawModeChange?;
33
44
  /** Indicator values precomputed on data/indicator change (not per frame). */
34
45
  private overlays;
35
46
  private interval;
36
47
  private type;
48
+ private showVolume;
49
+ private logScale;
50
+ private fitContent;
51
+ private priceFormat?;
52
+ private timeFormat?;
53
+ private priceTicks?;
54
+ private timeTicks?;
55
+ private axisFont?;
37
56
  /** requestAnimationFrame handle coalescing high-frequency redraws (0 = none queued). */
38
57
  private raf;
39
58
  private onNeedHistory?;
@@ -44,12 +63,38 @@ export declare class Chart {
44
63
  private yZoom;
45
64
  private hover;
46
65
  private drag;
66
+ /** two-finger pinch baseline: finger spreads + view at gesture start. */
67
+ private pinch;
47
68
  private lastActive;
48
69
  constructor(container: HTMLElement, opts?: ChartOptions);
49
70
  setCandles(candles: Candle[]): this;
50
71
  setInterval(interval: number): this;
51
72
  setChartType(type: ChartType): this;
73
+ /** Show or hide the volume panel (the price pane reclaims the space when hidden). */
74
+ setShowVolume(on: boolean): this;
75
+ /** Toggle the logarithmic price axis (equal pixels = equal % move). */
76
+ setLogScale(on: boolean): this;
77
+ /** Style the axes: fill-vs-tight candle spacing, custom price/time label formatters,
78
+ * tick counts, and the label font. Only the provided keys change. */
79
+ setAxis(opts: AxisOptions): this;
52
80
  setIndicators(indicators: Indicator[]): this;
81
+ /** Set the oscillator sub-panes (RSI / MACD) drawn below the volume panel. */
82
+ setOscillators(oscillators: Oscillator[]): this;
83
+ /** Arm a drawing tool ("trendline" / "hline"), or "none" for pan/zoom + select/move.
84
+ * Drawing tools are one-shot: after one drawing the mode auto-resets to "none". */
85
+ setDrawMode(mode: DrawMode): this;
86
+ /** Replace all drawings (does not fire onDrawingsChange). */
87
+ setDrawings(drawings: Drawing[]): this;
88
+ /** Current drawings (a copy). */
89
+ getDrawings(): Drawing[];
90
+ /** Remove a drawing by id. */
91
+ removeDrawing(id: string): this;
92
+ /** Remove the currently selected drawing, if any. */
93
+ deleteSelected(): this;
94
+ /** Remove all drawings. */
95
+ clearDrawings(): this;
96
+ private emitDrawings;
97
+ private exitDrawMode;
53
98
  /** Register a callback fired when the user pans/zooms near the start of loaded data
54
99
  * (so a feed can lazily load older candles). See connectFeed. */
55
100
  setNeedHistory(cb: (() => void) | undefined): this;
@@ -61,6 +106,10 @@ export declare class Chart {
61
106
  private get plotW();
62
107
  private measure;
63
108
  private clampView;
109
+ /** Assemble the {@link RenderInput} from current state — shared by render() and projection(). */
110
+ private buildInput;
111
+ /** Pixel↔data projection for the current frame (null when there are no candles). */
112
+ private projection;
64
113
  private render;
65
114
  /** Coalesce high-frequency redraws (drag/hover/wheel) into one per animation frame. */
66
115
  private scheduleRender;
@@ -73,9 +122,19 @@ export declare class Chart {
73
122
  private readonly onWheel;
74
123
  /** Zoom the time axis by factor `f`, keeping the candle under `cursorX` anchored. */
75
124
  private zoomTimeAt;
125
+ /** Topmost drawing under the pointer (within tolerance), or null. */
126
+ private hitTest;
76
127
  private readonly onDown;
77
128
  private readonly onMove;
129
+ /** Translate the dragged drawing by the pointer's data-space delta from the grab anchor. */
130
+ private applyDrawingDrag;
131
+ /** Apply an in-progress drag (pan or axis-zoom) from the current pointer position.
132
+ * Shared by mouse-move and single-finger touch-move. Returns true if a drag is active. */
133
+ private applyDrag;
78
134
  private readonly onUp;
79
135
  private readonly onLeave;
80
136
  private readonly onDouble;
137
+ private readonly onTouchStart;
138
+ private readonly onTouchMove;
139
+ private readonly onTouchEnd;
81
140
  }