@livo-build/charts 0.2.1 → 0.2.3

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.
@@ -1,10 +1,11 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef, useState } from "react";
3
3
  import { Chart } from "../core/chart";
4
4
  import { connectFeed } from "../core/feed";
5
5
  import { hyperliquidFeed, HL_INTERVAL_SECONDS } from "../core/live";
6
- import { formatValue } from "../core/format";
6
+ import { formatValue, formatVolume } from "../core/format";
7
7
  import { DEFAULT_THEME } from "../core/theme";
8
+ import { toolbarUi, typeButtons, drawButtons, loadingOverlay } from "./ui";
8
9
  const IVS = ["1m", "5m", "15m", "1h", "4h", "1d"];
9
10
  /**
10
11
  * A turnkey, REALTIME Hyperliquid trading chart. Streams live candles over WebSocket,
@@ -16,10 +17,15 @@ const IVS = ["1m", "5m", "15m", "1h", "4h", "1d"];
16
17
  * <HyperliquidChart coin="BTC" interval="15m" indicators={[{ type: "ema", period: 21 }]} />
17
18
  * ```
18
19
  */
19
- export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420, chartType = "candle", indicators, pageSize = 500, theme, toolbar = true, }) {
20
+ export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420, chartType = "candle", indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools = false, showVolume: showVolumeProp = true, volumeProfile: volumeProfileProp, pageSize = 500, theme, toolbar = true, fitContent = true, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont, }) {
20
21
  const [iv, setIv] = useState(interval);
21
22
  const [type, setType] = useState(chartType);
23
+ const [log, setLog] = useState(false);
24
+ const [showVolume, setShowVolume] = useState(showVolumeProp);
25
+ const [vpOn, setVpOn] = useState(!!volumeProfileProp);
26
+ const [drawMode, setDrawMode] = useState("none");
22
27
  const [last, setLast] = useState(null);
28
+ const [legend, setLegend] = useState(null);
23
29
  const [error, setError] = useState(null);
24
30
  const [loading, setLoading] = useState(true);
25
31
  const host = useRef(null);
@@ -29,7 +35,27 @@ export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420,
29
35
  useEffect(() => {
30
36
  if (!host.current)
31
37
  return;
32
- const c = new Chart(host.current, { height, theme: th, indicators });
38
+ const c = new Chart(host.current, {
39
+ height,
40
+ theme: th,
41
+ emptyText: "Loading…",
42
+ indicators,
43
+ oscillators,
44
+ drawings,
45
+ onDrawingsChange,
46
+ onDrawModeChange: setDrawMode,
47
+ showVolume: showVolumeProp,
48
+ volumeProfile: volumeProfileProp,
49
+ onCrosshair: setLegend,
50
+ fitContent,
51
+ maxBarWidth,
52
+ maxBodyWidth,
53
+ priceFormat,
54
+ timeFormat,
55
+ priceTicks,
56
+ timeTicks,
57
+ axisFont,
58
+ });
33
59
  chart.current = c;
34
60
  return () => {
35
61
  c.destroy();
@@ -41,6 +67,17 @@ export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420,
41
67
  useEffect(() => { chart.current?.setChartType(type); }, [type]);
42
68
  useEffect(() => { chart.current?.setTheme(th); }, [th]);
43
69
  useEffect(() => { chart.current?.setIndicators(indicators ?? []); }, [indicators]);
70
+ useEffect(() => { chart.current?.setOscillators(oscillators ?? []); }, [oscillators]);
71
+ useEffect(() => { chart.current?.setDrawMode(drawMode); }, [drawMode]);
72
+ useEffect(() => { chart.current?.setShowVolume(showVolume); }, [showVolume]);
73
+ useEffect(() => { chart.current?.setLogScale(log); }, [log]);
74
+ useEffect(() => { chart.current?.setEmptyText(loading ? "" : "no data"); }, [loading]); // overlay handles loading
75
+ useEffect(() => {
76
+ chart.current?.setVolumeProfile(vpOn ? (typeof volumeProfileProp === "object" ? volumeProfileProp : true) : false);
77
+ }, [vpOn, volumeProfileProp]);
78
+ useEffect(() => {
79
+ chart.current?.setAxis({ fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont });
80
+ }, [fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont]);
44
81
  // connect a realtime feed: latest page + WebSocket live + lazy older history.
45
82
  // Reconnects when coin/interval/testnet change (toolbar drives `iv`).
46
83
  useEffect(() => {
@@ -55,6 +92,7 @@ export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420,
55
92
  onCandles: (cs) => {
56
93
  setLast(cs[cs.length - 1] ?? null);
57
94
  setLoading(false);
95
+ setError(null); // a successful page clears a prior transient error
58
96
  },
59
97
  onError: (e) => {
60
98
  setError(e instanceof Error ? e.message : String(e));
@@ -63,17 +101,9 @@ export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420,
63
101
  });
64
102
  return () => conn.disconnect();
65
103
  }, [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 })] })] }));
