@livo-build/charts 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,9 +16,25 @@ export interface Point {
16
16
  }
17
17
  export type Denom = "USD" | "ETH";
18
18
  export type ChartType = "candle" | "line";
19
- export type IndicatorType = "sma" | "ema" | "wma" | "vwap";
19
+ export type IndicatorType = "sma" | "ema" | "wma" | "vwap" | "bollinger";
20
20
  export type PriceSource = "open" | "high" | "low" | "close";
21
- /** An overlay indicator drawn on the price pane (a moving average for now). */
21
+ /** 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). */
24
+ export interface Oscillator {
25
+ type: OscillatorType;
26
+ /** RSI lookback (default 14). Ignored by MACD. */
27
+ period?: number;
28
+ /** MACD fast / slow / signal EMA lengths (defaults 12 / 26 / 9). Ignored by RSI. */
29
+ fast?: number;
30
+ slow?: number;
31
+ signal?: number;
32
+ /** Pane height in px (default 84). */
33
+ height?: number;
34
+ /** Primary line color; defaults to a built-in palette slot. */
35
+ color?: string;
36
+ }
37
+ /** An overlay indicator drawn on the price pane (a moving average, VWAP, or Bollinger band). */
22
38
  export interface Indicator {
23
39
  type: IndicatorType;
24
40
  /** Lookback in candles. */
@@ -27,7 +43,26 @@ export interface Indicator {
27
43
  color?: string;
28
44
  /** Which price to average. Default "close". */
29
45
  source?: PriceSource;
46
+ /** Bollinger band width in standard deviations (default 2). Ignored by other types. */
47
+ mult?: number;
48
+ }
49
+ /** A point anchored in data space (so drawings stay attached across pan/zoom). */
50
+ export interface DrawingPoint {
51
+ /** unix seconds. */
52
+ time: number;
53
+ price: number;
30
54
  }
55
+ export type DrawingType = "trendline" | "hline";
56
+ /** A user-drawn annotation on the price pane. `hline` uses `a.price`; `trendline` uses `a`+`b`. */
57
+ export interface Drawing {
58
+ id: string;
59
+ type: DrawingType;
60
+ a: DrawingPoint;
61
+ b?: DrawingPoint;
62
+ color?: string;
63
+ }
64
+ /** Drawing interaction mode. `none` = pan/zoom + select/move; the others arm a one-shot draw. */
65
+ export type DrawMode = "none" | "trendline" | "hline";
31
66
  /** Transform applied to raw prices before aggregation. */
32
67
  export interface PriceTransform {
33
68
  /** "USD" leaves prices as-is; "ETH" divides by `ethUsd`. */
@@ -66,6 +101,11 @@ export interface ChartOptions {
66
101
  minBars?: number;
67
102
  /** cap on a candle's slot width in px — keeps sparse series tight, right-anchored (default 18). */
68
103
  maxBarWidth?: number;
104
+ /** cap on a candle's BODY (and volume bar) width in px (default 40). The body is 70% of
105
+ * the slot up to this cap; raise it for chunkier candles on sparse / `fitContent` series. */
106
+ maxBodyWidth?: number;
107
+ /** show the volume panel (default true). */
108
+ showVolume?: boolean;
69
109
  /** fraction of the plot used by the volume panel (default 0.18). */
70
110
  volumeRatio?: number;
71
111
  rightPad?: number;
@@ -75,6 +115,42 @@ export interface ChartOptions {
75
115
  onCrosshair?: (candle: Candle | null) => void;
76
116
  /** moving-average overlays drawn on the price pane. */
77
117
  indicators?: Indicator[];
118
+ /** oscillators (RSI / MACD) drawn in stacked sub-panes below the volume panel. */
119
+ oscillators?: Oscillator[];
120
+ /** user drawings (trendlines / horizontal price lines) to render on the price pane. */
121
+ drawings?: Drawing[];
122
+ /** fired whenever the user adds, moves, or deletes a drawing. */
123
+ onDrawingsChange?: (drawings: Drawing[]) => void;
124
+ /** fired when the active draw mode changes (e.g. auto-reset to "none" after one draw). */
125
+ onDrawModeChange?: (mode: DrawMode) => void;
126
+ /** logarithmic price axis — equal vertical distance = equal % move (default false). */
127
+ logScale?: boolean;
128
+ /** spread candles across the full plot width instead of right-anchoring a capped slot
129
+ * (default false in the core `Chart`; the React wrappers default it on). */
130
+ fitContent?: boolean;
131
+ /** y-axis price label formatter. Default adapts precision to the tick step. */
132
+ priceFormat?: (value: number) => string;
133
+ /** x-axis time label formatter. Default {@link formatTime}. */
134
+ timeFormat?: (time: number, interval: number) => string;
135
+ /** number of horizontal price gridlines / labels (default 5). */
136
+ priceTicks?: number;
137
+ /** approximate number of time-axis labels (default 7). */
138
+ timeTicks?: number;
139
+ /** canvas font for axis labels (default "10px ui-monospace, monospace"); color is `theme.axis`. */
140
+ axisFont?: string;
78
141
  /** fired when the user pans/zooms near the start of loaded data (lazy history). */
79
142
  onNeedHistory?: () => void;
80
143
  }
144
+ /** Axis label/format/layout overrides — the subset of {@link ChartOptions} that styles the ticks. */
145
+ export interface AxisOptions {
146
+ fitContent?: boolean;
147
+ priceFormat?: (value: number) => string;
148
+ timeFormat?: (time: number, interval: number) => string;
149
+ priceTicks?: number;
150
+ timeTicks?: number;
151
+ axisFont?: string;
152
+ /** max candle slot width in px (spacing). */
153
+ maxBarWidth?: number;
154
+ /** max candle body / volume-bar width in px. */
155
+ maxBodyWidth?: number;
156
+ }
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  export { Chart } from "./core/chart";
2
2
  export { buildOHLC, transformPrice } from "./core/ohlc";
3
- export { draw } from "./core/renderer";
4
- export type { RenderInput, Viewport, Pads, ResolvedOverlay } from "./core/renderer";
3
+ export { draw, slotWidth, windowOf, priceScale, timeScale, computeProjection, withAlpha, resolveOverlays } from "./core/renderer";
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
- export { DEFAULT_THEME } from "./core/theme";
8
- export { formatValue, formatVolume, formatTime } from "./core/format";
9
- export { sma, ema, wma, vwap, bollingerBands, sourceValues, computeIndicator, INDICATOR_PALETTE } from "./core/indicators";
7
+ export { DEFAULT_THEME, LIGHT_THEME, THEME_PRESETS } from "./core/theme";
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";
10
10
  export { fetchHyperliquidCandles, fetchHlCandleWindow, hyperliquidFeed, mapHlCandles, HL_INTERVAL_SECONDS } from "./core/live";
11
11
  export type { HlRawCandle, FetchHlCandlesOptions, HyperliquidFeedOptions } from "./core/live";
12
- export type { Candle, Point, Denom, ChartType, PriceTransform, ChartTheme, ChartOptions, Indicator, IndicatorType, PriceSource, } from "./core/types";
12
+ export type { Candle, Point, Denom, ChartType, PriceTransform, ChartTheme, ChartOptions, Indicator, IndicatorType, PriceSource, Oscillator, OscillatorType, Drawing, DrawingType, DrawingPoint, DrawMode, AxisOptions, } from "./core/types";
package/dist/index.js CHANGED
@@ -3,9 +3,9 @@
3
3
  // consumers never pull React into their bundle.
4
4
  export { Chart } from "./core/chart";
5
5
  export { buildOHLC, transformPrice } from "./core/ohlc";
6
- export { draw } from "./core/renderer";
6
+ export { draw, slotWidth, windowOf, priceScale, timeScale, computeProjection, withAlpha, resolveOverlays } from "./core/renderer";
7
7
  export { connectFeed } from "./core/feed";
8
- export { DEFAULT_THEME } from "./core/theme";
9
- export { formatValue, formatVolume, formatTime } from "./core/format";
10
- export { sma, ema, wma, vwap, bollingerBands, sourceValues, computeIndicator, INDICATOR_PALETTE } from "./core/indicators";
8
+ export { DEFAULT_THEME, LIGHT_THEME, THEME_PRESETS } from "./core/theme";
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";
11
11
  export { fetchHyperliquidCandles, fetchHlCandleWindow, hyperliquidFeed, mapHlCandles, HL_INTERVAL_SECONDS } from "./core/live";
@@ -1,4 +1,4 @@
1
- import type { ChartTheme, ChartType, Indicator } from "../core/types";
1
+ import type { ChartTheme, ChartType, Drawing, Indicator, Oscillator } 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;
@@ -9,11 +9,37 @@ export interface HyperliquidChartProps {
9
9
  chartType?: ChartType;
10
10
  /** Moving-average overlays, e.g. [{ type: "ema", period: 21 }]. */
11
11
  indicators?: Indicator[];
12
+ /** Oscillator sub-panes (RSI / MACD), e.g. [{ type: "macd" }]. */
13
+ oscillators?: Oscillator[];
14
+ /** Initial drawings (trendlines / horizontal lines). */
15
+ drawings?: Drawing[];
16
+ /** Fired whenever the user adds, moves, or deletes a drawing. */
17
+ onDrawingsChange?: (drawings: Drawing[]) => void;
18
+ /** Hide the trendline / h-line / clear drawing-tool buttons (shown by default). */
19
+ hideDrawingTools?: boolean;
20
+ /** Show the volume panel (default true). */
21
+ showVolume?: boolean;
12
22
  /** Candles per history page; older pages load as you scroll left (default 500). */
13
23
  pageSize?: number;
14
24
  theme?: Partial<ChartTheme>;
15
25
  /** Show the interval / candle-line toolbar. Default true. */
16
26
  toolbar?: boolean;
27
+ /** Spread candles across the full width instead of right-anchoring (default true). */
28
+ fitContent?: boolean;
29
+ /** Max candle slot width in px (spacing; default 18). */
30
+ maxBarWidth?: number;
31
+ /** Max candle body / volume-bar width in px (default 40). */
32
+ maxBodyWidth?: number;
33
+ /** y-axis price label formatter (default adapts precision to the tick step). */
34
+ priceFormat?: (value: number) => string;
35
+ /** x-axis time label formatter. */
36
+ timeFormat?: (time: number, interval: number) => string;
37
+ /** number of horizontal price gridlines / labels (default 5). */
38
+ priceTicks?: number;
39
+ /** approximate number of time-axis labels (default 7). */
40
+ timeTicks?: number;
41
+ /** canvas font for axis labels; color is `theme.axis`. */
42
+ axisFont?: string;
17
43
  }
18
44
  /**
19
45
  * A turnkey, REALTIME Hyperliquid trading chart. Streams live candles over WebSocket,
@@ -25,4 +51,4 @@ export interface HyperliquidChartProps {
25
51
  * <HyperliquidChart coin="BTC" interval="15m" indicators={[{ type: "ema", period: 21 }]} />
26
52
  * ```
27
53
  */
28
- export declare function HyperliquidChart({ coin, interval, testnet, height, chartType, indicators, pageSize, theme, toolbar, }: HyperliquidChartProps): import("react").JSX.Element;
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;
@@ -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 } 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,14 @@ 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, 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 [drawMode, setDrawMode] = useState("none");
22
26
  const [last, setLast] = useState(null);
27
+ const [legend, setLegend] = useState(null);
23
28
  const [error, setError] = useState(null);
24
29
  const [loading, setLoading] = useState(true);
25
30
  const host = useRef(null);
@@ -29,7 +34,25 @@ export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420,
29
34
  useEffect(() => {
30
35
  if (!host.current)
31
36
  return;
32
- const c = new Chart(host.current, { height, theme: th, indicators });
37
+ const c = new Chart(host.current, {
38
+ height,
39
+ theme: th,
40
+ indicators,
41
+ oscillators,
42
+ drawings,
43
+ onDrawingsChange,
44
+ onDrawModeChange: setDrawMode,
45
+ showVolume: showVolumeProp,
46
+ onCrosshair: setLegend,
47
+ fitContent,
48
+ maxBarWidth,
49
+ maxBodyWidth,
50
+ priceFormat,
51
+ timeFormat,
52
+ priceTicks,
53
+ timeTicks,
54
+ axisFont,
55
+ });
33
56
  chart.current = c;
34
57
  return () => {
35
58
  c.destroy();
@@ -41,6 +64,13 @@ export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420,
41
64
  useEffect(() => { chart.current?.setChartType(type); }, [type]);
42
65
  useEffect(() => { chart.current?.setTheme(th); }, [th]);
43
66
  useEffect(() => { chart.current?.setIndicators(indicators ?? []); }, [indicators]);
67
+ useEffect(() => { chart.current?.setOscillators(oscillators ?? []); }, [oscillators]);
68
+ useEffect(() => { chart.current?.setDrawMode(drawMode); }, [drawMode]);
69
+ useEffect(() => { chart.current?.setShowVolume(showVolume); }, [showVolume]);
70
+ useEffect(() => { chart.current?.setLogScale(log); }, [log]);
71
+ useEffect(() => {
72
+ chart.current?.setAxis({ fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont });
73
+ }, [fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont]);
44
74
  // connect a realtime feed: latest page + WebSocket live + lazy older history.
45
75
  // Reconnects when coin/interval/testnet change (toolbar drives `iv`).
46
76
  useEffect(() => {
@@ -63,17 +93,9 @@ export function HyperliquidChart({ coin, interval = "1h", testnet, height = 420,
63
93
  });
64
94
  return () => conn.disconnect();
65
95
  }, [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 })] })] }));
