From 5a17af848698a59f77fff93c389d1bca81f64340 Mon Sep 17 00:00:00 2001 From: Syasya Date: Thu, 31 Jul 2025 13:38:19 +0800 Subject: [PATCH] linechart update --- app/(admin)/adminDashboard/page.tsx | 16 +- components/dashboards/EnergyLineChart.tsx | 252 +++++++++++++++++----- package-lock.json | 16 ++ package.json | 1 + 4 files changed, 227 insertions(+), 58 deletions(-) diff --git a/app/(admin)/adminDashboard/page.tsx b/app/(admin)/adminDashboard/page.tsx index fd2f2bb..989a10a 100644 --- a/app/(admin)/adminDashboard/page.tsx +++ b/app/(admin)/adminDashboard/page.tsx @@ -25,6 +25,11 @@ const AdminDashboard = () => { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const siteIdMap: Record = { + '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 = { - '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 = () => {
- - +
diff --git a/components/dashboards/EnergyLineChart.tsx b/components/dashboards/EnergyLineChart.tsx index 4611ff3..851a64a 100644 --- a/components/dashboards/EnergyLineChart.tsx +++ b/components/dashboards/EnergyLineChart.tsx @@ -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(); + + 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); - // 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) => {
- {/* Time range selectors */}
-
@@ -164,3 +315,8 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => { export default EnergyLineChart; + + + + + diff --git a/package-lock.json b/package-lock.json index 4d5fa4d..3e8657c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index aefbe78..4c21650 100644 --- a/package.json +++ b/package.json @@ -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",