@alpic-ai/ui 0.0.0-dev.g2338b91 → 0.0.0-dev.g23fdbf3

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.
Files changed (113) hide show
  1. package/dist/components/accordion-card.d.mts +5 -6
  2. package/dist/components/accordion.d.mts +5 -6
  3. package/dist/components/alert.d.mts +9 -11
  4. package/dist/components/area-chart.d.mts +62 -0
  5. package/dist/components/area-chart.mjs +269 -0
  6. package/dist/components/attachment-tile.d.mts +1 -3
  7. package/dist/components/avatar.d.mts +8 -10
  8. package/dist/components/badge.d.mts +2 -4
  9. package/dist/components/bar-chart.d.mts +48 -0
  10. package/dist/components/bar-chart.mjs +245 -0
  11. package/dist/components/bar-list.d.mts +28 -0
  12. package/dist/components/bar-list.mjs +98 -0
  13. package/dist/components/breadcrumb.d.mts +10 -11
  14. package/dist/components/button.d.mts +6 -8
  15. package/dist/components/card.d.mts +9 -10
  16. package/dist/components/chart-card.d.mts +25 -0
  17. package/dist/components/chart-card.mjs +48 -0
  18. package/dist/components/chart-container.d.mts +20 -0
  19. package/dist/components/chart-container.mjs +37 -0
  20. package/dist/components/chart-legend.d.mts +16 -0
  21. package/dist/components/chart-legend.mjs +26 -0
  22. package/dist/components/chart-tooltip.d.mts +33 -0
  23. package/dist/components/chart-tooltip.mjs +52 -0
  24. package/dist/components/checkbox.d.mts +2 -3
  25. package/dist/components/collapsible.d.mts +4 -5
  26. package/dist/components/combobox.d.mts +12 -11
  27. package/dist/components/combobox.mjs +7 -4
  28. package/dist/components/command.d.mts +9 -10
  29. package/dist/components/copyable.d.mts +2 -3
  30. package/dist/components/description-list.d.mts +5 -6
  31. package/dist/components/dialog.d.mts +15 -17
  32. package/dist/components/donut-chart.d.mts +46 -0
  33. package/dist/components/donut-chart.mjs +185 -0
  34. package/dist/components/dropdown-menu.d.mts +18 -20
  35. package/dist/components/form.d.mts +38 -21
  36. package/dist/components/form.mjs +6 -6
  37. package/dist/components/github-button.d.mts +1 -2
  38. package/dist/components/grid-fx.d.mts +13 -0
  39. package/dist/components/grid-fx.mjs +188 -0
  40. package/dist/components/heatmap-chart.d.mts +40 -0
  41. package/dist/components/heatmap-chart.mjs +198 -0
  42. package/dist/components/input-group.d.mts +5 -7
  43. package/dist/components/input.d.mts +4 -5
  44. package/dist/components/input.mjs +2 -2
  45. package/dist/components/label.d.mts +2 -3
  46. package/dist/components/line-chart.d.mts +55 -0
  47. package/dist/components/line-chart.mjs +211 -0
  48. package/dist/components/page-loader.d.mts +1 -3
  49. package/dist/components/pagination.d.mts +3 -4
  50. package/dist/components/popover.d.mts +5 -6
  51. package/dist/components/radio-group.d.mts +3 -4
  52. package/dist/components/scroll-area.d.mts +3 -4
  53. package/dist/components/select-trigger-variants.d.mts +1 -3
  54. package/dist/components/select.d.mts +9 -10
  55. package/dist/components/separator.d.mts +2 -3
  56. package/dist/components/sheet.d.mts +11 -12
  57. package/dist/components/shimmer-text.d.mts +2 -2
  58. package/dist/components/sidebar.d.mts +34 -36
  59. package/dist/components/sidebar.mjs +10 -10
  60. package/dist/components/skeleton.d.mts +2 -4
  61. package/dist/components/sonner.d.mts +5 -6
  62. package/dist/components/spinner.d.mts +3 -5
  63. package/dist/components/stat.d.mts +30 -0
  64. package/dist/components/stat.mjs +107 -0
  65. package/dist/components/status-dot.d.mts +2 -4
  66. package/dist/components/switch.d.mts +2 -3
  67. package/dist/components/table.d.mts +10 -11
  68. package/dist/components/tabs.d.mts +12 -14
  69. package/dist/components/tag.d.mts +3 -5
  70. package/dist/components/task-progress.d.mts +1 -3
  71. package/dist/components/textarea.d.mts +3 -4
  72. package/dist/components/textarea.mjs +2 -2
  73. package/dist/components/toggle-group.d.mts +4 -6
  74. package/dist/components/toggle-group.mjs +3 -3
  75. package/dist/components/tooltip-icon-button.d.mts +1 -2
  76. package/dist/components/tooltip.d.mts +5 -6
  77. package/dist/components/typography.d.mts +4 -5
  78. package/dist/components/wizard.d.mts +4 -5
  79. package/dist/hooks/use-chart-theme.d.mts +18 -0
  80. package/dist/hooks/use-chart-theme.mjs +57 -0
  81. package/dist/hooks/use-mobile.mjs +3 -3
  82. package/dist/hooks/use-reduced-motion.d.mts +4 -0
  83. package/dist/hooks/use-reduced-motion.mjs +16 -0
  84. package/dist/lib/chart-palette.d.mts +4 -0
  85. package/dist/lib/chart-palette.mjs +95 -0
  86. package/dist/lib/chart.d.mts +14 -0
  87. package/dist/lib/chart.mjs +27 -0
  88. package/package.json +30 -29
  89. package/src/components/area-chart.tsx +339 -0
  90. package/src/components/bar-chart.tsx +300 -0
  91. package/src/components/bar-list.tsx +150 -0
  92. package/src/components/chart-card.tsx +63 -0
  93. package/src/components/chart-container.tsx +49 -0
  94. package/src/components/chart-legend.tsx +41 -0
  95. package/src/components/chart-tooltip.tsx +93 -0
  96. package/src/components/combobox.tsx +9 -2
  97. package/src/components/donut-chart.tsx +217 -0
  98. package/src/components/grid-fx.tsx +238 -0
  99. package/src/components/heatmap-chart.tsx +287 -0
  100. package/src/components/line-chart.tsx +264 -0
  101. package/src/components/stat.tsx +109 -0
  102. package/src/hooks/use-chart-theme.ts +75 -0
  103. package/src/hooks/use-reduced-motion.ts +17 -0
  104. package/src/lib/chart-palette.ts +110 -0
  105. package/src/lib/chart.ts +56 -0
  106. package/src/stories/area-chart.stories.tsx +200 -0
  107. package/src/stories/bar-chart.stories.tsx +169 -0
  108. package/src/stories/bar-list.stories.tsx +85 -0
  109. package/src/stories/donut-chart.stories.tsx +112 -0
  110. package/src/stories/heatmap-chart.stories.tsx +107 -0
  111. package/src/stories/line-chart.stories.tsx +146 -0
  112. package/src/stories/stat.stories.tsx +64 -0
  113. package/src/styles/tokens.css +63 -0