96
+ const info = legend ?? last;
97
+ const lc = info && info.c >= info.o ? th.up : th.down;
98
+ const { btn, sepStyle, barStyle, muted } = toolbarUi(th);
99
+ 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 })] })] }));
79
101
  }
@@ -1,12 +1,16 @@
1
- import type { ChartOptions, ChartTheme, Indicator, Point } from "../core/types";
1
+ import type { ChartOptions, ChartTheme, ChartType, Drawing, Indicator, Oscillator, Point } 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,37 @@ 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 / clear drawing-tool buttons (shown by default). */
34
+ hideDrawingTools?: boolean;
35
+ /** Show the volume panel (default true). */
36
+ showVolume?: boolean;
37
+ /** Initial series style (default "candle"); the toolbar can switch it after mount. */
38
+ defaultChartType?: ChartType;
39
+ /** Initial bucket interval in seconds (default 300 = 5m); the toolbar can switch it. */
40
+ defaultInterval?: number;
41
+ /** Spread candles across the full width instead of right-anchoring a capped slot.
42
+ * Default `true` — fills the container for sparse series. */
43
+ fitContent?: boolean;
44
+ /** Max candle slot width in px (spacing; default 18). */
45
+ maxBarWidth?: number;
46
+ /** Max candle body / volume-bar width in px (default 40). Raise for chunkier candles. */
47
+ maxBodyWidth?: number;
48
+ /** y-axis price label formatter (default adapts precision to the tick step). */
49
+ priceFormat?: (value: number) => string;
50
+ /** x-axis time label formatter. */
51
+ timeFormat?: (time: number, interval: number) => string;
52
+ /** number of horizontal price gridlines / labels (default 5). */
53
+ priceTicks?: number;
54
+ /** approximate number of time-axis labels (default 7). */
55
+ timeTicks?: number;
56
+ /** canvas font for axis labels (default "10px ui-monospace, monospace"); color is `theme.axis`. */
57
+ axisFont?: string;
23
58
  /** Advanced core options. */
