Skip to content

Commit 1191e7d

Browse files
fricobenclaude
andauthored
feat: add atomic UI architecture for chart components (#12)
Create reusable chart primitives in components/ui/charts/ that wrap Recharts with shared styling defaults. Refactor dashboard components to use these primitives, reducing code duplication. New UI primitives: - LineChart, BarChart, AreaChart, PieChart, StackedAreaChart - ChartCard (wrapper with title/selector slot) - ChartTooltip (consistent tooltip styling) - Shared constants (colors, axis/grid styles) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1fdc6f6 commit 1191e7d

16 files changed

Lines changed: 817 additions & 396 deletions

CLAUDE.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,72 @@ krakow/
170170

171171
---
172172

173+
## Atomic UI Architecture
174+
175+
### Component Organization
176+
177+
The project follows an atomic design pattern for chart components:
178+
179+
- **`components/ui/`** - Pure UI primitives (no data fetching, receive data via props)
180+
- **`components/[domain]/`** - Data-connected components that use UI primitives
181+
182+
### Chart UI Primitives
183+
184+
Reusable chart primitives are located in `src/components/ui/charts/`:
185+
186+
| Component | Description | Wraps |
187+
|-----------|-------------|-------|
188+
| `LineChart` | Multi-line time series charts | Recharts LineChart |
189+
| `BarChart` | Horizontal/vertical bar charts | Recharts BarChart |
190+
| `AreaChart` | Single area chart with gradient | Recharts AreaChart |
191+
| `PieChart` | Donut/pie charts | Recharts PieChart |
192+
| `StackedAreaChart` | Stacked percentage area charts | Recharts AreaChart |
193+
| `ChartTooltip` | Consistent tooltip styling | - |
194+
| `ChartCard` | Card wrapper with title slot | - |
195+
196+
### Shared Constants
197+
198+
`src/components/ui/charts/constants.ts` exports:
199+
- `MODEL_COLORS` - Color palette for model charts
200+
- `TOOL_COLORS` - Colors for each AI tool
201+
- `TOOL_LABELS` - Display names for tools
202+
- `AXIS_STYLE` - Consistent axis styling
203+
- `GRID_STYLE` - Grid line styling
204+
- `TOOLTIP_STYLE` - Tooltip container styling
205+
- `getColorFromString()` - Generate consistent color from string
206+
207+
### Usage Pattern
208+
209+
```tsx
210+
// Import from UI primitives
211+
import { LineChart, ChartCard, TOOL_COLORS } from "@/components/ui/charts";
212+
213+
// Data component handles fetching/transformation
214+
export function UsageByToolChart({ dailyActivity, unit }) {
215+
// Transform data...
216+
const lines = tools.map(tool => ({
217+
dataKey: tool,
218+
color: TOOL_COLORS[tool],
219+
label: TOOL_LABELS[tool],
220+
}));
221+
222+
return (
223+
<ChartCard title="Usage by IDE" rightSlot={<TimeframeSelector />}>
224+
<LineChart data={chartData} lines={lines} xAxisKey="date" />
225+
</ChartCard>
226+
);
227+
}
228+
```
229+
230+
### Adding New Charts
231+
232+
**DO NOT** create new chart components directly in `dashboard/`. Instead:
233+
1. Check if an existing UI primitive fits your need
234+
2. If not, create a new primitive in `components/ui/charts/`
235+
3. Then create the data-connected component in `components/dashboard/`
236+
237+
---
238+
173239
## Key Components
174240

175241
### Web Application (`src/`)
Lines changed: 48 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
"use client";
22

33
import { useState } from "react";
4-
import {
5-
AreaChart,
6-
Area,
7-
XAxis,
8-
YAxis,
9-
CartesianGrid,
10-
Tooltip,
11-
ResponsiveContainer,
12-
} from "recharts";
4+
import { AreaChart, ChartCard, ChartTooltip } from "@/components/ui/charts";
135
import { formatCurrency } from "@/lib/utils";
146

157
interface DailyActivity {
@@ -60,6 +52,12 @@ function filterByTimeRange(
6052
return data.filter((d) => d.date >= cutoffStr);
6153
}
6254

55+
// Format date for display
56+
const formatDate = (dateStr: string) => {
57+
const date = new Date(dateStr);
58+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
59+
};
60+
6361
export function CostTrendChart({ dailyActivity }: CostTrendChartProps) {
6462
const [timeRange, setTimeRange] = useState<TimeRange>("30D");
6563

@@ -81,12 +79,6 @@ export function CostTrendChart({ dailyActivity }: CostTrendChartProps) {
8179
const totalCost = filteredData.reduce((sum, d) => sum + d.cost, 0);
8280
const avgCost = totalCost / filteredData.length;
8381

84-
// Format date for x-axis
85-
const formatDate = (dateStr: string) => {
86-
const date = new Date(dateStr);
87-
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
88-
};
89-
9082
// Custom tooltip
9183
const CustomTooltip = ({
9284
active,
@@ -99,75 +91,52 @@ export function CostTrendChart({ dailyActivity }: CostTrendChartProps) {
9991
}) => {
10092
if (!active || !payload || !payload.length || !label) return null;
10193
return (
102-
<div className="bg-white border border-[#232323] rounded-lg p-3 shadow-[2px_2px_0_#232323]">
103-
<p className="font-bold text-sm mb-1">{formatDate(label)}</p>
104-
<p className="text-sm text-[#232323]/70">{formatCurrency(payload[0].value)}</p>
105-
</div>
94+
<ChartTooltip
95+
title={formatDate(label)}
96+
value={formatCurrency(payload[0].value)}
97+
/>
10698
);
10799
};
108100

109101
const timeRangeOptions: TimeRange[] = ["7D", "30D", "90D"];
110102

111-
return (
112-
<div className="card">
113-
<div className="flex items-center justify-between mb-4">
114-
<div>
115-
<h3 className="font-bold">Cost Trend</h3>
116-
<p className="text-xs text-[#232323]/50 mt-0.5">
117-
Total: {formatCurrency(totalCost)} | Avg: {formatCurrency(avgCost)}/day
118-
</p>
119-
</div>
120-
<div className="flex rounded-lg border border-[#232323]/20 overflow-hidden">
121-
{timeRangeOptions.map((range) => (
122-
<button
123-
key={range}
124-
onClick={() => setTimeRange(range)}
125-
className={`px-2.5 py-1 text-xs font-medium transition-colors ${
126-
timeRange === range
127-
? "bg-[#232323] text-white"
128-
: "bg-white text-[#232323]/70 hover:bg-[#232323]/5"
129-
}`}
130-
>
131-
{range}
132-
</button>
133-
))}
134-
</div>
135-
</div>
136-
<div className="h-[250px]">
137-
<ResponsiveContainer width="100%" height="100%">
138-
<AreaChart data={filteredData} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
139-
<defs>
140-
<linearGradient id="costGradient" x1="0" y1="0" x2="0" y2="1">
141-
<stop offset="5%" stopColor="#D63384" stopOpacity={0.3} />
142-
<stop offset="95%" stopColor="#D63384" stopOpacity={0} />
143-
</linearGradient>
144-
</defs>
145-
<CartesianGrid strokeDasharray="3 3" stroke="#232323" strokeOpacity={0.1} />
146-
<XAxis
147-
dataKey="date"
148-
tickFormatter={formatDate}
149-
tick={{ fontSize: 11, fill: "#232323", fillOpacity: 0.5 }}
150-
tickLine={false}
151-
axisLine={{ stroke: "#232323", strokeOpacity: 0.1 }}
152-
/>
153-
<YAxis
154-
tickFormatter={(v) => `$${v}`}
155-
tick={{ fontSize: 11, fill: "#232323", fillOpacity: 0.5 }}
156-
tickLine={false}
157-
axisLine={{ stroke: "#232323", strokeOpacity: 0.1 }}
158-
width={50}
159-
/>
160-
<Tooltip content={<CustomTooltip />} />
161-
<Area
162-
type="monotone"
163-
dataKey="cost"
164-
stroke="#D63384"
165-
strokeWidth={2}
166-
fill="url(#costGradient)"
167-
/>
168-
</AreaChart>
169-
</ResponsiveContainer>
170-
</div>
103+
const TimeRangeSelector = (
104+
<div className="flex rounded-lg border border-[#232323]/20 overflow-hidden">
105+
{timeRangeOptions.map((range) => (
106+
<button
107+
key={range}
108+
onClick={() => setTimeRange(range)}
109+
className={`px-2.5 py-1 text-xs font-medium transition-colors ${
110+
timeRange === range
111+
? "bg-[#232323] text-white"
112+
: "bg-white text-[#232323]/70 hover:bg-[#232323]/5"
113+
}`}
114+
>
115+
{range}
116+
</button>
117+
))}
171118
</div>
172119
);
120+
121+
return (
122+
<ChartCard
123+
title="Cost Trend"
124+
subtitle={`Total: ${formatCurrency(totalCost)} | Avg: ${formatCurrency(avgCost)}/day`}
125+
rightSlot={TimeRangeSelector}
126+
height={250}
127+
>
128+
<AreaChart
129+
data={filteredData}
130+
dataKey="cost"
131+
xAxisKey="date"
132+
color="#D63384"
133+
gradient
134+
gradientId="costTrendGradient"
135+
xAxisFormatter={formatDate}
136+
yAxisFormatter={(v) => `$${v}`}
137+
tooltipContent={<CustomTooltip />}
138+
height={250}
139+
/>
140+
</ChartCard>
141+
);
173142
}

0 commit comments

Comments
 (0)