Files
mockupAWS/frontend/src/components/charts/ComparisonBar.tsx
T
Luca Sacchi Ricciardi 285a748d6a
E2E Tests / Run E2E Tests (push) Has been cancelled
E2E Tests / Visual Regression Tests (push) Has been cancelled
E2E Tests / Smoke Tests (push) Has been cancelled
fix: update HTML title to mockupAWS
- Change generic 'frontend' title to 'mockupAWS - AWS Cost Simulator'
- Resolves frontend branding issue identified in testing
2026-04-07 18:45:02 +02:00

254 lines
7.5 KiB
TypeScript

import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
Cell,
} from 'recharts';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { CHART_PALETTE, formatCurrency, formatNumber } from './chart-utils';
import type { Scenario } from '@/types/api';
interface ComparisonMetric {
key: string;
name: string;
value: number;
}
interface ScenarioComparison {
scenario: Scenario;
metrics: ComparisonMetric[];
}
interface ComparisonBarChartProps {
scenarios: ScenarioComparison[];
metricKey: string;
title?: string;
description?: string;
isCurrency?: boolean;
}
interface ChartDataPoint {
name: string;
value: number;
color: string;
}
// Tooltip component defined outside main component
interface BarTooltipProps {
active?: boolean;
payload?: Array<{ payload: ChartDataPoint }>;
formatter?: (value: number) => string;
}
function BarTooltip({ active, payload, formatter }: BarTooltipProps) {
if (active && payload && payload.length && formatter) {
const item = payload[0].payload;
return (
<div className="rounded-lg border bg-popover p-3 shadow-md">
<p className="font-medium text-popover-foreground">{item.name}</p>
<p className="text-sm text-muted-foreground">
{formatter(item.value)}
</p>
</div>
);
}
return null;
}
export function ComparisonBarChart({
scenarios,
metricKey,
title = 'Scenario Comparison',
description,
isCurrency = false,
}: ComparisonBarChartProps) {
const chartData: ChartDataPoint[] = scenarios.map((s, index) => ({
name: s.scenario.name,
value: s.metrics.find((m) => m.key === metricKey)?.value || 0,
color: CHART_PALETTE[index % CHART_PALETTE.length],
}));
const formatter = isCurrency ? formatCurrency : formatNumber;
// Find min/max for color coding
const values = chartData.map((d) => d.value);
const minValue = Math.min(...values);
const maxValue = Math.max(...values);
const getBarColor = (value: number) => {
// For cost metrics, lower is better (green), higher is worse (red)
// For other metrics, higher is better
if (metricKey.includes('cost')) {
if (value === minValue) return '#10B981'; // Green for lowest cost
if (value === maxValue) return '#EF4444'; // Red for highest cost
} else {
if (value === maxValue) return '#10B981'; // Green for highest value
if (value === minValue) return '#EF4444'; // Red for lowest value
}
return '#F59E0B'; // Yellow for middle values
};
return (
<Card className="w-full">
<CardHeader className="pb-2">
<CardTitle className="text-lg font-semibold">{title}</CardTitle>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</CardHeader>
<CardContent>
<div className="h-[350px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={chartData}
margin={{ top: 20, right: 30, left: 20, bottom: 60 }}
layout="vertical"
>
<CartesianGrid
strokeDasharray="3 3"
stroke="hsl(var(--border))"
opacity={0.3}
horizontal={false}
/>
<XAxis
type="number"
tickFormatter={formatter}
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
type="category"
dataKey="name"
width={120}
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
interval={0}
/>
<Tooltip content={<BarTooltip formatter={formatter} />} />
<Bar
dataKey="value"
radius={[0, 4, 4, 0]}
animationDuration={800}
>
{chartData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={getBarColor(entry.value)}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<div className="flex justify-center gap-4 mt-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<span className="h-3 w-3 rounded-full bg-green-500" />
Best
</span>
<span className="flex items-center gap-1">
<span className="h-3 w-3 rounded-full bg-yellow-500" />
Average
</span>
<span className="flex items-center gap-1">
<span className="h-3 w-3 rounded-full bg-red-500" />
Worst
</span>
</div>
</CardContent>
</Card>
);
}
// Horizontal grouped bar chart for multi-metric comparison
export function GroupedComparisonChart({
scenarios,
metricKeys,
title = 'Multi-Metric Comparison',
description,
}: {
scenarios: ScenarioComparison[];
metricKeys: Array<{ key: string; name: string; isCurrency?: boolean }>;
title?: string;
description?: string;
}) {
// Transform data for grouped bar chart
const chartData = scenarios.map((s) => {
const dataPoint: Record<string, string | number> = {
name: s.scenario.name,
};
metricKeys.forEach((mk) => {
const metric = s.metrics.find((m) => m.key === mk.key);
dataPoint[mk.key] = metric?.value || 0;
});
return dataPoint;
});
return (
<Card className="w-full">
<CardHeader className="pb-2">
<CardTitle className="text-lg font-semibold">{title}</CardTitle>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</CardHeader>
<CardContent>
<div className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={chartData}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid
strokeDasharray="3 3"
stroke="hsl(var(--border))"
opacity={0.3}
/>
<XAxis
dataKey="name"
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--popover))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px',
}}
labelStyle={{ color: 'hsl(var(--popover-foreground))' }}
itemStyle={{ color: 'hsl(var(--popover-foreground))' }}
/>
<Legend wrapperStyle={{ paddingTop: '20px' }} />
{metricKeys.map((mk, index) => (
<Bar
key={mk.key}
dataKey={mk.key}
name={mk.name}
fill={CHART_PALETTE[index % CHART_PALETTE.length]}
radius={[4, 4, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
}