24
59
  options?: ChartOptions;
25
60
  }
@@ -28,4 +63,4 @@ export interface PriceChartProps {
28
63
  * OHLCV legend, X/Y zoom + pan, interval / USD-ETH / price-mcap / fullscreen toolbar.
29
64
  * Wraps the framework-agnostic {@link Chart}; styled inline (no CSS framework needed).
30
65
  */
31
- export declare function PriceChart({ swaps, ethUsd, tokenAddress, decimals, symbol, quote, height, rpcUrl, theme, indicators, options, }: PriceChartProps): import("react").JSX.Element;
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;
@@ -1,9 +1,10 @@
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
+ import { toolbarUi } from "./ui";
7
8
  const IVS = [["1s", 1], ["1m", 60], ["5m", 300], ["15m", 900], ["1h", 3600], ["4h", 14400], ["1d", 86400]];
8
9
  const supplyCache = new Map();
9
10
  /**
@@ -11,26 +12,33 @@ const supplyCache = new Map();
11
12
  * OHLCV legend, X/Y zoom + pan, interval / USD-ETH / price-mcap / fullscreen toolbar.
12
13
  * Wraps the framework-agnostic {@link Chart}; styled inline (no CSS framework needed).
13
14
  */
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");
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
+ const [iv, setIv] = useState(defaultInterval);
17
+ const [type, setType] = useState(defaultChartType);
17
18
  const [denom, setDenom] = useState("USD");
18
19
  const [mcap, setMcap] = useState(false);
19
20
  const [flip, setFlip] = useState(false);
21
+ const [log, setLog] = useState(!!options?.logScale);
22
+ const [showVolume, setShowVolume] = useState(showVolumeProp);
23
+ const [drawMode, setDrawMode] = useState("none");
20
24
  const [full, setFull] = useState(false);
21
- const [supply, setSupply] = useState(0);
25
+ const [fetchedSupply, setFetchedSupply] = useState(0);
22
26
  const [legend, setLegend] = useState(null);
23
27
  const host = useRef(null);
24
28
  const chart = useRef(null);
25
29
  const th = useMemo(() => ({ ...DEFAULT_THEME, ...(theme || {}) }), [theme]);
26
30
  const H = full && typeof window !== "undefined" ? window.innerHeight - 46 : height;
27
- // Market cap needs circulating supply totalSupply() via JSON-RPC, cached.
31
+ // A direct `supply` prop wins; otherwise market cap needs circulating supply, read once
32
+ // from totalSupply() over JSON-RPC (cached). Market cap is only available when one of
33
+ // the two is present — see `canMcap` below (we hide the toggle otherwise).
34
+ const supply = supplyProp || fetchedSupply;
35
+ const canMcap = supplyProp > 0 || !!tokenAddress;
28
36
  useEffect(() => {
29
- if (!mcap || !tokenAddress)
37
+ if (!mcap || supplyProp > 0 || !tokenAddress)
30
38
  return;
31
39
  const key = tokenAddress.toLowerCase();
32
40
  if (supplyCache.has(key)) {
33
- setSupply(supplyCache.get(key));
41
+ setFetchedSupply(supplyCache.get(key));
34
42
  return;
35
43
  }
36
44
  fetch(rpcUrl, {
@@ -44,17 +52,38 @@ export function PriceChart({ swaps, ethUsd = 0, tokenAddress, decimals = 18, sym
44
52
  if (hx && hx !== "0x") {
45
53
  const s = Number(BigInt(hx)) / Math.pow(10, decimals);
46
54
  supplyCache.set(key, s);
47
- setSupply(s);
55
+ setFetchedSupply(s);
48
56
  }
49
57
  })
50
58
  .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]);
59
+ }, [mcap, supplyProp, tokenAddress, decimals, rpcUrl]);
60
+ // Market cap only renders when we actually have a supply; falls back to price otherwise.
61
+ const showMcap = mcap && supply > 0;
62
+ const candles = useMemo(() => buildOHLC(swaps, iv, { denom, ethUsd, flip: showMcap ? false : flip, supply: showMcap ? supply : 0 }), [swaps, iv, denom, ethUsd, flip, showMcap, supply]);
53
63
  // create the core chart once
54
64
  useEffect(() => {
55
65
  if (!host.current)
56
66
  return;
57
- const c = new Chart(host.current, { height: H, theme: th, onCrosshair: setLegend, indicators, ...options });
67
+ const c = new Chart(host.current, {
68
+ height: H,
69
+ theme: th,
70
+ onCrosshair: setLegend,
71
+ indicators,
72
+ oscillators,
73
+ drawings,
74
+ onDrawingsChange,
75
+ onDrawModeChange: setDrawMode,
76
+ showVolume: showVolumeProp,
77
+ fitContent,
78
+ maxBarWidth,
79
+ maxBodyWidth,
80
+ priceFormat,
81
+ timeFormat,
82
+ priceTicks,
83
+ timeTicks,
84
+ axisFont,
85
+ ...options,
86
+ });
58
87
  chart.current = c;
59
88
  return () => {
60
89
  c.destroy();
@@ -67,21 +96,19 @@ export function PriceChart({ swaps, ethUsd = 0, tokenAddress, decimals = 18, sym
67
96
  useEffect(() => { chart.current?.setChartType(type); }, [type]);
68
97
  useEffect(() => { chart.current?.setTheme(th); }, [th]);
69
98
  useEffect(() => { chart.current?.setIndicators(indicators ?? []); }, [indicators]);
99
+ useEffect(() => { chart.current?.setOscillators(oscillators ?? []); }, [oscillators]);
100
+ useEffect(() => { chart.current?.setDrawMode(drawMode); }, [drawMode]);
101
+ useEffect(() => { chart.current?.setShowVolume(showVolume); }, [showVolume]);
102
+ useEffect(() => { chart.current?.setLogScale(log); }, [log]);
103
+ useEffect(() => {
104
+ chart.current?.setAxis({ fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont });
105
+ }, [fitContent, maxBarWidth, maxBodyWidth, priceFormat, timeFormat, priceTicks, timeTicks, axisFont]);
70
106
  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" } });
107
+ const { btn, sepStyle, barStyle, muted } = toolbarUi(th);
108
+ const sep = _jsx("span", { style: sepStyle });
82
109
  const lc = legend && legend.c >= legend.o ? th.up : th.down;
83
110
  const ivLabel = IVS.find(([, s]) => s === iv)?.[0] || "";
84
111
  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 })] })] }));
