linechart update

This commit is contained in:
Syasya 2025-07-31 13:38:19 +08:00
parent efdad6dfcc
commit 5a17af8486
4 changed files with 227 additions and 58 deletions

View File

@ -25,6 +25,11 @@ const AdminDashboard = () => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const siteIdMap: Record<SiteName, string> = {
'Site A': 'site_01',
'Site B': 'site_02',
'Site C': 'site_03',
};
const siteParam = searchParams?.get('site');
const validSiteNames: SiteName[] = ['Site A', 'Site B', 'Site C'];
@ -54,11 +59,6 @@ const AdminDashboard = () => {
useEffect(() => {
const fetchData = async () => {
const siteIdMap: Record<SiteName, string> = {
'Site A': 'site_01',
'Site B': 'site_02',
'Site C': 'site_03',
};
const siteId = siteIdMap[selectedSite];
const today = new Date();
@ -190,11 +190,7 @@ const AdminDashboard = () => {
<div className="grid md:grid-cols-2 gap-6 lg:flex-col justify-center">
<div ref={energyChartRef} className="pb-5">
<EnergyLineChart
consumption={timeSeriesData.consumption}
generation={timeSeriesData.generation}
/>
<EnergyLineChart siteId={siteIdMap[selectedSite]} />
</div>
<div ref={monthlyChartRef} className="pb-5">

View File

@ -2,6 +2,18 @@ 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);
@ -11,22 +23,116 @@ interface TimeSeriesEntry {
}
interface EnergyLineChartProps {
consumption: TimeSeriesEntry[];
generation: TimeSeriesEntry[];
siteId: string;
}
const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
function groupTimeSeries(
data: TimeSeriesEntry[],
mode: 'day' | 'daily' | 'weekly' | 'monthly' | 'yearly'
): TimeSeriesEntry[] {
const groupMap = new Map<string, number[]>();
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<any>(null);
const [viewMode, setViewMode] = useState<'day' | 'daily' | 'weekly' | 'monthly' | 'yearly'>('day');
const [consumption, setConsumption] = useState<TimeSeriesEntry[]>([]);
const [generation, setGeneration] = useState<TimeSeriesEntry[]>([]);
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);
// Generate sorted unique time labels from both series
const allTimes = Array.from(new Set([
...consumption.map(d => d.time),
...generation.map(d => d.time),
])).sort(); // e.g., ["00:00", "00:30", "01:00", ...]
...groupedConsumption.map(d => d.time),
...groupedGeneration.map(d => d.time),
])).sort();
// Map times to values
const consumptionMap = Object.fromEntries(consumption.map(d => [d.time, d.value]));
const generationMap = Object.fromEntries(generation.map(d => [d.time, d.value]));
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);
@ -37,6 +143,22 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
}
}, []);
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);
@ -46,7 +168,7 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
const yAxisSuggestedMax = maxValue * 1.15;
const data = {
labels: filteredLabels,
labels: filteredLabels.map(formatLabel),
datasets: [
{
label: 'Consumption',
@ -66,44 +188,48 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
};
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' as const },
zoom: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' as const },
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
mode: 'x' as const,
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
mode: 'x' as const,
},
pan: { enabled: true, mode: 'x' as const },
},
pan: { enabled: true, mode: 'x' as const },
tooltip: { enabled: true, mode: 'index' as const, intersect: false },
},
tooltip: { enabled: true, mode: 'index' as const, intersect: false },
},
scales: {
x: {
title: {
display: true,
text: 'Time (HH:MM)',
font: {
weight: 'bold' as const, // ✅ FIX: cast as 'const'
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 },
},
},
},
y: {
beginAtZero: true,
suggestedMax: yAxisSuggestedMax,
title: {
display: true,
text: 'Power (kW)',
font: {
weight: 'bold' as const, // ✅ FIX: cast as 'const'
},
},
},
},
} as const; // ✅ Ensures compatibility with chart.js types
} as const;
const handleResetZoom = () => {
chartRef.current?.resetZoom();
@ -119,9 +245,20 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
</button>
</div>
{/* Time range selectors */}
<div className="mb-4 flex gap-4 items-center">
<label className='font-medium'>
{viewMode === 'day' && (
<label className="font-medium">
Date:{' '}
<input
type="date"
value={selectedDate.toISOString().split('T')[0]}
onChange={(e) => setSelectedDate(new Date(e.target.value))}
className="border rounded p-1"
/>
</label>
)}
<label className="font-medium">
From:{' '}
<select
value={startIndex}
@ -132,11 +269,11 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
className="border rounded p-1"
>
{allTimes.map((label, idx) => (
<option key={idx} value={idx}>{label}</option>
<option key={idx} value={idx}>{formatLabel(label)}</option>
))}
</select>
</label>
<label className='font-medium'>
<label className="font-medium">
To:{' '}
<select
value={endIndex}
@ -147,10 +284,24 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
className="border rounded p-1"
>
{allTimes.map((label, idx) => (
<option key={idx} value={idx}>{label}</option>
<option key={idx} value={idx}>{formatLabel(label)}</option>
))}
</select>
</label>
<label className="font-medium">
View:{' '}
<select
value={viewMode}
onChange={(e) => setViewMode(e.target.value as typeof viewMode)}
className="border rounded p-1"
>
<option value="day">Day</option>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="yearly">Yearly</option>
</select>
</label>
</div>
<div className="h-96 w-full">
@ -164,3 +315,8 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
export default EnergyLineChart;

16
package-lock.json generated
View File

@ -23,6 +23,7 @@
"chart.js": "^4.4.9",
"chartjs-plugin-zoom": "^2.2.0",
"cookie": "^1.0.2",
"date-fns": "^4.1.0",
"eslint": "8.32.0",
"eslint-config-next": "13.1.2",
"html2canvas": "^1.4.1",
@ -1974,6 +1975,16 @@
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -7633,6 +7644,11 @@
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="
},
"date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",

View File

@ -24,6 +24,7 @@
"chart.js": "^4.4.9",
"chartjs-plugin-zoom": "^2.2.0",
"cookie": "^1.0.2",
"date-fns": "^4.1.0",
"eslint": "8.32.0",
"eslint-config-next": "13.1.2",
"html2canvas": "^1.4.1",