From 5b8f4dac53959b4f76e89e3b1fdfb1aaa9898b6a Mon Sep 17 00:00:00 2001 From: Saurabh Arora Date: Sun, 15 Mar 2026 15:29:22 -0700 Subject: [PATCH 1/3] feat: added a skill for data story telling and visualizations/ data products --- .opencode/skills/ai-dataviz/SKILL.md | 217 ++++ .../ai-dataviz/references/component-guide.md | 994 ++++++++++++++++++ 2 files changed, 1211 insertions(+) create mode 100644 .opencode/skills/ai-dataviz/SKILL.md create mode 100644 .opencode/skills/ai-dataviz/references/component-guide.md diff --git a/.opencode/skills/ai-dataviz/SKILL.md b/.opencode/skills/ai-dataviz/SKILL.md new file mode 100644 index 0000000000..347e58fd94 --- /dev/null +++ b/.opencode/skills/ai-dataviz/SKILL.md @@ -0,0 +1,217 @@ +--- +name: ai-dataviz +description: > + Build modern, AI-first data visualizations and data storytelling interfaces + using code-based component libraries (shadcn/ui, Recharts, Tremor, Nivo, D3, + Victory, visx) instead of legacy BI tools. Use this skill whenever the user + asks to visualize data, build dashboards, create analytics views, chart + metrics, tell a data story, build a reporting interface, create KPI cards, + plot graphs, or explore a dataset — even if they mention PowerBI, Tableau, + Streamlit, Metabase, Looker, Grafana, or similar tools. Also trigger when the + user says "make a dashboard", "show me the data", "chart this", "visualize + trends", "build an analytics page", "data story", or anything involving + turning raw data into interactive visual interfaces. If the task involves + presenting data visually — this is the skill. Always prefer building a real, + interactive, code-based UI over exporting to or recommending a BI platform. +--- + +# AI-First Data Visualization + +## Table of Contents + +1. [Philosophy](#philosophy) +2. [Technology Stack](#technology-stack) +3. [Building a Data Visualization](#building-a-data-visualization) + - [Step 1: Understand the Data Story](#step-1-understand-the-data-story) + - [Step 2: Choose the Right Chart Type](#step-2-choose-the-right-chart-type) + - [Step 3: Build the Interface](#step-3-build-the-interface) + - [Step 4: Apply Design Principles](#step-4-apply-design-principles) + - [Step 5: Add Interactivity & Annotations](#step-5-add-interactivity--annotations) + - [Step 6: Tell the Story](#step-6-tell-the-story) +4. [Environment-Specific Guidance](#environment-specific-guidance) +5. [Anti-Patterns to Avoid](#anti-patterns-to-avoid) +6. [Quick Reference](#quick-reference) + +--- + +## Philosophy + +AI agents can generate production-quality, interactive data interfaces in +minutes using modern component libraries — more customizable, no vendor lock-in, +embeddable anywhere. + +**Default behavior**: When the user hasn't specified a tool, build a code-first +interactive UI using the stack below. Don't ask — just deliver something great. + +**When the user explicitly names a tool** (PowerBI, Tableau, Looker, Metabase, +Grafana, Streamlit, etc.) — **use that tool**. They have a reason: org policy, +existing infra, stakeholder requirements. Help them do it well in their chosen +tool. Only suggest the code-first alternative if they ask for options or if +there's a clear technical blocker (e.g., the BI tool can't support what they +need). + +--- + +## Technology Stack + +Full library docs: `references/component-guide.md` + +### Framework Priority + +1. **React + Tailwind** — Default. Use when the environment supports JSX/TSX. +2. **HTML + CSS + Vanilla JS** — Fallback without React. Use D3 or Chart.js. +3. **Python (Plotly/Dash)** — Only for Python-only environments (Jupyter, scripts). + +### Component Libraries + +| Library | Best For | When to Use | +|---------|----------|-------------| +| **shadcn/ui charts** | General dashboards, most chart types | Default first choice | +| **Recharts** | Line, bar, area, composed, radar charts | Fine-grained control | +| **Tremor** | KPI cards, metric displays, full layouts | Complete analytics dashboards | +| **Nivo** | Heatmaps, treemaps, choropleth, calendar, Sankey | Advanced / exotic types | +| **visx** | Bespoke custom visualizations | D3-level control with React | +| **D3.js** | Force-directed graphs, DAGs, maps | Maximum flexibility | +| **Victory** | Animated charts | When animation quality matters most | + +**Supporting**: Tailwind CSS · Radix UI · Framer Motion · Lucide React · date-fns · Papaparse · lodash + +--- + +## Building a Data Visualization + +### Step 1: Understand the Data Story + +Before writing any code, identify: + +- **What question does the data answer?** ("How are costs trending?", "Where do users drop off?") +- **Who is the audience?** Executive → KPIs only. Analyst → drill-down + filters. Public → narrative flow. +- **What's the key insight?** Every great viz has ONE takeaway. Design around it. + +### Step 2: Choose the Right Chart Type + +| Data Relationship | Chart Type | Library | +|-------------------|-----------|---------| +| Trend over time | Line chart, Area chart | shadcn/Recharts | +| Comparison across categories | Bar chart (horizontal for many) | shadcn/Recharts | +| Part of a whole | Donut, Treemap | shadcn/Nivo | +| Distribution | Histogram, Box plot, Violin | Nivo/visx | +| Correlation | Scatter, Bubble chart | Recharts/visx | +| Geographic | Choropleth, Dot map | Nivo/D3 | +| Hierarchical | Treemap, Sunburst | Nivo | +| Flow / Process | Sankey, Funnel | Nivo/D3 | +| Single KPI | Metric card, Gauge, Sparkline | Tremor/shadcn | +| Multi-metric overview | Dashboard grid of cards | Tremor + shadcn | +| Ranking | Horizontal bar, Bar list | Tremor | +| Status / Progress | Tracker, Progress bar | Tremor | +| **Column / model lineage** | **Force-directed DAG** | **D3** | +| **Pipeline dependencies** | **Hierarchical tree, DAG** | **D3 / Nivo** | +| **Multi-dimensional quality** | **Radar / Spider chart** | **Recharts** | +| **Activity density over time** | **Calendar heatmap** | **Nivo** | +| **Incremental change breakdown** | **Waterfall chart** | **Recharts (custom)** | +| **Ranking shift over time** | **Bump chart** | **Recharts (custom)** | + +### Step 3: Build the Interface + +Start from this layout and remove what the data doesn't need: + +``` +┌─────────────────────────────────────────────────┐ +│ Header: Title + Description + Date Range │ +├─────────────────────────────────────────────────┤ +│ KPI Row: 3-5 metric cards with sparklines │ +├─────────────────────────────────────────────────┤ +│ Primary Visualization (largest chart) │ +├────────────────────┬────────────────────────────┤ +│ Secondary Chart │ Supporting Chart / Table │ +├────────────────────┴────────────────────────────┤ +│ Detail Table (sortable, filterable) │ +└─────────────────────────────────────────────────┘ +``` + +A single insight might just be one chart with a headline and annotation. +Scale complexity to match the data and audience. + +### Step 4: Apply Design Principles + +- **Data-ink ratio**: Remove chartjunk — unnecessary gridlines, redundant labels, decorative borders. +- **Color with purpose**: Encode meaning (red = bad, green = good, blue = neutral). Max 5–7 colors. Single-hue gradient for sequential data. +- **Typography hierarchy**: Title → subtitle (muted) → axis labels (small) → data labels. +- **Responsive**: `min-h-[VALUE]` on all charts. Grid stacks on mobile. Test at 375/768/1280px. +- **Whitespace**: Give charts room. A padded dashboard reads better than a dense wall. +- **Animation**: Entry transitions only. `duration-300` to `duration-500`. Never continuous. +- **Accessibility**: `aria-label` on charts, WCAG AA contrast, don't rely on color alone. + +### Step 5: Add Interactivity & Annotations + +**Interactivity priority order:** +1. Tooltips — exact values on hover (every chart) +2. Filtering — date range, category, segment +3. Sorting — click column headers in tables +4. Drill-down — click bar/slice to reveal detail +5. Cross-filtering — selection in one chart filters others +6. Export — CSV download of underlying data +7. Annotations — callouts that turn a chart into a story + +**Annotations** are the most underused technique in data storytelling. Every +chart with a clear insight should have at least one. Use them to mark: +- Inflection points and trend reversals +- Threshold crossings and goal lines (amber) +- External events: releases, incidents, campaigns (indigo / red) +- Anomalies that demand explanation (red) +- Achievements against target (green) + +Limit to **3 annotations per chart** — more and none stand out. Never overlap +data. Implementation patterns: `references/component-guide.md` → Annotation Patterns. + +### Step 6: Tell the Story + +- **Headline states the insight**: "Revenue grew 23% QoQ, driven by enterprise deals" — not "Q3 Revenue Chart" +- **Annotate key moments**: Mark inflection points, anomalies, goal lines directly on the chart +- **Contextual comparisons**: vs. prior period, vs. target, vs. benchmark +- **Progressive disclosure**: Overview first — detail on demand. Don't front-load complexity. + +--- + +## Environment-Specific Guidance + +| Environment | Approach | +|-------------|----------| +| **Claude Artifacts** | React (JSX), single file, default export. Available: `recharts`, `lodash`, `d3`, `lucide-react`, shadcn via `@/components/ui/*`, Tailwind. | +| **Claude Code / Terminal** | Full project: Vite + React + Tailwind. Add shadcn/ui + Recharts. Structure: `src/components/charts/`, `src/components/cards/`, `src/data/`. | +| **Python / Jupyter** | Plotly for charts, Plotly Dash for dashboards. Same design principles apply. | +| **Cursor / Bolt / other IDEs** | Match existing framework. Include install commands. Prefer shadcn/ui if already present, Tremor for dashboard-heavy apps. | + +--- + +## Anti-Patterns to Avoid + +- **Screenshot charts** — build interactive components, never static images +- **Defaulting to BI tools unprompted** — when no tool is specified, build code-first; don't suggest "just use Tableau" as a lazy out +- **Default matplotlib** — always customize if forced into Python +- **Rainbow palettes** — use deliberate, meaningful colors +- **3D charts** — almost never appropriate +- **Pie charts > 5 slices** — use horizontal bar instead +- **Unlabeled dual y-axes** — use two separate charts instead +- **Truncated bar axes** — always start at zero +- **Chartjunk** — no gratuitous gradients, shadows, or decoration + +--- + +## Quick Reference + +| Pattern | Implementation | +|---------|---------------| +| KPI card + sparkline | Tremor `Card` + `SparkAreaChart` or shadcn `Card` + axisless Recharts `AreaChart` | +| Time series | shadcn `ChartContainer` + Recharts `LineChart`. Add date picker, granularity toggle, prior-period dashed overlay. | +| Categorical comparison | Recharts `BarChart`. Horizontal bars for 10+ categories. Sort toggle + threshold line. | +| Funnel | Nivo `Funnel`. Show step labels, conversion %, drop-off highlight. | +| Geographic | Nivo `Choropleth` + GeoJSON. Color legend + hover tooltip. | +| Table with inline visuals | Tremor/shadcn `Table` with embedded sparklines, progress bars, badges. Sortable + searchable. | +| Force-directed DAG | D3 force simulation + React SVG. Rectangles nodes, directed edges, arrowheads, drag. Color by node type. | +| Radar / spider | Recharts `RadarChart`. Current vs. benchmark (dashed). `domain={[0,100]}` for consistent axes. | +| Calendar heatmap | Nivo `ResponsiveCalendar`. Single-hue sequential palette. Wrap in fixed-height div. | +| Waterfall | Recharts `ComposedChart` with invisible spacer `Bar` + colored value `Bar`. Green = positive, red = negative. | +| Annotations | Recharts `ReferenceLine` (goal/event), `ReferenceArea` (time window), custom dot renderer (anomalies). | + +Full implementations: `references/component-guide.md` diff --git a/.opencode/skills/ai-dataviz/references/component-guide.md b/.opencode/skills/ai-dataviz/references/component-guide.md new file mode 100644 index 0000000000..f7e747529d --- /dev/null +++ b/.opencode/skills/ai-dataviz/references/component-guide.md @@ -0,0 +1,994 @@ +# Component Library Reference Guide + +Detailed patterns, code snippets, and API references for building AI-first data +visualizations. Read the section relevant to your chosen library. + +## Table of Contents + +1. [shadcn/ui Charts](#shadcnui-charts) +2. [Recharts Patterns](#recharts-patterns) +3. [Tremor Dashboard Components](#tremor-dashboard-components) +4. [Nivo Advanced Charts](#nivo-advanced-charts) +5. [D3.js Custom Visualizations](#d3js-custom-visualizations) +6. [visx Low-Level Primitives](#visx-low-level-primitives) +7. [Layout Patterns](#layout-patterns) +8. [Color Systems](#color-systems) +9. [Data Transformation Patterns](#data-transformation-patterns) +10. [Force-Directed DAG](#force-directed-dag) +11. [Radar / Spider Chart](#radar--spider-chart) +12. [Calendar Heatmap](#calendar-heatmap) +13. [Waterfall Chart](#waterfall-chart) +14. [Annotation Patterns](#annotation-patterns) + +--- + +## shadcn/ui Charts + +shadcn/ui charts are built on top of Recharts and provide themed, accessible +chart components that integrate with the shadcn design system. + +### Core Concepts + +- **ChartContainer**: Wraps every chart. Accepts a `config` object and handles + theming, responsive sizing, and accessibility. +- **ChartConfig**: Defines labels, colors, and icons for each data series. + Decoupled from data — reusable across charts. +- **ChartTooltip / ChartTooltipContent**: Custom tooltip components styled to + match the design system. +- **ChartLegend / ChartLegendContent**: Interactive legend with toggle behavior. + +### Config Pattern + +```tsx +import { type ChartConfig } from "@/components/ui/chart" + +const chartConfig = { + revenue: { + label: "Revenue", + color: "hsl(var(--chart-1))", + }, + expenses: { + label: "Expenses", + color: "hsl(var(--chart-2))", + }, +} satisfies ChartConfig +``` + +### Bar Chart + +```tsx + + + + v.slice(0, 3)} + /> + } /> + } /> + + + + +``` + +### Area Chart with Gradient + +```tsx + + + + + + + + + + + } /> + + + +``` + +### Key Rules + +- Always set `min-h-[VALUE]` on ChartContainer (required for responsiveness) +- Use `accessibilityLayer` prop on the main chart component +- Use CSS variables `var(--color-{key})` for colors, not hardcoded values +- Use `ChartTooltip` + `ChartTooltipContent` instead of Recharts' default + +--- + +## Recharts Patterns + +When using Recharts directly (without shadcn wrapper), follow these patterns. + +### Composed Chart (Multiple Series Types) + +```tsx + + + + + + + + + + + +``` + +### Pie / Donut Chart + +```tsx + + + 0 = donut + outerRadius={100} + paddingAngle={2} + dataKey="value" + > + {data.map((entry, i) => ( + + ))} + + + + + +``` + +### Scatter Plot + +```tsx + + + + + + + + + + +``` + +--- + +## Tremor Dashboard Components + +Tremor excels at complete dashboard layouts with KPI cards, metric displays, and +integrated charts. + +### KPI Card + +```tsx +import { Card, BadgeDelta, SparkAreaChart } from "@tremor/react" + + +
+

