From b0bd9a4ce2f662b8566cf4f3f66d763cc1e51967 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:12:02 +0200 Subject: [PATCH] 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) --- .../composites/AreaChart/AreaChart.tsx | 107 ++++++++++++++++++ .../composites/BarChart/BarChart.tsx | 88 ++++++++++++++ .../composites/LineChart/LineChart.tsx | 101 +++++++++++++++++ src/design-system/composites/index.ts | 4 + 4 files changed, 300 insertions(+) create mode 100644 src/design-system/composites/AreaChart/AreaChart.tsx create mode 100644 src/design-system/composites/BarChart/BarChart.tsx create mode 100644 src/design-system/composites/LineChart/LineChart.tsx diff --git a/src/design-system/composites/AreaChart/AreaChart.tsx b/src/design-system/composites/AreaChart/AreaChart.tsx new file mode 100644 index 0000000..0618eb8 --- /dev/null +++ b/src/design-system/composites/AreaChart/AreaChart.tsx @@ -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>() + 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 = ( + String(v) : undefined} + yLabel={yLabel} + className={className} + > + {series.map((s, i) => { + const color = s.color ?? CHART_COLORS[i % CHART_COLORS.length] + return ( + + ) + })} + {thresholdValue != null && ( + + )} + + ) + + if (width) { + return
{chart}
+ } + + return chart +} diff --git a/src/design-system/composites/BarChart/BarChart.tsx b/src/design-system/composites/BarChart/BarChart.tsx new file mode 100644 index 0000000..d8533d6 --- /dev/null +++ b/src/design-system/composites/BarChart/BarChart.tsx @@ -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>() + 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 = ( + String(v) : undefined} + yLabel={yLabel} + className={className} + > + {series.map((s, i) => ( + + ))} + + ) + + if (width) { + return
{chart}
+ } + + return chart +} diff --git a/src/design-system/composites/LineChart/LineChart.tsx b/src/design-system/composites/LineChart/LineChart.tsx new file mode 100644 index 0000000..549a917 --- /dev/null +++ b/src/design-system/composites/LineChart/LineChart.tsx @@ -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>() + 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 = ( + String(v) : undefined} + yLabel={yLabel} + className={className} + > + {series.map((s, i) => ( + + ))} + {threshold && ( + + )} + + ) + + if (width) { + return
{chart}
+ } + + return chart +} diff --git a/src/design-system/composites/index.ts b/src/design-system/composites/index.ts index bef25e2..7d4ba44 100644 --- a/src/design-system/composites/index.ts +++ b/src/design-system/composites/index.ts @@ -42,6 +42,10 @@ export { TreeView } from './TreeView/TreeView' // Charts — ThemedChart wrapper + Recharts re-exports 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 { Line, Area, Bar,