@livo-build/charts 0.2.1

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.
@@ -0,0 +1,203 @@
1
+ import { formatValue, formatTime } from "./format";
2
+ import { computeIndicator, INDICATOR_PALETTE } from "./indicators";
3
+ /**
4
+ * Pure draw pass: renders grid, axes, the price series (candles or line), a volume
5
+ * panel, the last-price tag and the crosshair + axis labels. Returns the candle under
6
+ * the crosshair (or the last candle when idle, or null when empty) so the host can
7
+ * drive an OHLCV legend. No state is retained between calls.
8
+ */
9
+ export function draw(input) {
10
+ const { ctx, width: w, height: H, candles, view, hover, interval, type, yZoom, maxBarWidth, volH, theme, pads } = input;
11
+ const plotW = w - pads.right;
12
+ const priceH = H - pads.bottom - pads.top - volH;
13
+ if (theme.background) {
14
+ ctx.fillStyle = theme.background;
15
+ ctx.fillRect(0, 0, w, H);
16
+ }
17
+ else {
18
+ ctx.clearRect(0, 0, w, H);
19
+ }
20
+ ctx.font = "10px ui-monospace, monospace";
21
+ ctx.textBaseline = "middle";
22
+ if (!candles.length) {
23
+ ctx.fillStyle = theme.axis;
24
+ ctx.textAlign = "center";
25
+ ctx.fillText("no priced trades yet", plotW / 2, H / 2);
26
+ return null;
27
+ }
28
+ const count = Math.min(view.count, candles.length);
29
+ const end = candles.length - view.offset;
30
+ const start = Math.max(0, end - count);
31
+ const vis = candles.slice(start, end);
32
+ const n = vis.length;
33
+ // Cap the slot width and right-anchor — a few candles cluster on the right with empty
34
+ // space on the left (TradingView-style) rather than stretching across the whole plot.
35
+ const cw = Math.min(plotW / Math.max(count, 1), maxBarWidth);
36
+ const xOf = (j) => plotW - (n - 1 - j) * cw - cw / 2;
37
+ let lo = Infinity;
38
+ let hi = -Infinity;
39
+ let vmax = 0;
40
+ for (const c of vis) {
41
+ lo = Math.min(lo, c.l);
42
+ hi = Math.max(hi, c.h);
43
+ vmax = Math.max(vmax, c.vol);
44
+ }
45
+ const mid = (lo + hi) / 2;
46
+ const half = ((((hi - lo) / 2) || Math.abs(hi) * 0.1 || 1) * 1.08) / yZoom;
47
+ lo = mid - half;
48
+ hi = mid + half;
49
+ const rng = hi - lo || 1;
50
+ vmax = vmax || 1;
51
+ const yOf = (v) => pads.top + priceH * (1 - (v - lo) / rng);
52
+ const volBot = pads.top + priceH + volH;
53
+ const vy = (vol) => volBot - (vol / vmax) * volH * 0.92;
54
+ // grid + price axis (right gutter)
55
+ ctx.strokeStyle = theme.grid;
56
+ ctx.fillStyle = theme.axis;
57
+ ctx.lineWidth = 1;
58
+ ctx.textAlign = "left";
59
+ for (let i = 0; i <= 5; i++) {
60
+ const v = lo + (rng * i) / 5;
61
+ const y = yOf(v);
62
+ ctx.beginPath();
63
+ ctx.moveTo(0, y);
64
+ ctx.lineTo(plotW, y);
65
+ ctx.stroke();
66
+ ctx.fillText(formatValue(v), plotW + 6, y);
67
+ }
68
+ // time axis (bottom gutter), anchored to the latest candle
69
+ ctx.textAlign = "center";
70
+ const step = Math.max(1, Math.floor(n / 7));
71
+ for (let j = n - 1; j >= 0; j -= step) {
72
+ const x = xOf(j);
73
+ ctx.strokeStyle = theme.grid;
74
+ ctx.beginPath();
75
+ ctx.moveTo(x, pads.top);
76
+ ctx.lineTo(x, volBot);
77
+ ctx.stroke();
78
+ ctx.fillStyle = theme.axis;
79
+ ctx.fillText(formatTime(vis[j].time, interval), x, H - pads.bottom / 2);
80
+ }
81
+ const bw = Math.max(1, Math.min(cw * 0.7, 14));
82
+ // volume panel
83
+ for (let j = 0; j < n; j++) {
84
+ const c = vis[j];
85
+ const x = xOf(j);
86
+ ctx.fillStyle = c.c >= c.o ? theme.volUp : theme.volDown;
87
+ ctx.fillRect(x - bw / 2, vy(c.vol), bw, volBot - vy(c.vol));
88
+ }
89
+ // price series
90
+ if (type === "line") {
91
+ ctx.strokeStyle = theme.line;
92
+ ctx.lineWidth = 1.5;
93
+ ctx.beginPath();
94
+ for (let j = 0; j < n; j++) {
95
+ const x = xOf(j);
96
+ const y = yOf(vis[j].c);
97
+ if (j)
98
+ ctx.lineTo(x, y);
99
+ else
100
+ ctx.moveTo(x, y);
101
+ }
102
+ ctx.stroke();
103
+ ctx.lineWidth = 1;
104
+ }
105
+ else {
106
+ for (let j = 0; j < n; j++) {
107
+ const c = vis[j];
108
+ const x = xOf(j);
109
+ const col = c.c >= c.o ? theme.up : theme.down;
110
+ ctx.strokeStyle = col;
111
+ ctx.fillStyle = col;
112
+ ctx.beginPath();
113
+ ctx.moveTo(x, yOf(c.h));
114
+ ctx.lineTo(x, yOf(c.l));
115
+ ctx.stroke();
116
+ const yo = yOf(c.o);
117
+ const yc = yOf(c.c);
118
+ ctx.fillRect(x - bw / 2, Math.min(yo, yc), bw, Math.max(1, Math.abs(yc - yo)));
119
+ }
120
+ }
121
+ // indicator overlays (moving averages) — drawn from precomputed value-series when
122
+ // supplied (the Chart controller precomputes them on data change), else computed
123
+ // here from the indicator config for pure/standalone callers. Series align to the
124
+ // FULL candle array, so the left edge of the view is correct.
125
+ const overlays = input.overlays ??
126
+ (input.indicators ?? []).map((ind, ii) => ({
127
+ color: ind.color || INDICATOR_PALETTE[ii % INDICATOR_PALETTE.length],
128
+ values: computeIndicator(candles, ind),
129
+ }));
130
+ if (overlays.length) {
131
+ ctx.lineWidth = 1.3;
132
+ for (const ov of overlays) {
133
+ ctx.strokeStyle = ov.color;
134
+ ctx.beginPath();
135
+ let pen = false;
136
+ for (let j = 0; j < n; j++) {
137
+ const v = ov.values[start + j];
138
+ if (v == null) {
139
+ pen = false;
140
+ continue;
141
+ }
142
+ const x = xOf(j);
143
+ const y = yOf(v);
144
+ if (pen)
145
+ ctx.lineTo(x, y);
146
+ else {
147
+ ctx.moveTo(x, y);
148
+ pen = true;
149
+ }
150
+ }
151
+ ctx.stroke();
152
+ }
153
+ ctx.lineWidth = 1;
154
+ }
155
+ // last-price line + tag
156
+ const last = vis[n - 1];
157
+ const ly = yOf(last.c);
158
+ const lc = last.c >= last.o ? theme.up : theme.down;
159
+ ctx.strokeStyle = lc;
160
+ ctx.setLineDash([2, 2]);
161
+ ctx.beginPath();
162
+ ctx.moveTo(0, ly);
163
+ ctx.lineTo(plotW, ly);
164
+ ctx.stroke();
165
+ ctx.setLineDash([]);
166
+ ctx.fillStyle = lc;
167
+ ctx.fillRect(plotW, ly - 8, pads.right, 16);
168
+ ctx.fillStyle = theme.tagText;
169
+ ctx.textAlign = "left";
170
+ ctx.fillText(formatValue(last.c), plotW + 6, ly);
171
+ // crosshair + axis labels
172
+ let active = last;
173
+ if (hover && hover.x < plotW && hover.y > 0 && hover.y < H - pads.bottom) {
174
+ const idx = Math.max(0, Math.min(n - 1, Math.round((hover.x - (plotW - (n - 1) * cw - cw / 2)) / cw)));
175
+ active = vis[idx];
176
+ const cx = xOf(idx);
177
+ ctx.strokeStyle = theme.crosshair;
178
+ ctx.setLineDash([3, 3]);
179
+ ctx.beginPath();
180
+ ctx.moveTo(cx, pads.top);
181
+ ctx.lineTo(cx, volBot);
182
+ ctx.moveTo(0, hover.y);
183
+ ctx.lineTo(plotW, hover.y);
184
+ ctx.stroke();
185
+ ctx.setLineDash([]);
186
+ if (hover.y < pads.top + priceH) {
187
+ const pv = lo + rng * (1 - (hover.y - pads.top) / priceH);
188
+ ctx.fillStyle = theme.axisTagBg;
189
+ ctx.fillRect(plotW, hover.y - 8, pads.right, 16);
190
+ ctx.fillStyle = theme.axisTagText;
191
+ ctx.textAlign = "left";
192
+ ctx.fillText(formatValue(pv), plotW + 6, hover.y);
193
+ }
194
+ const tl = formatTime(active.time, interval);
195
+ const tw = ctx.measureText(tl).width + 12;
196
+ ctx.fillStyle = theme.axisTagBg;
197
+ ctx.fillRect(cx - tw / 2, H - pads.bottom, tw, pads.bottom);
198
+ ctx.fillStyle = theme.axisTagText;
199
+ ctx.textAlign = "center";
200
+ ctx.fillText(tl, cx, H - pads.bottom / 2);
201
+ }
202
+ return active;
203
+ }
@@ -0,0 +1,3 @@
1
+ import type { ChartTheme } from "./types";
2
+ /** Default dark theme (TradingView-ish palette). */
3
+ export declare const DEFAULT_THEME: ChartTheme;
@@ -0,0 +1,14 @@
1
+ /** Default dark theme (TradingView-ish palette). */
2
+ export const DEFAULT_THEME = {
3
+ up: "#26a69a",
4
+ down: "#ef5350",
5
+ line: "#2962ff",
6
+ grid: "#1c2030",
7
+ axis: "#787b86",
8
+ crosshair: "#9598a1",
9
+ volUp: "rgba(38,166,154,.5)",
10
+ volDown: "rgba(239,83,80,.5)",
11
+ tagText: "#0a0a0a",
12
+ axisTagBg: "#2a2e39",
13
+ axisTagText: "#d1d4dc",
14
+ };
@@ -0,0 +1,80 @@
1
+ /** A single OHLC candle with aggregated volume. `time` is the unix-seconds bucket start. */
2
+ export interface Candle {
3
+ time: number;
4
+ o: number;
5
+ h: number;
6
+ l: number;
7
+ c: number;
8
+ /** summed quote/USD volume in the bucket. */
9
+ vol: number;
10
+ }
11
+ /** A priced trade: timestamp (unix seconds), price, and optional volume (quote/USD). */
12
+ export interface Point {
13
+ t: number;
14
+ p: number;
15
+ v?: number;
16
+ }
17
+ export type Denom = "USD" | "ETH";
18
+ export type ChartType = "candle" | "line";
19
+ export type IndicatorType = "sma" | "ema" | "wma" | "vwap";
20
+ export type PriceSource = "open" | "high" | "low" | "close";
21
+ /** An overlay indicator drawn on the price pane (a moving average for now). */
22
+ export interface Indicator {
23
+ type: IndicatorType;
24
+ /** Lookback in candles. */
25
+ period: number;
26
+ /** Stroke color; defaults to a built-in palette slot by position. */
27
+ color?: string;
28
+ /** Which price to average. Default "close". */
29
+ source?: PriceSource;
30
+ }
31
+ /** Transform applied to raw prices before aggregation. */
32
+ export interface PriceTransform {
33
+ /** "USD" leaves prices as-is; "ETH" divides by `ethUsd`. */
34
+ denom?: Denom;
35
+ /** USD price of 1 ETH — required when `denom === "ETH"`. */
36
+ ethUsd?: number;
37
+ /** Invert the price (TOKEN/QUOTE → QUOTE/TOKEN). Ignored when `supply` is set. */
38
+ flip?: boolean;
39
+ /** Circulating supply — when > 0 the series becomes market cap (price × supply). */
40
+ supply?: number;
41
+ }
42
+ export interface ChartTheme {
43
+ up: string;
44
+ down: string;
45
+ /** line-mode stroke. */
46
+ line: string;
47
+ grid: string;
48
+ axis: string;
49
+ crosshair: string;
50
+ volUp: string;
51
+ volDown: string;
52
+ /** text on the last-price tag. */
53
+ tagText: string;
54
+ /** crosshair axis-label pill. */
55
+ axisTagBg: string;
56
+ axisTagText: string;
57
+ background?: string;
58
+ }
59
+ export interface ChartOptions {
60
+ /** canvas height in CSS px (default 420). */
61
+ height?: number;
62
+ theme?: Partial<ChartTheme>;
63
+ /** candles visible on first render (default 120). */
64
+ initialBars?: number;
65
+ /** minimum candles when fully zoomed in (default 20). */
66
+ minBars?: number;
67
+ /** cap on a candle's slot width in px — keeps sparse series tight, right-anchored (default 18). */
68
+ maxBarWidth?: number;
69
+ /** fraction of the plot used by the volume panel (default 0.18). */
70
+ volumeRatio?: number;
71
+ rightPad?: number;
72
+ bottomPad?: number;
73
+ topPad?: number;
74
+ /** called with the candle under the crosshair (or the last candle when idle / null when empty). */
75
+ onCrosshair?: (candle: Candle | null) => void;
76
+ /** moving-average overlays drawn on the price pane. */
77
+ indicators?: Indicator[];
78
+ /** fired when the user pans/zooms near the start of loaded data (lazy history). */
79
+ onNeedHistory?: () => void;
80
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ export { Chart } from "./core/chart";
2
+ export { buildOHLC, transformPrice } from "./core/ohlc";
3
+ export { draw } from "./core/renderer";
4
+ export type { RenderInput, Viewport, Pads, ResolvedOverlay } from "./core/renderer";
5
+ export { connectFeed } from "./core/feed";
6
+ export type { ChartFeed, ConnectFeedOptions, FeedConnection, LoadHistoryParams } from "./core/feed";
7
+ export { DEFAULT_THEME } from "./core/theme";
8
+ export { formatValue, formatVolume, formatTime } from "./core/format";
9
+ export { sma, ema, wma, vwap, bollingerBands, sourceValues, computeIndicator, INDICATOR_PALETTE } from "./core/indicators";
10
+ export { fetchHyperliquidCandles, fetchHlCandleWindow, hyperliquidFeed, mapHlCandles, HL_INTERVAL_SECONDS } from "./core/live";
11
+ export type { HlRawCandle, FetchHlCandlesOptions, HyperliquidFeedOptions } from "./core/live";
12
+ export type { Candle, Point, Denom, ChartType, PriceTransform, ChartTheme, ChartOptions, Indicator, IndicatorType, PriceSource, } from "./core/types";
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ // livo-charts — framework-agnostic core.
2
+ // React bindings live in the `@livo-build/charts/react` subpath so non-React
3
+ // consumers never pull React into their bundle.
4
+ export { Chart } from "./core/chart";
5
+ export { buildOHLC, transformPrice } from "./core/ohlc";
6
+ export { draw } from "./core/renderer";
7
+ export { connectFeed } from "./core/feed";
8
+ export { DEFAULT_THEME } from "./core/theme";
9
+ export { formatValue, formatVolume, formatTime } from "./core/format";
10
+ export { sma, ema, wma, vwap, bollingerBands, sourceValues, computeIndicator, INDICATOR_PALETTE } from "./core/indicators";
11
+ export { fetchHyperliquidCandles, fetchHlCandleWindow, hyperliquidFeed, mapHlCandles, HL_INTERVAL_SECONDS } from "./core/live";
@@ -0,0 +1,28 @@
1
+ import type { ChartTheme, ChartType, Indicator } from "../core/types";
2
+ export interface HyperliquidChartProps {
3
+ /** Hyperliquid coin symbol, e.g. "BTC", "ETH", or a builder-dex name like "xyz:TSLA". */
4
+ coin: string;
5
+ /** Initial interval (default "1h"). */
6
+ interval?: string;
7
+ testnet?: boolean;
8
+ height?: number;
9
+ chartType?: ChartType;
10
+ /** Moving-average overlays, e.g. [{ type: "ema", period: 21 }]. */
11
+ indicators?: Indicator[];
12
+ /** Candles per history page; older pages load as you scroll left (default 500). */
13
+ pageSize?: number;
14
+ theme?: Partial<ChartTheme>;
15
+ /** Show the interval / candle-line toolbar. Default true. */
16
+ toolbar?: boolean;
17
+ }
18
+ /**
19
+ * A turnkey, REALTIME Hyperliquid trading chart. Streams live candles over WebSocket,
20
+ * lazily loads older history as the user scrolls left (infinite back-scroll), and
21
+ * supports moving-average overlays — all with no API key. Renders via the
22
+ * framework-agnostic {@link Chart} (zoom/pan preserved across updates). Styled inline.
23
+ *
24
+ * ```tsx
25
+ * <HyperliquidChart coin="BTC" interval="15m" indicators={[{ type: "ema", period: 21 }]} />
26
+ * ```
27
+ */
28
+ export declare function HyperliquidChart({ coin, interval, testnet, height, chartType, indicators, pageSize, theme, toolbar, }: HyperliquidChartProps): import("react").JSX.Element;
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { Chart } from "../core/chart";
4
+ import { connectFeed } from "../core/feed";
5
+ import { hyperliquidFeed, HL_INTERVAL_SECONDS } from "../core/live";
6
+ import { formatValue } from "../core/format";
7
+ import { DEFAULT_THEME } from "../core/theme";
8
+ const IVS = ["1m", "5m", "15m", "1h", "4h", "1d"];
9
+ /**
10
+ * A turnkey, REALTIME Hyperliquid trading chart. Streams live candles over WebSocket,
11
+ * lazily loads older history as the user scrolls left (infinite back-scroll), and
12
+ * supports moving-average overlays — all with no API key. Renders via the
13
+ * framework-agnostic {@link Chart} (zoom/pan preserved across updates). Styled inline.
14
+ *
15
+ * ```tsx
16
+ * <HyperliquidChart coin="BTC" interval="15m" indicators={[{ type: "ema", period: 21 }]} />
17
+ * ```
18
+ */
19
+ export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420, chartType = "candle", indicators, pageSize = 500, theme, toolbar = true, }) {
20
+ const [iv, setIv] = useState(interval);
21
+ const [type, setType] = useState(chartType);
22
+ const [last, setLast] = useState(null);
23
+ const [error, setError] = useState(null);
24
+ const [loading, setLoading] = useState(true);
25
+ const host = useRef(null);
26
+ const chart = useRef(null);
27
+ const th = useMemo(() => ({ ...DEFAULT_THEME, ...(theme || {}) }), [theme]);
28
+ // create the core chart once
29
+ useEffect(() => {
30
+ if (!host.current)
31
+ return;
32
+ const c = new Chart(host.current, { height, theme: th, indicators });
33
+ chart.current = c;
34
+ return () => {
35
+ c.destroy();
36
+ chart.current = null;
37
+ };
38
+ // eslint-disable-next-line react-hooks/exhaustive-deps
39
+ }, []);
40
+ useEffect(() => { chart.current?.setHeight(height); }, [height]);
41
+ useEffect(() => { chart.current?.setChartType(type); }, [type]);
42
+ useEffect(() => { chart.current?.setTheme(th); }, [th]);
43
+ useEffect(() => { chart.current?.setIndicators(indicators ?? []); }, [indicators]);
44
+ // connect a realtime feed: latest page + WebSocket live + lazy older history.
45
+ // Reconnects when coin/interval/testnet change (toolbar drives `iv`).
46
+ useEffect(() => {
47
+ if (!chart.current)
48
+ return;
49
+ setLoading(true);
50
+ setError(null);
51
+ const feed = hyperliquidFeed({ coin, interval: iv, testnet });
52
+ const conn = connectFeed(chart.current, feed, {
53
+ interval: HL_INTERVAL_SECONDS[iv] ?? 3600,
54
+ pageSize,
55
+ onCandles: (cs) => {
56
+ setLast(cs[cs.length - 1] ?? null);
57
+ setLoading(false);
58
+ },
59
+ onError: (e) => {
60
+ setError(e instanceof Error ? e.message : String(e));
61
+ setLoading(false);
62
+ },
63
+ });
64
+ return () => conn.disconnect();
65
+ }, [coin, iv, testnet, pageSize]);
66
+ const lc = last && last.c >= last.o ? th.up : th.down;
67
+ const btn = (on) => ({
68
+ padding: "4px 8px",
69
+ fontSize: 12,
70
+ fontWeight: 500,
71
+ border: "none",
72
+ borderRadius: 4,
73
+ cursor: "pointer",
74
+ background: on ? "#3f4453" : "transparent",
75
+ color: on ? "#e4e7ec" : "#9aa0ad",
76
+ });
77
+ const sep = _jsx("span", { style: { width: 1, height: 16, margin: "0 4px", background: "#2a2e39" } });
78
+ return (_jsxs("div", { style: { position: "relative", userSelect: "none" }, children: [toolbar && (_jsxs("div", { style: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 2, borderBottom: "1px solid #1f232e", paddingBottom: 4, marginBottom: 4 }, children: [IVS.map((l) => (_jsx("button", { style: btn(iv === l), onClick: () => setIv(l), children: l }, l))), sep, _jsx("button", { style: btn(type === "candle"), title: "Candles", onClick: () => setType("candle"), children: "\u25AE\u25AF" }), _jsx("button", { style: btn(type === "line"), title: "Line", onClick: () => setType("line"), children: "\u2571" })] })), _jsxs("div", { style: { position: "relative" }, children: [_jsxs("div", { style: { position: "absolute", left: 8, top: 4, zIndex: 10, pointerEvents: "none", fontSize: 11, fontFamily: "ui-monospace, monospace" }, children: [_jsxs("div", { style: { color: "#b4b9c4" }, children: [coin, " \u00B7 ", iv, testnet ? " · testnet" : ""] }), last ? (_jsx("div", { style: { color: lc }, children: formatValue(last.c) })) : error ? (_jsx("div", { style: { color: th.down }, children: error })) : loading ? (_jsx("div", { style: { color: "#787b86" }, children: "loading\u2026" })) : null] }), _jsx("div", { ref: host })] })] }));
79
+ }
@@ -0,0 +1,31 @@
1
+ import type { ChartOptions, ChartTheme, Indicator, Point } from "../core/types";
2
+ export interface PriceChartProps {
3
+ /** Priced trades — aggregated into candles client-side. `v` is per-trade volume (quote/USD). */
4
+ swaps: Point[];
5
+ /** USD price of 1 ETH, required for the ETH denomination toggle. */
6
+ ethUsd?: number;
7
+ /** Token address — enables the Market-cap toggle (fetches `totalSupply()`). */
8
+ tokenAddress?: string;
9
+ /** Token decimals (for the market-cap supply read). */
10
+ decimals?: number;
11
+ /** Base symbol shown in the legend. */
12
+ symbol?: string;
13
+ /** Quote symbol shown in the legend. */
14
+ quote?: string;
15
+ /** Canvas height in px (default 420). */
16
+ height?: number;
17
+ /** JSON-RPC endpoint for the market-cap `totalSupply()` read (default cloudflare-eth). */
18
+ rpcUrl?: string;
19
+ /** Theme overrides. */
20
+ theme?: Partial<ChartTheme>;
21
+ /** Moving-average overlays, e.g. [{ type: "ema", period: 21 }]. */
22
+ indicators?: Indicator[];
23
+ /** Advanced core options. */
24
+ options?: ChartOptions;
25
+ }
26
+ /**
27
+ * A self-contained trading chart: candles/line, volume panel, crosshair + axis labels,
28
+ * OHLCV legend, X/Y zoom + pan, interval / USD-ETH / price-mcap / fullscreen toolbar.
29
+ * Wraps the framework-agnostic {@link Chart}; styled inline (no CSS framework needed).
30
+ */
31
+ export declare function PriceChart({ swaps, ethUsd, tokenAddress, decimals, symbol, quote, height, rpcUrl, theme, indicators, options, }: PriceChartProps): import("react").JSX.Element;
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { Chart } from "../core/chart";
4
+ import { buildOHLC } from "../core/ohlc";
5
+ import { formatValue, formatVolume } from "../core/format";
6
+ import { DEFAULT_THEME } from "../core/theme";
7
+ const IVS = [["1s", 1], ["1m", 60], ["5m", 300], ["15m", 900], ["1h", 3600], ["4h", 14400], ["1d", 86400]];
8
+ const supplyCache = new Map();
9
+ /**
10
+ * A self-contained trading chart: candles/line, volume panel, crosshair + axis labels,
11
+ * OHLCV legend, X/Y zoom + pan, interval / USD-ETH / price-mcap / fullscreen toolbar.
12
+ * Wraps the framework-agnostic {@link Chart}; styled inline (no CSS framework needed).
13
+ */
14
+ export function PriceChart({ swaps, ethUsd = 0, tokenAddress, decimals = 18, symbol = "", quote = "WETH", height = 420, rpcUrl = "https://cloudflare-eth.com", theme, indicators, options, }) {
15
+ const [iv, setIv] = useState(300);
16
+ const [type, setType] = useState("candle");
17
+ const [denom, setDenom] = useState("USD");
18
+ const [mcap, setMcap] = useState(false);
19
+ const [flip, setFlip] = useState(false);
20
+ const [full, setFull] = useState(false);
21
+ const [supply, setSupply] = useState(0);
22
+ const [legend, setLegend] = useState(null);
23
+ const host = useRef(null);
24
+ const chart = useRef(null);
25
+ const th = useMemo(() => ({ ...DEFAULT_THEME, ...(theme || {}) }), [theme]);
26
+ const H = full && typeof window !== "undefined" ? window.innerHeight - 46 : height;
27
+ // Market cap needs circulating supply — totalSupply() via JSON-RPC, cached.
28
+ useEffect(() => {
29
+ if (!mcap || !tokenAddress)
30
+ return;
31
+ const key = tokenAddress.toLowerCase();
32
+ if (supplyCache.has(key)) {
33
+ setSupply(supplyCache.get(key));
34
+ return;
35
+ }
36
+ fetch(rpcUrl, {
37
+ method: "POST",
38
+ headers: { "content-type": "application/json" },
39
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_call", params: [{ to: tokenAddress, data: "0x18160ddd" }, "latest"] }),
40
+ })
41
+ .then((r) => r.json())
42
+ .then((j) => {
43
+ const hx = j.result;
44
+ if (hx && hx !== "0x") {
45
+ const s = Number(BigInt(hx)) / Math.pow(10, decimals);
46
+ supplyCache.set(key, s);
47
+ setSupply(s);
48
+ }
49
+ })
50
+ .catch(() => { });
51
+ }, [mcap, tokenAddress, decimals, rpcUrl]);
52
+ const candles = useMemo(() => buildOHLC(swaps, iv, { denom, ethUsd, flip: mcap ? false : flip, supply: mcap ? supply : 0 }), [swaps, iv, denom, ethUsd, flip, mcap, supply]);
53
+ // create the core chart once
54
+ useEffect(() => {
55
+ if (!host.current)
56
+ return;
57
+ const c = new Chart(host.current, { height: H, theme: th, onCrosshair: setLegend, indicators, ...options });
58
+ chart.current = c;
59
+ return () => {
60
+ c.destroy();
61
+ chart.current = null;
62
+ };
63
+ // eslint-disable-next-line react-hooks/exhaustive-deps
64
+ }, []);
65
+ useEffect(() => { chart.current?.setHeight(H); }, [H]);
66
+ useEffect(() => { chart.current?.setInterval(iv); }, [iv]);
67
+ useEffect(() => { chart.current?.setChartType(type); }, [type]);
68
+ useEffect(() => { chart.current?.setTheme(th); }, [th]);
69
+ useEffect(() => { chart.current?.setIndicators(indicators ?? []); }, [indicators]);
70
+ useEffect(() => { chart.current?.setCandles(candles); }, [candles]);
71
+ const btn = (on) => ({
72
+ padding: "4px 8px",
73
+ fontSize: 12,
74
+ fontWeight: 500,
75
+ border: "none",
76
+ borderRadius: 4,
77
+ cursor: "pointer",
78
+ background: on ? "#3f4453" : "transparent",
79
+ color: on ? "#e4e7ec" : "#9aa0ad",
80
+ });
81
+ const sep = _jsx("span", { style: { width: 1, height: 16, margin: "0 4px", background: "#2a2e39" } });
82
+ const lc = legend && legend.c >= legend.o ? th.up : th.down;
83
+ const ivLabel = IVS.find(([, s]) => s === iv)?.[0] || "";
84
+ return (_jsxs("div", { style: full
85
+ ? { position: "fixed", inset: 0, zIndex: 50, background: "#0a0a0a", padding: 8, userSelect: "none" }
86
+ : { position: "relative", userSelect: "none" }, children: [_jsxs("div", { style: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 2, borderBottom: "1px solid #1f232e", paddingBottom: 4, marginBottom: 4 }, children: [IVS.map(([l, s]) => (_jsx("button", { style: btn(iv === s), onClick: () => setIv(s), children: l }, s))), sep, _jsx("button", { style: btn(type === "candle"), title: "Candles", onClick: () => setType("candle"), children: "\u25AE\u25AF" }), _jsx("button", { style: btn(type === "line"), title: "Line", onClick: () => setType("line"), children: "\u2571" }), sep, _jsx("button", { style: btn(!mcap), onClick: () => setMcap(false), children: "Price" }), _jsx("button", { style: btn(mcap), title: "Market cap (price \u00D7 supply)", onClick: () => setMcap(true), children: "MCap" }), sep, _jsx("button", { style: btn(denom === "USD"), onClick: () => setDenom("USD"), children: "USD" }), _jsx("button", { style: btn(denom === "ETH"), onClick: () => setDenom("ETH"), children: "ETH" }), !mcap ? _jsx("button", { style: btn(flip), title: "Invert pair", onClick: () => setFlip((f) => !f), children: "\u21C4" }) : null, _jsx("button", { style: { ...btn(full), marginLeft: "auto" }, title: "Fullscreen", onClick: () => setFull((f) => !f), children: "\u26F6" })] }), _jsxs("div", { style: { position: "relative" }, children: [_jsxs("div", { style: { position: "absolute", left: 8, top: 4, zIndex: 10, pointerEvents: "none", fontSize: 11, fontFamily: "ui-monospace, monospace" }, children: [_jsxs("div", { style: { color: "#b4b9c4" }, children: [symbol || "—", mcap ? " · mcap" : flip ? ` ${quote}/${symbol}` : ` /${quote}`, " \u00B7 ", ivLabel] }), legend ? (_jsxs("div", { style: { color: lc }, children: ["O ", formatValue(legend.o), " H ", formatValue(legend.h), " L ", formatValue(legend.l), " C ", formatValue(legend.c), " ", _jsxs("span", { style: { color: "#787b86" }, children: ["Vol ", formatVolume(legend.vol)] })] })) : null] }), _jsx("div", { ref: host })] })] }));
87
+ }
@@ -0,0 +1,4 @@
1
+ export { PriceChart } from "./react/PriceChart";
2
+ export type { PriceChartProps } from "./react/PriceChart";
3
+ export { HyperliquidChart } from "./react/HyperliquidChart";
4
+ export type { HyperliquidChartProps } from "./react/HyperliquidChart";
package/dist/react.js ADDED
@@ -0,0 +1,3 @@
1
+ // livo-charts — React bindings. Import from `@livo-build/charts/react`.
2
+ export { PriceChart } from "./react/PriceChart";
3
+ export { HyperliquidChart } from "./react/HyperliquidChart";
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@livo-build/charts",
3
+ "version": "0.2.1",
4
+ "description": "livo-charts — a lightweight, dependency-free canvas charting library (candlesticks, zoom/pan, crosshair, moving-average indicators, live Hyperliquid feed) with a framework-agnostic core and a React wrapper.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./react": {
15
+ "types": "./dist/react.d.ts",
16
+ "import": "./dist/react.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "sideEffects": false,
26
+ "keywords": [
27
+ "livo",
28
+ "charts",
29
+ "candlestick",
30
+ "canvas",
31
+ "trading",
32
+ "react"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/livo-projects/livo-mcp.git",
40
+ "directory": "packages/charts"
41
+ },
42
+ "scripts": {
43
+ "typecheck": "tsc -p tsconfig.json",
44
+ "build": "tsc -p tsconfig.build.json",
45
+ "test": "vitest run",
46
+ "prepublishOnly": "npm run build",
47
+ "clean": "rm -rf dist"
48
+ },
49
+ "peerDependencies": {
50
+ "react": ">=17",
51
+ "react-dom": ">=17"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "react": {
55
+ "optional": true
56
+ },
57
+ "react-dom": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "devDependencies": {
62
+ "@types/react": "^18.3.0",
63
+ "react": "^18.3.0",
64
+ "typescript": "^5.7.0",
65
+ "vitest": "^2.1.0"
66
+ }
67
+ }