linechart update
This commit is contained in:
parent
efdad6dfcc
commit
5a17af8486
@ -25,6 +25,11 @@ const AdminDashboard = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
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 siteParam = searchParams?.get('site');
|
||||||
const validSiteNames: SiteName[] = ['Site A', 'Site B', 'Site C'];
|
const validSiteNames: SiteName[] = ['Site A', 'Site B', 'Site C'];
|
||||||
@ -54,11 +59,6 @@ const AdminDashboard = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
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 siteId = siteIdMap[selectedSite];
|
||||||
const today = new Date();
|
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 className="grid md:grid-cols-2 gap-6 lg:flex-col justify-center">
|
||||||
<div ref={energyChartRef} className="pb-5">
|
<div ref={energyChartRef} className="pb-5">
|
||||||
<EnergyLineChart
|
<EnergyLineChart siteId={siteIdMap[selectedSite]} />
|
||||||
consumption={timeSeriesData.consumption}
|
|
||||||
generation={timeSeriesData.generation}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div ref={monthlyChartRef} className="pb-5">
|
<div ref={monthlyChartRef} className="pb-5">
|
||||||
|
@ -2,6 +2,18 @@ import React, { useRef, useEffect, useState } from 'react';
|
|||||||
import { Line } from 'react-chartjs-2';
|
import { Line } from 'react-chartjs-2';
|
||||||
import ChartJS from 'chart.js/auto';
|
import ChartJS from 'chart.js/auto';
|
||||||
import zoomPlugin from 'chartjs-plugin-zoom';
|
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);
|
ChartJS.register(zoomPlugin);
|
||||||
|
|
||||||
@ -11,22 +23,116 @@ interface TimeSeriesEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface EnergyLineChartProps {
|
interface EnergyLineChartProps {
|
||||||
consumption: TimeSeriesEntry[];
|
siteId: string;
|
||||||
generation: TimeSeriesEntry[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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([
|
const allTimes = Array.from(new Set([
|
||||||
...consumption.map(d => d.time),
|
...groupedConsumption.map(d => d.time),
|
||||||
...generation.map(d => d.time),
|
...groupedGeneration.map(d => d.time),
|
||||||
])).sort(); // e.g., ["00:00", "00:30", "01:00", ...]
|
])).sort();
|
||||||
|
|
||||||
// Map times to values
|
const consumptionMap = Object.fromEntries(groupedConsumption.map(d => [d.time, d.value]));
|
||||||
const consumptionMap = Object.fromEntries(consumption.map(d => [d.time, d.value]));
|
const generationMap = Object.fromEntries(groupedGeneration.map(d => [d.time, d.value]));
|
||||||
const generationMap = Object.fromEntries(generation.map(d => [d.time, d.value]));
|
|
||||||
|
|
||||||
const [startIndex, setStartIndex] = useState(0);
|
const [startIndex, setStartIndex] = useState(0);
|
||||||
const [endIndex, setEndIndex] = useState(allTimes.length - 1);
|
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 filteredLabels = allTimes.slice(startIndex, endIndex + 1);
|
||||||
const filteredConsumption = filteredLabels.map(t => consumptionMap[t] ?? null);
|
const filteredConsumption = filteredLabels.map(t => consumptionMap[t] ?? null);
|
||||||
const filteredGeneration = filteredLabels.map(t => generationMap[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 yAxisSuggestedMax = maxValue * 1.15;
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
labels: filteredLabels,
|
labels: filteredLabels.map(formatLabel),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Consumption',
|
label: 'Consumption',
|
||||||
@ -84,10 +206,17 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
|
|||||||
x: {
|
x: {
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Time (HH:MM)',
|
text:
|
||||||
font: {
|
viewMode === 'day'
|
||||||
weight: 'bold' as const, // ✅ FIX: cast as 'const'
|
? 'Time (HH:MM)'
|
||||||
},
|
: viewMode === 'daily'
|
||||||
|
? 'Day'
|
||||||
|
: viewMode === 'weekly'
|
||||||
|
? 'Week'
|
||||||
|
: viewMode === 'monthly'
|
||||||
|
? 'Month'
|
||||||
|
: 'Year',
|
||||||
|
font: { weight: 'bold' as const },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
@ -96,14 +225,11 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
|
|||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Power (kW)',
|
text: 'Power (kW)',
|
||||||
font: {
|
font: { weight: 'bold' as const },
|
||||||
weight: 'bold' as const, // ✅ FIX: cast as 'const'
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
} as const;
|
||||||
} as const; // ✅ Ensures compatibility with chart.js types
|
|
||||||
|
|
||||||
|
|
||||||
const handleResetZoom = () => {
|
const handleResetZoom = () => {
|
||||||
chartRef.current?.resetZoom();
|
chartRef.current?.resetZoom();
|
||||||
@ -119,9 +245,20 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Time range selectors */}
|
|
||||||
<div className="mb-4 flex gap-4 items-center">
|
<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:{' '}
|
From:{' '}
|
||||||
<select
|
<select
|
||||||
value={startIndex}
|
value={startIndex}
|
||||||
@ -132,11 +269,11 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
|
|||||||
className="border rounded p-1"
|
className="border rounded p-1"
|
||||||
>
|
>
|
||||||
{allTimes.map((label, idx) => (
|
{allTimes.map((label, idx) => (
|
||||||
<option key={idx} value={idx}>{label}</option>
|
<option key={idx} value={idx}>{formatLabel(label)}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label className='font-medium'>
|
<label className="font-medium">
|
||||||
To:{' '}
|
To:{' '}
|
||||||
<select
|
<select
|
||||||
value={endIndex}
|
value={endIndex}
|
||||||
@ -147,10 +284,24 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
|
|||||||
className="border rounded p-1"
|
className="border rounded p-1"
|
||||||
>
|
>
|
||||||
{allTimes.map((label, idx) => (
|
{allTimes.map((label, idx) => (
|
||||||
<option key={idx} value={idx}>{label}</option>
|
<option key={idx} value={idx}>{formatLabel(label)}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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>
|
||||||
|
|
||||||
<div className="h-96 w-full">
|
<div className="h-96 w-full">
|
||||||
@ -164,3 +315,8 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
|
|||||||
export default EnergyLineChart;
|
export default EnergyLineChart;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
16
package-lock.json
generated
16
package-lock.json
generated
@ -23,6 +23,7 @@
|
|||||||
"chart.js": "^4.4.9",
|
"chart.js": "^4.4.9",
|
||||||
"chartjs-plugin-zoom": "^2.2.0",
|
"chartjs-plugin-zoom": "^2.2.0",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"eslint": "8.32.0",
|
"eslint": "8.32.0",
|
||||||
"eslint-config-next": "13.1.2",
|
"eslint-config-next": "13.1.2",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
@ -1974,6 +1975,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="
|
"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": {
|
"debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
"chart.js": "^4.4.9",
|
"chart.js": "^4.4.9",
|
||||||
"chartjs-plugin-zoom": "^2.2.0",
|
"chartjs-plugin-zoom": "^2.2.0",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"eslint": "8.32.0",
|
"eslint": "8.32.0",
|
||||||
"eslint-config-next": "13.1.2",
|
"eslint-config-next": "13.1.2",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user