104
+ const info = legend ?? last;
105
+ const lc = info && info.c >= info.o ? th.up : th.down;
106
+ const { btn, sepStyle, barStyle, muted } = toolbarUi(th);
107
+ const sep = _jsx("span", { style: sepStyle });
108
+ return (_jsxs("div", { style: { position: "relative", userSelect: "none" }, children: [toolbar && (_jsxs("div", { style: barStyle, children: [IVS.map((l) => (_jsx("button", { style: btn(iv === l), onClick: () => setIv(l), children: l }, l))), sep, typeButtons(th, type, setType), sep, _jsx("button", { style: btn(showVolume), title: "Toggle volume panel", onClick: () => setShowVolume((v) => !v), children: "Vol" }), _jsx("button", { style: btn(vpOn), title: "Volume-by-price histogram", onClick: () => setVpOn((v) => !v), children: "VP" }), _jsx("button", { style: btn(log), title: "Logarithmic price axis", onClick: () => setLog((v) => !v), children: "Log" }), !hideDrawingTools ? (_jsxs(_Fragment, { children: [sep, drawButtons(th, drawMode, setDrawMode, () => chart.current?.clearDrawings())] })) : null] })), _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: muted }, children: [coin, " \u00B7 ", iv, testnet ? " · testnet" : ""] }), error ? (_jsx("div", { style: { color: th.down }, children: error })) : info ? (_jsxs("div", { style: { color: lc }, children: ["O ", formatValue(info.o), " H ", formatValue(info.h), " L ", formatValue(info.l), " C ", formatValue(info.c), " ", _jsxs("span", { style: { color: muted }, children: ["Vol ", formatVolume(info.vol)] })] })) : null] }), _jsx("div", { ref: host }), loading ? loadingOverlay(th) : null] })] }));
79
109
  }
