Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
24 KiB
Recharts Migration Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the design system's hand-rolled SVG chart components with a single ThemedChart wrapper around Recharts, using Recharts-native data format.
Architecture: Add Recharts as a DS dependency. Create ThemedChart component that renders ResponsiveContainer + ComposedChart with pre-themed grid/axes/tooltip. Consumers compose Recharts elements (<Line>, <Area>, <Bar>, <ReferenceLine>) as children. Delete old LineChart/, AreaChart/, BarChart/, _chart-utils.ts. Migrate the server UI's AgentInstance.tsx to the new API.
Tech Stack: React 19, Recharts, CSS Modules, Vitest
File Structure
Design System (C:\Users\Hendrik\Documents\projects\design-system):
| Action | Path | Purpose |
|---|---|---|
| Create | src/design-system/composites/ThemedChart/ThemedChart.tsx |
Wrapper component: ResponsiveContainer + ComposedChart + themed axes/grid/tooltip |
| Create | src/design-system/composites/ThemedChart/ChartTooltip.tsx |
Custom tooltip with timestamp header + series values |
| Create | src/design-system/composites/ThemedChart/ChartTooltip.module.css |
Tooltip styles using DS tokens |
| Create | src/design-system/composites/ThemedChart/ThemedChart.test.tsx |
Render tests for ThemedChart |
| Modify | src/design-system/utils/rechartsTheme.ts |
Move CHART_COLORS definition here (was in _chart-utils.ts) |
| Modify | src/design-system/composites/index.ts |
Remove old chart exports, add ThemedChart + Recharts re-exports |
| Modify | COMPONENT_GUIDE.md |
Update charting strategy section |
| Modify | package.json |
Add recharts dependency, bump version |
| Delete | src/design-system/composites/LineChart/ |
Old hand-rolled SVG line chart |
| Delete | src/design-system/composites/AreaChart/ |
Old hand-rolled SVG area chart |
| Delete | src/design-system/composites/BarChart/ |
Old hand-rolled SVG bar chart |
| Delete | src/design-system/composites/_chart-utils.ts |
Old chart utilities |
Server UI (C:\Users\Hendrik\Documents\projects\cameleer3-server):
| Action | Path | Purpose |
|---|---|---|
| Modify | ui/src/pages/AgentInstance/AgentInstance.tsx |
Migrate 6 charts to ThemedChart + Recharts children |
| Modify | ui/package.json |
Update @cameleer/design-system to new version |
Task 1: Add Recharts Dependency and Move CHART_COLORS
Files:
-
Modify:
package.json -
Modify:
src/design-system/utils/rechartsTheme.ts -
Step 1: Install recharts
cd C:\Users\Hendrik\Documents\projects\design-system
npm install recharts
- Step 2: Move CHART_COLORS into rechartsTheme.ts
Replace the entire file src/design-system/utils/rechartsTheme.ts with:
export const CHART_COLORS = [
'var(--chart-1)',
'var(--chart-2)',
'var(--chart-3)',
'var(--chart-4)',
'var(--chart-5)',
'var(--chart-6)',
'var(--chart-7)',
'var(--chart-8)',
]
/**
* Pre-configured Recharts prop objects that match the design system's
* chart styling. Used internally by ThemedChart and available for
* consumers composing Recharts directly.
*/
export const rechartsTheme = {
colors: CHART_COLORS,
cartesianGrid: {
stroke: 'var(--border-subtle)',
strokeDasharray: '3 3',
vertical: false,
},
xAxis: {
tick: { fontSize: 9, fontFamily: 'var(--font-mono)', fill: 'var(--text-faint)' },
axisLine: { stroke: 'var(--border-subtle)' },
tickLine: false as const,
},
yAxis: {
tick: { fontSize: 9, fontFamily: 'var(--font-mono)', fill: 'var(--text-faint)' },
axisLine: false as const,
tickLine: false as const,
},
tooltip: {
contentStyle: {
background: 'var(--bg-surface)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-sm)',
boxShadow: 'var(--shadow-md)',
fontSize: 11,
padding: '6px 10px',
},
labelStyle: {
color: 'var(--text-muted)',
fontSize: 11,
marginBottom: 4,
},
itemStyle: {
color: 'var(--text-primary)',
fontFamily: 'var(--font-mono)',
fontSize: 11,
padding: 0,
},
cursor: { stroke: 'var(--text-faint)' },
},
legend: {
wrapperStyle: {
fontSize: 11,
color: 'var(--text-secondary)',
},
},
} as const
- Step 3: Verify build compiles
npm run build:lib
Expected: Build succeeds. The old chart components still import CHART_COLORS from _chart-utils.ts which still exists — they'll be deleted in Task 4.
- Step 4: Commit
git add package.json package-lock.json src/design-system/utils/rechartsTheme.ts
git commit -m "chore: add recharts dependency, move CHART_COLORS to rechartsTheme"
Task 2: Create ChartTooltip Component
Files:
-
Create:
src/design-system/composites/ThemedChart/ChartTooltip.tsx -
Create:
src/design-system/composites/ThemedChart/ChartTooltip.module.css -
Step 1: Create tooltip CSS
Create src/design-system/composites/ThemedChart/ChartTooltip.module.css:
.tooltip {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-md);
padding: 6px 10px;
font-size: 12px;
}
.time {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-muted);
margin-bottom: 4px;
padding-bottom: 3px;
border-bottom: 1px solid var(--border-subtle);
}
.row {
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 2px;
}
.row:last-child {
margin-bottom: 0;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.label {
color: var(--text-muted);
font-size: 11px;
}
.value {
font-family: var(--font-mono);
font-weight: 600;
font-size: 11px;
color: var(--text-primary);
}
- Step 2: Create ChartTooltip component
Create src/design-system/composites/ThemedChart/ChartTooltip.tsx:
import type { TooltipProps } from 'recharts'
import styles from './ChartTooltip.module.css'
function formatValue(val: number): string {
if (val >= 1_000_000) return `${(val / 1_000_000).toFixed(1)}M`
if (val >= 1000) return `${(val / 1000).toFixed(1)}k`
if (Number.isInteger(val)) return String(val)
return val.toFixed(1)
}
function formatTimestamp(val: unknown): string | null {
if (val == null) return null
const str = String(val)
const ms = typeof val === 'number' && val > 1e12 ? val
: typeof val === 'number' && val > 1e9 ? val * 1000
: Date.parse(str)
if (isNaN(ms)) return str
return new Date(ms).toLocaleString([], {
month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit', second: '2-digit',
})
}
export function ChartTooltip({ active, payload, label }: TooltipProps<number, string>) {
if (!active || !payload?.length) return null
const timeLabel = formatTimestamp(label)
return (
<div className={styles.tooltip}>
{timeLabel && <div className={styles.time}>{timeLabel}</div>}
{payload.map((entry) => (
<div key={entry.dataKey as string} className={styles.row}>
<span className={styles.dot} style={{ background: entry.color }} />
<span className={styles.label}>{entry.name}:</span>
<span className={styles.value}>{formatValue(entry.value as number)}</span>
</div>
))}
</div>
)
}
- Step 3: Commit
git add src/design-system/composites/ThemedChart/
git commit -m "feat: add ChartTooltip component for ThemedChart"
Task 3: Create ThemedChart Component
Files:
-
Create:
src/design-system/composites/ThemedChart/ThemedChart.tsx -
Create:
src/design-system/composites/ThemedChart/ThemedChart.test.tsx -
Step 1: Create ThemedChart component
Create src/design-system/composites/ThemedChart/ThemedChart.tsx:
import {
ResponsiveContainer,
ComposedChart,
CartesianGrid,
XAxis,
YAxis,
Tooltip,
} from 'recharts'
import { rechartsTheme } from '../../utils/rechartsTheme'
import { ChartTooltip } from './ChartTooltip'
interface ThemedChartProps {
data: Record<string, any>[]
height?: number
xDataKey?: string
xType?: 'number' | 'category'
xTickFormatter?: (value: any) => string
yTickFormatter?: (value: any) => string
yLabel?: string
children: React.ReactNode
className?: string
}
export function ThemedChart({
data,
height = 200,
xDataKey = 'time',
xType = 'category',
xTickFormatter,
yTickFormatter,
yLabel,
children,
className,
}: ThemedChartProps) {
if (!data.length) {
return null
}
return (
<div className={className} style={{ width: '100%', height }}>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={data} margin={{ top: 4, right: 8, bottom: 0, left: 0 }}>
<CartesianGrid {...rechartsTheme.cartesianGrid} />
<XAxis
dataKey={xDataKey}
type={xType}
{...rechartsTheme.xAxis}
tickFormatter={xTickFormatter}
/>
<YAxis
{...rechartsTheme.yAxis}
tickFormatter={yTickFormatter}
label={yLabel ? {
value: yLabel,
angle: -90,
position: 'insideLeft',
style: { fontSize: 11, fill: 'var(--text-muted)' },
} : undefined}
/>
<Tooltip content={<ChartTooltip />} cursor={rechartsTheme.tooltip.cursor} />
{children}
</ComposedChart>
</ResponsiveContainer>
</div>
)
}
- Step 2: Write test
Create src/design-system/composites/ThemedChart/ThemedChart.test.tsx:
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { ThemedChart } from './ThemedChart'
import { Line } from 'recharts'
// Recharts uses ResizeObserver internally
class ResizeObserverMock {
observe() {}
unobserve() {}
disconnect() {}
}
globalThis.ResizeObserver = ResizeObserverMock as any
describe('ThemedChart', () => {
it('renders nothing when data is empty', () => {
const { container } = render(
<ThemedChart data={[]}>
<Line dataKey="value" />
</ThemedChart>,
)
expect(container.innerHTML).toBe('')
})
it('renders a chart container when data is provided', () => {
const data = [
{ time: '10:00', value: 10 },
{ time: '10:01', value: 20 },
]
const { container } = render(
<ThemedChart data={data} height={160}>
<Line dataKey="value" />
</ThemedChart>,
)
expect(container.querySelector('.recharts-responsive-container')).toBeTruthy()
})
it('applies custom className', () => {
const data = [{ time: '10:00', value: 5 }]
const { container } = render(
<ThemedChart data={data} className="my-chart">
<Line dataKey="value" />
</ThemedChart>,
)
expect(container.querySelector('.my-chart')).toBeTruthy()
})
})
- Step 3: Run tests
npx vitest run src/design-system/composites/ThemedChart/ThemedChart.test.tsx
Expected: 3 tests pass.
- Step 4: Verify lib build
npm run build:lib
Expected: Build succeeds.
- Step 5: Commit
git add src/design-system/composites/ThemedChart/
git commit -m "feat: add ThemedChart wrapper component"
Task 4: Update Barrel Exports and Delete Old Charts
Files:
-
Modify:
src/design-system/composites/index.ts -
Delete:
src/design-system/composites/LineChart/ -
Delete:
src/design-system/composites/AreaChart/ -
Delete:
src/design-system/composites/BarChart/ -
Delete:
src/design-system/composites/_chart-utils.ts -
Step 1: Update composites/index.ts
Remove these lines:
export { AreaChart } from './AreaChart/AreaChart'
export { BarChart } from './BarChart/BarChart'
export { LineChart } from './LineChart/LineChart'
export { CHART_COLORS } from './_chart-utils'
export type { ChartSeries, DataPoint } from './_chart-utils'
Add in their place:
// Charts — ThemedChart wrapper + Recharts re-exports
export { ThemedChart } from './ThemedChart/ThemedChart'
export { CHART_COLORS, rechartsTheme } from '../utils/rechartsTheme'
export {
Line, Area, Bar,
ReferenceLine, ReferenceArea,
Legend, Brush,
} from 'recharts'
- Step 2: Remove the rechartsTheme re-export from main index.ts
In src/design-system/index.ts, remove this line (it's now re-exported via composites):
export * from './utils/rechartsTheme'
Replace with a targeted export that avoids double-exporting CHART_COLORS:
export { rechartsTheme } from './utils/rechartsTheme'
Wait — actually composites/index.ts already re-exports both CHART_COLORS and rechartsTheme. And index.ts does export * from './composites'. So the main index.ts line export * from './utils/rechartsTheme' would cause a duplicate export of both symbols. Remove it entirely:
Delete this line from src/design-system/index.ts:
export * from './utils/rechartsTheme'
- Step 3: Delete old chart directories and utilities
rm -rf src/design-system/composites/LineChart
rm -rf src/design-system/composites/AreaChart
rm -rf src/design-system/composites/BarChart
rm src/design-system/composites/_chart-utils.ts
- Step 4: Verify lib build
npm run build:lib
Expected: Build succeeds. The old components are gone, ThemedChart and Recharts re-exports are the new public API.
- Step 5: Run all tests
npx vitest run
Expected: All tests pass. No test files existed for the deleted components.
- Step 6: Commit
git add -A
git commit -m "feat!: replace custom chart components with ThemedChart + Recharts
BREAKING: LineChart, AreaChart, BarChart, ChartSeries, DataPoint removed.
Use ThemedChart with Recharts children (Line, Area, Bar, etc.) instead."
Task 5: Update COMPONENT_GUIDE.md
Files:
-
Modify:
COMPONENT_GUIDE.md -
Step 1: Update the charting strategy section
Find the section starting with ## Charting Strategy (around line 183) and replace through line 228 with:
## Charting Strategy
The design system provides a **ThemedChart** wrapper component that applies consistent styling to Recharts charts. Recharts is bundled as a dependency — consumers do not need to install it separately.
### Usage
```tsx
import { ThemedChart, Line, Area, ReferenceLine, CHART_COLORS } from '@cameleer/design-system'
const data = metrics.map(m => ({ time: m.timestamp, cpu: m.value * 100 }))
<ThemedChart data={data} height={160} xDataKey="time" yLabel="%">
<Area dataKey="cpu" stroke={CHART_COLORS[0]} fill={CHART_COLORS[0]} fillOpacity={0.1} />
<ReferenceLine y={85} stroke="var(--error)" strokeDasharray="5 3" label="Alert" />
</ThemedChart>
ThemedChart Props
| Prop | Type | Default | Description |
|---|---|---|---|
data |
Record<string, any>[] |
required | Flat array of data objects |
height |
number |
200 |
Chart height in pixels |
xDataKey |
string |
"time" |
Key for x-axis values |
xType |
'number' | 'category' |
"category" |
X-axis scale type |
xTickFormatter |
(value: any) => string |
— | Custom x-axis label formatter |
yTickFormatter |
(value: any) => string |
— | Custom y-axis label formatter |
yLabel |
string |
— | Y-axis label text |
children |
ReactNode |
required | Recharts elements (Line, Area, Bar, etc.) |
className |
string |
— | Container CSS class |
Available Recharts Re-exports
Line, Area, Bar, ReferenceLine, ReferenceArea, Legend, Brush
For chart types not covered (treemap, radar, pie, sankey), import from recharts directly and use rechartsTheme for consistent styling.
Theme Utilities
| Export | Purpose |
|---|---|
CHART_COLORS |
Array of var(--chart-1) through var(--chart-8) |
rechartsTheme |
Pre-configured prop objects for Recharts sub-components |
- [ ] **Step 2: Update the component index table**
Find the rows for `AreaChart`, `BarChart`, `LineChart` in the component index table and replace all three with:
| ThemedChart | composite | Recharts wrapper with themed axes, grid, and tooltip |
- [ ] **Step 3: Update the decision tree**
Find lines 57-60 (the chart decision tree entries):
- Time series → LineChart, AreaChart
- Categorical comparison → BarChart
Replace with:
- Time series → ThemedChart with
<Line>or<Area> - Categorical comparison → ThemedChart with
<Bar>
- [ ] **Step 4: Commit**
```bash
git add COMPONENT_GUIDE.md
git commit -m "docs: update COMPONENT_GUIDE for ThemedChart migration"
Task 6: Publish Design System
Files:
-
Modify:
package.json(version bump) -
Step 1: Bump version
In package.json, change "version" to "0.1.47".
- Step 2: Build and verify
npm run build:lib
- Step 3: Commit and tag
git add package.json
git commit -m "chore: bump version to 0.1.47"
git tag v0.1.47
git push && git push --tags
- Step 4: Wait for CI to publish
Wait for the Gitea CI pipeline to build and publish @cameleer/design-system@0.1.47 to the npm registry. Verify with:
npm view @cameleer/design-system@0.1.47 version
Task 7: Migrate Server UI AgentInstance Charts
Files:
-
Modify:
C:\Users\Hendrik\Documents\projects\cameleer3-server\ui\src\pages\AgentInstance\AgentInstance.tsx -
Modify:
C:\Users\Hendrik\Documents\projects\cameleer3-server\ui\package.json -
Step 1: Update design system dependency
cd C:\Users\Hendrik\Documents\projects\cameleer3-server\ui
npm install @cameleer/design-system@0.1.47
- Step 2: Update imports in AgentInstance.tsx
Replace the chart-related imports:
Old:
import {
StatCard, StatusDot, Badge, LineChart, AreaChart, BarChart,
EventFeed, Spinner, EmptyState, SectionHeader, MonoText,
LogViewer, ButtonGroup, useGlobalFilters,
} from '@cameleer/design-system'
New:
import {
StatCard, StatusDot, Badge,
ThemedChart, Line, Area, ReferenceLine, CHART_COLORS,
EventFeed, Spinner, EmptyState, SectionHeader, MonoText,
LogViewer, ButtonGroup, useGlobalFilters,
} from '@cameleer/design-system'
- Step 3: Replace data prep — JVM metrics
Replace the 4 JVM series useMemo blocks (cpuSeries, heapSeries, threadSeries, gcSeries) with flat data builders:
const formatTime = (t: string) =>
new Date(t).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
// JVM chart data — merge all metrics into flat objects by time bucket
const cpuData = useMemo(() => {
const pts = jvmMetrics?.metrics?.['process.cpu.usage.value'];
if (!pts?.length) return [];
return pts.map((p: any) => ({ time: p.time, cpu: p.value * 100 }));
}, [jvmMetrics]);
const heapData = useMemo(() => {
const pts = jvmMetrics?.metrics?.['jvm.memory.used.value'];
if (!pts?.length) return [];
return pts.map((p: any) => ({ time: p.time, heap: p.value / (1024 * 1024) }));
}, [jvmMetrics]);
const threadData = useMemo(() => {
const pts = jvmMetrics?.metrics?.['jvm.threads.live.value'];
if (!pts?.length) return [];
return pts.map((p: any) => ({ time: p.time, threads: p.value }));
}, [jvmMetrics]);
const gcData = useMemo(() => {
const pts = jvmMetrics?.metrics?.['jvm.gc.pause.total_time'];
if (!pts?.length) return [];
return pts.map((p: any) => ({ time: p.time, gc: p.value }));
}, [jvmMetrics]);
- Step 4: Replace data prep — throughput and error
Replace the throughputSeries and errorSeries useMemo blocks:
const throughputData = useMemo(() => {
if (!chartData.length) return [];
return chartData.map((d: any) => ({ time: d.date.toISOString(), throughput: d.throughput }));
}, [chartData]);
const errorData = useMemo(() => {
if (!chartData.length) return [];
return chartData.map((d: any) => ({ time: d.date.toISOString(), errorPct: d.errorPct }));
}, [chartData]);
- Step 5: Replace chart JSX — CPU Usage
Replace the CPU chart card content:
{cpuData.length ? (
<ThemedChart data={cpuData} height={160} xDataKey="time"
xTickFormatter={formatTime} yLabel="%">
<Area dataKey="cpu" name="CPU %" stroke={CHART_COLORS[0]}
fill={CHART_COLORS[0]} fillOpacity={0.1} strokeWidth={2} dot={false} />
<ReferenceLine y={85} stroke="var(--error)" strokeDasharray="5 3"
label={{ value: 'Alert', position: 'right', fill: 'var(--error)', fontSize: 9 }} />
</ThemedChart>
) : (
<EmptyState title="No data" description="No CPU metrics available" />
)}
- Step 6: Replace chart JSX — Memory (Heap)
Replace the heap chart card content:
{heapData.length ? (
<ThemedChart data={heapData} height={160} xDataKey="time"
xTickFormatter={formatTime} yLabel="MB">
<Area dataKey="heap" name="Heap MB" stroke={CHART_COLORS[0]}
fill={CHART_COLORS[0]} fillOpacity={0.1} strokeWidth={2} dot={false} />
{heapMax != null && (
<ReferenceLine y={heapMax / (1024 * 1024)} stroke="var(--error)" strokeDasharray="5 3"
label={{ value: 'Max Heap', position: 'right', fill: 'var(--error)', fontSize: 9 }} />
)}
</ThemedChart>
) : (
<EmptyState title="No data" description="No heap metrics available" />
)}
- Step 7: Replace chart JSX — Throughput
{throughputData.length ? (
<ThemedChart data={throughputData} height={160} xDataKey="time"
xTickFormatter={formatTime} yLabel="msg/s">
<Line dataKey="throughput" name="msg/s" stroke={CHART_COLORS[0]}
strokeWidth={2} dot={false} />
</ThemedChart>
) : (
<EmptyState title="No data" description="No throughput data in range" />
)}
- Step 8: Replace chart JSX — Error Rate
{errorData.length ? (
<ThemedChart data={errorData} height={160} xDataKey="time"
xTickFormatter={formatTime} yLabel="%">
<Line dataKey="errorPct" name="Error %" stroke={CHART_COLORS[0]}
strokeWidth={2} dot={false} />
</ThemedChart>
) : (
<EmptyState title="No data" description="No error data in range" />
)}
- Step 9: Replace chart JSX — Thread Count
{threadData.length ? (
<ThemedChart data={threadData} height={160} xDataKey="time"
xTickFormatter={formatTime} yLabel="threads">
<Line dataKey="threads" name="Threads" stroke={CHART_COLORS[0]}
strokeWidth={2} dot={false} />
</ThemedChart>
) : (
<EmptyState title="No data" description="No thread metrics available" />
)}
- Step 10: Replace chart JSX — GC Pauses
{gcData.length ? (
<ThemedChart data={gcData} height={160} xDataKey="time"
xTickFormatter={formatTime} yLabel="ms">
<Area dataKey="gc" name="GC ms" stroke={CHART_COLORS[1]}
fill={CHART_COLORS[1]} fillOpacity={0.1} strokeWidth={2} dot={false} />
</ThemedChart>
) : (
<EmptyState title="No data" description="No GC metrics available" />
)}
- Step 11: Update the Thread Count meta display
The thread count meta currently reads from threadSeries. Update to read from threadData:
Old:
{threadSeries
? `${threadSeries[0].data[threadSeries[0].data.length - 1]?.y.toFixed(0)} active`
: ''}
New:
{threadData.length
? `${threadData[threadData.length - 1].threads.toFixed(0)} active`
: ''}
- Step 12: Build server UI
cd C:\Users\Hendrik\Documents\projects\cameleer3-server\ui
npm run build
Expected: Build succeeds.
- Step 13: Commit and push
cd C:\Users\Hendrik\Documents\projects\cameleer3-server
git add ui/
git commit -m "feat: migrate agent charts to ThemedChart + Recharts
Replace custom LineChart/AreaChart/BarChart usage with ThemedChart
wrapper. Data format changed from ChartSeries[] to Recharts-native
flat objects. Uses DS v0.1.47."
git push