@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.
- package/README.md +209 -20
- package/dist/core/chart.d.ts +105 -4
- package/dist/core/chart.js +482 -39
- package/dist/core/feed.js +27 -6
- package/dist/core/format.d.ts +13 -1
- package/dist/core/format.js +38 -3
- package/dist/core/indicators.d.ts +50 -0
- package/dist/core/indicators.js +181 -0
- package/dist/core/ohlc.d.ts +10 -0
- package/dist/core/ohlc.js +30 -0
- package/dist/core/polymarket.d.ts +44 -0
- package/dist/core/polymarket.js +92 -0
- package/dist/core/renderer.d.ts +96 -1
- package/dist/core/renderer.js +534 -64
- package/dist/core/signals.d.ts +63 -0
- package/dist/core/signals.js +234 -0
- package/dist/core/theme.d.ts +5 -1
- package/dist/core/theme.js +33 -12
- package/dist/core/types.d.ts +102 -3
- package/dist/index.d.ts +13 -8
- package/dist/index.js +8 -6
- package/dist/react/HyperliquidChart.d.ts +30 -2
- package/dist/react/HyperliquidChart.js +47 -17
- package/dist/react/PolymarketChart.d.ts +40 -0
- package/dist/react/PolymarketChart.js +95 -0
- package/dist/react/PriceChart.d.ts +54 -4
- package/dist/react/PriceChart.js +66 -27
- package/dist/react/SignalsChart.d.ts +37 -0
- package/dist/react/SignalsChart.js +95 -0
- package/dist/react/ui.d.ts +24 -0
- package/dist/react/ui.js +75 -0
- package/dist/react.d.ts +4 -0
- package/dist/react.js +2 -0
- package/package.json +2 -2
|
@@ -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, {
|
|
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
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
/**
|
|
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
|
|
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;
|
package/dist/react/PriceChart.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
16
|
-
const [type, setType] = useState(
|
|
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 [
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
60
|
+
setFetchedSupply(s);
|
|
48
61
|
}
|
|
49
62
|
})
|
|
50
63
|
.catch(() => { });
|
|
51
|
-
}, [mcap, tokenAddress, decimals, rpcUrl]);
|
|
52
|
-
|
|
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, {
|
|
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 = (
|
|
72
|
-
|
|
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 =
|
|
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:
|
|
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
|
+
}
|