Revenue

+ +12.3% +
+

$1.24M

+ +
+``` + +### Dashboard Grid + +```tsx +
+ + + + +
+``` + +### Area Chart with Tremor + +```tsx +import { AreaChart, Card, Title } from "@tremor/react" + + + Monthly Revenue + `$${(v / 1000).toFixed(0)}k`} + className="h-72 mt-4" + showAnimation + /> + +``` + +### Bar List (Ranking) + +```tsx +import { BarList, Card, Title } from "@tremor/react" + + + Top Traffic Sources + + +``` + +### Tracker (Status Grid) + +```tsx +import { Tracker } from "@tremor/react" + + ({ + color: d.uptime > 99.9 ? "emerald" : d.uptime > 99 ? "yellow" : "red", + tooltip: `${d.date}: ${d.uptime}%`, + }))} + className="mt-2" +/> +``` + +--- + +## Nivo Advanced Charts + +Nivo is ideal for chart types that shadcn/Recharts don't cover well. + +### Heatmap + +```tsx +import { ResponsiveHeatMap } from "@nivo/heatmap" + +
+ +
+``` + +### Treemap + +```tsx +import { ResponsiveTreeMap } from "@nivo/treemap" + +
+ +
+``` + +### Sankey (Flow Diagram) + +```tsx +import { ResponsiveSankey } from "@nivo/sankey" + +
+ +
+``` + +### Choropleth (Geographic) + +```tsx +import { ResponsiveChoropleth } from "@nivo/geo" + +
+ +
+``` + +--- + +## D3.js Custom Visualizations + +Use D3 when you need complete control. In React, use D3 for calculations and +React for rendering (the "D3 for math, React for DOM" pattern). + +### Force-Directed Graph + +```tsx +const simulation = d3.forceSimulation(nodes) + .force("link", d3.forceLink(links).id(d => d.id).distance(80)) + .force("charge", d3.forceManyBody().strength(-200)) + .force("center", d3.forceCenter(width / 2, height / 2)) + .force("collision", d3.forceCollide().radius(20)) + +// Render with React SVG elements, update positions on simulation tick +``` + +### Arc / Radial Layout + +```tsx +const arc = d3.arc() + .innerRadius(innerR) + .outerRadius(outerR) + .cornerRadius(3) + +const pie = d3.pie() + .sort(null) + .value(d => d.value) + .padAngle(0.02) +``` + +--- + +## visx Low-Level Primitives + +visx (by Airbnb) provides D3-powered React components. Use when you want D3 +precision with React rendering. + +### Key Modules + +- `@visx/shape` — Bars, lines, areas, arcs, pies +- `@visx/axis` — Configurable axes +- `@visx/scale` — D3 scales as hooks +- `@visx/tooltip` — Tooltip positioning +- `@visx/grid` — Background grids +- `@visx/gradient` — SVG gradients +- `@visx/responsive` — `ParentSize` wrapper for responsive charts + +### Pattern + +```tsx +import { scaleBand, scaleLinear } from "@visx/scale" +import { Bar } from "@visx/shape" +import { AxisBottom, AxisLeft } from "@visx/axis" +import { Group } from "@visx/group" + +const xScale = scaleBand({ domain: data.map(getX), range: [0, width], padding: 0.3 }) +const yScale = scaleLinear({ domain: [0, max(data, getY)], range: [height, 0] }) + + + + {data.map((d) => ( + + ))} + + + + +``` + +--- + +## Layout Patterns + +### Dashboard Grid (Tailwind) + +```tsx +// Full dashboard layout +
+ {/* Header */} +
+
+

Analytics

+

Your performance overview

+
+ +
+ + {/* KPI Row */} +
+ {kpis.map(kpi => )} +
+ + {/* Charts Row */} +
+ {/* Primary chart */} + {/* Secondary chart */} +
+ + {/* Detail Table */} + {/* DataTable component */} +
+``` + +### Card Component (shadcn-style) + +```tsx +
+
+

{title}

+ +
+
+

{value}

+

0 ? "text-green-600" : "text-red-600")}> + {delta > 0 ? "+" : ""}{delta}% from last period +

