feat: add LineChart, AreaChart, BarChart wrapper components
Wrap ThemedChart with convenient series-based API that transforms ChartSeries[] into the flat record format ThemedChart expects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
107
src/design-system/composites/AreaChart/AreaChart.tsx
Normal file
107
src/design-system/composites/AreaChart/AreaChart.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { Area, ReferenceLine } from 'recharts'
|
||||||
|
import { ThemedChart } from '../ThemedChart/ThemedChart'
|
||||||
|
import { CHART_COLORS } from '../../utils/rechartsTheme'
|
||||||
|
|
||||||
|
export interface DataPoint {
|
||||||
|
x: any
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartSeries {
|
||||||
|
label: string
|
||||||
|
data: DataPoint[]
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AreaChartProps {
|
||||||
|
series: ChartSeries[]
|
||||||
|
height?: number
|
||||||
|
width?: number
|
||||||
|
yLabel?: string
|
||||||
|
xLabel?: string
|
||||||
|
thresholdValue?: number
|
||||||
|
thresholdLabel?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(d: Date): string {
|
||||||
|
const h = String(d.getHours()).padStart(2, '0')
|
||||||
|
const m = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
return `${h}:${m}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AreaChart({
|
||||||
|
series,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
yLabel,
|
||||||
|
xLabel,
|
||||||
|
thresholdValue,
|
||||||
|
thresholdLabel,
|
||||||
|
className,
|
||||||
|
}: AreaChartProps) {
|
||||||
|
const { data, hasDateX } = useMemo(() => {
|
||||||
|
const map = new Map<string, Record<string, any>>()
|
||||||
|
let dateDetected = false
|
||||||
|
|
||||||
|
for (const s of series) {
|
||||||
|
for (const pt of s.data) {
|
||||||
|
const isDate = pt.x instanceof Date
|
||||||
|
if (isDate) dateDetected = true
|
||||||
|
const key = isDate ? pt.x.getTime().toString() : String(pt.x)
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, { _x: isDate ? formatTime(pt.x) : pt.x })
|
||||||
|
}
|
||||||
|
map.get(key)![s.label] = pt.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: Array.from(map.values()), hasDateX: dateDetected }
|
||||||
|
}, [series])
|
||||||
|
|
||||||
|
const chart = (
|
||||||
|
<ThemedChart
|
||||||
|
data={data}
|
||||||
|
height={height}
|
||||||
|
xDataKey="_x"
|
||||||
|
xType={hasDateX ? 'category' : 'category'}
|
||||||
|
xTickFormatter={hasDateX ? (v: any) => String(v) : undefined}
|
||||||
|
yLabel={yLabel}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{series.map((s, i) => {
|
||||||
|
const color = s.color ?? CHART_COLORS[i % CHART_COLORS.length]
|
||||||
|
return (
|
||||||
|
<Area
|
||||||
|
key={s.label}
|
||||||
|
type="monotone"
|
||||||
|
dataKey={s.label}
|
||||||
|
stroke={color}
|
||||||
|
fill={color}
|
||||||
|
fillOpacity={0.15}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{thresholdValue != null && (
|
||||||
|
<ReferenceLine
|
||||||
|
y={thresholdValue}
|
||||||
|
stroke="var(--text-muted)"
|
||||||
|
strokeDasharray="4 4"
|
||||||
|
label={thresholdLabel ? {
|
||||||
|
value: thresholdLabel,
|
||||||
|
position: 'insideTopRight',
|
||||||
|
style: { fontSize: 10, fill: 'var(--text-muted)' },
|
||||||
|
} : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ThemedChart>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (width) {
|
||||||
|
return <div style={{ width }}>{chart}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return chart
|
||||||
|
}
|
||||||
88
src/design-system/composites/BarChart/BarChart.tsx
Normal file
88
src/design-system/composites/BarChart/BarChart.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { Bar } from 'recharts'
|
||||||
|
import { ThemedChart } from '../ThemedChart/ThemedChart'
|
||||||
|
import { CHART_COLORS } from '../../utils/rechartsTheme'
|
||||||
|
|
||||||
|
export interface DataPoint {
|
||||||
|
x: any
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartSeries {
|
||||||
|
label: string
|
||||||
|
data: DataPoint[]
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BarChartProps {
|
||||||
|
series: ChartSeries[]
|
||||||
|
height?: number
|
||||||
|
width?: number
|
||||||
|
yLabel?: string
|
||||||
|
xLabel?: string
|
||||||
|
stacked?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(d: Date): string {
|
||||||
|
const h = String(d.getHours()).padStart(2, '0')
|
||||||
|
const m = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
return `${h}:${m}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BarChart({
|
||||||
|
series,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
yLabel,
|
||||||
|
xLabel,
|
||||||
|
stacked,
|
||||||
|
className,
|
||||||
|
}: BarChartProps) {
|
||||||
|
const { data, hasDateX } = useMemo(() => {
|
||||||
|
const map = new Map<string, Record<string, any>>()
|
||||||
|
let dateDetected = false
|
||||||
|
|
||||||
|
for (const s of series) {
|
||||||
|
for (const pt of s.data) {
|
||||||
|
const isDate = pt.x instanceof Date
|
||||||
|
if (isDate) dateDetected = true
|
||||||
|
const key = isDate ? pt.x.getTime().toString() : String(pt.x)
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, { _x: isDate ? formatTime(pt.x) : pt.x })
|
||||||
|
}
|
||||||
|
map.get(key)![s.label] = pt.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: Array.from(map.values()), hasDateX: dateDetected }
|
||||||
|
}, [series])
|
||||||
|
|
||||||
|
const chart = (
|
||||||
|
<ThemedChart
|
||||||
|
data={data}
|
||||||
|
height={height}
|
||||||
|
xDataKey="_x"
|
||||||
|
xType="category"
|
||||||
|
xTickFormatter={hasDateX ? (v: any) => String(v) : undefined}
|
||||||
|
yLabel={yLabel}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{series.map((s, i) => (
|
||||||
|
<Bar
|
||||||
|
key={s.label}
|
||||||
|
dataKey={s.label}
|
||||||
|
fill={s.color ?? CHART_COLORS[i % CHART_COLORS.length]}
|
||||||
|
radius={[2, 2, 0, 0]}
|
||||||
|
{...(stacked ? { stackId: 'stack' } : {})}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ThemedChart>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (width) {
|
||||||
|
return <div style={{ width }}>{chart}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return chart
|
||||||
|
}
|
||||||
101
src/design-system/composites/LineChart/LineChart.tsx
Normal file
101
src/design-system/composites/LineChart/LineChart.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { Line, ReferenceLine } from 'recharts'
|
||||||
|
import { ThemedChart } from '../ThemedChart/ThemedChart'
|
||||||
|
import { CHART_COLORS } from '../../utils/rechartsTheme'
|
||||||
|
|
||||||
|
export interface DataPoint {
|
||||||
|
x: any
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartSeries {
|
||||||
|
label: string
|
||||||
|
data: DataPoint[]
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LineChartProps {
|
||||||
|
series: ChartSeries[]
|
||||||
|
height?: number
|
||||||
|
width?: number
|
||||||
|
yLabel?: string
|
||||||
|
xLabel?: string
|
||||||
|
threshold?: { value: number; label: string }
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(d: Date): string {
|
||||||
|
const h = String(d.getHours()).padStart(2, '0')
|
||||||
|
const m = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
return `${h}:${m}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LineChart({
|
||||||
|
series,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
yLabel,
|
||||||
|
xLabel,
|
||||||
|
threshold,
|
||||||
|
className,
|
||||||
|
}: LineChartProps) {
|
||||||
|
const { data, hasDateX } = useMemo(() => {
|
||||||
|
const map = new Map<string, Record<string, any>>()
|
||||||
|
let dateDetected = false
|
||||||
|
|
||||||
|
for (const s of series) {
|
||||||
|
for (const pt of s.data) {
|
||||||
|
const isDate = pt.x instanceof Date
|
||||||
|
if (isDate) dateDetected = true
|
||||||
|
const key = isDate ? pt.x.getTime().toString() : String(pt.x)
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, { _x: isDate ? formatTime(pt.x) : pt.x })
|
||||||
|
}
|
||||||
|
map.get(key)![s.label] = pt.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: Array.from(map.values()), hasDateX: dateDetected }
|
||||||
|
}, [series])
|
||||||
|
|
||||||
|
const chart = (
|
||||||
|
<ThemedChart
|
||||||
|
data={data}
|
||||||
|
height={height}
|
||||||
|
xDataKey="_x"
|
||||||
|
xType={hasDateX ? 'category' : 'category'}
|
||||||
|
xTickFormatter={hasDateX ? (v: any) => String(v) : undefined}
|
||||||
|
yLabel={yLabel}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{series.map((s, i) => (
|
||||||
|
<Line
|
||||||
|
key={s.label}
|
||||||
|
type="monotone"
|
||||||
|
dataKey={s.label}
|
||||||
|
stroke={s.color ?? CHART_COLORS[i % CHART_COLORS.length]}
|
||||||
|
dot={false}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{threshold && (
|
||||||
|
<ReferenceLine
|
||||||
|
y={threshold.value}
|
||||||
|
stroke="var(--text-muted)"
|
||||||
|
strokeDasharray="4 4"
|
||||||
|
label={{
|
||||||
|
value: threshold.label,
|
||||||
|
position: 'insideTopRight',
|
||||||
|
style: { fontSize: 10, fill: 'var(--text-muted)' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ThemedChart>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (width) {
|
||||||
|
return <div style={{ width }}>{chart}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return chart
|
||||||
|
}
|
||||||
@@ -42,6 +42,10 @@ export { TreeView } from './TreeView/TreeView'
|
|||||||
|
|
||||||
// Charts — ThemedChart wrapper + Recharts re-exports
|
// Charts — ThemedChart wrapper + Recharts re-exports
|
||||||
export { ThemedChart } from './ThemedChart/ThemedChart'
|
export { ThemedChart } from './ThemedChart/ThemedChart'
|
||||||
|
export { LineChart } from './LineChart/LineChart'
|
||||||
|
export { AreaChart } from './AreaChart/AreaChart'
|
||||||
|
export { BarChart } from './BarChart/BarChart'
|
||||||
|
export type { ChartSeries, DataPoint } from './LineChart/LineChart'
|
||||||
export { CHART_COLORS, rechartsTheme } from '../utils/rechartsTheme'
|
export { CHART_COLORS, rechartsTheme } from '../utils/rechartsTheme'
|
||||||
export {
|
export {
|
||||||
Line, Area, Bar,
|
Line, Area, Bar,
|
||||||
|
|||||||
Reference in New Issue
Block a user