285a748d6a
- Change generic 'frontend' title to 'mockupAWS - AWS Cost Simulator' - Resolves frontend branding issue identified in testing
254 lines
7.5 KiB
TypeScript
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>
|
|
);
|
|
}
|