busi488energy/src/components/charts/correlation-chart.tsx
Joey Eamigh 3251e30a2e
fix: use direct hex color for correlation chart axis labels
CSS variable hsl(var(--muted-foreground)) doesn't resolve in SVG
context. Use #a1a1aa (zinc-400) directly for reliable visibility
on dark background.
2026-02-11 14:48:26 -05:00

179 lines
5.8 KiB
TypeScript

'use client';
import { useMemo } from 'react';
import { CartesianGrid, Label, Scatter, ScatterChart, XAxis, YAxis, ZAxis } from 'recharts';
import type { SuperJSONResult } from 'superjson';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card.js';
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from '@/components/ui/chart.js';
import { deserialize } from '@/lib/superjson.js';
interface RegionCapacityPrice {
region_code: string;
avg_price: number;
total_capacity_mw: number;
}
interface CorrelationChartProps {
data: SuperJSONResult;
}
const REGION_COLORS: Record<string, string> = {
PJM: 'hsl(210, 90%, 55%)',
ERCOT: 'hsl(25, 90%, 55%)',
CAISO: 'hsl(140, 70%, 45%)',
NYISO: 'hsl(280, 70%, 55%)',
ISONE: 'hsl(340, 70%, 55%)',
MISO: 'hsl(55, 80%, 50%)',
SPP: 'hsl(180, 60%, 45%)',
};
const chartConfig: ChartConfig = {
correlation: {
label: 'DC Capacity vs. Avg Price',
color: 'hsl(210, 90%, 55%)',
},
};
interface ScatterPoint {
region_code: string;
total_capacity_mw: number;
avg_price: number;
fill: string;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function getNumberProp(obj: Record<string, unknown>, key: string): number {
const val = obj[key];
return typeof val === 'number' ? val : 0;
}
function getScatterPayload(obj: Record<string, unknown>): ScatterPoint | null {
const payload = obj['payload'];
if (!isRecord(payload)) return null;
const regionCode = typeof payload['region_code'] === 'string' ? payload['region_code'] : '';
const fill = typeof payload['fill'] === 'string' ? payload['fill'] : 'hsl(0,0%,50%)';
return {
region_code: regionCode,
total_capacity_mw: getNumberProp(payload, 'total_capacity_mw'),
avg_price: getNumberProp(payload, 'avg_price'),
fill,
};
}
function CustomDot(props: unknown): React.JSX.Element {
if (!isRecord(props)) return <g />;
const cx = getNumberProp(props, 'cx');
const cy = getNumberProp(props, 'cy');
const payload = getScatterPayload(props);
if (!payload) return <g />;
return (
<g>
<circle cx={cx} cy={cy} r={8} fill={payload.fill} fillOpacity={0.7} stroke={payload.fill} strokeWidth={2} />
<text x={cx} y={cy - 14} textAnchor="middle" fill="hsl(var(--foreground))" fontSize={11} fontWeight={600}>
{payload.region_code}
</text>
</g>
);
}
export function CorrelationChart({ data }: CorrelationChartProps) {
const rows = useMemo(() => deserialize<RegionCapacityPrice[]>(data), [data]);
const scatterData: ScatterPoint[] = useMemo(
() =>
rows
.filter(r => r.total_capacity_mw > 0 || r.avg_price > 0)
.map(r => ({
region_code: r.region_code,
total_capacity_mw: Math.round(r.total_capacity_mw),
avg_price: Number(r.avg_price.toFixed(2)),
fill: REGION_COLORS[r.region_code] ?? 'hsl(0, 0%, 50%)',
})),
[rows],
);
if (scatterData.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle>DC Capacity vs. Electricity Price</CardTitle>
<CardDescription>No data available for correlation analysis.</CardDescription>
</CardHeader>
<CardContent>
<div className="flex h-64 items-center justify-center rounded-lg border border-dashed border-border">
<p className="text-sm text-muted-foreground">
No data available. Ingest price data and seed datacenters first.
</p>
</div>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle>DC Capacity vs. Electricity Price</CardTitle>
<CardDescription>Datacenter capacity (MW) versus average electricity price per region</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-[350px] w-full">
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-border/30" />
<XAxis
type="number"
dataKey="total_capacity_mw"
name="DC Capacity"
tick={{ fontSize: 11, fill: '#a1a1aa' }}
tickLine={false}
axisLine={false}
tickFormatter={(v: number) => `${v} MW`}>
<Label
value="Total DC Capacity (MW)"
offset={-10}
position="insideBottom"
style={{ fontSize: 11, fill: '#a1a1aa' }}
/>
</XAxis>
<YAxis
type="number"
dataKey="avg_price"
name="Avg Price"
tick={{ fontSize: 11, fill: '#a1a1aa' }}
tickLine={false}
axisLine={false}
tickFormatter={(v: number) => `$${v}`}>
<Label
value="Avg Price ($/MWh)"
angle={-90}
position="insideLeft"
style={{ fontSize: 11, fill: '#a1a1aa' }}
/>
</YAxis>
<ZAxis range={[200, 200]} />
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value, name) => {
const nameStr = String(name);
const valStr = String(value);
if (nameStr === 'DC Capacity') return [`${valStr} MW`, undefined];
if (nameStr === 'Avg Price') return [`$${valStr}/MWh`, undefined];
return [valStr, undefined];
}}
/>
}
/>
<Scatter name="Regions" data={scatterData} shape={CustomDot} />
</ScatterChart>
</ChartContainer>
</CardContent>
</Card>
);
}