Files
ems/frontend/src/components/PriceChart.tsx
Dusan Vojacek ca6bd4ab2a responsivita: výšky grafů přes tailwind chart-*, viewport-fit=cover
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:24:10 +02:00

114 lines
3.6 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react'
import {
CartesianGrid,
Legend,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts'
import { getJson } from '../api/postgrest'
import { instantPragueDay, pragueCalendarDay } from '../lib/pragueDate'
import type { SiteEffectivePriceRow } from '../types/ems'
function parseNum(v: string | number | null | undefined): number | null {
if (v == null) return null
if (typeof v === 'number' && !Number.isNaN(v)) return v
const n = Number(v)
return Number.isFinite(n) ? n : null
}
export type PricePoint = {
label: string
buy: number | null
sell: number | null
}
type Props = {
siteId: number | null
pollMs?: number
}
/** Efektivní nákup / prodej (Kč/kWh) pro dnešní den v Europe/Prague. */
export function PriceChart({ siteId, pollMs = 120_000 }: Props) {
const [points, setPoints] = useState<PricePoint[]>([])
const [ready, setReady] = useState(false)
const load = useCallback(async () => {
if (siteId == null) {
setPoints([])
setReady(true)
return
}
try {
const rows = await getJson<SiteEffectivePriceRow[]>('/vw_site_effective_price', {
site_id: `eq.${siteId}`,
order: 'interval_start.desc',
limit: '200',
})
const today = pragueCalendarDay()
const todayRows = Array.isArray(rows)
? rows.filter((r) => instantPragueDay(r.interval_start) === today)
: []
todayRows.sort((a, b) => new Date(a.interval_start).getTime() - new Date(b.interval_start).getTime())
const mapped: PricePoint[] = todayRows.map((r) => {
const t = new Date(r.interval_start)
return {
label: t.toLocaleTimeString('cs-CZ', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'Europe/Prague',
}),
buy: parseNum(r.effective_buy_price_czk_kwh),
sell: parseNum(r.effective_sell_price_czk_kwh),
}
})
setPoints(mapped)
} catch {
setPoints([])
} finally {
setReady(true)
}
}, [siteId])
useEffect(() => {
void load()
const id = window.setInterval(() => void load(), pollMs)
return () => window.clearInterval(id)
}, [load, pollMs])
if (!ready || points.length === 0) {
return (
<div className="h-chart-sm w-full animate-pulse rounded-xl border border-slate-800 bg-slate-900/40 sm:h-chart-md lg:h-chart-xl" />
)
}
return (
<div className="h-chart-sm w-full rounded-xl border border-slate-800 bg-slate-900/40 p-2 pt-4 sm:h-chart-md lg:h-chart-xl">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={points} margin={{ top: 8, right: 12, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.6} />
<XAxis dataKey="label" tick={{ fill: '#94a3b8', fontSize: 10 }} interval="preserveStartEnd" />
<YAxis
tick={{ fill: '#94a3b8', fontSize: 11 }}
label={{ value: 'Kč/kWh', angle: -90, position: 'insideLeft', fill: '#64748b', fontSize: 11 }}
/>
<Tooltip
contentStyle={{
backgroundColor: '#0f172a',
border: '1px solid #1e293b',
borderRadius: '8px',
}}
/>
<Legend wrapperStyle={{ fontSize: 12 }} />
<Line type="stepAfter" dataKey="buy" name="Nákup" stroke="#f97316" strokeWidth={2} dot={false} connectNulls />
<Line type="stepAfter" dataKey="sell" name="Prodej" stroke="#38bdf8" strokeWidth={2} dot={false} connectNulls />
</LineChart>
</ResponsiveContainer>
</div>
)
}