@@ -0,0 +1,40 @@
1
+ import type { ChartTheme, ChartType, Drawing, Indicator, Oscillator, VolumeProfileConfig } from "../core/types";
2
+ export interface PolymarketChartProps {
3
+ /** The Polymarket CLOB token id (an outcome of a market) to chart. */
4
+ tokenId: string;
5
+ /** Initial candle bucket in seconds (default 3600 = 1h); the toolbar can switch it. */
6
+ bucketSeconds?: number;
7
+ /** Override the CLOB host. */
8
+ host?: string;
9
+ /** Live poll cadence in ms (default 30s; 0 = fetch once). */
10
+ refetchMs?: number;
11
+ /** Candles per history page; older pages load as you scroll left (default 500). */
12
+ pageSize?: number;
13
+ height?: number;
14
+ chartType?: ChartType;
15
+ indicators?: Indicator[];
16
+ oscillators?: Oscillator[];
17
+ drawings?: Drawing[];
18
+ onDrawingsChange?: (drawings: Drawing[]) => void;
19
+ hideDrawingTools?: boolean;
20
+ /** Show the (always-empty) volume panel — off by default since price-history has no volume. */
21
+ showVolume?: boolean;
22
+ volumeProfile?: VolumeProfileConfig;
23
+ theme?: Partial<ChartTheme>;
24
+ toolbar?: boolean;
25
+ fitContent?: boolean;
26
+ /** Outcome label shown in the legend (e.g. "Yes"). */
27
+ label?: string;
28
+ /** y-axis formatter (default a 0–100% probability). */
29
+ priceFormat?: (value: number) => string;
30
+ }
31
+ /**
32
+ * A turnkey, live chart for a Polymarket outcome. Bucketed OHLC from the public CLOB
33
+ * price-history endpoint (no key), polled for a building candle. Prices are probabilities,
34
+ * so the y-axis defaults to a percentage. Renders via the framework-agnostic {@link Chart}.
35
+ *
36
+ * ```tsx
37
+ * <PolymarketChart tokenId={yesTokenId} bucketSeconds={3600} label="Yes" />
38
+ * ```
39
+ */
40
+ export declare function PolymarketChart({ tokenId, bucketSeconds, host, refetchMs, pageSize, height, chartType, indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools, showVolume: showVolumeProp, volumeProfile: volumeProfileProp, theme, toolbar, fitContent, label, priceFormat, }: PolymarketChartProps): import("react").JSX.Element;
@@ -0,0 +1,95 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, 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 { polymarketFeed } from "../core/polymarket";
6
+ import { DEFAULT_THEME } from "../core/theme";
7
+ import { toolbarUi, typeButtons, drawButtons, loadingOverlay } from "./ui";
8
+ const BUCKETS = [["5m", 300], ["1h", 3600], ["6h", 21600], ["1d", 86400]];
9
+ const pct = (v) => `${(v * 100).toFixed(1)}%`;
10
+ /**
11
+ * A turnkey, live chart for a Polymarket outcome. Bucketed OHLC from the public CLOB
12
+ * price-history endpoint (no key), polled for a building candle. Prices are probabilities,
13
+ * so the y-axis defaults to a percentage. Renders via the framework-agnostic {@link Chart}.
14
+ *
15
+ * ```tsx
16
+ * <PolymarketChart tokenId={yesTokenId} bucketSeconds={3600} label="Yes" />
17
+ * ```
18
+ */
19
+ export function PolymarketChart({ tokenId, bucketSeconds = 3600, host, refetchMs, pageSize = 500, height = 420, chartType = "line", indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools = false, showVolume: showVolumeProp = false, volumeProfile: volumeProfileProp, theme, toolbar = true, fitContent = true, label, priceFormat = pct, }) {
20
+ const [secs, setSecs] = useState(bucketSeconds);
21
+ const [type, setType] = useState(chartType);
22
+ const [log, setLog] = useState(false);
23
+ const [vpOn, setVpOn] = useState(!!volumeProfileProp);
24
+ const [drawMode, setDrawMode] = useState("none");
25
+ const [last, setLast] = useState(null);
26
+ const [legend, setLegend] = useState(null);
27
+ const [error, setError] = useState(null);
28
+ const [loading, setLoading] = useState(true);
29
+ const host_ = useRef(null);
30
+ const chart = useRef(null);
31
+ const th = useMemo(() => ({ ...DEFAULT_THEME, ...(theme || {}) }), [theme]);
32
+ useEffect(() => {
33
+ if (!host_.current)
34
+ return;
35
+ const c = new Chart(host_.current, {
36
+ height,
37
+ theme: th,
38
+ emptyText: "Loading…",
39
+ indicators,
40
+ oscillators,
41
+ drawings,
42
+ onDrawingsChange,
43
+ onDrawModeChange: setDrawMode,
44
+ showVolume: showVolumeProp,
45
+ volumeProfile: volumeProfileProp,
46
+ onCrosshair: setLegend,
47
+ fitContent,
48
+ priceFormat,
49
+ });
50
+ chart.current = c;
51
+ return () => {
52
+ c.destroy();
53
+ chart.current = null;
54
+ };
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ }, []);
57
+ useEffect(() => { chart.current?.setHeight(height); }, [height]);
58
+ useEffect(() => { chart.current?.setChartType(type); }, [type]);
59
+ useEffect(() => { chart.current?.setTheme(th); }, [th]);
60
+ useEffect(() => { chart.current?.setIndicators(indicators ?? []); }, [indicators]);
61
+ useEffect(() => { chart.current?.setOscillators(oscillators ?? []); }, [oscillators]);
62
+ useEffect(() => { chart.current?.setDrawMode(drawMode); }, [drawMode]);
63
+ useEffect(() => { chart.current?.setLogScale(log); }, [log]);
64
+ useEffect(() => { chart.current?.setEmptyText(loading ? "" : "no data"); }, [loading]); // overlay handles loading
65
+ useEffect(() => {
66
+ chart.current?.setVolumeProfile(vpOn ? (typeof volumeProfileProp === "object" ? volumeProfileProp : true) : false);
67
+ }, [vpOn, volumeProfileProp]);
68
+ useEffect(() => {
69
+ if (!chart.current)
70
+ return;
71
+ setLoading(true);
72
+ setError(null);
73
+ const feed = polymarketFeed({ tokenId, bucketSeconds: secs, host, refetchMs });
74
+ const conn = connectFeed(chart.current, feed, {
75
+ interval: secs,
76
+ pageSize,
77
+ onCandles: (cs) => {
78
+ setLast(cs[cs.length - 1] ?? null);
79
+ setLoading(false);
80
+ setError(null); // a successful page clears a prior transient error
81
+ },
82
+ onError: (e) => {
83
+ setError(e instanceof Error ? e.message : String(e));
84
+ setLoading(false);
85
+ },
86
+ });
87
+ return () => conn.disconnect();
88
+ }, [tokenId, secs, host, refetchMs, pageSize]);
89
+ const info = legend ?? last;
90
+ const lc = info && info.c >= info.o ? th.up : th.down;
91
+ const { btn, sepStyle, barStyle, muted } = toolbarUi(th);
92
+ const sep = _jsx("span", { style: sepStyle });
93
+ const ivLabel = BUCKETS.find(([, s]) => s === secs)?.[0] ?? `${secs}s`;
94
+ return (_jsxs("div", { style: { position: "relative", userSelect: "none" }, children: [toolbar && (_jsxs("div", { style: barStyle, children: [BUCKETS.map(([l, s]) => (_jsx("button", { style: btn(secs === s), onClick: () => setSecs(s), children: l }, s))), sep, typeButtons(th, type, setType), sep, _jsx("button", { style: btn(vpOn), title: "Volume-by-price histogram", onClick: () => setVpOn((v) => !v), children: "VP" }), _jsx("button", { style: btn(log), title: "Logarithmic price axis", onClick: () => setLog((v) => !v), children: "Log" }), !hideDrawingTools ? (_jsxs(_Fragment, { children: [sep, drawButtons(th, drawMode, setDrawMode, () => chart.current?.clearDrawings())] })) : null] })), _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: muted }, children: [label ? `${label} · ` : "", "prob \u00B7 ", ivLabel] }), error ? (_jsx("div", { style: { color: th.down }, children: error })) : info ? (_jsxs("div", { style: { color: lc }, children: ["O ", pct(info.o), " H ", pct(info.h), " L ", pct(info.l), " C ", pct(info.c)] })) : null] }), _jsx("div", { ref: host_ }), loading ? loadingOverlay(th) : null] })] }));
95
+ }
@@ -1,12 +1,16 @@
1
- import type { ChartOptions, ChartTheme, Indicator, Point } from "../core/types";
1
+ import type { ChartOptions, ChartTheme, ChartType, Drawing, Indicator, Oscillator, Point, VolumeProfileConfig } from "../core/types";
2
2
  export interface PriceChartProps {
3
3
  /** Priced trades — aggregated into candles client-side. `v` is per-trade volume (quote/USD). */
4
4
  swaps: Point[];
5
5
  /** USD price of 1 ETH, required for the ETH denomination toggle. */
6
6
  ethUsd?: number;
7
- /** Token address — enables the Market-cap toggle (fetches `totalSupply()`). */
7
+ /** Circulating/total supply — enables the Market-cap toggle directly (MCap = price ×
8
+ * supply). Use this when you already know the supply (no RPC call needed). */
9
+ supply?: number;
10
+ /** Token address — enables the Market-cap toggle by fetching `totalSupply()` over RPC
11
+ * (only used when `supply` isn't provided). */
8
12
  tokenAddress?: string;
9
- /** Token decimals (for the market-cap supply read). */
13
+ /** Token decimals (for the market-cap `totalSupply()` read). */
10
14
  decimals?: number;
11
15
  /** Base symbol shown in the legend. */
12
16
  symbol?: string;
@@ -20,6 +24,52 @@ export interface PriceChartProps {
20
24
  theme?: Partial<ChartTheme>;
21
25
  /** Moving-average overlays, e.g. [{ type: "ema", period: 21 }]. */
22
26
  indicators?: Indicator[];
27
+ /** Oscillator sub-panes (RSI / MACD), e.g. [{ type: "rsi", period: 14 }]. */
28
+ oscillators?: Oscillator[];
29
+ /** Initial drawings (trendlines / horizontal lines). The chart manages them after mount. */
30
+ drawings?: Drawing[];
31
+ /** Fired whenever the user adds, moves, or deletes a drawing. */
32
+ onDrawingsChange?: (drawings: Drawing[]) => void;
33
+ /** Hide the trendline / h-line / fib / rect / clear drawing-tool buttons (shown by default). */
34
+ hideDrawingTools?: boolean;
35
+ /** Show the volume panel (default true). */
36
+ showVolume?: boolean;
37
+ /** Volume-by-price histogram (default off); the toolbar adds a VP toggle when set. */
38
+ volumeProfile?: VolumeProfileConfig;
39
+ /** Reference price for the `baseline` chart type (default: the first visible close). */
40
+ baselinePrice?: number;
41
+ /** Initial series style (default "candle"); the toolbar can switch it after mount. */
42
+ defaultChartType?: ChartType;
43
+ /** Initial bucket interval in seconds (default 300 = 5m); the toolbar can switch it. */
44
+ defaultInterval?: number;
45
+ /** Toolbar interval presets as `[label, seconds]`. Default `1s…1d`. Scope this to the
46
+ * intervals your data actually covers — a thin dataset bucketed at `1h` collapses into a
47
+ * couple of fat candles. (`PriceChart` aggregates the `swaps` you pass; it doesn't fetch
48
+ * more — supply a wider range via `onIntervalChange` if you want coarser buckets to fill.) */
49
+ intervals?: [string, number][];
50
+ /** Fired when the user picks a different interval (seconds) — load a wider range here. */
51
+ onIntervalChange?: (seconds: number) => void;
52
+ /** Fired when the user pans/zooms within ~one viewport of the oldest candle. Prepend
53
+ * older trades to `swaps` here for infinite back-history (`PriceChart` re-aggregates and
54
+ * the right-anchored view keeps the position stable). No-op if you pass a fixed array. */
55
+ onLoadOlder?: () => void;
56
+ /** Spread candles across the full width instead of right-anchoring a capped slot.
57
+ * Default `true` — fills the container for sparse series. */
58
+ fitContent?: boolean;
59
+ /** Max candle slot width in px (spacing; default 18). */
60
+ maxBarWidth?: number;
61
+ /** Max candle body / volume-bar width in px (default 40). Raise for chunkier candles. */
62
+ maxBodyWidth?: number;
63
+ /** y-axis price label formatter (default adapts precision to the tick step). */
64
+ priceFormat?: (value: number) => string;
65
+ /** x-axis time label formatter. */
66
+ timeFormat?: (time: number, interval: number) => string;
67
+ /** number of horizontal price gridlines / labels (default 5). */
68
+ priceTicks?: number;
69
+ /** approximate number of time-axis labels (default 7). */
70
+ timeTicks?: number;
71
+ /** canvas font for axis labels (default "10px ui-monospace, monospace"); color is `theme.axis`. */
72
+ axisFont?: string;
23
73
  /** Advanced core options. */
24
74
  options?: ChartOptions;
25
75
  }
@@ -28,4 +78,4 @@ export interface PriceChartProps {
28
78
  * OHLCV legend, X/Y zoom + pan, interval / USD-ETH / price-mcap / fullscreen toolbar.
29
79
  * Wraps the framework-agnostic {@link Chart}; styled inline (no CSS framework needed).
30
80
  */
31
- export declare function PriceChart({ swaps, ethUsd, tokenAddress, decimals, symbol, quote, height, rpcUrl, theme, indicators, options, }: PriceChartProps): import("react").JSX.Element;
81
+ export declare function PriceChart({ swaps, ethUsd, supply: supplyProp, tokenAddress, decimals, symbol, quote, height, rpcUrl, theme, indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools, showVolume: showVolumeProp, volumeProfile: volumeProfileProp, baselinePrice, defaultChartType, defaultInterval, intervals, onIntervalChange, onLoadOlder, fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont, options, }: PriceChartProps): import("react").JSX.Element;
@@ -1,36 +1,49 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef, useState } from "react";
3
3
  import { Chart } from "../core/chart";
4
4
  import { buildOHLC } from "../core/ohlc";
5
5
  import { formatValue, formatVolume } from "../core/format";
6
6
  import { DEFAULT_THEME } from "../core/theme";
7
- const IVS = [["1s", 1], ["1m", 60], ["5m", 300], ["15m", 900], ["1h", 3600], ["4h", 14400], ["1d", 86400]];
7
+ import { toolbarUi } from "./ui";
8
+ import { typeButtons, drawButtons } from "./ui";
9
+ const DEFAULT_IVS = [["1s", 1], ["1m", 60], ["5m", 300], ["15m", 900], ["1h", 3600], ["4h", 14400], ["1d", 86400]];
8
10
  const supplyCache = new Map();
9
11
  /**
10
12
  * A self-contained trading chart: candles/line, volume panel, crosshair + axis labels,
11
13
  * OHLCV legend, X/Y zoom + pan, interval / USD-ETH / price-mcap / fullscreen toolbar.
12
14
  * Wraps the framework-agnostic {@link Chart}; styled inline (no CSS framework needed).
13
15
  */
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");
16
+ export function PriceChart({ swaps, ethUsd = 0, supply: supplyProp = 0, tokenAddress, decimals = 18, symbol = "", quote = "WETH", height = 420, rpcUrl = "https://cloudflare-eth.com", theme, indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools = false, showVolume: showVolumeProp = true, volumeProfile: volumeProfileProp, baselinePrice, defaultChartType = "candle", defaultInterval = 300, intervals = DEFAULT_IVS, onIntervalChange, onLoadOlder, fitContent = true, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont, options, }) {
17
+ const [iv, setIv] = useState(defaultInterval);
18
+ const [type, setType] = useState(defaultChartType);
17
19
  const [denom, setDenom] = useState("USD");
18
20
  const [mcap, setMcap] = useState(false);
19
21
  const [flip, setFlip] = useState(false);
22
+ const [log, setLog] = useState(!!options?.logScale);
23
+ const [showVolume, setShowVolume] = useState(showVolumeProp);
24
+ const [vpOn, setVpOn] = useState(!!volumeProfileProp);
25
+ const [drawMode, setDrawMode] = useState("none");
20
26
  const [full, setFull] = useState(false);
21
- const [supply, setSupply] = useState(0);
27
+ const [fetchedSupply, setFetchedSupply] = useState(0);
22
28
  const [legend, setLegend] = useState(null);
23
29
  const host = useRef(null);
24
30
  const chart = useRef(null);
31
+ // keep the latest onLoadOlder callable from the once-created chart without re-mounting it.
32
+ const onLoadOlderRef = useRef(onLoadOlder);
33
+ onLoadOlderRef.current = onLoadOlder;
25
34
  const th = useMemo(() => ({ ...DEFAULT_THEME, ...(theme || {}) }), [theme]);
26
35
  const H = full && typeof window !== "undefined" ? window.innerHeight - 46 : height;
27
- // Market cap needs circulating supply totalSupply() via JSON-RPC, cached.
36
+ // A direct `supply` prop wins; otherwise market cap needs circulating supply, read once
37
+ // from totalSupply() over JSON-RPC (cached). Market cap is only available when one of
38
+ // the two is present — see `canMcap` below (we hide the toggle otherwise).
39
+ const supply = supplyProp || fetchedSupply;
40
+ const canMcap = supplyProp > 0 || !!tokenAddress;
28
41
  useEffect(() => {
29
- if (!mcap || !tokenAddress)
42
+ if (!mcap || supplyProp > 0 || !tokenAddress)
30
43
  return;
31
44
  const key = tokenAddress.toLowerCase();
32
45
  if (supplyCache.has(key)) {
33
- setSupply(supplyCache.get(key));
46
+ setFetchedSupply(supplyCache.get(key));
34
47
  return;
35
48
  }
36
49
  fetch(rpcUrl, {
@@ -44,17 +57,41 @@ export function PriceChart({ swaps, ethUsd = 0, tokenAddress, decimals = 18, sym
44
57
  if (hx && hx !== "0x") {
45
58
  const s = Number(BigInt(hx)) / Math.pow(10, decimals);
46
59
  supplyCache.set(key, s);
47
- setSupply(s);
60
+ setFetchedSupply(s);
48
61
  }
49
62
  })
50
63
  .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]);
64
+ }, [mcap, supplyProp, tokenAddress, decimals, rpcUrl]);
65
+ // Market cap only renders when we actually have a supply; falls back to price otherwise.
66
+ const showMcap = mcap && supply > 0;
67
+ const candles = useMemo(() => buildOHLC(swaps, iv, { denom, ethUsd, flip: showMcap ? false : flip, supply: showMcap ? supply : 0 }), [swaps, iv, denom, ethUsd, flip, showMcap, supply]);
53
68
  // create the core chart once
54
69
  useEffect(() => {
55
70
  if (!host.current)
56
71
  return;
57
- const c = new Chart(host.current, { height: H, theme: th, onCrosshair: setLegend, indicators, ...options });
72
+ const c = new Chart(host.current, {
73
+ height: H,
74
+ theme: th,
75
+ onCrosshair: setLegend,
76
+ onNeedHistory: () => onLoadOlderRef.current?.(),
77
+ indicators,
78
+ oscillators,
79
+ drawings,
80
+ onDrawingsChange,
81
+ onDrawModeChange: setDrawMode,
82
+ showVolume: showVolumeProp,
83
+ volumeProfile: volumeProfileProp,
84
+ baselinePrice,
85
+ fitContent,
86
+ maxBarWidth,
87
+ maxBodyWidth,
88
+ priceFormat,
89
+ timeFormat,
90
+ priceTicks,
91
+ timeTicks,
92
+ axisFont,
93
+ ...options,
94
+ });
58
95
  chart.current = c;
59
96
  return () => {
60
97
  c.destroy();
@@ -67,21 +104,23 @@ export function PriceChart({ swaps, ethUsd = 0, tokenAddress, decimals = 18, sym
67
104
  useEffect(() => { chart.current?.setChartType(type); }, [type]);
68
105
  useEffect(() => { chart.current?.setTheme(th); }, [th]);
69
106
  useEffect(() => { chart.current?.setIndicators(indicators ?? []); }, [indicators]);
107
+ useEffect(() => { chart.current?.setOscillators(oscillators ?? []); }, [oscillators]);
108
+ useEffect(() => { chart.current?.setDrawMode(drawMode); }, [drawMode]);
109
+ useEffect(() => { chart.current?.setShowVolume(showVolume); }, [showVolume]);
110
+ useEffect(() => { chart.current?.setLogScale(log); }, [log]);
111
+ useEffect(() => {
112
+ chart.current?.setVolumeProfile(vpOn ? (typeof volumeProfileProp === "object" ? volumeProfileProp : true) : false);
113
+ }, [vpOn, volumeProfileProp]);
114
+ useEffect(() => { chart.current?.setBaseline(baselinePrice); }, [baselinePrice]);
115
+ useEffect(() => {
116
+ chart.current?.setAxis({ fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont });
117
+ }, [fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont]);
70
118
  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" } });
119
+ const { btn, sepStyle, barStyle, muted } = toolbarUi(th);
120
+ const sep = _jsx("span", { style: sepStyle });
82
121
  const lc = legend && legend.c >= legend.o ? th.up : th.down;
83
- const ivLabel = IVS.find(([, s]) => s === iv)?.[0] || "";
122
+ const ivLabel = intervals.find(([, s]) => s === iv)?.[0] || "";
84
123
  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 })] })] }));
