import React, { useRef, useEffect, useState } from 'react'; import { Line } from 'react-chartjs-2'; import ChartJS from 'chart.js/auto'; import zoomPlugin from 'chartjs-plugin-zoom'; import { getISOWeek, startOfDay, endOfDay, startOfWeek, endOfWeek, startOfMonth, endOfMonth, startOfYear, endOfYear, } from 'date-fns'; import { fetchPowerTimeseries } from '@/app/utils/api'; ChartJS.register(zoomPlugin); interface TimeSeriesEntry { time: string; value: number; } interface EnergyLineChartProps { siteId: string; } function groupTimeSeries( data: TimeSeriesEntry[], mode: 'day' | 'daily' | 'weekly' | 'monthly' | 'yearly' ): TimeSeriesEntry[] { const groupMap = new Map(); for (const entry of data) { const date = new Date(entry.time); let key = ''; switch (mode) { case 'day': const local = new Date(date.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' })); const hour = local.getHours(); const minute = local.getMinutes() < 30 ? '00' : '30'; key = `${hour.toString().padStart(2, '0')}:${minute}`; break; case 'daily': key = date.toLocaleDateString('en-MY', { timeZone: 'Asia/Kuala_Lumpur', weekday: 'short', day: '2-digit', month: 'short', }); break; case 'weekly': key = `${date.getFullYear()}-W${String(getISOWeek(date)).padStart(2, '0')}`; break; case 'monthly': key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; break; case 'yearly': key = date.getFullYear().toString(); break; } if (!groupMap.has(key)) groupMap.set(key, []); groupMap.get(key)!.push(entry.value); } return Array.from(groupMap.entries()).map(([time, values]) => ({ time, value: values.reduce((sum, v) => sum + v, 0), })); } const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => { const chartRef = useRef(null); const [viewMode, setViewMode] = useState<'day' | 'daily' | 'weekly' | 'monthly' | 'yearly'>('day'); const [consumption, setConsumption] = useState([]); const [generation, setGeneration] = useState([]); const [selectedDate, setSelectedDate] = useState(new Date()); useEffect(() => { const now = new Date(); let start: Date; let end: Date; switch (viewMode) { case 'day': start = startOfDay(selectedDate); end = endOfDay(selectedDate); break; case 'daily': start = startOfWeek(now, { weekStartsOn: 1 }); end = endOfWeek(now, { weekStartsOn: 1 }); break; case 'weekly': start = startOfMonth(now); end = endOfMonth(now); break; case 'monthly': start = startOfYear(now); end = endOfYear(now); break; case 'yearly': start = new Date('2020-01-01'); end = now; break; } const isoStart = start.toISOString(); const isoEnd = end.toISOString(); const fetchData = async () => { try { const res = await fetchPowerTimeseries(siteId, isoStart, isoEnd); setConsumption(res.consumption); setGeneration(res.generation); } catch (error) { console.error('Failed to fetch energy timeseries:', error); } }; fetchData(); }, [siteId, viewMode, selectedDate]); const groupedConsumption = groupTimeSeries(consumption, viewMode); const groupedGeneration = groupTimeSeries(generation, viewMode); const allTimes = Array.from(new Set([ ...groupedConsumption.map(d => d.time), ...groupedGeneration.map(d => d.time), ])).sort((a, b) => new Date(a).getTime() - new Date(b).getTime()); const consumptionMap = Object.fromEntries(groupedConsumption.map(d => [d.time, d.value])); const generationMap = Object.fromEntries(groupedGeneration.map(d => [d.time, d.value])); const [startIndex, setStartIndex] = useState(0); const [endIndex, setEndIndex] = useState(allTimes.length - 1); useEffect(() => { if (typeof window !== 'undefined') { import('hammerjs'); } }, []); useEffect(() => { setStartIndex(0); setEndIndex(allTimes.length - 1); }, [viewMode, allTimes.length]); const formatLabel = (key: string) => { switch (viewMode) { case 'monthly': return new Date(`${key}-01`).toLocaleString('en-GB', { month: 'short', year: 'numeric' }); case 'weekly': return key.replace('-', ' '); default: return key; } }; const filteredLabels = allTimes.slice(startIndex, endIndex + 1); const filteredConsumption = filteredLabels.map(t => consumptionMap[t] ?? null); const filteredGeneration = filteredLabels.map(t => generationMap[t] ?? null); const allValues = [...filteredConsumption, ...filteredGeneration].filter(v => v !== null) as number[]; const maxValue = allValues.length > 0 ? Math.max(...allValues) : 0; const yAxisSuggestedMax = maxValue * 1.15; const data = { labels: filteredLabels.map(formatLabel), datasets: [ { label: 'Consumption', data: filteredConsumption, borderColor: '#8884d8', tension: 0.4, fill: false, }, { label: 'Generation', data: filteredGeneration, borderColor: '#82ca9d', tension: 0.4, fill: false, }, ], }; const options = { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top' as const }, zoom: { zoom: { wheel: { enabled: true }, pinch: { enabled: true }, mode: 'x' as const, }, pan: { enabled: true, mode: 'x' as const }, }, tooltip: { enabled: true, mode: 'index' as const, intersect: false }, }, scales: { x: { title: { display: true, text: viewMode === 'day' ? 'Time (HH:MM)' : viewMode === 'daily' ? 'Day' : viewMode === 'weekly' ? 'Week' : viewMode === 'monthly' ? 'Month' : 'Year', font: { weight: 'bold' as const }, }, }, y: { beginAtZero: true, suggestedMax: yAxisSuggestedMax, title: { display: true, text: 'Power (kW)', font: { weight: 'bold' as const }, }, }, }, } as const; const handleResetZoom = () => { chartRef.current?.resetZoom(); }; return (

Energy Consumption & Generation

{viewMode === 'day' && ( )}
); }; export default EnergyLineChart;