@reopt-ai/opt-charts
v1.8.2
Published
Data visualization components for Reopt interfaces. Recharts adapters, SVG visualizations, chart frames, and chart-specific shells.
Maintainers
Readme
@reopt-ai/opt-charts
Chart primitives, chart-specific shells, and metadata for the reopt design
system. @reopt-ai/opt-ui/visuals keeps legacy compatibility re-exports, but
new chart work should import this package directly.
Skill support
There is no dedicated opt-charts-install skill in
reopt-ai/reopt-skills yet. If the
app does not already use opt-ui, the opt-ui-install skill can prepare the
shared Tailwind and theme foundation:
npx skills add reopt-ai/reopt-skills/opt-ui-installIt does not install opt-charts. Install this package manually with the command below.
Install
bun add @reopt-ai/opt-charts rechartsreact and react-dom are peer dependencies. recharts is a peer so product
apps can control the visualization runtime version.
Exports
| Export | Purpose |
| ------------------------------ | --------------------------------------------------------- |
| @reopt-ai/opt-charts | All client chart components, shells, utilities, and types |
| @reopt-ai/opt-charts/time | Server-safe date math and time-axis functions |
| @reopt-ai/opt-charts/visuals | Low-level chart visuals |
| @reopt-ai/opt-charts/shells | Product-ready chart shells |
| @reopt-ai/opt-charts/meta | Component metadata for docs and tooling |
Example
import { LineChart } from "@reopt-ai/opt-charts";
export function RevenueTrend() {
return (
<LineChart
data={[
{ month: "Jan", revenue: 120 },
{ month: "Feb", revenue: 164 },
]}
xKey="month"
yKey="revenue"
/>
);
}Time-series example
TimeSeriesChart treats the x-axis as time rather than a list of labels. Pass explicit bounds and granularity so empty buckets, partial trailing buckets, annotations, and drag selection all share the same window.
import { TimeSeriesChart } from "@reopt-ai/opt-charts";
const from = Date.UTC(2026, 7, 24, 0);
const to = Date.UTC(2026, 7, 24, 4);
<TimeSeriesChart
data={[
{ t: from, requests: 42 },
{ t: from + 3_600_000, requests: 88 },
{ t: from + 7_200_000, requests: 130 },
{ t: from + 10_800_000, requests: 104 },
]}
series={[{ dataKey: "requests", name: "Requests", type: "area" }]}
from={from}
to={to}
granularity="hour"
timeZone="UTC"
locale="en-US"
gradient
selectable
onRangeSelect={(range) => console.log(range)}
aria-label="Hourly requests"
/>;Loading and small multiples
Use LoadingChart to preserve the chart's visual weight while a query is running. Use SmallMultiples when one shared scale and a repeated mark make comparisons across segments easier than overlapping every series in one plot.
import { LoadingChart, SmallMultiples } from "@reopt-ai/opt-charts";
return loading ? (
<LoadingChart variant="area" height={220} />
) : (
<SmallMultiples
series={countrySeries}
dataKey="events"
from={from}
to={to}
granularity="day"
sharedYAxis
maxPanels={6}
aria-label="Events by country"
/>
);Drill into a retention cohort
Provide onCellSelect when a populated cell should open the cohort behind the percentage. Empty cells remain inert, and cellLabel gives every control a complete accessible name.
import { RetentionHeatmap, type RetentionCellRef } from "@reopt-ai/opt-charts";
const [selected, setSelected] = useState<RetentionCellRef | null>(null);
<RetentionHeatmap
data={cohorts}
periods={["Day 0", "Day 1", "Day 7", "Day 30"]}
onCellSelect={setSelected}
cellLabel={(cell) =>
`${cell.cohort}, day ${cell.period}, ${cell.value}% retained`
}
aria-label="Weekly retention cohorts"
/>;Draw a journey as a path flow
PathFlowChart takes the columns of a step-positioned journey — the top items at each step, plus how many sessions fell outside the top rows ("other"), ended there ("drop-off") or started there ("entry") — and the edges between adjacent columns. The grid variant draws fixed cards with a share bar; sankey draws nodes proportional to their count with a drop-off tail. Hovering a card lights up everything connected to it, and renderCardActions puts a menu inside a card without the menu's clicks reading as card clicks.
import { PathFlowChart, type PathFlowData } from "@reopt-ai/opt-charts";
const flow: PathFlowData = {
steps: [
{ index: 0, rows: [{ key: "/", count: 1200 }], dropOffCount: 300 },
{
index: 1,
rows: [
{ key: "/pricing", count: 560 },
{ key: "/docs", count: 380 },
],
otherCount: 260,
},
],
edges: [
{
stepIndex: 0,
source: "/",
target: "/pricing",
count: 480,
avgSeconds: 42,
},
{ stepIndex: 0, source: "/", target: "/docs", count: 300 },
{ stepIndex: 0, source: "/", target: null, count: 120 },
],
};
<PathFlowChart
data={flow}
variant="grid"
onCardClick={(card) => console.log(card.key, card.stepIndex)}
aria-label="User flow"
/>;The model behind the layout is exported too: buildPathFlowModel, connectedPathFlowKeys and dominantPathFlowChain let a page answer "which chain is this card on" without re-deriving the geometry. FunnelChart gained a variant (list, bars, horizontal) so the chain can be previewed as a funnel in the same visual language.
Resolve time without React
Use @reopt-ai/opt-charts/time in Route Handlers, server code, URL parsers, and tests. The subpath has no React or Recharts dependency and uses the same date-math grammar and bucket calculations as TimeSeriesChart.
import {
formatBucketLabel,
resolveTimeRange,
timeTicks,
} from "@reopt-ai/opt-charts/time";
const range = resolveTimeRange(
{ from: "now-7d/d", to: "now/d" },
{ now: Date.now(), timeZone: "Asia/Seoul" },
);
if (range) {
const ticks = timeTicks(range.from, range.to, {
granularity: "day",
timeZone: "Asia/Seoul",
});
const labels = ticks.map((tick) =>
formatBucketLabel(tick, "day", {
locale: "ko-KR",
timeZone: "Asia/Seoul",
}),
);
}Development
bun run --filter @reopt-ai/opt-charts lint
bun run --filter @reopt-ai/opt-charts typecheck
bun run --filter @reopt-ai/opt-charts test
bun run --filter @reopt-ai/opt-charts buildThe package emits ESM, CJS, and type declarations under dist/.
