'use client'; import { useMemo, useState } from 'react'; import { Area, AreaChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts'; import type { SuperJSONResult } from 'superjson'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card.js'; import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, type ChartConfig, } from '@/components/ui/chart.js'; import { getCategoryColor } from '@/lib/schemas/geopolitical.js'; import { deserialize } from '@/lib/superjson.js'; import { cn } from '@/lib/utils.js'; import type { GeopoliticalEventRow, OilPriceRow } from '@/actions/conflict.js'; interface OilChartProps { oilData: SuperJSONResult; events?: SuperJSONResult; } interface PivotedRow { timestamp: number; label: string; wti_crude?: number; brent_crude?: number; spread?: number; } const chartConfig: ChartConfig = { brent_crude: { label: 'Brent Crude', color: 'hsl(0, 70%, 55%)' }, wti_crude: { label: 'WTI Crude', color: 'hsl(210, 80%, 55%)' }, spread: { label: 'WTI-Brent Spread', color: 'hsl(45, 80%, 55%)' }, }; export function OilChart({ oilData, events }: OilChartProps) { const [showSpread, setShowSpread] = useState(false); const rows = useMemo(() => deserialize(oilData), [oilData]); const eventRows = useMemo(() => (events ? deserialize(events) : []), [events]); const pivoted = useMemo(() => { const byDay = new Map(); for (const row of rows) { const dayKey = row.timestamp.toISOString().slice(0, 10); if (!byDay.has(dayKey)) { byDay.set(dayKey, { timestamp: row.timestamp.getTime(), label: row.timestamp.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), }); } const pivot = byDay.get(dayKey)!; if (row.commodity === 'wti_crude') pivot.wti_crude = row.price; if (row.commodity === 'brent_crude') pivot.brent_crude = row.price; } const result = Array.from(byDay.values()).sort((a, b) => a.timestamp - b.timestamp); // Calculate spread for (const row of result) { if (row.brent_crude !== undefined && row.wti_crude !== undefined) { row.spread = row.brent_crude - row.wti_crude; } } return result; }, [rows]); // Find event timestamps within data range for annotation markers const eventMarkers = useMemo(() => { if (pivoted.length === 0 || eventRows.length === 0) return []; const minTs = pivoted[0]!.timestamp; const maxTs = pivoted[pivoted.length - 1]!.timestamp; return eventRows.filter(e => { const ts = e.timestamp.getTime(); return ts >= minTs && ts <= maxTs && (e.severity === 'critical' || e.severity === 'high'); }); }, [pivoted, eventRows]); if (pivoted.length === 0) { return ( Oil Prices No oil price data available yet. ); } return (
Oil Prices WTI & Brent crude with geopolitical event annotations
{showSpread ? ( `$${v}`} /> [`$${Number(value).toFixed(2)}`, undefined]} />} /> } /> ) : ( `$${v}`} /> [`$${Number(value).toFixed(2)}/bbl`, undefined]} />} /> } /> )} {/* Event annotations below chart */} {eventMarkers.length > 0 && (
{eventMarkers.map(e => { const catColor = getCategoryColor(e.category); return ( {e.timestamp.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}: {e.title} ); })}
)}
); }