@@ -0,0 +1,75 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ // Recharts draws to SVG/canvas and cannot read CSS custom properties, so resolve to hex and re-read on .dark toggle.
6
+ export interface ChartTheme {
7
+ foreground: string;
8
+ mutedForeground: string;
9
+ axisForeground: string;
10
+ grid: string;
11
+ border: string;
12
+ card: string;
13
+ popover: string;
14
+ destructive: string;
15
+ warning: string;
16
+ success: string;
17
+ fontMono: string;
18
+ isDark: boolean;
19
+ }
20
+
21
+ const TOKEN_MAP: Record<Exclude<keyof ChartTheme, "isDark">, string> = {
22
+ foreground: "--color-foreground",
23
+ mutedForeground: "--color-muted-foreground",
24
+ axisForeground: "--color-quaternary-foreground",
25
+ grid: "--color-sidebar-border",
26
+ border: "--color-border",
27
+ card: "--color-card",
28
+ popover: "--color-popover",
29
+ destructive: "--color-destructive",
30
+ warning: "--color-warning",
31
+ success: "--color-success",
32
+ fontMono: "--font-mono",
33
+ };
34
+
35
+ const FALLBACK: ChartTheme = {
36
+ foreground: "#121e1e",
37
+ mutedForeground: "#3a4848",
38
+ axisForeground: "#6f7f7f",
39
+ grid: "#e3eaea",
40
+ border: "#acb8b8",
41
+ card: "#f8fafa",
42
+ popover: "#ffffff",
43
+ destructive: "#d92d20",
44
+ warning: "#dc6803",
45
+ success: "#079455",
46
+ fontMono: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
47
+ isDark: false,
48
+ };
49
+
50
+ const readTheme = (): ChartTheme => {
51
+ if (typeof window === "undefined") {
52
+ return FALLBACK;
53
+ }
54
+ const styles = window.getComputedStyle(document.documentElement);
55
+ const entries = Object.entries(TOKEN_MAP).map(([key, token]) => {
56
+ const value = styles.getPropertyValue(token).trim();
57
+ return [key, value || FALLBACK[key as keyof ChartTheme]] as const;
58
+ });
59
+ const colors = Object.fromEntries(entries) as unknown as Omit<ChartTheme, "isDark">;
60
+ return { ...colors, isDark: document.documentElement.classList.contains("dark") };
61
+ };
62
+
63
+ export function useChartTheme() {
64
+ const [theme, setTheme] = React.useState<ChartTheme>(readTheme);
65
+
66
+ React.useEffect(() => {
67
+ const update = () => setTheme(readTheme());
68
+ update();
69
+ const observer = new MutationObserver(update);
70
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
71
+ return () => observer.disconnect();
72
+ }, []);
73
+
74
+ return theme;
75
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ export function useReducedMotion() {
6
+ const [reduced, setReduced] = React.useState(false);
7
+
8
+ React.useEffect(() => {
9
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
10
+ const update = () => setReduced(query.matches);
11
+ update();
12
+ query.addEventListener("change", update);
13
+ return () => query.removeEventListener("change", update);
14
+ }, []);
15
+
16
+ return reduced;
17
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Chart palettes — the locked "paired system" from the analytics-v2 design.
3
+ *
4
+ * These are charting-API color values (consumed by Recharts at draw time, which
5
+ * cannot read CSS custom properties), so per the design-system rules they live
6
+ * as documented hex constants rather than `--color-*` tokens. They are
7
+ * theme-independent: the same series colors read on light and dark canvases.
8
+ * Only the chrome (grid, axis text, tooltip surface, semantic colors) is
9
+ * theme-aware — see `useChartTheme`.
10
+ *
11
+ * Rules baked in:
12
+ * - Pink leads (index 0) — never buried, fixing the V1 "pink last" bug.
13
+ * - Adjacent dashboard cards alternate lead palette (magenta / cyan) — the
14
+ * page decides which to pass; the component just receives one.
15
+ */
16
+
17
+ export const MAGENTA_PALETTE = [
18
+ "#e90060",
19
+ "#ff7eb6",
20
+ "#9b5de5",
21
+ "#5b8def",
22
+ "#36c5f0",
23
+ "#6eece7",
24
+ "#d98a3d",
25
+ "#2bb6a3",
26
+ ] as const;
27
+
28
+ export const CYAN_PALETTE = [
29
+ "#17b8cf",
30
+ "#41ddc9",
31
+ "#3f8ff0",
32
+ "#8b6cf0",
33
+ "#c46bf0",
34
+ "#ff7eb6",
35
+ "#e90060",
36
+ "#c98be0",
37
+ ] as const;
38
+
39
+ export type ChartPaletteName = "magenta" | "cyan";
40
+
41
+ export const CHART_PALETTES: Record<ChartPaletteName, readonly string[]> = {
42
+ magenta: MAGENTA_PALETTE,
43
+ cyan: CYAN_PALETTE,
44
+ };
45
+
46
+ export const heatRampMagenta = (empty: string) => [empty, "#5b0a31", "#a3034a", "#e90060", "#ff7eb6"];
47
+ export const heatRampMint = (empty: string) => [empty, "#0c4a52", "#17b8cf", "#41ddc9", "#6eece7"];
48
+
49
+ // Empty heat cell — the only theme-dependent charting value, so it travels as a
50
+ // light/dark pair rather than a `--color-*` token (heatmaps draw to SVG fills).
51
+ export const HEAT_EMPTY = { light: "#eef3f3", dark: "#102222" } as const;
52
+
53
+ const HEAT_RAMPS: Record<ChartPaletteName, (empty: string) => string[]> = {
54
+ magenta: heatRampMagenta,
55
+ cyan: heatRampMint,
56
+ };
57
+
58
+ export const heatRamp = (palette: ChartPaletteName, empty: string) => HEAT_RAMPS[palette](empty);
59
+
60
+ const hexToRgb = (hex: string) => {
61
+ const value = Number.parseInt(hex.slice(1), 16);
62
+ return [(value >> 16) & 255, (value >> 8) & 255, value & 255] as const;
63
+ };
64
+
65
+ export const luminance = (hex: string) => {
66
+ const linear = hexToRgb(hex).map((channel) => {
67
+ const normalized = channel / 255;
68
+ return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
69
+ });
70
+ // biome-ignore lint/style/noNonNullAssertion: hexToRgb always yields 3 channels
71
+ return 0.2126 * linear[0]! + 0.7152 * linear[1]! + 0.0722 * linear[2]!;
72
+ };
73
+
74
+ export const paletteColor = (palette: readonly string[], index: number) => {
75
+ // biome-ignore lint/style/noNonNullAssertion: modulo guarantees a valid index
76
+ return palette[((index % palette.length) + palette.length) % palette.length]!;
77
+ };
78
+
79
+ const RAMP_ENDS: Record<ChartPaletteName, readonly [string, string]> = {
80
+ magenta: ["#e90060", "#ff7eb6"],
81
+ cyan: ["#17b8cf", "#6eece7"],
82
+ };
83
+
84
+ const mixHex = (from: string, to: string, fraction: number) => {
85
+ const [fromR, fromG, fromB] = hexToRgb(from);
86
+ const [toR, toG, toB] = hexToRgb(to);
87
+ const ratio = Math.min(1, Math.max(0, fraction));
88
+ const channel = (start: number, end: number) => Math.round(start + (end - start) * ratio);
89
+ return `#${[channel(fromR, toR), channel(fromG, toG), channel(fromB, toB)]
90
+ .map((value) => value.toString(16).padStart(2, "0"))
91
+ .join("")}`;
92
+ };
93
+
94
+ export const rampColor = (palette: ChartPaletteName, fraction: number) => {
95
+ const [from, to] = RAMP_ENDS[palette];
96
+ return mixHex(from, to, fraction);
97
+ };
98
+
99
+ /**
100
+ * Interpolate continuously across a multi-stop heat ramp (e.g. `heatRampMagenta`),
101
+ * so a normalized 0..1 value reads as a smooth single-hue gradient rather than
102
+ * the five discrete bands.
103
+ */
104
+ export const heatColor = (stops: readonly string[], fraction: number) => {
105
+ const clamped = Math.min(1, Math.max(0, fraction));
106
+ const segment = (stops.length - 1) * clamped;
107
+ const lowerIndex = Math.min(stops.length - 2, Math.floor(segment));
108
+ // biome-ignore lint/style/noNonNullAssertion: lowerIndex is clamped to a valid pair
109
+ return mixHex(stops[lowerIndex]!, stops[lowerIndex + 1]!, segment - lowerIndex);
110
+ };
@@ -0,0 +1,56 @@
1
+ import type { ChartTheme } from "../hooks/use-chart-theme";
2
+ import { luminance, paletteColor } from "./chart-palette";
3
+
4
+ /**
5
+ * Declarative description of one plotted series. The same shape drives area,
6
+ * line and bar charts — what differs is how each chart renders it.
7
+ */
8
+ export interface ChartSeries {
9
+ key: string;
10
+ name?: string;
11
+ color?: string;
12
+ semantic?: "error" | "warning" | "success";
13
+ dashed?: boolean;
14
+ }
15
+
16
+ export interface ResolvedSeries extends ChartSeries {
17
+ name: string;
18
+ color: string;
19
+ }
20
+
21
+ export const formatShare = (fraction: number) => `${(fraction * 100).toFixed(fraction >= 0.1 ? 0 : 1)}%`;
22
+
23
+ const semanticColor = (theme: ChartTheme, semantic: NonNullable<ChartSeries["semantic"]>) => {
24
+ if (semantic === "error") {
25
+ return theme.destructive;
26
+ }
27
+ if (semantic === "warning") {
28
+ return theme.warning;
29
+ }
30
+ return theme.success;
31
+ };
32
+
33
+ /**
34
+ * Resolve each series to a concrete color and name. Color precedence:
35
+ * explicit `color` → `semantic` token → palette slot by declared index (so the
36
+ * lead series keeps the lead color even after the bands are reordered).
37
+ */
38
+ export const resolveSeries = (
39
+ series: readonly ChartSeries[],
40
+ palette: readonly string[],
41
+ theme: ChartTheme,
42
+ ): ResolvedSeries[] =>
43
+ series.map((entry, index) => ({
44
+ ...entry,
45
+ name: entry.name ?? entry.key,
46
+ color: entry.color ?? (entry.semantic ? semanticColor(theme, entry.semantic) : paletteColor(palette, index)),
47
+ }));
48
+
49
+ /**
50
+ * Stacked bands read cleanest as a vertical gradient: darkest at the baseline,
51
+ * lightest at the top edge. Sorting by luminance enforces that for any palette
52
+ * (this is what fixed the "muddy cyan" stack in the lab). Render order maps to
53
+ * stack order in Recharts — first entry sits at the bottom.
54
+ */
55
+ export const orderByLuminance = (series: ResolvedSeries[]) =>
56
+ [...series].sort((lower, upper) => luminance(lower.color) - luminance(upper.color));
@@ -0,0 +1,200 @@
1
+ import type { Story } from "@ladle/react";
2
+
3
+ import { AreaChart } from "../components/area-chart";
4
+ import { ChartCard } from "../components/chart-card";
5
+ import { GridFx } from "../components/grid-fx";
6
+ import { Stat } from "../components/stat";
7
+
8
+ export default { title: "Charts/Area Chart" };
9
+
10
+ const mulberry32 = (seed: number) => () => {
11
+ seed |= 0;
12
+ seed = (seed + 0x6d2b79f5) | 0;
13
+ let hash = Math.imul(seed ^ (seed >>> 15), 1 | seed);
14
+ hash = (hash + Math.imul(hash ^ (hash >>> 7), 61 | hash)) ^ hash;
15
+ return ((hash ^ (hash >>> 14)) >>> 0) / 4294967296;
16
+ };
17
+
18
+ const CLIENTS = ["ChatGPT", "Claude Code", "Claude", "Anthropic", "Goose", "VS Code"];
19
+ const WEIGHTS = [0.42, 0.24, 0.12, 0.09, 0.07, 0.06];
20
+
21
+ const hourLabel = (index: number, length: number) =>
22
+ `${String(Math.round((index / (length - 1)) * 24)).padStart(2, "0")}:00`;
23
+
24
+ const genStacked = (seed: number) => {
25
+ const rnd = mulberry32(seed);
26
+ const length = 25;
27
+ return Array.from({ length }, (_, index) => {
28
+ const tide = 0.55 + 0.45 * Math.sin((index / length) * Math.PI * 1.6 - 0.4);
29
+ const row: Record<string, number | string> = { t: hourLabel(index, length) };
30
+ CLIENTS.forEach((client, clientIndex) => {
31
+ // biome-ignore lint/style/noNonNullAssertion: index aligned with CLIENTS
32
+ row[client] = Math.max(2, Math.round(120 * WEIGHTS[clientIndex]! * tide * (0.7 + rnd() * 0.6)));
33
+ });
34
+ return row;
35
+ });
36
+ };
37
+
38
+ const genSeries = (seed: number, scale: number) => {
39
+ const rnd = mulberry32(seed);
40
+ const length = 30;
41
+ let value = scale * 0.6;
42
+ return Array.from({ length }, (_, index) => {
43
+ value = Math.max(scale * 0.12, value + (rnd() - 0.46) * scale * 0.18);
44
+ return { t: hourLabel(index, length), v: Math.round(value * (1 + Math.sin(index / 5) * 0.16)) };
45
+ });
46
+ };
47
+
48
+ const genLatency = (seed: number) => {
49
+ const rnd = mulberry32(seed);
50
+ const length = 30;
51
+ let p50 = 240;
52
+ let p95 = 720;
53
+ return Array.from({ length }, (_, index) => {
54
+ p50 = Math.max(120, p50 + (rnd() - 0.48) * 40);
55
+ p95 = Math.max(p50 + 200, p95 + (rnd() - 0.46) * 120);
56
+ return { t: hourLabel(index, length), p50: Math.round(p50), p95: Math.round(p95 * (index === 19 ? 1.7 : 1)) };
57
+ });
58
+ };
59
+
60
+ const genErrors = (seed: number) => {
61
+ const rnd = mulberry32(seed);
62
+ const length = 30;
63
+ return Array.from({ length }, (_, index) => {
64
+ const burst = index > 14 && index < 20 ? 3.4 : 1;
65
+ return {
66
+ t: hourLabel(index, length),
67
+ mcp: Math.max(0, Math.round(4 * (0.3 + rnd()) * burst)),
68
+ tool: Math.max(0, Math.round(6 * (0.3 + rnd()) * burst)),
69
+ };
70
+ });
71
+ };
72
+
73
+ const stacked = genStacked(7);
74
+ const tokens = genSeries(11, 84000);
75
+ const latency = genLatency(31);
76
+ const errors = genErrors(41);
77
+
78
+ const sessionsSeries = CLIENTS.map((key) => ({ key }));
79
+ const nf = (value: number) => value.toLocaleString("en-US");
80
+ const fmtK = (value: number) => (value >= 1000 ? `${(value / 1000).toFixed(value >= 10000 ? 0 : 1)}k` : `${value}`);
81
+ const ms = (value: number) => `${value}ms`;
82
+
83
+ const sessionsTotal = stacked.reduce(
84
+ (sum, row) => sum + CLIENTS.reduce((acc, client) => acc + (row[client] as number), 0),
85
+ 0,
86
+ );
87
+ const sessionsSpark = stacked.map((row) => CLIENTS.reduce((acc, client) => acc + (row[client] as number), 0));
88
+
89
+ const latencyPeak = latency.reduce((best, row) => (row.p95 > best.p95 ? row : best));
90
+ const errorsPeak = errors.reduce((best, row) => (row.mcp + row.tool > best.mcp + best.tool ? row : best));
91
+
92
+ export const AllVariants: Story = () => (
93
+ <div className="chart-canvas mx-auto max-w-[1600px] p-8">
94
+ <GridFx />
95
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
96
+ <ChartCard
97
+ palette="magenta"
98
+ accent="left"
99
+ kicker="Last 24h"
100
+ title="Sessions"
101
+ description="Full-width · left accent"
102
+ className="md:col-span-2 xl:col-span-3"
103
+ >
104
+ <AreaChart data={stacked} index="t" series={sessionsSeries} variant="stacked" legend valueFormatter={nf} />
105
+ </ChartCard>
106
+
107
+ <ChartCard palette="magenta" kicker="Last 24h" title="Sessions" description="By client">
108
+ <Stat
109
+ value={nf(sessionsTotal)}
110
+ unit="sessions"
111
+ delta={{ value: 12.4, direction: "up" }}
112
+ sparkline={sessionsSpark}
113
+ />
114
+ <AreaChart data={stacked} index="t" series={sessionsSeries} variant="stacked" legend valueFormatter={nf} />
115
+ </ChartCard>
116
+
117
+ <ChartCard palette="magenta" kicker="Last 24h" title="Sessions share" description="Composition over time">
118
+ <AreaChart data={stacked} index="t" series={sessionsSeries} variant="expand" legend valueFormatter={nf} />
119
+ </ChartCard>
120
+
121
+ <ChartCard palette="magenta" kicker="Last 24h" title="Sessions" description="Grouped overlay · last values">
122
+ <AreaChart
123
+ data={stacked}
124
+ index="t"
125
+ series={sessionsSeries.slice(0, 3)}
126
+ variant="grouped"
127
+ lastValueLabel
128
+ legend
129
+ valueFormatter={nf}
130
+ />
131
+ </ChartCard>
132
+
133
+ <ChartCard palette="cyan" kicker="Last 24h" title="Output tokens" description="Value flags · last value">
134
+ <Stat value="1.4M" unit="tokens" delta={{ value: 8.1, direction: "up" }} />
135
+ <AreaChart
136
+ data={tokens}
137
+ index="t"
138
+ series={[{ key: "v", name: "tokens" }]}
139
+ variant="grouped"
140
+ lastValueLabel
141
+ valueFlags
142
+ valueFormatter={fmtK}
143
+ />
144
+ </ChartCard>
145
+
146
+ <ChartCard palette="magenta" kicker="Last 24h" title="Request latency" description="p50 vs p95 · SLA band + peak">
147
+ <Stat value="240ms" unit="p50 · current" delta={{ value: 4.2, direction: "down", invert: true }} />
148
+ <AreaChart
149
+ data={latency}
150
+ index="t"
151
+ series={[
152
+ { key: "p50", name: "p50" },
153
+ { key: "p95", name: "p95", dashed: true, color: "#9b5de5" },
154
+ ]}
155
+ variant="grouped"
156
+ referenceLine={{ y: 1000, label: "SLA 1s", band: true }}
157
+ markers={[{ x: latencyPeak.t, y: latencyPeak.p95, label: "peak", color: "#9b5de5" }]}
158
+ legend
159
+ valueFormatter={ms}
160
+ />
161
+ </ChartCard>
162
+
163
+ <ChartCard palette="magenta" kicker="Last 24h" title="Errors" description="Semantic red · palette-independent">
164
+ <Stat value="312" unit="errors" delta={{ value: 0, direction: "up", invert: true, label: "burst 16:00" }} />
165
+ <AreaChart
166
+ data={errors}
167
+ index="t"
168
+ series={[
169
+ { key: "tool", name: "Tool errors", semantic: "warning" },
170
+ { key: "mcp", name: "MCP errors", semantic: "error" },
171
+ ]}
172
+ variant="stacked"
173
+ markers={[{ x: errorsPeak.t, y: errorsPeak.mcp + errorsPeak.tool, label: "burst" }]}
174
+ legend
175
+ valueFormatter={nf}
176
+ />
177
+ </ChartCard>
178
+
179
+ <ChartCard palette="cyan" kicker="Last 24h" title="Tasks" description="Hatch lead · stepped">
180
+ <AreaChart
181
+ data={tokens}
182
+ index="t"
183
+ series={[{ key: "v", name: "tokens" }]}
184
+ curve="step"
185
+ variant="grouped"
186
+ texture
187
+ valueFormatter={fmtK}
188
+ />
189
+ </ChartCard>
190
+
191
+ <ChartCard palette="magenta" kicker="State" title="Loading" description="Mono spinner placeholder">
192
+ <AreaChart data={[]} index="t" series={sessionsSeries} loading />
193
+ </ChartCard>
194
+
195
+ <ChartCard palette="magenta" kicker="State" title="Empty" description="No data in range">
196
+ <AreaChart data={[]} index="t" series={sessionsSeries} />
197
+ </ChartCard>
198
+ </div>
199
+ </div>
200
+ );
@@ -0,0 +1,169 @@
1
+ import type { Story } from "@ladle/react";
2
+
3
+ import { BarChart } from "../components/bar-chart";
4
+ import { ChartCard } from "../components/chart-card";
5
+ import { GridFx } from "../components/grid-fx";
6
+ import { Stat } from "../components/stat";
7
+
8
+ export default { title: "Charts/Bar Chart" };
9
+
10
+ const mulberry32 = (seed: number) => () => {
11
+ seed |= 0;
12
+ seed = (seed + 0x6d2b79f5) | 0;
13
+ let hash = Math.imul(seed ^ (seed >>> 15), 1 | seed);
14
+ hash = (hash + Math.imul(hash ^ (hash >>> 7), 61 | hash)) ^ hash;
15
+ return ((hash ^ (hash >>> 14)) >>> 0) / 4294967296;
16
+ };
17
+
18
+ const CLIENTS = ["ChatGPT", "Claude Code", "Claude", "Anthropic", "Goose", "VS Code"];
19
+ const WEIGHTS = [0.42, 0.24, 0.12, 0.09, 0.07, 0.06];
20
+ const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
21
+
22
+ const genDailyStacked = (seed: number) => {
23
+ const rnd = mulberry32(seed);
24
+ return DAYS.map((day, dayIndex) => {
25
+ const tide = 0.6 + 0.4 * Math.sin((dayIndex / DAYS.length) * Math.PI * 1.4);
26
+ const row: Record<string, number | string> = { t: day };
27
+ CLIENTS.forEach((client, clientIndex) => {
28
+ // biome-ignore lint/style/noNonNullAssertion: index aligned with CLIENTS
29
+ row[client] = Math.max(4, Math.round(900 * WEIGHTS[clientIndex]! * tide * (0.7 + rnd() * 0.6)));
30
+ });
31
+ return row;
32
+ });
33
+ };
34
+
35
+ const genDuration = (seed: number) => {
36
+ const rnd = mulberry32(seed);
37
+ return DAYS.map((day, dayIndex) => ({
38
+ t: day,
39
+ p50: Math.round(240 + rnd() * 120),
40
+ p95: Math.round(820 + rnd() * 360 * (dayIndex === 4 ? 1.6 : 1)),
41
+ }));
42
+ };
43
+
44
+ const genErrors = (seed: number) => {
45
+ const rnd = mulberry32(seed);
46
+ return DAYS.map((day, dayIndex) => {
47
+ const burst = dayIndex === 4 ? 3 : 1;
48
+ return {
49
+ t: day,
50
+ tool: Math.max(0, Math.round(6 * (0.4 + rnd()) * burst)),
51
+ mcp: Math.max(0, Math.round(4 * (0.4 + rnd()) * burst)),
52
+ };
53
+ });
54
+ };
55
+
56
+ const genThroughput = (seed: number) => {
57
+ const rnd = mulberry32(seed);
58
+ return DAYS.map((day) => ({ t: day, v: Math.round(38000 + rnd() * 46000) }));
59
+ };
60
+
61
+ const stacked = genDailyStacked(7);
62
+ const duration = genDuration(19);
63
+ const errors = genErrors(41);
64
+ const throughput = genThroughput(11);
65
+
66
+ const sessionsSeries = CLIENTS.map((key) => ({ key }));
67
+ const nf = (value: number) => value.toLocaleString("en-US");
68
+ const fmtK = (value: number) => (value >= 1000 ? `${(value / 1000).toFixed(value >= 10000 ? 0 : 1)}k` : `${value}`);
69
+ const ms = (value: number) => `${value}ms`;
70
+
71
+ const sessionsTotal = stacked.reduce(
72
+ (sum, row) => sum + CLIENTS.reduce((acc, client) => acc + (row[client] as number), 0),
73
+ 0,
74
+ );
75
+ const sessionsSpark = stacked.map((row) => CLIENTS.reduce((acc, client) => acc + (row[client] as number), 0));
76
+
77
+ const errorsPeak = errors.reduce((best, row) => (row.mcp + row.tool > best.mcp + best.tool ? row : best));
78
+
79
+ export const AllVariants: Story = () => (
80
+ <div className="chart-canvas mx-auto max-w-[1600px] p-8">
81
+ <GridFx />
82
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
83
+ <ChartCard
84
+ palette="magenta"
85
+ accent="left"
86
+ kicker="Last 7d"
87
+ title="Sessions"
88
+ description="Full-width · left accent"
89
+ className="md:col-span-2 xl:col-span-3"
90
+ >
91
+ <BarChart data={stacked} index="t" series={sessionsSeries} variant="stacked" legend valueFormatter={nf} />
92
+ </ChartCard>
93
+
94
+ <ChartCard palette="magenta" kicker="Last 7d" title="Sessions" description="By client · stacked">
95
+ <Stat
96
+ value={nf(sessionsTotal)}
97
+ unit="sessions"
98
+ delta={{ value: 9.3, direction: "up" }}
99
+ sparkline={sessionsSpark}
100
+ />
101
+ <BarChart data={stacked} index="t" series={sessionsSeries} variant="stacked" legend valueFormatter={nf} />
102
+ </ChartCard>
103
+
104
+ <ChartCard palette="magenta" kicker="Last 7d" title="Sessions share" description="Composition · 100% stacked">
105
+ <BarChart data={stacked} index="t" series={sessionsSeries} variant="expand" legend valueFormatter={nf} />
106
+ </ChartCard>
107
+
108
+ <ChartCard palette="magenta" kicker="Last 7d" title="Request duration" description="p50 vs p95 · grouped">
109
+ <BarChart
110
+ data={duration}
111
+ index="t"
112
+ series={[
113
+ { key: "p50", name: "p50" },
114
+ { key: "p95", name: "p95", color: "#9b5de5" },
115
+ ]}
116
+ variant="grouped"
117
+ legend
118
+ valueFormatter={ms}
119
+ />
120
+ </ChartCard>
121
+
122
+ <ChartCard palette="cyan" kicker="Last 7d" title="Output tokens" description="Single series · value labels">
123
+ <Stat value="1.4M" unit="tokens" delta={{ value: 8.1, direction: "up" }} />
124
+ <BarChart
125
+ data={throughput}
126
+ index="t"
127
+ series={[{ key: "v", name: "tokens" }]}
128
+ valueLabels
129
+ valueFormatter={fmtK}
130
+ />
131
+ </ChartCard>
132
+
133
+ <ChartCard palette="magenta" kicker="Last 7d" title="Errors" description="Semantic red · burst marker">
134
+ <Stat value="312" unit="errors" delta={{ value: 0, direction: "up", invert: true, label: "burst Fri" }} />
135
+ <BarChart
136
+ data={errors}
137
+ index="t"
138
+ series={[
139
+ { key: "tool", name: "Tool errors", semantic: "warning" },
140
+ { key: "mcp", name: "MCP errors", semantic: "error" },
141
+ ]}
142
+ variant="stacked"
143
+ markers={[{ x: errorsPeak.t, y: errorsPeak.mcp + errorsPeak.tool, label: "burst" }]}
144
+ legend
145
+ valueFormatter={nf}
146
+ />
147
+ </ChartCard>
148
+
149
+ <ChartCard palette="cyan" kicker="Last 7d" title="Throughput" description="Hatch lead · SLA band">
150
+ <BarChart
151
+ data={throughput}
152
+ index="t"
153
+ series={[{ key: "v", name: "tokens" }]}
154
+ texture
155
+ referenceLine={{ y: 80000, label: "capacity", band: true }}
156
+ valueFormatter={fmtK}
157
+ />
158
+ </ChartCard>
159
+
160
+ <ChartCard palette="magenta" kicker="State" title="Loading" description="Mono spinner placeholder">
161
+ <BarChart data={[]} index="t" series={sessionsSeries} loading />
162
+ </ChartCard>
163
+
164
+ <ChartCard palette="magenta" kicker="State" title="Empty" description="No data in range">
165
+ <BarChart data={[]} index="t" series={sessionsSeries} />
166
+ </ChartCard>
167
+ </div>
168
+ </div>
169
+ );