+
+
+``` + +--- + +## Color Systems + +### Semantic Palette (recommended) + +```css +/* Use CSS variables for theming */ +--chart-1: 221.2 83.2% 53.3%; /* Primary blue */ +--chart-2: 142.1 76.2% 36.3%; /* Success green */ +--chart-3: 24.6 95% 53.1%; /* Warning orange */ +--chart-4: 346.8 77.2% 49.8%; /* Danger red */ +--chart-5: 262.1 83.3% 57.8%; /* Accent purple */ +``` + +### Sequential (for gradients / heatmaps) + +Single hue, vary lightness: `blue-100` → `blue-900` (Tailwind scale) or use +Nivo's built-in schemes: `blues`, `greens`, `oranges`, `purples`. + +### Diverging (for +/- values) + +Red ↔ White ↔ Green or Red ↔ Grey ↔ Blue. Center on zero or mean. + +### Categorical (for distinct groups) + +Max 7 colors. Use Tailwind's `500` shade variants: +`blue-500`, `emerald-500`, `amber-500`, `rose-500`, `violet-500`, `cyan-500`, +`orange-500`. + +--- + +## Data Transformation Patterns + +### Reshape for Recharts + +Recharts expects flat arrays where each object represents one data point with +all series as keys: + +```ts +// Input: { date, category, value } rows +// Output: { date, category_A: value, category_B: value } +const pivoted = _.chain(rawData) + .groupBy("date") + .map((items, date) => ({ + date, + ..._.fromPairs(items.map(i => [i.category, i.value])) + })) + .value() +``` + +### Aggregate for KPI Cards + +```ts +const kpis = { + total: _.sumBy(data, "revenue"), + average: _.meanBy(data, "revenue"), + max: _.maxBy(data, "revenue"), + count: data.length, + growth: ((current - previous) / previous * 100).toFixed(1), +} +``` + +### Hierarchical for Treemaps + +```ts +// Flat → hierarchical +const tree = { + name: "root", + children: _.chain(data) + .groupBy("category") + .map((items, name) => ({ + name, + children: items.map(i => ({ name: i.label, value: i.amount })), + })) + .value(), +} +``` + +### Time Bucketing + +```ts +import { format, startOfWeek, startOfMonth } from "date-fns" + +const weeklyData = _.chain(data) + .groupBy(d => format(startOfWeek(new Date(d.date)), "yyyy-MM-dd")) + .map((items, week) => ({ + week, + total: _.sumBy(items, "value"), + count: items.length, + })) + .value() +``` + +### Waterfall Pre-computation + +```ts +// Compute running start position for each waterfall bar +// Input: [{ name, value }] — positive = gain, negative = loss +const waterfallData = rawItems.reduce((acc, item, i) => { + const prev = acc[i - 1] + const start = prev ? prev.start + prev.value : 0 + return [...acc, { ...item, start, end: start + item.value }] +}, []) +``` + +--- + +## Force-Directed DAG + +Use D3's force simulation to render lineage graphs, dependency trees, and +pipeline DAGs. The pattern: D3 computes positions, React renders SVG elements. + +```tsx +import { useEffect, useRef } from "react" +import * as d3 from "d3" + +interface DagNode { id: string; label: string; type: "source" | "middle" | "output" } +interface DagLink { source: string; target: string; label?: string } + +const NODE_COLORS: Record = { + source: { fill: "#dbeafe", stroke: "#3b82f6" }, + middle: { fill: "#f1f5f9", stroke: "#94a3b8" }, + output: { fill: "#dcfce7", stroke: "#22c55e" }, +} + +export function ForceDAG({ nodes, links }: { nodes: DagNode[]; links: DagLink[] }) { + const svgRef = useRef(null) + + useEffect(() => { + if (!svgRef.current) return + const width = svgRef.current.clientWidth || 800 + const height = 500 + + const svg = d3.select(svgRef.current).attr("height", height) + svg.selectAll("*").remove() + + // Arrowhead marker + svg.append("defs").append("marker") + .attr("id", "dag-arrow") + .attr("viewBox", "0 -5 10 10") + .attr("refX", 22).attr("refY", 0) + .attr("markerWidth", 6).attr("markerHeight", 6) + .attr("orient", "auto") + .append("path").attr("d", "M0,-5L10,0L0,5").attr("fill", "#94a3b8") + + const nodesCopy = nodes.map(n => ({ ...n })) + const linksCopy = links.map(l => ({ ...l })) + + const sim = d3.forceSimulation(nodesCopy as any) + .force("link", d3.forceLink(linksCopy).id((d: any) => d.id).distance(140)) + .force("charge", d3.forceManyBody().strength(-350)) + .force("center", d3.forceCenter(width / 2, height / 2)) + .force("collision", d3.forceCollide().radius(50)) + + const linkSel = svg.append("g").selectAll("line") + .data(linksCopy).join("line") + .attr("stroke", "#cbd5e1").attr("stroke-width", 1.5) + .attr("marker-end", "url(#dag-arrow)") + + const nodeSel = svg.append("g").selectAll("g") + .data(nodesCopy).join("g") + .call( + d3.drag() + .on("start", (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y }) + .on("drag", (e, d) => { d.fx = e.x; d.fy = e.y }) + .on("end", (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null }) + ) + + nodeSel.append("rect") + .attr("x", -54).attr("y", -18) + .attr("width", 108).attr("height", 36).attr("rx", 6) + .attr("fill", (d: any) => NODE_COLORS[d.type].fill) + .attr("stroke", (d: any) => NODE_COLORS[d.type].stroke) + .attr("stroke-width", 1.5) + + nodeSel.append("text") + .attr("text-anchor", "middle").attr("dy", "0.35em") + .attr("font-size", 11).attr("fill", "#374151") + .text((d: any) => d.label.length > 16 ? d.label.slice(0, 15) + "…" : d.label) + + sim.on("tick", () => { + linkSel + .attr("x1", (d: any) => d.source.x).attr("y1", (d: any) => d.source.y) + .attr("x2", (d: any) => d.target.x).attr("y2", (d: any) => d.target.y) + nodeSel.attr("transform", (d: any) => `translate(${d.x},${d.y})`) + }) + + return () => { sim.stop() } + }, [nodes, links]) + + return +} +``` + +**Key rules:** +- Always copy nodes/links before passing to D3 (D3 mutates them with `x`/`y`/`vx`/`vy`) +- Use `clientWidth` for responsive width — read it after mount +- Truncate long labels inside the node rect; show full label in a tooltip on hover +- `alphaTarget(0)` in drag end lets the simulation cool down naturally + +--- + +## Radar / Spider Chart + +Use Recharts `RadarChart` for multi-dimensional scoring — SQL quality dimensions, +test coverage breakdown, feature completeness, etc. + +```tsx +import { + RadarChart, Radar, PolarGrid, PolarAngleAxis, + PolarRadiusAxis, ResponsiveContainer, Legend, Tooltip, +} from "recharts" + +// Data shape: one object per axis dimension +// [{ dimension: "Performance", score: 72, benchmark: 85 }, ...] +interface RadarDatum { dimension: string; score: number; benchmark?: number } + +export function QualityRadar({ data }: { data: RadarDatum[] }) { + return ( + + + + + + + {/* Optional benchmark / target overlay */} + + [`${v}`, ""]} + /> + + + + ) +} +``` + +**Usage notes:** +- `domain={[0, 100]}` — keep consistent so comparisons are meaningful +- Dashed benchmark line gives context without overwhelming the primary series +- Avoid more than 2 series; the chart becomes unreadable beyond that + +--- + +## Calendar Heatmap + +Nivo `ResponsiveCalendar` renders one square per day colored by value intensity. +Use for query frequency, pipeline run density, incident counts, or daily usage. + +```tsx +import { ResponsiveCalendar } from "@nivo/calendar" + +// Data shape: [{ day: "2026-03-15", value: 42 }, ...] +interface CalendarDatum { day: string; value: number } + +export function ActivityCalendar({ + data, + from, + to, +}: { + data: CalendarDatum[] + from: string // "YYYY-MM-DD" + to: string +}) { + return ( +
+ ( +
+ {day}: {value} +
+ )} + /> +
+ ) +} +``` + +**Notes:** +- Set `height` on the wrapper div, not on the Nivo component +- Use a single-hue sequential palette (light → dark) — avoid rainbow +- For sparse data, `emptyColor="#f8fafc"` (near-white) makes gaps non-distracting + +--- + +## Waterfall Chart + +Recharts doesn't have a native waterfall, but a stacked `Bar` with an invisible +spacer achieves the same result. + +```tsx +import { ComposedChart, Bar, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts" + +interface WaterfallItem { name: string; value: number; start: number } + +// Pre-compute running start positions (see Data Transformation Patterns) +function toWaterfallSeries(items: { name: string; value: number }[]): WaterfallItem[] { + let running = 0 + return items.map(item => { + const start = item.value >= 0 ? running : running + item.value + running += item.value + return { name: item.name, value: Math.abs(item.value), start, _raw: item.value } + }) +} + +export function WaterfallChart({ items }: { items: { name: string; value: number }[] }) { + const data = toWaterfallSeries(items) + + return ( + + + + + + { + if (name === "start") return null // hide spacer in tooltip + const raw = (props.payload as any)._raw + return [`${raw >= 0 ? "+" : ""}${raw.toLocaleString()}`, ""] + }} + /> + {/* Invisible spacer — lifts the visible bar to the right Y position */} + + {/* Visible bar — colored by positive/negative */} + + {data.map((entry, i) => ( + = 0 ? "#22c55e" : "#ef4444"} + /> + ))} + + + + ) +} +``` + +**Key rules:** +- The spacer bar (`fill="transparent"`) must use `isAnimationActive={false}` — animating it reveals the trick +- Hide the spacer from the tooltip by returning `null` in the formatter +- For a "total" bar at the end, set `start: 0` and `value: runningTotal` with a distinct color (e.g., slate) + +--- + +## Annotation Patterns + +Annotations are what make a chart tell a *story*. Add them after the core chart +is working. Recharts provides `ReferenceLine` and `ReferenceArea` for this. + +### Goal / threshold line + +```tsx +import { ReferenceLine } from "recharts" + + +``` + +### Time range highlight (incident, campaign, sprint) + +```tsx +import { ReferenceArea } from "recharts" + + +``` + +### Floating callout label on a reference line + +```tsx +const CalloutLabel = ({ + viewBox, + label, + color = "#1e293b", +}: { + viewBox?: { x: number; y: number } + label: string + color?: string +}) => { + if (!viewBox) return null + const { x, y } = viewBox + const w = label.length * 7 + 16 + return ( + + + + {label} + + + + ) +} + +// Usage +} +/> +``` + +### Anomaly dot highlight + +```tsx +// Custom dot renderer — normal points get a small circle; anomalies get a +// larger highlighted dot with a warning indicator above it + { + const { cx, cy, payload, key } = props + if (!payload?.isAnomaly) { + return + } + return ( + + + + + ▲ + + + ) + }} +/> +``` + +### Rules for annotations + +- **Limit to 3 per chart** — more annotations dilute all of them +- **Color by urgency**: amber (`#f59e0b`) = target/goal, red (`#ef4444`) = incident/risk, + indigo (`#6366f1`) = event/release, green (`#22c55e`) = achievement +- **Never overlap data** — use `position: "insideTopRight"` or `"insideTopLeft"` on + `ReferenceArea` labels; use the floating `CalloutLabel` above the line for `ReferenceLine` +- **Pair with tooltip** — the annotation names the event; the tooltip shows the value From 6be67dcd1c23ed9d673d30e8196bfad6a7bec26d Mon Sep 17 00:00:00 2001 From: Saurabh Arora Date: Sun, 15 Mar 2026 16:54:22 -0700 Subject: [PATCH 2/3] fix: rename skill to data-viz --- .opencode/skills/{ai-dataviz => data-viz}/SKILL.md | 2 +- .../{ai-dataviz => data-viz}/references/component-guide.md | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename .opencode/skills/{ai-dataviz => data-viz}/SKILL.md (99%) rename .opencode/skills/{ai-dataviz => data-viz}/references/component-guide.md (100%) diff --git a/.opencode/skills/ai-dataviz/SKILL.md b/.opencode/skills/data-viz/SKILL.md similarity index 99% rename from .opencode/skills/ai-dataviz/SKILL.md rename to .opencode/skills/data-viz/SKILL.md index 347e58fd94..cf4bc514be 100644 --- a/.opencode/skills/ai-dataviz/SKILL.md +++ b/.opencode/skills/data-viz/SKILL.md @@ -1,5 +1,5 @@ --- -name: ai-dataviz +name: data-viz description: > Build modern, AI-first data visualizations and data storytelling interfaces using code-based component libraries (shadcn/ui, Recharts, Tremor, Nivo, D3, diff --git a/.opencode/skills/ai-dataviz/references/component-guide.md b/.opencode/skills/data-viz/references/component-guide.md similarity index 100% rename from .opencode/skills/ai-dataviz/references/component-guide.md rename to .opencode/skills/data-viz/references/component-guide.md From 2c56313a949ac60e2b32b0287a78ae2022a3f152 Mon Sep 17 00:00:00 2001 From: Saurabh Arora Date: Sun, 15 Mar 2026 17:19:30 -0700 Subject: [PATCH 3/3] fix: reduce skills.md and references files by 60% --- .opencode/skills/data-viz/SKILL.md | 256 ++--- .../data-viz/references/component-guide.md | 990 ++++-------------- 2 files changed, 282 insertions(+), 964 deletions(-) diff --git a/.opencode/skills/data-viz/SKILL.md b/.opencode/skills/data-viz/SKILL.md index cf4bc514be..ad6a021691 100644 --- a/.opencode/skills/data-viz/SKILL.md +++ b/.opencode/skills/data-viz/SKILL.md @@ -1,217 +1,135 @@ --- name: data-viz description: > - Build modern, AI-first data visualizations and data storytelling interfaces - using code-based component libraries (shadcn/ui, Recharts, Tremor, Nivo, D3, - Victory, visx) instead of legacy BI tools. Use this skill whenever the user - asks to visualize data, build dashboards, create analytics views, chart - metrics, tell a data story, build a reporting interface, create KPI cards, - plot graphs, or explore a dataset — even if they mention PowerBI, Tableau, - Streamlit, Metabase, Looker, Grafana, or similar tools. Also trigger when the - user says "make a dashboard", "show me the data", "chart this", "visualize - trends", "build an analytics page", "data story", or anything involving - turning raw data into interactive visual interfaces. If the task involves - presenting data visually — this is the skill. Always prefer building a real, - interactive, code-based UI over exporting to or recommending a BI platform. + Build modern, interactive data visualizations and dashboards using code-based + component libraries (shadcn/ui, Recharts, Tremor, Nivo, D3, Victory, visx). + Use this skill whenever the user asks to visualize data, build dashboards, + create analytics views, chart metrics, tell a data story, build a reporting + interface, create KPI cards, plot graphs, or explore a dataset — even if they + mention PowerBI, Tableau, Streamlit, Metabase, Looker, Grafana, or similar + tools. Also trigger when the user says "make a dashboard", "show me the data", + "chart this", "visualize trends", "build an analytics page", "data story", or + anything involving turning raw data into interactive visual interfaces. If the + task involves presenting data visually — this is the skill. Always prefer + building a real, interactive, code-based UI over exporting to or recommending + a BI platform. --- # AI-First Data Visualization -## Table of Contents - -1. [Philosophy](#philosophy) -2. [Technology Stack](#technology-stack) -3. [Building a Data Visualization](#building-a-data-visualization) - - [Step 1: Understand the Data Story](#step-1-understand-the-data-story) - - [Step 2: Choose the Right Chart Type](#step-2-choose-the-right-chart-type) - - [Step 3: Build the Interface](#step-3-build-the-interface) - - [Step 4: Apply Design Principles](#step-4-apply-design-principles) - - [Step 5: Add Interactivity & Annotations](#step-5-add-interactivity--annotations) - - [Step 6: Tell the Story](#step-6-tell-the-story) -4. [Environment-Specific Guidance](#environment-specific-guidance) -5. [Anti-Patterns to Avoid](#anti-patterns-to-avoid) -6. [Quick Reference](#quick-reference) - ---- - ## Philosophy -AI agents can generate production-quality, interactive data interfaces in -minutes using modern component libraries — more customizable, no vendor lock-in, -embeddable anywhere. - -**Default behavior**: When the user hasn't specified a tool, build a code-first -interactive UI using the stack below. Don't ask — just deliver something great. - -**When the user explicitly names a tool** (PowerBI, Tableau, Looker, Metabase, -Grafana, Streamlit, etc.) — **use that tool**. They have a reason: org policy, -existing infra, stakeholder requirements. Help them do it well in their chosen -tool. Only suggest the code-first alternative if they ask for options or if -there's a clear technical blocker (e.g., the BI tool can't support what they -need). - ---- +Build production-quality interactive data interfaces with modern component libraries — no vendor lock-in, embeddable anywhere. When no tool is specified, build code-first. When the user explicitly names a BI tool, use it — only suggest code-first if they ask for options or hit a technical blocker. ## Technology Stack -Full library docs: `references/component-guide.md` +Full API patterns & code: `references/component-guide.md` ### Framework Priority -1. **React + Tailwind** — Default. Use when the environment supports JSX/TSX. -2. **HTML + CSS + Vanilla JS** — Fallback without React. Use D3 or Chart.js. -3. **Python (Plotly/Dash)** — Only for Python-only environments (Jupyter, scripts). +1. **React + Tailwind** — Default when JSX/TSX supported +2. **HTML + CSS + Vanilla JS** — Fallback (use D3 or Chart.js) +3. **Python (Plotly/Dash)** — Python-only environments only -### Component Libraries +### Library Selection -| Library | Best For | When to Use | -|---------|----------|-------------| -| **shadcn/ui charts** | General dashboards, most chart types | Default first choice | -| **Recharts** | Line, bar, area, composed, radar charts | Fine-grained control | -| **Tremor** | KPI cards, metric displays, full layouts | Complete analytics dashboards | -| **Nivo** | Heatmaps, treemaps, choropleth, calendar, Sankey | Advanced / exotic types | -| **visx** | Bespoke custom visualizations | D3-level control with React | -| **D3.js** | Force-directed graphs, DAGs, maps | Maximum flexibility | -| **Victory** | Animated charts | When animation quality matters most | +| Library | Best For | +|---------|----------| +| **shadcn/ui charts** | Default first choice — general dashboards, most chart types | +| **Recharts** | Line, bar, area, composed, radar — fine-grained control | +| **Tremor** | KPI cards, metric displays, full dashboard layouts | +| **Nivo** | Heatmaps, treemaps, choropleth, calendar, Sankey, funnel | +| **visx** | Bespoke custom viz — D3-level control with React | +| **D3.js** | Force-directed graphs, DAGs, maps — maximum flexibility | +| **Victory** | When animation quality matters most | **Supporting**: Tailwind CSS · Radix UI · Framer Motion · Lucide React · date-fns · Papaparse · lodash ---- - -## Building a Data Visualization +## Building a Visualization ### Step 1: Understand the Data Story -Before writing any code, identify: +Before code, identify: **What question does the data answer?** Who is the audience (exec → KPIs only, analyst → drill-down, public → narrative)? **What's the ONE key insight?** Design around it. -- **What question does the data answer?** ("How are costs trending?", "Where do users drop off?") -- **Who is the audience?** Executive → KPIs only. Analyst → drill-down + filters. Public → narrative flow. -- **What's the key insight?** Every great viz has ONE takeaway. Design around it. - -### Step 2: Choose the Right Chart Type +### Step 2: Choose Chart Type | Data Relationship | Chart Type | Library | -|-------------------|-----------|---------| -| Trend over time | Line chart, Area chart | shadcn/Recharts | -| Comparison across categories | Bar chart (horizontal for many) | shadcn/Recharts | -| Part of a whole | Donut, Treemap | shadcn/Nivo | -| Distribution | Histogram, Box plot, Violin | Nivo/visx | -| Correlation | Scatter, Bubble chart | Recharts/visx | +|---|---|---| +| Trend over time | Line, Area | shadcn/Recharts | +| Category comparison | Bar (horizontal if many) | shadcn/Recharts | +| Part of whole | Donut, Treemap | shadcn/Nivo | +| Distribution | Histogram, Box, Violin | Nivo/visx | +| Correlation | Scatter, Bubble | Recharts/visx | | Geographic | Choropleth, Dot map | Nivo/D3 | | Hierarchical | Treemap, Sunburst | Nivo | | Flow / Process | Sankey, Funnel | Nivo/D3 | | Single KPI | Metric card, Gauge, Sparkline | Tremor/shadcn | | Multi-metric overview | Dashboard grid of cards | Tremor + shadcn | | Ranking | Horizontal bar, Bar list | Tremor | -| Status / Progress | Tracker, Progress bar | Tremor | -| **Column / model lineage** | **Force-directed DAG** | **D3** | -| **Pipeline dependencies** | **Hierarchical tree, DAG** | **D3 / Nivo** | -| **Multi-dimensional quality** | **Radar / Spider chart** | **Recharts** | -| **Activity density over time** | **Calendar heatmap** | **Nivo** | -| **Incremental change breakdown** | **Waterfall chart** | **Recharts (custom)** | -| **Ranking shift over time** | **Bump chart** | **Recharts (custom)** | +| Column/model lineage | Force-directed DAG | D3 | +| Pipeline dependencies | Hierarchical tree, DAG | D3/Nivo | +| Multi-dimensional quality | Radar/Spider | Recharts | +| Activity density over time | Calendar heatmap | Nivo | +| Incremental change breakdown | Waterfall | Recharts (custom) | ### Step 3: Build the Interface -Start from this layout and remove what the data doesn't need: +Start from this layout — remove what the data doesn't need: ``` -┌─────────────────────────────────────────────────┐ -│ Header: Title + Description + Date Range │ -├─────────────────────────────────────────────────┤ -│ KPI Row: 3-5 metric cards with sparklines │ -├─────────────────────────────────────────────────┤ -│ Primary Visualization (largest chart) │ -├────────────────────┬────────────────────────────┤ -│ Secondary Chart │ Supporting Chart / Table │ -├────────────────────┴────────────────────────────┤ -│ Detail Table (sortable, filterable) │ -└─────────────────────────────────────────────────┘ +┌─────────────────────────────────────────┐ +│ Header: Title + Description + Date Range│ +├─────────────────────────────────────────┤ +│ KPI Row: 3-5 metric cards + sparklines │ +├─────────────────────────────────────────┤ +│ Primary Visualization (largest chart) │ +├──────────────────┬──────────────────────┤ +│ Secondary Chart │ Supporting Chart/Tbl │ +├──────────────────┴──────────────────────┤ +│ Detail Table (sortable, filterable) │ +└─────────────────────────────────────────┘ ``` -A single insight might just be one chart with a headline and annotation. -Scale complexity to match the data and audience. - -### Step 4: Apply Design Principles - -- **Data-ink ratio**: Remove chartjunk — unnecessary gridlines, redundant labels, decorative borders. -- **Color with purpose**: Encode meaning (red = bad, green = good, blue = neutral). Max 5–7 colors. Single-hue gradient for sequential data. -- **Typography hierarchy**: Title → subtitle (muted) → axis labels (small) → data labels. -- **Responsive**: `min-h-[VALUE]` on all charts. Grid stacks on mobile. Test at 375/768/1280px. -- **Whitespace**: Give charts room. A padded dashboard reads better than a dense wall. -- **Animation**: Entry transitions only. `duration-300` to `duration-500`. Never continuous. -- **Accessibility**: `aria-label` on charts, WCAG AA contrast, don't rely on color alone. - -### Step 5: Add Interactivity & Annotations - -**Interactivity priority order:** -1. Tooltips — exact values on hover (every chart) -2. Filtering — date range, category, segment -3. Sorting — click column headers in tables -4. Drill-down — click bar/slice to reveal detail -5. Cross-filtering — selection in one chart filters others -6. Export — CSV download of underlying data -7. Annotations — callouts that turn a chart into a story - -**Annotations** are the most underused technique in data storytelling. Every -chart with a clear insight should have at least one. Use them to mark: -- Inflection points and trend reversals -- Threshold crossings and goal lines (amber) -- External events: releases, incidents, campaigns (indigo / red) -- Anomalies that demand explanation (red) -- Achievements against target (green) - -Limit to **3 annotations per chart** — more and none stand out. Never overlap -data. Implementation patterns: `references/component-guide.md` → Annotation Patterns. - -### Step 6: Tell the Story +A single insight might just be one chart with a headline and annotation. Scale complexity to audience. -- **Headline states the insight**: "Revenue grew 23% QoQ, driven by enterprise deals" — not "Q3 Revenue Chart" -- **Annotate key moments**: Mark inflection points, anomalies, goal lines directly on the chart -- **Contextual comparisons**: vs. prior period, vs. target, vs. benchmark -- **Progressive disclosure**: Overview first — detail on demand. Don't front-load complexity. +### Step 4: Design Principles ---- +- **Data-ink ratio**: Remove chartjunk — unnecessary gridlines, redundant labels, decorative borders +- **Color with purpose**: Encode meaning (red=bad, green=good, blue=neutral). Max 5-7 colors. Single-hue gradient for sequential data +- **Typography hierarchy**: Title → subtitle (muted) → axis labels (small) → data labels +- **Responsive**: `min-h-[VALUE]` on all charts. Grid stacks on mobile +- **Animation**: Entry transitions only, `duration-300` to `duration-500`. Never continuous +- **Accessibility**: `aria-label` on charts, WCAG AA contrast, don't rely on color alone -## Environment-Specific Guidance +### Step 5: Interactivity & Annotations -| Environment | Approach | -|-------------|----------| -| **Claude Artifacts** | React (JSX), single file, default export. Available: `recharts`, `lodash`, `d3`, `lucide-react`, shadcn via `@/components/ui/*`, Tailwind. | -| **Claude Code / Terminal** | Full project: Vite + React + Tailwind. Add shadcn/ui + Recharts. Structure: `src/components/charts/`, `src/components/cards/`, `src/data/`. | -| **Python / Jupyter** | Plotly for charts, Plotly Dash for dashboards. Same design principles apply. | -| **Cursor / Bolt / other IDEs** | Match existing framework. Include install commands. Prefer shadcn/ui if already present, Tremor for dashboard-heavy apps. | +**Priority**: Tooltips (every chart) → Filtering → Sorting → Drill-down → Cross-filtering → Export → Annotations ---- +**Annotations** turn charts into stories. Mark: inflection points, threshold crossings (amber), external events (indigo/red), anomalies (red), achievements (green). **Limit 3 per chart.** Implementation: `references/component-guide.md` → Annotation Patterns. -## Anti-Patterns to Avoid +### Step 6: Tell the Story -- **Screenshot charts** — build interactive components, never static images -- **Defaulting to BI tools unprompted** — when no tool is specified, build code-first; don't suggest "just use Tableau" as a lazy out -- **Default matplotlib** — always customize if forced into Python -- **Rainbow palettes** — use deliberate, meaningful colors -- **3D charts** — almost never appropriate -- **Pie charts > 5 slices** — use horizontal bar instead -- **Unlabeled dual y-axes** — use two separate charts instead -- **Truncated bar axes** — always start at zero -- **Chartjunk** — no gratuitous gradients, shadows, or decoration +- **Headline states insight**: "Revenue grew 23% QoQ, driven by enterprise" — not "Q3 Revenue Chart" +- **Annotate key moments** directly on chart +- **Contextual comparisons**: vs. prior period, vs. target, vs. benchmark +- **Progressive disclosure**: Overview first — detail on demand ---- +## Environment-Specific Guidance -## Quick Reference - -| Pattern | Implementation | -|---------|---------------| -| KPI card + sparkline | Tremor `Card` + `SparkAreaChart` or shadcn `Card` + axisless Recharts `AreaChart` | -| Time series | shadcn `ChartContainer` + Recharts `LineChart`. Add date picker, granularity toggle, prior-period dashed overlay. | -| Categorical comparison | Recharts `BarChart`. Horizontal bars for 10+ categories. Sort toggle + threshold line. | -| Funnel | Nivo `Funnel`. Show step labels, conversion %, drop-off highlight. | -| Geographic | Nivo `Choropleth` + GeoJSON. Color legend + hover tooltip. | -| Table with inline visuals | Tremor/shadcn `Table` with embedded sparklines, progress bars, badges. Sortable + searchable. | -| Force-directed DAG | D3 force simulation + React SVG. Rectangles nodes, directed edges, arrowheads, drag. Color by node type. | -| Radar / spider | Recharts `RadarChart`. Current vs. benchmark (dashed). `domain={[0,100]}` for consistent axes. | -| Calendar heatmap | Nivo `ResponsiveCalendar`. Single-hue sequential palette. Wrap in fixed-height div. | -| Waterfall | Recharts `ComposedChart` with invisible spacer `Bar` + colored value `Bar`. Green = positive, red = negative. | -| Annotations | Recharts `ReferenceLine` (goal/event), `ReferenceArea` (time window), custom dot renderer (anomalies). | - -Full implementations: `references/component-guide.md` +| Environment | Approach | +|---|---| +| **Claude Artifacts** | React (JSX), single file, default export. Available: `recharts`, `lodash`, `d3`, `lucide-react`, shadcn via `@/components/ui/*`, Tailwind | +| **Claude Code / Terminal** | Vite + React + Tailwind. Add shadcn/ui + Recharts. Structure: `src/components/charts/`, `src/components/cards/`, `src/data/` | +| **Python / Jupyter** | Plotly for charts, Plotly Dash for dashboards | +| **Cursor / Bolt / other IDEs** | Match existing framework. Prefer shadcn/ui if present | + +## Anti-Patterns + +- Screenshot/static charts — build interactive components +- Defaulting to BI tools unprompted — build code-first when no tool specified +- Default matplotlib — always customize in Python +- Rainbow palettes — use deliberate, meaningful colors +- 3D charts — almost never appropriate +- Pie charts > 5 slices — use horizontal bar +- Unlabeled dual y-axes — use two separate charts +- Truncated bar axes — always start at zero diff --git a/.opencode/skills/data-viz/references/component-guide.md b/.opencode/skills/data-viz/references/component-guide.md index f7e747529d..8f792da07f 100644 --- a/.opencode/skills/data-viz/references/component-guide.md +++ b/.opencode/skills/data-viz/references/component-guide.md @@ -1,186 +1,60 @@ -# Component Library Reference Guide +# Component Library Reference -Detailed patterns, code snippets, and API references for building AI-first data -visualizations. Read the section relevant to your chosen library. +Non-obvious patterns, gotchas, and custom implementations. Standard library usage (basic bar/line/area/pie/scatter) is well-documented — this covers what agents get wrong or can't infer. ## Table of Contents -1. [shadcn/ui Charts](#shadcnui-charts) -2. [Recharts Patterns](#recharts-patterns) -3. [Tremor Dashboard Components](#tremor-dashboard-components) -4. [Nivo Advanced Charts](#nivo-advanced-charts) -5. [D3.js Custom Visualizations](#d3js-custom-visualizations) -6. [visx Low-Level Primitives](#visx-low-level-primitives) -7. [Layout Patterns](#layout-patterns) -8. [Color Systems](#color-systems) -9. [Data Transformation Patterns](#data-transformation-patterns) -10. [Force-Directed DAG](#force-directed-dag) -11. [Radar / Spider Chart](#radar--spider-chart) -12. [Calendar Heatmap](#calendar-heatmap) -13. [Waterfall Chart](#waterfall-chart) -14. [Annotation Patterns](#annotation-patterns) +1. [shadcn/ui Charts](#shadcnui-charts) — Config pattern & key rules +2. [Tremor Essentials](#tremor-essentials) — KPI cards, dashboard grid +3. [Nivo Gotchas](#nivo-gotchas) — Height wrapper, common props +4. [D3 + React Pattern](#d3--react-pattern) — Force-directed DAG +5. [Layout Patterns](#layout-patterns) — Dashboard grid, card component +6. [Color Systems](#color-systems) — Semantic, sequential, diverging, categorical +7. [Data Transformations](#data-transformations) — Recharts pivot, KPI aggregate, treemap, time bucketing +8. [Waterfall Chart](#waterfall-chart) — Custom Recharts pattern +9. [Radar / Spider Chart](#radar--spider-chart) — Key rules +10. [Calendar Heatmap](#calendar-heatmap) — Nivo setup +11. [Annotation Patterns](#annotation-patterns) — Goal lines, highlights, callouts, anomaly dots --- ## shadcn/ui Charts -shadcn/ui charts are built on top of Recharts and provide themed, accessible -chart components that integrate with the shadcn design system. - -### Core Concepts - -- **ChartContainer**: Wraps every chart. Accepts a `config` object and handles - theming, responsive sizing, and accessibility. -- **ChartConfig**: Defines labels, colors, and icons for each data series. - Decoupled from data — reusable across charts. -- **ChartTooltip / ChartTooltipContent**: Custom tooltip components styled to - match the design system. -- **ChartLegend / ChartLegendContent**: Interactive legend with toggle behavior. - -### Config Pattern +Built on Recharts with themed, accessible wrappers. **Unique config pattern — don't skip this.** ```tsx import { type ChartConfig } from "@/components/ui/chart" const chartConfig = { - revenue: { - label: "Revenue", - color: "hsl(var(--chart-1))", - }, - expenses: { - label: "Expenses", - color: "hsl(var(--chart-2))", - }, + revenue: { label: "Revenue", color: "hsl(var(--chart-1))" }, + expenses: { label: "Expenses", color: "hsl(var(--chart-2))" }, } satisfies ChartConfig ``` -### Bar Chart - -```tsx - - - - v.slice(0, 3)} - /> - } /> - } /> - - - - -``` - -### Area Chart with Gradient - -```tsx - - - - - - - - - - - } /> - - - -``` - -### Key Rules - -- Always set `min-h-[VALUE]` on ChartContainer (required for responsiveness) +**Key rules:** +- Always `min-h-[VALUE]` on `ChartContainer` (required for responsiveness) - Use `accessibilityLayer` prop on the main chart component -- Use CSS variables `var(--color-{key})` for colors, not hardcoded values -- Use `ChartTooltip` + `ChartTooltipContent` instead of Recharts' default +- Colors via CSS variables `var(--color-{key})`, never hardcoded +- Use `ChartTooltip` + `ChartTooltipContent`, not Recharts defaults +- Use `ChartLegend` + `ChartLegendContent` for interactive legends ---- - -## Recharts Patterns - -When using Recharts directly (without shadcn wrapper), follow these patterns. - -### Composed Chart (Multiple Series Types) +### Area Chart Gradient (common pattern) ```tsx - - - - - - - - - - - -``` - -### Pie / Donut Chart - -```tsx - - - 0 = donut - outerRadius={100} - paddingAngle={2} - dataKey="value" - > - {data.map((entry, i) => ( - - ))} - - - - - -``` - -### Scatter Plot - -```tsx - - - - - - - - - - + + + + + + + ``` --- -## Tremor Dashboard Components - -Tremor excels at complete dashboard layouts with KPI cards, metric displays, and -integrated charts. +## Tremor Essentials -### KPI Card +### KPI Card Pattern ```tsx import { Card, BadgeDelta, SparkAreaChart } from "@tremor/react" @@ -191,13 +65,8 @@ import { Card, BadgeDelta, SparkAreaChart } from "@tremor/react" +12.3%

$1.24M

- + ``` @@ -205,233 +74,111 @@ import { Card, BadgeDelta, SparkAreaChart } from "@tremor/react" ```tsx
- - - - + {kpis.map(kpi => )}
``` -### Area Chart with Tremor +### Tremor AreaChart / BarList ```tsx -import { AreaChart, Card, Title } from "@tremor/react" - - - Monthly Revenue - `$${(v / 1000).toFixed(0)}k`} - className="h-72 mt-4" - showAnimation - /> - -``` + `$${(v/1000).toFixed(0)}k`} + className="h-72 mt-4" showAnimation /> -### Bar List (Ranking) - -```tsx -import { BarList, Card, Title } from "@tremor/react" - - - Top Traffic Sources - - -``` - -### Tracker (Status Grid) - -```tsx -import { Tracker } from "@tremor/react" - - ({ - color: d.uptime > 99.9 ? "emerald" : d.uptime > 99 ? "yellow" : "red", - tooltip: `${d.date}: ${d.uptime}%`, - }))} - className="mt-2" -/> + ``` --- -## Nivo Advanced Charts - -Nivo is ideal for chart types that shadcn/Recharts don't cover well. +## Nivo Gotchas -### Heatmap +- **Always set `height` on the wrapper div**, not on the Nivo component: `
` +- Use `emptyColor="#f3f4f6"` for missing data cells +- For heatmaps: `colors={{ type: "sequential", scheme: "blues" }}` +- For treemaps: `identity="name"` + `value="value"` + `labelSkipSize={12}` +- For Sankey: `enableLinkGradient` + `linkBlendMode="multiply"` for polish +- For Choropleth: needs GeoJSON features, `projectionScale={150}`, `projectionTranslation={[0.5, 0.5]}` -```tsx -import { ResponsiveHeatMap } from "@nivo/heatmap" - -
- -
-``` - -### Treemap - -```tsx -import { ResponsiveTreeMap } from "@nivo/treemap" - -
- -
-``` - -### Sankey (Flow Diagram) +--- -```tsx -import { ResponsiveSankey } from "@nivo/sankey" - -
- -
-``` +## D3 + React Pattern: Force-Directed DAG -### Choropleth (Geographic) +Use for lineage graphs, dependency trees, pipeline DAGs. **D3 computes positions, React renders SVG.** ```tsx -import { ResponsiveChoropleth } from "@nivo/geo" - -
- -
-``` - ---- - -## D3.js Custom Visualizations +import { useEffect, useRef } from "react" +import * as d3 from "d3" -Use D3 when you need complete control. In React, use D3 for calculations and -React for rendering (the "D3 for math, React for DOM" pattern). +interface DagNode { id: string; label: string; type: "source" | "middle" | "output" } +interface DagLink { source: string; target: string } -### Force-Directed Graph +const NODE_COLORS: Record = { + source: { fill: "#dbeafe", stroke: "#3b82f6" }, + middle: { fill: "#f1f5f9", stroke: "#94a3b8" }, + output: { fill: "#dcfce7", stroke: "#22c55e" }, +} -```tsx -const simulation = d3.forceSimulation(nodes) - .force("link", d3.forceLink(links).id(d => d.id).distance(80)) - .force("charge", d3.forceManyBody().strength(-200)) - .force("center", d3.forceCenter(width / 2, height / 2)) - .force("collision", d3.forceCollide().radius(20)) +export function ForceDAG({ nodes, links }: { nodes: DagNode[]; links: DagLink[] }) { + const svgRef = useRef(null) -// Render with React SVG elements, update positions on simulation tick -``` + useEffect(() => { + if (!svgRef.current) return + const width = svgRef.current.clientWidth || 800, height = 500 + const svg = d3.select(svgRef.current).attr("height", height) + svg.selectAll("*").remove() -### Arc / Radial Layout + // Arrowhead marker + svg.append("defs").append("marker") + .attr("id", "dag-arrow").attr("viewBox", "0 -5 10 10") + .attr("refX", 22).attr("refY", 0) + .attr("markerWidth", 6).attr("markerHeight", 6).attr("orient", "auto") + .append("path").attr("d", "M0,-5L10,0L0,5").attr("fill", "#94a3b8") -```tsx -const arc = d3.arc() - .innerRadius(innerR) - .outerRadius(outerR) - .cornerRadius(3) - -const pie = d3.pie() - .sort(null) - .value(d => d.value) - .padAngle(0.02) -``` + // CRITICAL: Copy arrays — D3 mutates them with x/y/vx/vy + const nodesCopy = nodes.map(n => ({ ...n })) + const linksCopy = links.map(l => ({ ...l })) ---- + const sim = d3.forceSimulation(nodesCopy as any) + .force("link", d3.forceLink(linksCopy).id((d: any) => d.id).distance(140)) + .force("charge", d3.forceManyBody().strength(-350)) + .force("center", d3.forceCenter(width / 2, height / 2)) + .force("collision", d3.forceCollide().radius(50)) -## visx Low-Level Primitives + const linkSel = svg.append("g").selectAll("line") + .data(linksCopy).join("line") + .attr("stroke", "#cbd5e1").attr("stroke-width", 1.5) + .attr("marker-end", "url(#dag-arrow)") -visx (by Airbnb) provides D3-powered React components. Use when you want D3 -precision with React rendering. + const nodeSel = svg.append("g").selectAll("g") + .data(nodesCopy).join("g") + .call(d3.drag() + .on("start", (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y }) + .on("drag", (e, d) => { d.fx = e.x; d.fy = e.y }) + .on("end", (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null })) -### Key Modules + nodeSel.append("rect").attr("x", -54).attr("y", -18) + .attr("width", 108).attr("height", 36).attr("rx", 6) + .attr("fill", (d: any) => NODE_COLORS[d.type].fill) + .attr("stroke", (d: any) => NODE_COLORS[d.type].stroke).attr("stroke-width", 1.5) -- `@visx/shape` — Bars, lines, areas, arcs, pies -- `@visx/axis` — Configurable axes -- `@visx/scale` — D3 scales as hooks -- `@visx/tooltip` — Tooltip positioning -- `@visx/grid` — Background grids -- `@visx/gradient` — SVG gradients -- `@visx/responsive` — `ParentSize` wrapper for responsive charts + nodeSel.append("text").attr("text-anchor", "middle").attr("dy", "0.35em") + .attr("font-size", 11).attr("fill", "#374151") + .text((d: any) => d.label.length > 16 ? d.label.slice(0, 15) + "…" : d.label) -### Pattern + sim.on("tick", () => { + linkSel.attr("x1", (d: any) => d.source.x).attr("y1", (d: any) => d.source.y) + .attr("x2", (d: any) => d.target.x).attr("y2", (d: any) => d.target.y) + nodeSel.attr("transform", (d: any) => `translate(${d.x},${d.y})`) + }) + return () => { sim.stop() } + }, [nodes, links]) -```tsx -import { scaleBand, scaleLinear } from "@visx/scale" -import { Bar } from "@visx/shape" -import { AxisBottom, AxisLeft } from "@visx/axis" -import { Group } from "@visx/group" - -const xScale = scaleBand({ domain: data.map(getX), range: [0, width], padding: 0.3 }) -const yScale = scaleLinear({ domain: [0, max(data, getY)], range: [height, 0] }) - - - - {data.map((d) => ( - - ))} - - - - + return +} ``` +**Rules:** Always copy nodes/links before D3. Use `clientWidth` for responsive width. Truncate labels, show full on hover. `alphaTarget(0)` on drag end lets sim cool naturally. + --- ## Layout Patterns @@ -439,9 +186,7 @@ const yScale = scaleLinear({ domain: [0, max(data, getY)], range: [height, 0] }) ### Dashboard Grid (Tailwind) ```tsx -// Full dashboard layout
- {/* Header */}

Analytics

@@ -449,24 +194,18 @@ const yScale = scaleLinear({ domain: [0, max(data, getY)], range: [height, 0] })
- - {/* KPI Row */}
{kpis.map(kpi => )}
- - {/* Charts Row */}
{/* Primary chart */} {/* Secondary chart */}
- - {/* Detail Table */} - {/* DataTable component */} + {/* DataTable */}
``` -### Card Component (shadcn-style) +### shadcn-style Card ```tsx
@@ -487,508 +226,169 @@ const yScale = scaleLinear({ domain: [0, max(data, getY)], range: [height, 0] }) ## Color Systems -### Semantic Palette (recommended) - +**Semantic (default):** ```css -/* Use CSS variables for theming */ ---chart-1: 221.2 83.2% 53.3%; /* Primary blue */ ---chart-2: 142.1 76.2% 36.3%; /* Success green */ ---chart-3: 24.6 95% 53.1%; /* Warning orange */ ---chart-4: 346.8 77.2% 49.8%; /* Danger red */ ---chart-5: 262.1 83.3% 57.8%; /* Accent purple */ +--chart-1: 221.2 83.2% 53.3%; /* Blue */ +--chart-2: 142.1 76.2% 36.3%; /* Green */ +--chart-3: 24.6 95% 53.1%; /* Orange */ +--chart-4: 346.8 77.2% 49.8%; /* Red */ +--chart-5: 262.1 83.3% 57.8%; /* Purple */ ``` -### Sequential (for gradients / heatmaps) - -Single hue, vary lightness: `blue-100` → `blue-900` (Tailwind scale) or use -Nivo's built-in schemes: `blues`, `greens`, `oranges`, `purples`. - -### Diverging (for +/- values) +**Sequential** (heatmaps/gradients): Single hue light→dark. Tailwind `blue-100`→`blue-900` or Nivo schemes: `blues`, `greens`, `oranges`. -Red ↔ White ↔ Green or Red ↔ Grey ↔ Blue. Center on zero or mean. +**Diverging** (+/- values): Red ↔ White ↔ Green, or Red ↔ Grey ↔ Blue. Center on zero. -### Categorical (for distinct groups) - -Max 7 colors. Use Tailwind's `500` shade variants: -`blue-500`, `emerald-500`, `amber-500`, `rose-500`, `violet-500`, `cyan-500`, -`orange-500`. +**Categorical** (distinct groups): Max 7. Tailwind `500` shades: `blue`, `emerald`, `amber`, `rose`, `violet`, `cyan`, `orange`. --- -## Data Transformation Patterns +## Data Transformations -### Reshape for Recharts +### Pivot for Recharts -Recharts expects flat arrays where each object represents one data point with -all series as keys: +Recharts needs flat arrays with all series as keys per data point: ```ts -// Input: { date, category, value } rows -// Output: { date, category_A: value, category_B: value } -const pivoted = _.chain(rawData) - .groupBy("date") - .map((items, date) => ({ - date, - ..._.fromPairs(items.map(i => [i.category, i.value])) - })) +// { date, category, value } rows → { date, cat_A: val, cat_B: val } +const pivoted = _.chain(rawData).groupBy("date") + .map((items, date) => ({ date, ..._.fromPairs(items.map(i => [i.category, i.value])) })) .value() ``` -### Aggregate for KPI Cards +### KPI Aggregation ```ts const kpis = { - total: _.sumBy(data, "revenue"), - average: _.meanBy(data, "revenue"), - max: _.maxBy(data, "revenue"), - count: data.length, + total: _.sumBy(data, "revenue"), average: _.meanBy(data, "revenue"), + max: _.maxBy(data, "revenue"), count: data.length, growth: ((current - previous) / previous * 100).toFixed(1), } ``` -### Hierarchical for Treemaps +### Flat → Hierarchical (Treemaps) ```ts -// Flat → hierarchical -const tree = { - name: "root", - children: _.chain(data) - .groupBy("category") - .map((items, name) => ({ - name, - children: items.map(i => ({ name: i.label, value: i.amount })), - })) - .value(), -} +const tree = { name: "root", children: _.chain(data).groupBy("category") + .map((items, name) => ({ name, children: items.map(i => ({ name: i.label, value: i.amount })) })).value() } ``` ### Time Bucketing ```ts -import { format, startOfWeek, startOfMonth } from "date-fns" - -const weeklyData = _.chain(data) - .groupBy(d => format(startOfWeek(new Date(d.date)), "yyyy-MM-dd")) - .map((items, week) => ({ - week, - total: _.sumBy(items, "value"), - count: items.length, - })) - .value() -``` - -### Waterfall Pre-computation - -```ts -// Compute running start position for each waterfall bar -// Input: [{ name, value }] — positive = gain, negative = loss -const waterfallData = rawItems.reduce((acc, item, i) => { - const prev = acc[i - 1] - const start = prev ? prev.start + prev.value : 0 - return [...acc, { ...item, start, end: start + item.value }] -}, []) +import { format, startOfWeek } from "date-fns" +const weekly = _.chain(data).groupBy(d => format(startOfWeek(new Date(d.date)), "yyyy-MM-dd")) + .map((items, week) => ({ week, total: _.sumBy(items, "value"), count: items.length })).value() ``` --- -## Force-Directed DAG +## Waterfall Chart -Use D3's force simulation to render lineage graphs, dependency trees, and -pipeline DAGs. The pattern: D3 computes positions, React renders SVG elements. +Recharts has no native waterfall. Use stacked Bar with invisible spacer: ```tsx -import { useEffect, useRef } from "react" -import * as d3 from "d3" - -interface DagNode { id: string; label: string; type: "source" | "middle" | "output" } -interface DagLink { source: string; target: string; label?: string } - -const NODE_COLORS: Record = { - source: { fill: "#dbeafe", stroke: "#3b82f6" }, - middle: { fill: "#f1f5f9", stroke: "#94a3b8" }, - output: { fill: "#dcfce7", stroke: "#22c55e" }, +function toWaterfallSeries(items: { name: string; value: number }[]) { + let running = 0 + return items.map(item => { + const start = item.value >= 0 ? running : running + item.value + running += item.value + return { name: item.name, value: Math.abs(item.value), start, _raw: item.value } + }) } -export function ForceDAG({ nodes, links }: { nodes: DagNode[]; links: DagLink[] }) { - const svgRef = useRef(null) - - useEffect(() => { - if (!svgRef.current) return - const width = svgRef.current.clientWidth || 800 - const height = 500 - - const svg = d3.select(svgRef.current).attr("height", height) - svg.selectAll("*").remove() - - // Arrowhead marker - svg.append("defs").append("marker") - .attr("id", "dag-arrow") - .attr("viewBox", "0 -5 10 10") - .attr("refX", 22).attr("refY", 0) - .attr("markerWidth", 6).attr("markerHeight", 6) - .attr("orient", "auto") - .append("path").attr("d", "M0,-5L10,0L0,5").attr("fill", "#94a3b8") - - const nodesCopy = nodes.map(n => ({ ...n })) - const linksCopy = links.map(l => ({ ...l })) - - const sim = d3.forceSimulation(nodesCopy as any) - .force("link", d3.forceLink(linksCopy).id((d: any) => d.id).distance(140)) - .force("charge", d3.forceManyBody().strength(-350)) - .force("center", d3.forceCenter(width / 2, height / 2)) - .force("collision", d3.forceCollide().radius(50)) - - const linkSel = svg.append("g").selectAll("line") - .data(linksCopy).join("line") - .attr("stroke", "#cbd5e1").attr("stroke-width", 1.5) - .attr("marker-end", "url(#dag-arrow)") - - const nodeSel = svg.append("g").selectAll("g") - .data(nodesCopy).join("g") - .call( - d3.drag() - .on("start", (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y }) - .on("drag", (e, d) => { d.fx = e.x; d.fy = e.y }) - .on("end", (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null }) - ) - - nodeSel.append("rect") - .attr("x", -54).attr("y", -18) - .attr("width", 108).attr("height", 36).attr("rx", 6) - .attr("fill", (d: any) => NODE_COLORS[d.type].fill) - .attr("stroke", (d: any) => NODE_COLORS[d.type].stroke) - .attr("stroke-width", 1.5) - - nodeSel.append("text") - .attr("text-anchor", "middle").attr("dy", "0.35em") - .attr("font-size", 11).attr("fill", "#374151") - .text((d: any) => d.label.length > 16 ? d.label.slice(0, 15) + "…" : d.label) - - sim.on("tick", () => { - linkSel - .attr("x1", (d: any) => d.source.x).attr("y1", (d: any) => d.source.y) - .attr("x2", (d: any) => d.target.x).attr("y2", (d: any) => d.target.y) - nodeSel.attr("transform", (d: any) => `translate(${d.x},${d.y})`) - }) - - return () => { sim.stop() } - }, [nodes, links]) - - return -} +// In ComposedChart: + + + {data.map((e, i) => = 0 ? "#22c55e" : "#ef4444"} />)} + ``` -**Key rules:** -- Always copy nodes/links before passing to D3 (D3 mutates them with `x`/`y`/`vx`/`vy`) -- Use `clientWidth` for responsive width — read it after mount -- Truncate long labels inside the node rect; show full label in a tooltip on hover -- `alphaTarget(0)` in drag end lets the simulation cool down naturally +**Critical:** Spacer bar must have `isAnimationActive={false}` (animating it reveals the trick). Hide spacer from tooltip by returning `null` in formatter. For "total" bar: `start: 0`, `value: runningTotal`, distinct color (slate). --- ## Radar / Spider Chart -Use Recharts `RadarChart` for multi-dimensional scoring — SQL quality dimensions, -test coverage breakdown, feature completeness, etc. - ```tsx -import { - RadarChart, Radar, PolarGrid, PolarAngleAxis, - PolarRadiusAxis, ResponsiveContainer, Legend, Tooltip, -} from "recharts" - -// Data shape: one object per axis dimension -// [{ dimension: "Performance", score: 72, benchmark: 85 }, ...] -interface RadarDatum { dimension: string; score: number; benchmark?: number } - -export function QualityRadar({ data }: { data: RadarDatum[] }) { - return ( - - - - - - - {/* Optional benchmark / target overlay */} - - [`${v}`, ""]} - /> - - - - ) -} + + + + + + + + ``` -**Usage notes:** -- `domain={[0, 100]}` — keep consistent so comparisons are meaningful -- Dashed benchmark line gives context without overwhelming the primary series -- Avoid more than 2 series; the chart becomes unreadable beyond that +**Rules:** `domain={[0, 100]}` for consistent comparison. Dashed benchmark gives context. Max 2 series. --- ## Calendar Heatmap -Nivo `ResponsiveCalendar` renders one square per day colored by value intensity. -Use for query frequency, pipeline run density, incident counts, or daily usage. - ```tsx import { ResponsiveCalendar } from "@nivo/calendar" -// Data shape: [{ day: "2026-03-15", value: 42 }, ...] -interface CalendarDatum { day: string; value: number } - -export function ActivityCalendar({ - data, - from, - to, -}: { - data: CalendarDatum[] - from: string // "YYYY-MM-DD" - to: string -}) { - return ( -
- ( -
- {day}: {value} -
- )} - /> -
- ) -} -``` - -**Notes:** -- Set `height` on the wrapper div, not on the Nivo component -- Use a single-hue sequential palette (light → dark) — avoid rainbow -- For sparse data, `emptyColor="#f8fafc"` (near-white) makes gaps non-distracting - ---- - -## Waterfall Chart - -Recharts doesn't have a native waterfall, but a stacked `Bar` with an invisible -spacer achieves the same result. - -```tsx -import { ComposedChart, Bar, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts" - -interface WaterfallItem { name: string; value: number; start: number } - -// Pre-compute running start positions (see Data Transformation Patterns) -function toWaterfallSeries(items: { name: string; value: number }[]): WaterfallItem[] { - let running = 0 - return items.map(item => { - const start = item.value >= 0 ? running : running + item.value - running += item.value - return { name: item.name, value: Math.abs(item.value), start, _raw: item.value } - }) -} - -export function WaterfallChart({ items }: { items: { name: string; value: number }[] }) { - const data = toWaterfallSeries(items) - - return ( - - - - - - { - if (name === "start") return null // hide spacer in tooltip - const raw = (props.payload as any)._raw - return [`${raw >= 0 ? "+" : ""}${raw.toLocaleString()}`, ""] - }} - /> - {/* Invisible spacer — lifts the visible bar to the right Y position */} - - {/* Visible bar — colored by positive/negative */} - - {data.map((entry, i) => ( - = 0 ? "#22c55e" : "#ef4444"} - /> - ))} - - - - ) -} +
+ +
``` -**Key rules:** -- The spacer bar (`fill="transparent"`) must use `isAnimationActive={false}` — animating it reveals the trick -- Hide the spacer from the tooltip by returning `null` in the formatter -- For a "total" bar at the end, set `start: 0` and `value: runningTotal` with a distinct color (e.g., slate) +**Rules:** Height on wrapper div, not component. Single-hue sequential palette. `emptyColor` near-white for sparse data. --- ## Annotation Patterns -Annotations are what make a chart tell a *story*. Add them after the core chart -is working. Recharts provides `ReferenceLine` and `ReferenceArea` for this. +Annotations turn charts into stories. **Limit 3 per chart.** + +**Color by type:** amber `#f59e0b` = target/goal, red `#ef4444` = incident/risk, indigo `#6366f1` = event/release, green `#22c55e` = achievement. -### Goal / threshold line +### Goal/Threshold Line ```tsx -import { ReferenceLine } from "recharts" - - + ``` -### Time range highlight (incident, campaign, sprint) +### Time Range Highlight ```tsx -import { ReferenceArea } from "recharts" - - + ``` -### Floating callout label on a reference line +### Floating Callout Label ```tsx -const CalloutLabel = ({ - viewBox, - label, - color = "#1e293b", -}: { - viewBox?: { x: number; y: number } - label: string - color?: string -}) => { +const CalloutLabel = ({ viewBox, label, color = "#1e293b" }: { viewBox?: { x: number; y: number }; label: string; color?: string }) => { if (!viewBox) return null - const { x, y } = viewBox - const w = label.length * 7 + 16 - return ( - - - - {label} - - - - ) + const { x, y } = viewBox, w = label.length * 7 + 16 + return ( + + {label} + + ) } - -// Usage -} -/> +// Usage: } /> ``` -### Anomaly dot highlight +### Anomaly Dot Highlight ```tsx -// Custom dot renderer — normal points get a small circle; anomalies get a -// larger highlighted dot with a warning indicator above it - { - const { cx, cy, payload, key } = props - if (!payload?.isAnomaly) { - return - } - return ( - - - - - ▲ - - - ) - }} -/> -``` - -### Rules for annotations - -- **Limit to 3 per chart** — more annotations dilute all of them -- **Color by urgency**: amber (`#f59e0b`) = target/goal, red (`#ef4444`) = incident/risk, - indigo (`#6366f1`) = event/release, green (`#22c55e`) = achievement -- **Never overlap data** — use `position: "insideTopRight"` or `"insideTopLeft"` on - `ReferenceArea` labels; use the floating `CalloutLabel` above the line for `ReferenceLine` -- **Pair with tooltip** — the annotation names the event; the tooltip shows the value + { + const { cx, cy, payload, key } = props + if (!payload?.isAnomaly) return + return ( + + + + ) +}} /> +``` + +**Rules:** Never overlap data. Use `position: "insideTopRight"/"insideTopLeft"` on labels. Pair annotations with tooltips — annotation names the event, tooltip shows the value.