@livo-build/charts 0.2.2 → 0.2.4

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.
@@ -15,20 +15,24 @@ export interface Point {
15
15
  v?: number;
16
16
  }
17
17
  export type Denom = "USD" | "ETH";
18
- export type ChartType = "candle" | "line";
18
+ /** Price-series style: candlesticks, a line, a two-tone baseline area, or Heikin-Ashi candles. */
19
+ export type ChartType = "candle" | "line" | "baseline" | "heikin";
19
20
  export type IndicatorType = "sma" | "ema" | "wma" | "vwap" | "bollinger";
20
21
  export type PriceSource = "open" | "high" | "low" | "close";
21
22
  /** Oscillators render in their own stacked sub-pane below the volume panel. */
22
- export type OscillatorType = "rsi" | "macd";
23
- /** An oscillator drawn in a dedicated sub-pane (RSI band 0–100, or MACD line+signal+histogram). */
23
+ export type OscillatorType = "rsi" | "macd" | "stoch" | "atr";
24
+ /** An oscillator drawn in a dedicated sub-pane (RSI/Stochastic 0–100 band, MACD, or ATR). */
24
25
  export interface Oscillator {
25
26
  type: OscillatorType;
26
- /** RSI lookback (default 14). Ignored by MACD. */
27
+ /** Lookback — RSI/ATR period, or the Stochastic %K period (defaults 14). Ignored by MACD. */
27
28
  period?: number;
28
- /** MACD fast / slow / signal EMA lengths (defaults 12 / 26 / 9). Ignored by RSI. */
29
+ /** MACD fast / slow / signal EMA lengths (defaults 12 / 26 / 9). Ignored by the others. */
29
30
  fast?: number;
30
31
  slow?: number;
31
32
  signal?: number;
33
+ /** Stochastic %D period (default 3) and %K smoothing (default 1 = fast). Ignored by others. */
34
+ dPeriod?: number;
35
+ smooth?: number;
32
36
  /** Pane height in px (default 84). */
33
37
  height?: number;
34
38
  /** Primary line color; defaults to a built-in palette slot. */
@@ -52,8 +56,11 @@ export interface DrawingPoint {
52
56
  time: number;
53
57
  price: number;
54
58
  }
55
- export type DrawingType = "trendline" | "hline";
56
- /** A user-drawn annotation on the price pane. `hline` uses `a.price`; `trendline` uses `a`+`b`. */
59
+ export type DrawingType = "trendline" | "hline" | "fib" | "rect";
60
+ /**
61
+ * A user-drawn annotation on the price pane. `hline` uses `a.price`; `trendline`, `fib`
62
+ * (retracement levels between the two prices) and `rect` (box) use both `a` and `b`.
63
+ */
57
64
  export interface Drawing {
58
65
  id: string;
59
66
  type: DrawingType;
@@ -62,7 +69,7 @@ export interface Drawing {
62
69
  color?: string;
63
70
  }
64
71
  /** Drawing interaction mode. `none` = pan/zoom + select/move; the others arm a one-shot draw. */
65
- export type DrawMode = "none" | "trendline" | "hline";
72
+ export type DrawMode = "none" | "trendline" | "hline" | "fib" | "rect";
66
73
  /** Transform applied to raw prices before aggregation. */
67
74
  export interface PriceTransform {
68
75
  /** "USD" leaves prices as-is; "ETH" divides by `ethUsd`. */
@@ -91,6 +98,15 @@ export interface ChartTheme {
91
98
  axisTagText: string;
92
99
  background?: string;
93
100
  }
101
+ /** Volume-by-price overlay config. `true` uses defaults; an object tunes it. */
102
+ export type VolumeProfileConfig = boolean | {
103
+ /** number of price bands (default 24). */
104
+ buckets?: number;
105
+ /** fraction of the plot width the widest bar occupies (default 0.16). */
106
+ width?: number;
107
+ /** bar fill color (default a translucent `theme.axis`). */
108
+ color?: string;
109
+ };
94
110
  export interface ChartOptions {
95
111
  /** canvas height in CSS px (default 420). */
96
112
  height?: number;
@@ -125,6 +141,13 @@ export interface ChartOptions {
125
141
  onDrawModeChange?: (mode: DrawMode) => void;
126
142
  /** logarithmic price axis — equal vertical distance = equal % move (default false). */
127
143
  logScale?: boolean;
144
+ /** centered text shown when there are no candles (default "no priced trades yet"). Feeds
145
+ * set this to "Loading…" while the first page is in flight. Pass "" to draw nothing. */
146
+ emptyText?: string;
147
+ /** reference price for the `baseline` chart type (default: the first visible candle's close). */
148
+ baselinePrice?: number;
149
+ /** volume-by-price histogram drawn on the price pane (default off). */
150
+ volumeProfile?: VolumeProfileConfig;
128
151
  /** spread candles across the full plot width instead of right-anchoring a capped slot
129
152
  * (default false in the core `Chart`; the React wrappers default it on). */
130
153
  fitContent?: boolean;
package/dist/index.d.ts CHANGED
@@ -1,12 +1,17 @@
1
- export { Chart } from "./core/chart";
2
- export { buildOHLC, transformPrice } from "./core/ohlc";
1
+ export { Chart, needsHistory, maxPanOffset } from "./core/chart";
2
+ export { buildOHLC, transformPrice, heikinAshi } from "./core/ohlc";
3
3
  export { draw, slotWidth, windowOf, priceScale, timeScale, computeProjection, withAlpha, resolveOverlays } from "./core/renderer";
4
4
  export type { RenderInput, Viewport, Pads, ResolvedOverlay, Projection } from "./core/renderer";
5
5
  export { connectFeed } from "./core/feed";
6
6
  export type { ChartFeed, ConnectFeedOptions, FeedConnection, LoadHistoryParams } from "./core/feed";
7
7
  export { DEFAULT_THEME, LIGHT_THEME, THEME_PRESETS } from "./core/theme";
8
8
  export { formatValue, formatAxisValue, formatVolume, formatTime } from "./core/format";
9
- export { sma, ema, wma, vwap, bollingerBands, rsi, macd, sourceValues, computeIndicator, INDICATOR_PALETTE } from "./core/indicators";
9
+ export { sma, ema, wma, vwap, bollingerBands, rsi, macd, stochastic, atr, volumeProfile, sourceValues, computeIndicator, INDICATOR_PALETTE } from "./core/indicators";
10
+ export type { VolumeBucket, VolumeProfileResult } from "./core/indicators";
10
11
  export { fetchHyperliquidCandles, fetchHlCandleWindow, hyperliquidFeed, mapHlCandles, HL_INTERVAL_SECONDS } from "./core/live";
11
12
  export type { HlRawCandle, FetchHlCandlesOptions, HyperliquidFeedOptions } from "./core/live";
12
- export type { Candle, Point, Denom, ChartType, PriceTransform, ChartTheme, ChartOptions, Indicator, IndicatorType, PriceSource, Oscillator, OscillatorType, Drawing, DrawingType, DrawingPoint, DrawMode, AxisOptions, } from "./core/types";
13
+ export { fetchPolymarketPriceHistory, polymarketFeed, POLYMARKET_CLOB } from "./core/polymarket";
14
+ export type { PolyHistoryPoint, FetchPolyHistoryOptions, PolymarketFeedOptions } from "./core/polymarket";
15
+ export { fetchSignalsMarket, fetchSignalsSwaps, signalsFeed, sparkCandles, rejectPriceOutliers, DEFAULT_SIGNALS_URL } from "./core/signals";
16
+ export type { SignalsMarket, SignalsFeedOptions } from "./core/signals";
17
+ export type { Candle, Point, Denom, ChartType, PriceTransform, ChartTheme, ChartOptions, Indicator, IndicatorType, PriceSource, Oscillator, OscillatorType, Drawing, DrawingType, DrawingPoint, DrawMode, AxisOptions, VolumeProfileConfig, } from "./core/types";
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  // livo-charts — framework-agnostic core.
2
2
  // React bindings live in the `@livo-build/charts/react` subpath so non-React
3
3
  // consumers never pull React into their bundle.
4
- export { Chart } from "./core/chart";
5
- export { buildOHLC, transformPrice } from "./core/ohlc";
4
+ export { Chart, needsHistory, maxPanOffset } from "./core/chart";
5
+ export { buildOHLC, transformPrice, heikinAshi } from "./core/ohlc";
6
6
  export { draw, slotWidth, windowOf, priceScale, timeScale, computeProjection, withAlpha, resolveOverlays } from "./core/renderer";
7
7
  export { connectFeed } from "./core/feed";
8
8
  export { DEFAULT_THEME, LIGHT_THEME, THEME_PRESETS } from "./core/theme";
9
9
  export { formatValue, formatAxisValue, formatVolume, formatTime } from "./core/format";
10
- export { sma, ema, wma, vwap, bollingerBands, rsi, macd, sourceValues, computeIndicator, INDICATOR_PALETTE } from "./core/indicators";
10
+ export { sma, ema, wma, vwap, bollingerBands, rsi, macd, stochastic, atr, volumeProfile, sourceValues, computeIndicator, INDICATOR_PALETTE } from "./core/indicators";
11
11
  export { fetchHyperliquidCandles, fetchHlCandleWindow, hyperliquidFeed, mapHlCandles, HL_INTERVAL_SECONDS } from "./core/live";
12
+ export { fetchPolymarketPriceHistory, polymarketFeed, POLYMARKET_CLOB } from "./core/polymarket";
13
+ export { fetchSignalsMarket, fetchSignalsSwaps, signalsFeed, sparkCandles, rejectPriceOutliers, DEFAULT_SIGNALS_URL } from "./core/signals";
@@ -1,4 +1,4 @@
1
- import type { ChartTheme, ChartType, Drawing, Indicator, Oscillator } from "../core/types";
1
+ import type { ChartTheme, ChartType, Drawing, Indicator, Oscillator, VolumeProfileConfig } from "../core/types";
2
2
  export interface HyperliquidChartProps {
3
3
  /** Hyperliquid coin symbol, e.g. "BTC", "ETH", or a builder-dex name like "xyz:TSLA". */
4
4
  coin: string;
@@ -19,6 +19,8 @@ export interface HyperliquidChartProps {
19
19
  hideDrawingTools?: boolean;
20
20
  /** Show the volume panel (default true). */
21
21
  showVolume?: boolean;
22
+ /** Volume-by-price histogram (default off); the toolbar adds a VP toggle when set. */
23
+ volumeProfile?: VolumeProfileConfig;
22
24
  /** Candles per history page; older pages load as you scroll left (default 500). */
23
25
  pageSize?: number;
24
26
  theme?: Partial<ChartTheme>;
@@ -51,4 +53,4 @@ export interface HyperliquidChartProps {
51
53
  * <HyperliquidChart coin="BTC" interval="15m" indicators={[{ type: "ema", period: 21 }]} />
52
54
  * ```
53
55
  */
54
- export declare function HyperliquidChart({ coin, interval, testnet, height, chartType, indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools, showVolume: showVolumeProp, pageSize, theme, toolbar, fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont, }: HyperliquidChartProps): import("react").JSX.Element;
56
+ export declare function HyperliquidChart({ coin, interval, testnet, height, chartType, indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools, showVolume: showVolumeProp, volumeProfile: volumeProfileProp, pageSize, theme, toolbar, fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont, }: HyperliquidChartProps): import("react").JSX.Element;
@@ -5,7 +5,7 @@ import { connectFeed } from "../core/feed";
5
5
  import { hyperliquidFeed, HL_INTERVAL_SECONDS } from "../core/live";
6
6
  import { formatValue, formatVolume } from "../core/format";
7
7
  import { DEFAULT_THEME } from "../core/theme";
8
- import { toolbarUi } from "./ui";
8
+ import { toolbarUi, typeButtons, drawButtons, loadingOverlay } from "./ui";
9
9
  const IVS = ["1m", "5m", "15m", "1h", "4h", "1d"];
10
10
  /**
11
11
  * A turnkey, REALTIME Hyperliquid trading chart. Streams live candles over WebSocket,
@@ -17,11 +17,12 @@ const IVS = ["1m", "5m", "15m", "1h", "4h", "1d"];
17
17
  * <HyperliquidChart coin="BTC" interval="15m" indicators={[{ type: "ema", period: 21 }]} />
18
18
  * ```
19
19
  */
20
- export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420, chartType = "candle", indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools = false, showVolume: showVolumeProp = true, pageSize = 500, theme, toolbar = true, fitContent = true, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont, }) {
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, }) {
21
21
  const [iv, setIv] = useState(interval);
22
22
  const [type, setType] = useState(chartType);
23
23
  const [log, setLog] = useState(false);
24
24
  const [showVolume, setShowVolume] = useState(showVolumeProp);
25
+ const [vpOn, setVpOn] = useState(!!volumeProfileProp);
25
26
  const [drawMode, setDrawMode] = useState("none");
26
27
  const [last, setLast] = useState(null);
27
28
  const [legend, setLegend] = useState(null);
@@ -37,12 +38,14 @@ export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420,
37
38
  const c = new Chart(host.current, {
38
39
  height,
39
40
  theme: th,
41
+ emptyText: "Loading…",
40
42
  indicators,
41
43
  oscillators,
42
44
  drawings,
43
45
  onDrawingsChange,
44
46
  onDrawModeChange: setDrawMode,
45
47
  showVolume: showVolumeProp,
48
+ volumeProfile: volumeProfileProp,
46
49
  onCrosshair: setLegend,
47
50
  fitContent,
48
51
  maxBarWidth,
@@ -68,6 +71,10 @@ export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420,
68
71
  useEffect(() => { chart.current?.setDrawMode(drawMode); }, [drawMode]);
69
72
  useEffect(() => { chart.current?.setShowVolume(showVolume); }, [showVolume]);
70
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]);
71
78
  useEffect(() => {
72
79
  chart.current?.setAxis({ fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont });
73
80
  }, [fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont]);
@@ -85,6 +92,7 @@ export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420,
85
92
  onCandles: (cs) => {
86
93
  setLast(cs[cs.length - 1] ?? null);
87
94
  setLoading(false);
95
+ setError(null); // a successful page clears a prior transient error
88
96
  },
89
97
  onError: (e) => {
90
98
  setError(e instanceof Error ? e.message : String(e));
@@ -97,5 +105,5 @@ export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420,
97
105
  const lc = info && info.c >= info.o ? th.up : th.down;
98
106
  const { btn, sepStyle, barStyle, muted } = toolbarUi(th);
99
107
  const sep = _jsx("span", { style: sepStyle });
100
- 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, _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(showVolume), title: "Toggle volume panel", onClick: () => setShowVolume((v) => !v), children: "Vol" }), _jsx("button", { style: btn(log), title: "Logarithmic price axis", onClick: () => setLog((v) => !v), children: "Log" }), !hideDrawingTools ? (_jsxs(_Fragment, { children: [sep, _jsx("button", { style: btn(drawMode === "trendline"), title: "Draw trendline (drag)", onClick: () => setDrawMode((m) => (m === "trendline" ? "none" : "trendline")), children: "\u2197" }), _jsx("button", { style: btn(drawMode === "hline"), title: "Draw horizontal line (click)", onClick: () => setDrawMode((m) => (m === "hline" ? "none" : "hline")), children: "\u2014" }), _jsx("button", { style: btn(false), title: "Clear drawings (double-click a line to delete one)", onClick: () => chart.current?.clearDrawings(), children: "\u232B" })] })) : 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" : ""] }), 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)] })] })) : error ? (_jsx("div", { style: { color: th.down }, children: error })) : loading ? (_jsx("div", { style: { color: muted }, children: "loading\u2026" })) : null] }), _jsx("div", { ref: host })] })] }));
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] })] }));
101
109
  }