124
+ ? { position: "fixed", inset: 0, zIndex: 50, background: th.background ?? "#0a0a0a", padding: 8, userSelect: "none" }
125
+ : { position: "relative", userSelect: "none" }, children: [_jsxs("div", { style: barStyle, children: [intervals.map(([l, s]) => (_jsx("button", { style: btn(iv === s), onClick: () => { setIv(s); onIntervalChange?.(s); }, children: l }, s))), sep, typeButtons(th, type, setType), canMcap ? (_jsxs(_Fragment, { children: [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" })] })) : null, ethUsd > 0 ? (_jsxs(_Fragment, { children: [sep, _jsx("button", { style: btn(denom === "USD"), onClick: () => setDenom("USD"), children: "USD" }), _jsx("button", { style: btn(denom === "ETH"), title: "Denominate in ETH (price \u00F7 ETH/USD)", onClick: () => setDenom("ETH"), children: "ETH" })] })) : null, !showMcap ? (_jsxs(_Fragment, { children: [sep, _jsx("button", { style: btn(flip), title: "Invert pair", onClick: () => setFlip((f) => !f), children: "\u21C4" })] })) : null, sep, _jsx("button", { style: btn(showVolume), title: "Toggle volume panel", onClick: () => setShowVolume((v) => !v), children: "Vol" }), _jsx("button", { style: btn(vpOn), title: "Volume-by-price histogram", onClick: () => setVpOn((v) => !v), children: "VP" }), _jsx("button", { style: btn(log), title: "Logarithmic price axis", onClick: () => setLog((v) => !v), children: "Log" }), !hideDrawingTools ? (_jsxs(_Fragment, { children: [sep, drawButtons(th, drawMode, setDrawMode, () => chart.current?.clearDrawings())] })) : 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: muted }, children: [symbol || "—", showMcap ? " · 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: muted }, children: ["Vol ", formatVolume(legend.vol)] })] })) : null] }), _jsx("div", { ref: host })] })] }));
87
126
  }