112
+ ? { 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 })] })] }));
87
114
  }
@@ -0,0 +1,13 @@
1
+ import type { CSSProperties } from "react";
2
+ import type { ChartTheme } from "../core/types";
3
+ /**
4
+ * Toolbar/legend styling derived from the chart {@link ChartTheme}, so the chrome tracks
5
+ * the chart instead of being hardcoded dark. Active buttons reuse the crosshair-pill
6
+ * colors (`axisTagBg`/`axisTagText`); muted text is `axis`; rules use `grid`.
7
+ */
8
+ export declare function toolbarUi(theme: ChartTheme): {
9
+ btn: (on: boolean) => CSSProperties;
10
+ sepStyle: CSSProperties;
11
+ barStyle: CSSProperties;
12
+ muted: string;
13
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Toolbar/legend styling derived from the chart {@link ChartTheme}, so the chrome tracks
3
+ * the chart instead of being hardcoded dark. Active buttons reuse the crosshair-pill
4
+ * colors (`axisTagBg`/`axisTagText`); muted text is `axis`; rules use `grid`.
5
+ */
6
+ export function toolbarUi(theme) {
7
+ const btn = (on) => ({
8
+ padding: "4px 8px",
9
+ fontSize: 12,
10
+ fontWeight: 500,
11
+ lineHeight: 1.2,
12
+ border: "none",
13
+ borderRadius: 4,
14
+ cursor: "pointer",
15
+ background: on ? theme.axisTagBg : "transparent",
16
+ color: on ? theme.axisTagText : theme.axis,
17
+ });
18
+ const sepStyle = { width: 1, height: 16, margin: "0 4px", background: theme.grid };
19
+ const barStyle = {
20
+ display: "flex",
21
+ flexWrap: "wrap",
22
+ alignItems: "center",
23
+ gap: 2,
24
+ borderBottom: `1px solid ${theme.grid}`,
25
+ paddingBottom: 4,
26
+ marginBottom: 4,
27
+ };
28
+ return { btn, sepStyle, barStyle, muted: theme.axis };
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livo-build/charts",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "livo-charts — a lightweight, dependency-free canvas charting library (candlesticks, zoom/pan, crosshair, moving-average indicators, live Hyperliquid feed) with a framework-agnostic core and a React wrapper.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",