@@ -0,0 +1,45 @@
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). Ignored when `liveUrl` is set. */
10
+ refetchMs?: number;
11
+ /**
12
+ * Optional Livo indexer base (e.g. "https://polymarket.livo.build") — upgrades the live
13
+ * candle from the 30s poll to INSTANT SSE updates. History still loads from the CLOB.
14
+ */
15
+ liveUrl?: string;
16
+ /** Candles per history page; older pages load as you scroll left (default 500). */
17
+ pageSize?: number;
18
+ height?: number;
19
+ chartType?: ChartType;
20
+ indicators?: Indicator[];
21
+ oscillators?: Oscillator[];
22
+ drawings?: Drawing[];
23
+ onDrawingsChange?: (drawings: Drawing[]) => void;
24
+ hideDrawingTools?: boolean;
25
+ /** Show the (always-empty) volume panel — off by default since price-history has no volume. */
26
+ showVolume?: boolean;
27
+ volumeProfile?: VolumeProfileConfig;
28
+ theme?: Partial<ChartTheme>;
29
+ toolbar?: boolean;
30
+ fitContent?: boolean;
31
+ /** Outcome label shown in the legend (e.g. "Yes"). */
32
+ label?: string;
33
+ /** y-axis formatter (default a 0–100% probability). */
34
+ priceFormat?: (value: number) => string;
35
+ }
36
+ /**
37
+ * A turnkey, live chart for a Polymarket outcome. Bucketed OHLC from the public CLOB
38
+ * price-history endpoint (no key), polled for a building candle. Prices are probabilities,
39
+ * so the y-axis defaults to a percentage. Renders via the framework-agnostic {@link Chart}.
40
+ *
41
+ * ```tsx
42
+ * <PolymarketChart tokenId={yesTokenId} bucketSeconds={3600} label="Yes" />
43
+ * ```
44
+ */
45
+ export declare function PolymarketChart({ tokenId, bucketSeconds, host, refetchMs, liveUrl, 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, liveUrl, 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, liveUrl });
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, liveUrl, 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,4 +1,4 @@
1
- import type { ChartOptions, ChartTheme, ChartType, Drawing, Indicator, Oscillator, 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[];
@@ -30,14 +30,29 @@ export interface PriceChartProps {
30
30
  drawings?: Drawing[];
31
31
  /** Fired whenever the user adds, moves, or deletes a drawing. */
32
32
  onDrawingsChange?: (drawings: Drawing[]) => void;
33
- /** Hide the trendline / h-line / clear drawing-tool buttons (shown by default). */
33
+ /** Hide the trendline / h-line / fib / rect / clear drawing-tool buttons (shown by default). */
34
34
  hideDrawingTools?: boolean;
35
35
  /** Show the volume panel (default true). */
36
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;
37
41
  /** Initial series style (default "candle"); the toolbar can switch it after mount. */
38
42
  defaultChartType?: ChartType;
39
43
  /** Initial bucket interval in seconds (default 300 = 5m); the toolbar can switch it. */
40
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;
41
56
  /** Spread candles across the full width instead of right-anchoring a capped slot.
42
57
  * Default `true` — fills the container for sparse series. */
43
58
  fitContent?: boolean;
@@ -63,4 +78,4 @@ export interface PriceChartProps {
63
78
  * OHLCV legend, X/Y zoom + pan, interval / USD-ETH / price-mcap / fullscreen toolbar.
64
79
  * Wraps the framework-agnostic {@link Chart}; styled inline (no CSS framework needed).
65
80
  */
66
- export declare function PriceChart({ swaps, ethUsd, supply: supplyProp, tokenAddress, decimals, symbol, quote, height, rpcUrl, theme, indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools, showVolume: showVolumeProp, defaultChartType, defaultInterval, fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont, 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;
@@ -5,14 +5,15 @@ 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
- const IVS = [["1s", 1], ["1m", 60], ["5m", 300], ["15m", 900], ["1h", 3600], ["4h", 14400], ["1d", 86400]];
8
+ import { typeButtons, drawButtons } from "./ui";
9
+ const DEFAULT_IVS = [["1s", 1], ["1m", 60], ["5m", 300], ["15m", 900], ["1h", 3600], ["4h", 14400], ["1d", 86400]];
9
10
  const supplyCache = new Map();
10
11
  /**
11
12
  * A self-contained trading chart: candles/line, volume panel, crosshair + axis labels,
12
13
  * OHLCV legend, X/Y zoom + pan, interval / USD-ETH / price-mcap / fullscreen toolbar.
13
14
  * Wraps the framework-agnostic {@link Chart}; styled inline (no CSS framework needed).
14
15
  */
15
- 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, defaultChartType = "candle", defaultInterval = 300, fitContent = true, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont, options, }) {
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, }) {
16
17
  const [iv, setIv] = useState(defaultInterval);
17
18
  const [type, setType] = useState(defaultChartType);
18
19
  const [denom, setDenom] = useState("USD");
@@ -20,12 +21,16 @@ export function PriceChart({ swaps, ethUsd = 0, supply: supplyProp = 0, tokenAdd
20
21
  const [flip, setFlip] = useState(false);
21
22
  const [log, setLog] = useState(!!options?.logScale);
22
23
  const [showVolume, setShowVolume] = useState(showVolumeProp);
24
+ const [vpOn, setVpOn] = useState(!!volumeProfileProp);
23
25
  const [drawMode, setDrawMode] = useState("none");
24
26
  const [full, setFull] = useState(false);
25
27
  const [fetchedSupply, setFetchedSupply] = useState(0);
26
28
  const [legend, setLegend] = useState(null);
27
29
  const host = useRef(null);
28
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;
29
34
  const th = useMemo(() => ({ ...DEFAULT_THEME, ...(theme || {}) }), [theme]);
30
35
  const H = full && typeof window !== "undefined" ? window.innerHeight - 46 : height;
31
36
  // A direct `supply` prop wins; otherwise market cap needs circulating supply, read once
@@ -68,12 +73,15 @@ export function PriceChart({ swaps, ethUsd = 0, supply: supplyProp = 0, tokenAdd
68
73
  height: H,
69
74
  theme: th,
70
75
  onCrosshair: setLegend,
76
+ onNeedHistory: () => onLoadOlderRef.current?.(),
71
77
  indicators,
72
78
  oscillators,
73
79
  drawings,
74
80
  onDrawingsChange,
75
81
  onDrawModeChange: setDrawMode,
76
82
  showVolume: showVolumeProp,
83
+ volumeProfile: volumeProfileProp,
84
+ baselinePrice,
77
85
  fitContent,
78
86
  maxBarWidth,
79
87
  maxBodyWidth,
@@ -100,6 +108,10 @@ export function PriceChart({ swaps, ethUsd = 0, supply: supplyProp = 0, tokenAdd
100
108
  useEffect(() => { chart.current?.setDrawMode(drawMode); }, [drawMode]);
101
109
  useEffect(() => { chart.current?.setShowVolume(showVolume); }, [showVolume]);
102
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]);
103
115
  useEffect(() => {
104
116
  chart.current?.setAxis({ fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont });
105
117
  }, [fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont]);
@@ -107,8 +119,8 @@ export function PriceChart({ swaps, ethUsd = 0, supply: supplyProp = 0, tokenAdd
107
119
  const { btn, sepStyle, barStyle, muted } = toolbarUi(th);
108
120
  const sep = _jsx("span", { style: sepStyle });
109
121
  const lc = legend && legend.c >= legend.o ? th.up : th.down;
110
- const ivLabel = IVS.find(([, s]) => s === iv)?.[0] || "";
122
+ const ivLabel = intervals.find(([, s]) => s === iv)?.[0] || "";
111
123
  return (_jsxs("div", { style: full
112
124
  ? { position: "fixed", inset: 0, zIndex: 50, background: th.background ?? "#0a0a0a", padding: 8, userSelect: "none" }
113
- : { position: "relative", userSelect: "none" }, children: [_jsxs("div", { style: barStyle, 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" }), 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, sep, _jsx("button", { style: btn(denom === "USD"), onClick: () => setDenom("USD"), children: "USD" }), _jsx("button", { style: btn(denom === "ETH"), onClick: () => setDenom("ETH"), children: "ETH" }), !showMcap ? _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(log), title: "Logarithmic price axis", onClick: () => setLog((v) => !v), children: "Log" }), !hideDrawingTools ? (_jsxs(_Fragment, { children: [sep, _jsx("button", { style: btn(drawMode === "trendline"), title: "Draw trendline (drag)", onClick: () => setDrawMode((m) => (m === "trendline" ? "none" : "trendline")), children: "\u2197" }), _jsx("button", { style: btn(drawMode === "hline"), title: "Draw horizontal line (click)", onClick: () => setDrawMode((m) => (m === "hline" ? "none" : "hline")), children: "\u2014" }), _jsx("button", { style: btn(false), title: "Clear drawings (double-click a line to delete one)", onClick: () => chart.current?.clearDrawings(), children: "\u232B" })] })) : 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 })] })] }));
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 })] })] }));
114
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
+ }