@@ -0,0 +1,37 @@
1
+ import type { ChartTheme, ChartType, Drawing, Indicator, Oscillator, VolumeProfileConfig } from "../core/types";
2
+ export interface SignalsChartProps {
3
+ /** Token to chart — pool address, base-token address, or pair symbol (e.g. "PEPE"). */
4
+ token: string;
5
+ /** Signal Radar engine base URL (default https://signals.livo.build). */
6
+ url?: string;
7
+ /** Initial candle bucket in seconds (default 300 = 5m); the toolbar can switch it. */
8
+ bucketSeconds?: number;
9
+ /** Live poll cadence in ms (default 15s; 0 = fetch once). */
10
+ refetchMs?: number;
11
+ height?: number;
12
+ chartType?: ChartType;
13
+ indicators?: Indicator[];
14
+ oscillators?: Oscillator[];
15
+ drawings?: Drawing[];
16
+ onDrawingsChange?: (drawings: Drawing[]) => void;
17
+ hideDrawingTools?: boolean;
18
+ /** Show the (always-empty) volume panel — off by default since the snapshot has no volume. */
19
+ showVolume?: boolean;
20
+ volumeProfile?: VolumeProfileConfig;
21
+ theme?: Partial<ChartTheme>;
22
+ toolbar?: boolean;
23
+ fitContent?: boolean;
24
+ /** y-axis price formatter (default a range-aware USD value). */
25
+ priceFormat?: (value: number) => string;
26
+ }
27
+ /**
28
+ * A turnkey, live chart for a token tracked by the Livo signals engine ("Signal Radar").
29
+ * Seeds from the engine snapshot's recent-closes sparkline and polls for a building candle
30
+ * (no key). History is shallow (the snapshot's `spark`) — for deep history use a chart
31
+ * backed by an indexer feed. Renders via the framework-agnostic {@link Chart}.
32
+ *
33
+ * ```tsx
34
+ * <SignalsChart token="PEPE" bucketSeconds={300} />
35
+ * ```
36
+ */
37
+ export declare function SignalsChart({ token, url, bucketSeconds, refetchMs, height, chartType, indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools, showVolume: showVolumeProp, volumeProfile: volumeProfileProp, theme, toolbar, fitContent, priceFormat, }: SignalsChartProps): import("react").JSX.Element;
@@ -0,0 +1,95 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, 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 { signalsFeed } from "../core/signals";
6
+ import { formatValue } from "../core/format";
7
+ import { DEFAULT_THEME } from "../core/theme";
8
+ import { toolbarUi, typeButtons, drawButtons, loadingOverlay } from "./ui";
9
+ const BUCKETS = [["1m", 60], ["5m", 300], ["15m", 900], ["1h", 3600], ["4h", 14400], ["1d", 86400]];
10
+ /**
11
+ * A turnkey, live chart for a token tracked by the Livo signals engine ("Signal Radar").
12
+ * Seeds from the engine snapshot's recent-closes sparkline and polls for a building candle
13
+ * (no key). History is shallow (the snapshot's `spark`) — for deep history use a chart
14
+ * backed by an indexer feed. Renders via the framework-agnostic {@link Chart}.
15
+ *
16
+ * ```tsx
17
+ * <SignalsChart token="PEPE" bucketSeconds={300} />
18
+ * ```
19
+ */
20
+ export function SignalsChart({ token, url, bucketSeconds = 300, refetchMs, height = 420, chartType = "line", indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools = false, showVolume: showVolumeProp = false, volumeProfile: volumeProfileProp, theme, toolbar = true, fitContent = true, priceFormat, }) {
21
+ const [secs, setSecs] = useState(bucketSeconds);
22
+ const [type, setType] = useState(chartType);
23
+ const [log, setLog] = useState(false);
24
+ const [vpOn, setVpOn] = useState(!!volumeProfileProp);
25
+ const [drawMode, setDrawMode] = useState("none");
26
+ const [last, setLast] = useState(null);
27
+ const [legend, setLegend] = useState(null);
28
+ const [error, setError] = useState(null);
29
+ const [loading, setLoading] = useState(true);
30
+ const host = useRef(null);
31
+ const chart = useRef(null);
32
+ const th = useMemo(() => ({ ...DEFAULT_THEME, ...(theme || {}) }), [theme]);
33
+ useEffect(() => {
34
+ if (!host.current)
35
+ return;
36
+ const c = new Chart(host.current, {
37
+ height,
38
+ theme: th,
39
+ emptyText: "Loading…",
40
+ indicators,
41
+ oscillators,
42
+ drawings,
43
+ onDrawingsChange,
44
+ onDrawModeChange: setDrawMode,
45
+ showVolume: showVolumeProp,
46
+ volumeProfile: volumeProfileProp,
47
+ onCrosshair: setLegend,
48
+ fitContent,
49
+ priceFormat,
50
+ });
51
+ chart.current = c;
52
+ return () => {
53
+ c.destroy();
54
+ chart.current = null;
55
+ };
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, []);
58
+ useEffect(() => { chart.current?.setHeight(height); }, [height]);
59
+ useEffect(() => { chart.current?.setChartType(type); }, [type]);
60
+ useEffect(() => { chart.current?.setTheme(th); }, [th]);
61
+ useEffect(() => { chart.current?.setIndicators(indicators ?? []); }, [indicators]);
62
+ useEffect(() => { chart.current?.setOscillators(oscillators ?? []); }, [oscillators]);
63
+ useEffect(() => { chart.current?.setDrawMode(drawMode); }, [drawMode]);
64
+ useEffect(() => { chart.current?.setLogScale(log); }, [log]);
65
+ useEffect(() => { chart.current?.setEmptyText(loading ? "" : "no data"); }, [loading]); // overlay handles loading
66
+ useEffect(() => {
67
+ chart.current?.setVolumeProfile(vpOn ? (typeof volumeProfileProp === "object" ? volumeProfileProp : true) : false);
68
+ }, [vpOn, volumeProfileProp]);
69
+ useEffect(() => {
70
+ if (!chart.current)
71
+ return;
72
+ setLoading(true);
73
+ setError(null);
74
+ const feed = signalsFeed({ token, url, bucketSeconds: secs, refetchMs });
75
+ const conn = connectFeed(chart.current, feed, {
76
+ interval: secs,
77
+ onCandles: (cs) => {
78
+ setLast(cs[cs.length - 1] ?? null);
79
+ setLoading(false);
80
+ setError(null); // a successful page clears a prior transient error
81
+ },
82
+ onError: (e) => {
83
+ setError(e instanceof Error ? e.message : String(e));
84
+ setLoading(false);
85
+ },
86
+ });
87
+ return () => conn.disconnect();
88
+ }, [token, url, secs, refetchMs]);
89
+ const info = legend ?? last;
90
+ const lc = info && info.c >= info.o ? th.up : th.down;
91
+ const { btn, sepStyle, barStyle, muted } = toolbarUi(th);
92
+ const sep = _jsx("span", { style: sepStyle });
93
+ const ivLabel = BUCKETS.find(([, s]) => s === secs)?.[0] ?? `${secs}s`;
94
+ return (_jsxs("div", { style: { position: "relative", userSelect: "none" }, children: [toolbar && (_jsxs("div", { style: barStyle, children: [BUCKETS.map(([l, s]) => (_jsx("button", { style: btn(secs === s), onClick: () => setSecs(s), children: l }, s))), sep, typeButtons(th, type, setType), sep, _jsx("button", { style: btn(vpOn), title: "Volume-by-price histogram", onClick: () => setVpOn((v) => !v), children: "VP" }), _jsx("button", { style: btn(log), title: "Logarithmic price axis", onClick: () => setLog((v) => !v), children: "Log" }), !hideDrawingTools ? (_jsxs(_Fragment, { children: [sep, drawButtons(th, drawMode, setDrawMode, () => chart.current?.clearDrawings())] })) : null] })), _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: muted }, children: [token.length > 14 ? `${token.slice(0, 6)}…${token.slice(-4)}` : token, " \u00B7 ", ivLabel, " \u00B7 radar"] }), error ? (_jsx("div", { style: { color: th.down }, children: error })) : info ? (_jsxs("div", { style: { color: lc }, children: ["O ", formatValue(info.o), " H ", formatValue(info.h), " L ", formatValue(info.l), " C ", formatValue(info.c)] })) : null] }), _jsx("div", { ref: host }), loading ? loadingOverlay(th) : null] })] }));
95
+ }