From 7d9643bd1b84cacc5023391c7970d8f8ff4c4ad5 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:36:19 +0200 Subject: [PATCH] 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. --- .../composites/AreaChart/AreaChart.module.css | 167 ----------- .../composites/AreaChart/AreaChart.tsx | 268 ------------------ .../composites/BarChart/BarChart.module.css | 144 ---------- .../composites/BarChart/BarChart.tsx | 242 ---------------- .../composites/LineChart/LineChart.module.css | 156 ---------- .../composites/LineChart/LineChart.tsx | 247 ---------------- src/design-system/composites/_chart-utils.ts | 101 ------- src/design-system/composites/index.ts | 14 +- src/design-system/index.ts | 2 +- 9 files changed, 9 insertions(+), 1332 deletions(-) delete mode 100644 src/design-system/composites/AreaChart/AreaChart.module.css delete mode 100644 src/design-system/composites/AreaChart/AreaChart.tsx delete mode 100644 src/design-system/composites/BarChart/BarChart.module.css delete mode 100644 src/design-system/composites/BarChart/BarChart.tsx delete mode 100644 src/design-system/composites/LineChart/LineChart.module.css delete mode 100644 src/design-system/composites/LineChart/LineChart.tsx delete mode 100644 src/design-system/composites/_chart-utils.ts diff --git a/src/design-system/composites/AreaChart/AreaChart.module.css b/src/design-system/composites/AreaChart/AreaChart.module.css deleted file mode 100644 index b36f8ca..0000000 --- a/src/design-system/composites/AreaChart/AreaChart.module.css +++ /dev/null @@ -1,167 +0,0 @@ -.wrapper { - position: relative; - display: flex; - flex-direction: column; - gap: 4px; - width: 100%; -} - -.svg { - display: block; - overflow: visible; -} - -.empty { - display: flex; - align-items: center; - justify-content: center; - color: var(--text-muted); - font-size: 12px; - height: 120px; -} - -/* Grid */ -.gridLine { - stroke: var(--border-subtle); - stroke-width: 1; - stroke-dasharray: 3 3; -} - -.axisLabel { - font-family: var(--font-mono); - font-size: 9px; - fill: var(--text-faint); -} - -/* Threshold line */ -.thresholdLine { - stroke: var(--error); - stroke-width: 1.5; - stroke-dasharray: 5 3; -} - -.thresholdLabel { - font-family: var(--font-mono); - font-size: 9px; - fill: var(--error); -} - -/* Area + line */ -.area { - opacity: 0.1; -} - -.line { - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; -} - -/* Crosshair */ -.crosshair { - stroke: var(--text-faint); - stroke-width: 1; - stroke-dasharray: 3 3; - pointer-events: none; -} - -/* Tooltip */ -.tooltip { - position: absolute; - 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; - pointer-events: none; - z-index: 10; - white-space: nowrap; -} - -.tooltipTime { - 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); -} - -.tooltipRow { - display: flex; - align-items: center; - gap: 5px; - margin-bottom: 2px; -} - -.tooltipRow:last-child { - margin-bottom: 0; -} - -.tooltipDot { - width: 6px; - height: 6px; - border-radius: 50%; - flex-shrink: 0; -} - -.tooltipLabel { - color: var(--text-muted); -} - -.tooltipValue { - font-family: var(--font-mono); - font-weight: 600; - color: var(--text-primary); -} - -/* Legend */ -.legend { - display: flex; - flex-wrap: wrap; - gap: 12px; - padding-left: 48px; -} - -.legendItem { - display: flex; - align-items: center; - gap: 5px; - font-size: 12px; - color: var(--text-secondary); -} - -.legendDot { - width: 8px; - height: 8px; - border-radius: 50%; - flex-shrink: 0; -} - -.legendLabel { - font-size: 12px; -} - -/* Axis labels */ -.yLabel { - font-size: 12px; - color: var(--text-muted); - writing-mode: vertical-lr; - transform: rotate(180deg); - text-align: center; - position: absolute; - left: 4px; - top: 0; - bottom: 28px; - display: flex; - align-items: center; - justify-content: center; -} - -.xLabel { - text-align: center; - font-size: 12px; - color: var(--text-muted); - padding-left: 48px; -} diff --git a/src/design-system/composites/AreaChart/AreaChart.tsx b/src/design-system/composites/AreaChart/AreaChart.tsx deleted file mode 100644 index 6f0f54f..0000000 --- a/src/design-system/composites/AreaChart/AreaChart.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import { useState } from 'react' -import styles from './AreaChart.module.css' -import { - computeYScale, - computeXScale, - seriesPoints, - seriesPath, - formatAxisLabel, - CHART_COLORS, - type ChartSeries, -} from '../_chart-utils' - -interface Threshold { - value: number - label: string -} - -interface AreaChartProps { - series: ChartSeries[] - xLabel?: string - yLabel?: string - threshold?: Threshold - height?: number - width?: number - className?: string -} - -const Y_TICK_COUNT = 4 -const DIMS = { - paddingTop: 12, - paddingRight: 16, - paddingBottom: 28, - paddingLeft: 48, -} - -export function AreaChart({ - series, - xLabel, - yLabel, - threshold, - height = 200, - width = 400, - className, -}: AreaChartProps) { - const [tooltip, setTooltip] = useState<{ - x: number - y: number - xLabel: string - values: { label: string; value: number; color: string }[] - } | null>(null) - - const dims = { ...DIMS, width, height } - const allData = series.flatMap((s) => s.data) - - if (allData.length === 0) { - return
No data
- } - - const { max, toY } = computeYScale(series, dims, threshold?.value) - const { toX } = computeXScale(series, dims) - const plotH = height - dims.paddingTop - dims.paddingBottom - const plotW = width - dims.paddingLeft - dims.paddingRight - const bottomY = dims.paddingTop + plotH - - // Y-axis ticks - const yTicks = Array.from({ length: Y_TICK_COUNT + 1 }, (_, i) => - Math.round((max / Y_TICK_COUNT) * i), - ) - - // X-axis ticks (first, middle, last) - const firstSeries = series[0] - const xSamples = - firstSeries && firstSeries.data.length > 0 - ? [ - firstSeries.data[0].x, - firstSeries.data[Math.floor(firstSeries.data.length / 2)]?.x, - firstSeries.data[firstSeries.data.length - 1].x, - ].filter(Boolean) - : [] - - function handleMouseMove(e: React.MouseEvent) { - const rect = e.currentTarget.getBoundingClientRect() - const mx = e.clientX - rect.left - const my = e.clientY - rect.top - - // Find closest x value - const pctX = (mx - dims.paddingLeft) / plotW - const firstS = series[0] - const idx0 = Math.max(0, Math.min(firstS.data.length - 1, Math.round(pctX * (firstS.data.length - 1)))) - const xVal = firstS.data[idx0]?.x - const xLabel = xVal instanceof Date - ? xVal.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }) - : typeof xVal === 'number' && xVal > 1e10 - ? new Date(xVal).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }) - : String(xVal ?? '') - - const values = series.map((s, i) => { - const idx = Math.round(pctX * (s.data.length - 1)) - const clamped = Math.max(0, Math.min(s.data.length - 1, idx)) - const pt = s.data[clamped] - return { - label: s.label, - value: pt?.y ?? 0, - color: s.color ?? CHART_COLORS[i % CHART_COLORS.length], - } - }) - - setTooltip({ x: mx, y: my, xLabel, values }) - } - - return ( -
- {yLabel &&
{yLabel}
} - setTooltip(null)} - aria-label="Area chart" - role="img" - > - {/* Grid lines */} - {yTicks.map((val) => { - const y = toY(val) - return ( - - - - {formatAxisLabel(val)} - - - ) - })} - - {/* X-axis labels */} - {xSamples.map((xVal, i) => { - const xPos = toX(xVal) - const xv = xVal instanceof Date ? xVal : new Date(xVal as number) - const label = - xVal instanceof Date || (typeof xVal === 'number' && xVal > 1e10) - ? xv.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - : formatAxisLabel(xVal as number) - const anchor = i === 0 ? 'start' : i === xSamples.length - 1 ? 'end' : 'middle' - return ( - - {label} - - ) - })} - - {/* SLA threshold line */} - {threshold && ( - - - - {threshold.label} - - - )} - - {/* Area fills */} - {series.map((s, i) => { - const color = s.color ?? CHART_COLORS[i % CHART_COLORS.length] - const areaD = seriesPath(s, toX, toY, bottomY) - return ( - - ) - })} - - {/* Lines */} - {series.map((s, i) => { - const color = s.color ?? CHART_COLORS[i % CHART_COLORS.length] - const pts = seriesPoints(s, toX, toY) - return ( - - ) - })} - - {/* Crosshair */} - {tooltip && ( - - )} - - - {/* Tooltip */} - {tooltip && ( -
- {tooltip.xLabel && ( -
{tooltip.xLabel}
- )} - {tooltip.values.map((v) => ( -
- - {v.label}: - {formatAxisLabel(v.value)} -
- ))} -
- )} - - {/* Legend */} - {series.length > 1 && ( -
- {series.map((s, i) => ( -
- - {s.label} -
- ))} -
- )} - - {xLabel &&
{xLabel}
} -
- ) -} diff --git a/src/design-system/composites/BarChart/BarChart.module.css b/src/design-system/composites/BarChart/BarChart.module.css deleted file mode 100644 index 61b1683..0000000 --- a/src/design-system/composites/BarChart/BarChart.module.css +++ /dev/null @@ -1,144 +0,0 @@ -.wrapper { - position: relative; - display: flex; - flex-direction: column; - gap: 4px; - width: 100%; -} - -.svg { - display: block; - overflow: visible; -} - -.empty { - display: flex; - align-items: center; - justify-content: center; - color: var(--text-muted); - font-size: 12px; - height: 120px; -} - -.gridLine { - stroke: var(--border-subtle); - stroke-width: 1; - stroke-dasharray: 3 3; -} - -.axisLabel { - font-family: var(--font-mono); - font-size: 9px; - fill: var(--text-faint); -} - -.catLabel { - font-family: var(--font-mono); - font-size: 9px; - fill: var(--text-faint); -} - -.bar { - cursor: pointer; - transition: opacity 0.1s; -} - -.bar:hover { - opacity: 0.8; -} - -.tooltip { - position: absolute; - 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; - pointer-events: none; - z-index: 10; - white-space: nowrap; -} - -.tooltipTitle { - font-weight: 600; - color: var(--text-primary); - margin-bottom: 4px; - font-size: 12px; -} - -.tooltipRow { - display: flex; - align-items: center; - gap: 5px; - margin-bottom: 2px; -} - -.tooltipRow:last-child { - margin-bottom: 0; -} - -.tooltipDot { - width: 6px; - height: 6px; - border-radius: 50%; - flex-shrink: 0; -} - -.tooltipLabel { - color: var(--text-muted); -} - -.tooltipValue { - font-family: var(--font-mono); - font-weight: 600; - color: var(--text-primary); -} - -.legend { - display: flex; - flex-wrap: wrap; - gap: 12px; - padding-left: 48px; -} - -.legendItem { - display: flex; - align-items: center; - gap: 5px; - font-size: 12px; - color: var(--text-secondary); -} - -.legendDot { - width: 8px; - height: 8px; - border-radius: 50%; - flex-shrink: 0; -} - -.legendLabel { - font-size: 12px; -} - -.yLabel { - font-size: 12px; - color: var(--text-muted); - writing-mode: vertical-lr; - transform: rotate(180deg); - text-align: center; - position: absolute; - left: 4px; - top: 0; - bottom: 40px; - display: flex; - align-items: center; - justify-content: center; -} - -.xLabel { - text-align: center; - font-size: 12px; - color: var(--text-muted); - padding-left: 48px; -} diff --git a/src/design-system/composites/BarChart/BarChart.tsx b/src/design-system/composites/BarChart/BarChart.tsx deleted file mode 100644 index a1e114a..0000000 --- a/src/design-system/composites/BarChart/BarChart.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { useState } from 'react' -import styles from './BarChart.module.css' -import { formatAxisLabel, CHART_COLORS } from '../_chart-utils' - -interface BarSeries { - label: string - data: { x: string; y: number }[] - color?: string -} - -interface BarChartProps { - series: BarSeries[] - stacked?: boolean - height?: number - width?: number - xLabel?: string - yLabel?: string - className?: string -} - -const PADDING = { top: 12, right: 16, bottom: 40, left: 48 } -const Y_TICK_COUNT = 4 -const BAR_GAP = 0.2 // fraction of bar group width reserved for gaps - -export function BarChart({ - series, - stacked = false, - height = 200, - width = 400, - xLabel, - yLabel, - className, -}: BarChartProps) { - const [tooltip, setTooltip] = useState<{ - x: number - y: number - label: string - values: { series: string; value: number; color: string }[] - } | null>(null) - - if (series.length === 0 || series[0].data.length === 0) { - return
No data
- } - - // Collect all x categories (union across all series) - const categories = Array.from(new Set(series.flatMap((s) => s.data.map((d) => d.x)))) - const numCats = categories.length - - const plotW = width - PADDING.left - PADDING.right - const plotH = height - PADDING.top - PADDING.bottom - - // Compute max Y - let maxY = 0 - if (stacked) { - for (const cat of categories) { - const sum = series.reduce((acc, s) => { - const pt = s.data.find((d) => d.x === cat) - return acc + (pt?.y ?? 0) - }, 0) - maxY = Math.max(maxY, sum) - } - } else { - maxY = Math.max(...series.flatMap((s) => s.data.map((d) => d.y))) - } - maxY = maxY || 1 - - const yTicks = Array.from({ length: Y_TICK_COUNT + 1 }, (_, i) => - Math.round((maxY / Y_TICK_COUNT) * i), - ) - const toY = (val: number) => PADDING.top + plotH - (val / maxY) * plotH - const bottomY = PADDING.top + plotH - - const catWidth = plotW / numCats - const groupGap = catWidth * BAR_GAP - const groupW = catWidth - groupGap - - function handleMouseEnter( - catLabel: string, - mx: number, - my: number, - values: { series: string; value: number; color: string }[], - ) { - setTooltip({ x: mx, y: my, label: catLabel, values }) - } - - function showBarTooltip(e: React.MouseEvent, cat: string) { - const rect = e.currentTarget.closest('svg')!.getBoundingClientRect() - handleMouseEnter( - cat, - e.clientX - rect.left, - e.clientY - rect.top, - series.map((ss, ssi) => ({ - series: ss.label, - value: ss.data.find((d) => d.x === cat)?.y ?? 0, - color: ss.color ?? CHART_COLORS[ssi % CHART_COLORS.length], - })), - ) - } - - return ( -
- {yLabel &&
{yLabel}
} - setTooltip(null)} - aria-label="Bar chart" - role="img" - > - {/* Grid lines */} - {yTicks.map((val) => { - const y = toY(val) - return ( - - - - {formatAxisLabel(val)} - - - ) - })} - - {/* Bars */} - {categories.map((cat, ci) => { - const groupX = PADDING.left + ci * catWidth + groupGap / 2 - - if (stacked) { - let stackY = bottomY - return ( - - {series.map((s, si) => { - const pt = s.data.find((d) => d.x === cat) - const val = pt?.y ?? 0 - const barH = (val / maxY) * plotH - const color = s.color ?? CHART_COLORS[si % CHART_COLORS.length] - const y = stackY - barH - stackY -= barH - return ( - showBarTooltip(e, cat)} - /> - ) - })} - - {cat} - - - ) - } - - // Grouped - const barW = groupW / series.length - return ( - - {series.map((s, si) => { - const pt = s.data.find((d) => d.x === cat) - const val = pt?.y ?? 0 - const barH = (val / maxY) * plotH - const color = s.color ?? CHART_COLORS[si % CHART_COLORS.length] - return ( - showBarTooltip(e, cat)} - /> - ) - })} - - {cat} - - - ) - })} - - - {/* Tooltip */} - {tooltip && ( -
-
{tooltip.label}
- {tooltip.values.map((v) => ( -
- - {v.series}: - {formatAxisLabel(v.value)} -
- ))} -
- )} - - {/* Legend */} - {series.length > 1 && ( -
- {series.map((s, i) => ( -
- - {s.label} -
- ))} -
- )} - - {xLabel &&
{xLabel}
} -
- ) -} diff --git a/src/design-system/composites/LineChart/LineChart.module.css b/src/design-system/composites/LineChart/LineChart.module.css deleted file mode 100644 index 89f76af..0000000 --- a/src/design-system/composites/LineChart/LineChart.module.css +++ /dev/null @@ -1,156 +0,0 @@ -.wrapper { - position: relative; - display: flex; - flex-direction: column; - gap: 4px; - width: 100%; -} - -.svg { - display: block; - overflow: visible; -} - -.empty { - display: flex; - align-items: center; - justify-content: center; - color: var(--text-muted); - font-size: 12px; - height: 120px; -} - -.gridLine { - stroke: var(--border-subtle); - stroke-width: 1; - stroke-dasharray: 3 3; -} - -.axisLabel { - font-family: var(--font-mono); - font-size: 9px; - fill: var(--text-faint); -} - -.thresholdLine { - stroke: var(--error); - stroke-width: 1.5; - stroke-dasharray: 5 3; -} - -.thresholdLabel { - font-family: var(--font-mono); - font-size: 9px; - fill: var(--error); -} - -.line { - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; -} - -.crosshair { - stroke: var(--text-faint); - stroke-width: 1; - stroke-dasharray: 3 3; - pointer-events: none; -} - -.tooltip { - position: absolute; - 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; - pointer-events: none; - z-index: 10; - white-space: nowrap; -} - -.tooltipTime { - 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); -} - -.tooltipRow { - display: flex; - align-items: center; - gap: 5px; - margin-bottom: 2px; -} - -.tooltipRow:last-child { - margin-bottom: 0; -} - -.tooltipDot { - width: 6px; - height: 6px; - border-radius: 50%; - flex-shrink: 0; -} - -.tooltipLabel { - color: var(--text-muted); -} - -.tooltipValue { - font-family: var(--font-mono); - font-weight: 600; - color: var(--text-primary); -} - -.legend { - display: flex; - flex-wrap: wrap; - gap: 12px; - padding-left: 48px; -} - -.legendItem { - display: flex; - align-items: center; - gap: 5px; - font-size: 12px; - color: var(--text-secondary); -} - -.legendDot { - width: 8px; - height: 8px; - border-radius: 50%; - flex-shrink: 0; -} - -.legendLabel { - font-size: 12px; -} - -.yLabel { - font-size: 12px; - color: var(--text-muted); - writing-mode: vertical-lr; - transform: rotate(180deg); - text-align: center; - position: absolute; - left: 4px; - top: 0; - bottom: 28px; - display: flex; - align-items: center; - justify-content: center; -} - -.xLabel { - text-align: center; - font-size: 12px; - color: var(--text-muted); - padding-left: 48px; -} diff --git a/src/design-system/composites/LineChart/LineChart.tsx b/src/design-system/composites/LineChart/LineChart.tsx deleted file mode 100644 index 96f5c60..0000000 --- a/src/design-system/composites/LineChart/LineChart.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import { useState } from 'react' -import styles from './LineChart.module.css' -import { - computeYScale, - computeXScale, - seriesPoints, - formatAxisLabel, - CHART_COLORS, - type ChartSeries, -} from '../_chart-utils' - -interface Threshold { - value: number - label: string -} - -interface LineChartProps { - series: ChartSeries[] - xLabel?: string - yLabel?: string - threshold?: Threshold - height?: number - width?: number - className?: string -} - -const Y_TICK_COUNT = 4 -const DIMS = { - paddingTop: 12, - paddingRight: 16, - paddingBottom: 28, - paddingLeft: 48, -} - -export function LineChart({ - series, - xLabel, - yLabel, - threshold, - height = 200, - width = 400, - className, -}: LineChartProps) { - const [tooltip, setTooltip] = useState<{ - x: number - y: number - xLabel: string - values: { label: string; value: number; color: string }[] - } | null>(null) - - const dims = { ...DIMS, width, height } - const allData = series.flatMap((s) => s.data) - - if (allData.length === 0) { - return
No data
- } - - const { max, toY } = computeYScale(series, dims, threshold?.value) - const { toX } = computeXScale(series, dims) - const plotH = height - dims.paddingTop - dims.paddingBottom - const plotW = width - dims.paddingLeft - dims.paddingRight - const bottomY = dims.paddingTop + plotH - - const yTicks = Array.from({ length: Y_TICK_COUNT + 1 }, (_, i) => - Math.round((max / Y_TICK_COUNT) * i), - ) - - const firstSeries = series[0] - const xSamples = - firstSeries && firstSeries.data.length > 0 - ? [ - firstSeries.data[0].x, - firstSeries.data[Math.floor(firstSeries.data.length / 2)]?.x, - firstSeries.data[firstSeries.data.length - 1].x, - ].filter(Boolean) - : [] - - function handleMouseMove(e: React.MouseEvent) { - const rect = e.currentTarget.getBoundingClientRect() - const mx = e.clientX - rect.left - const my = e.clientY - rect.top - const pctX = (mx - dims.paddingLeft) / plotW - - const firstS = series[0] - const idx0 = Math.max(0, Math.min(firstS.data.length - 1, Math.round(pctX * (firstS.data.length - 1)))) - const xVal = firstS.data[idx0]?.x - const xLabel = xVal instanceof Date - ? xVal.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }) - : typeof xVal === 'number' && xVal > 1e10 - ? new Date(xVal).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }) - : String(xVal ?? '') - - const values = series.map((s, i) => { - const idx = Math.round(pctX * (s.data.length - 1)) - const clamped = Math.max(0, Math.min(s.data.length - 1, idx)) - const pt = s.data[clamped] - return { - label: s.label, - value: pt?.y ?? 0, - color: s.color ?? CHART_COLORS[i % CHART_COLORS.length], - } - }) - - setTooltip({ x: mx, y: my, xLabel, values }) - } - - return ( -
- {yLabel &&
{yLabel}
} - setTooltip(null)} - aria-label="Line chart" - role="img" - > - {/* Grid lines */} - {yTicks.map((val) => { - const y = toY(val) - return ( - - - - {formatAxisLabel(val)} - - - ) - })} - - {/* X-axis labels */} - {xSamples.map((xVal, i) => { - const xPos = toX(xVal) - const xv = xVal instanceof Date ? xVal : new Date(xVal as number) - const label = - xVal instanceof Date || (typeof xVal === 'number' && xVal > 1e10) - ? xv.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - : formatAxisLabel(xVal as number) - const anchor = i === 0 ? 'start' : i === xSamples.length - 1 ? 'end' : 'middle' - return ( - - {label} - - ) - })} - - {/* Threshold line */} - {threshold && ( - - - - {threshold.label} - - - )} - - {/* Lines only (no area fill) */} - {series.map((s, i) => { - const color = s.color ?? CHART_COLORS[i % CHART_COLORS.length] - const pts = seriesPoints(s, toX, toY) - return ( - - ) - })} - - {/* Crosshair */} - {tooltip && ( - - )} - - - {/* Tooltip */} - {tooltip && ( -
- {tooltip.xLabel && ( -
{tooltip.xLabel}
- )} - {tooltip.values.map((v) => ( -
- - {v.label}: - {formatAxisLabel(v.value)} -
- ))} -
- )} - - {/* Legend */} - {series.length > 1 && ( -
- {series.map((s, i) => ( -
- - {s.label} -
- ))} -
- )} - - {xLabel &&
{xLabel}
} -
- ) -} diff --git a/src/design-system/composites/_chart-utils.ts b/src/design-system/composites/_chart-utils.ts deleted file mode 100644 index da8cbe9..0000000 --- a/src/design-system/composites/_chart-utils.ts +++ /dev/null @@ -1,101 +0,0 @@ -export interface DataPoint { - x: number | Date - y: number -} - -export interface ChartSeries { - label: string - data: DataPoint[] - color?: string -} - -export interface ChartDimensions { - width: number - height: number - paddingTop: number - paddingRight: number - paddingBottom: number - paddingLeft: number -} - -export function computeYScale( - series: ChartSeries[], - dims: ChartDimensions, - threshold?: number, -) { - const allY = series.flatMap((s) => s.data.map((d) => d.y)) - if (threshold != null) allY.push(threshold) - const min = 0 - const max = Math.max(...allY, 1) - const range = max - min - - const plotH = dims.height - dims.paddingTop - dims.paddingBottom - const toY = (val: number) => dims.paddingTop + plotH - ((val - min) / range) * plotH - - return { min, max, range, toY } -} - -export function computeXScale(series: ChartSeries[], dims: ChartDimensions) { - const allX = series.flatMap((s) => - s.data.map((d) => (d.x instanceof Date ? d.x.getTime() : d.x)), - ) - const minX = Math.min(...allX) - const maxX = Math.max(...allX) - const rangeX = maxX - minX || 1 - - const plotW = dims.width - dims.paddingLeft - dims.paddingRight - const toX = (val: number | Date) => { - const v = val instanceof Date ? val.getTime() : val - return dims.paddingLeft + ((v - minX) / rangeX) * plotW - } - - return { minX, maxX, rangeX, toX } -} - -export function seriesPoints( - series: ChartSeries, - toX: (v: number | Date) => number, - toY: (v: number) => number, -): string { - return series.data.map((d) => `${toX(d.x).toFixed(1)},${toY(d.y).toFixed(1)}`).join(' ') -} - -export function seriesPath( - series: ChartSeries, - toX: (v: number | Date) => number, - toY: (v: number) => number, - bottomY: number, -): string { - if (series.data.length === 0) return '' - const pts = series.data.map((d) => `${toX(d.x).toFixed(1)},${toY(d.y).toFixed(1)}`) - const firstX = toX(series.data[0].x).toFixed(1) - const lastX = toX(series.data[series.data.length - 1].x).toFixed(1) - return `M${pts.join(' L')} L${lastX},${bottomY} L${firstX},${bottomY} Z` -} - -export function formatAxisLabel(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) -} - -export function formatXLabel(val: number | Date, _totalPoints: number): string { - if (val instanceof Date || (typeof val === 'number' && val > 1e10)) { - const d = val instanceof Date ? val : new Date(val) - return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - } - return formatAxisLabel(val as number) -} - -// Default color tokens for series (fallback when no color given) -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)', -] diff --git a/src/design-system/composites/index.ts b/src/design-system/composites/index.ts index d423caa..bef25e2 100644 --- a/src/design-system/composites/index.ts +++ b/src/design-system/composites/index.ts @@ -1,8 +1,6 @@ export { Accordion } from './Accordion/Accordion' export { AlertDialog } from './AlertDialog/AlertDialog' -export { AreaChart } from './AreaChart/AreaChart' export { AvatarGroup } from './AvatarGroup/AvatarGroup' -export { BarChart } from './BarChart/BarChart' export { Breadcrumb } from './Breadcrumb/Breadcrumb' export { CommandPalette } from './CommandPalette/CommandPalette' export type { SearchResult, SearchCategory, ScopeFilter } from './CommandPalette/types' @@ -20,7 +18,6 @@ export { KpiStrip } from './KpiStrip/KpiStrip' export type { KpiItem, KpiStripProps } from './KpiStrip/KpiStrip' export type { FeedEvent } from './EventFeed/EventFeed' export { FilterBar } from './FilterBar/FilterBar' -export { LineChart } from './LineChart/LineChart' export { LogViewer } from './LogViewer/LogViewer' export type { LogEntry, LogViewerProps } from './LogViewer/LogViewer' export { LoginDialog } from './LoginForm/LoginDialog' @@ -43,6 +40,11 @@ export { Tabs } from './Tabs/Tabs' export { ToastProvider, useToast } from './Toast/Toast' export { TreeView } from './TreeView/TreeView' -// Chart utilities for consumers using Recharts or custom charts -export { CHART_COLORS } from './_chart-utils' -export type { ChartSeries, DataPoint } from './_chart-utils' +// 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' diff --git a/src/design-system/index.ts b/src/design-system/index.ts index dd485c1..254049a 100644 --- a/src/design-system/index.ts +++ b/src/design-system/index.ts @@ -11,4 +11,4 @@ export { BreadcrumbProvider, useBreadcrumb } from './providers/BreadcrumbProvide export type { BreadcrumbItem } from './providers/BreadcrumbProvider' export * from './utils/hashColor' export * from './utils/timePresets' -export { rechartsTheme } from './utils/rechartsTheme' +