correct data queries
This commit is contained in:
parent
0885771131
commit
a82e62b9b4
@ -19,6 +19,7 @@ import DatePicker from 'react-datepicker';
|
|||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
import './datepicker-dark.css'; // custom dark mode styles
|
import './datepicker-dark.css'; // custom dark mode styles
|
||||||
|
|
||||||
|
|
||||||
ChartJS.register(zoomPlugin);
|
ChartJS.register(zoomPlugin);
|
||||||
|
|
||||||
interface TimeSeriesEntry {
|
interface TimeSeriesEntry {
|
||||||
@ -30,9 +31,48 @@ interface EnergyLineChartProps {
|
|||||||
siteId: string;
|
siteId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function powerSeriesToEnergySeries(
|
||||||
|
data: TimeSeriesEntry[],
|
||||||
|
guessMinutes = 30
|
||||||
|
): TimeSeriesEntry[] {
|
||||||
|
if (!data?.length) return [];
|
||||||
|
|
||||||
|
// Ensure ascending by time
|
||||||
|
const sorted = [...data].sort(
|
||||||
|
(a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
const out: TimeSeriesEntry[] = [];
|
||||||
|
let lastDeltaMs: number | null = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < sorted.length; i++) {
|
||||||
|
const t0 = new Date(sorted[i].time).getTime();
|
||||||
|
const p0 = sorted[i].value; // kW
|
||||||
|
|
||||||
|
let deltaMs: number;
|
||||||
|
if (i < sorted.length - 1) {
|
||||||
|
const t1 = new Date(sorted[i + 1].time).getTime();
|
||||||
|
deltaMs = Math.max(0, t1 - t0);
|
||||||
|
if (deltaMs > 0) lastDeltaMs = deltaMs;
|
||||||
|
} else {
|
||||||
|
// For the last point, assume previous cadence or a guess
|
||||||
|
deltaMs = lastDeltaMs ?? guessMinutes * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = deltaMs / (1000 * 60 * 60);
|
||||||
|
const kwh = p0 * hours; // kW * h = kWh
|
||||||
|
|
||||||
|
out.push({ time: sorted[i].time, value: kwh });
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function groupTimeSeries(
|
function groupTimeSeries(
|
||||||
data: TimeSeriesEntry[],
|
data: TimeSeriesEntry[],
|
||||||
mode: 'day' | 'daily' | 'weekly' | 'monthly' | 'yearly'
|
mode: 'day' | 'daily' | 'weekly' | 'monthly' | 'yearly',
|
||||||
|
agg: 'mean' | 'max' | 'sum' = 'mean'
|
||||||
): TimeSeriesEntry[] {
|
): TimeSeriesEntry[] {
|
||||||
const groupMap = new Map<string, number[]>();
|
const groupMap = new Map<string, number[]>();
|
||||||
|
|
||||||
@ -41,19 +81,22 @@ function groupTimeSeries(
|
|||||||
let key = '';
|
let key = '';
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'day':
|
case 'day': {
|
||||||
const local = new Date(date.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' }));
|
const local = new Date(
|
||||||
const hour = local.getHours();
|
date.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' })
|
||||||
const minute = local.getMinutes() < 30 ? '00' : '30';
|
);
|
||||||
const adjusted = new Date(local.setMinutes(minute === '00' ? 0 : 30, 0)); // zero seconds
|
const minute = local.getMinutes() < 30 ? 0 : 30;
|
||||||
key = adjusted.toISOString(); // ✅ full timestamp key
|
local.setMinutes(minute, 0, 0);
|
||||||
|
key = local.toISOString();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'daily':
|
case 'daily':
|
||||||
key = date.toLocaleDateString('en-MY', {
|
key = date.toLocaleDateString('en-MY', {
|
||||||
timeZone: 'Asia/Kuala_Lumpur',
|
timeZone: 'Asia/Kuala_Lumpur',
|
||||||
weekday: 'short',
|
weekday: 'short',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'weekly':
|
case 'weekly':
|
||||||
@ -71,11 +114,18 @@ function groupTimeSeries(
|
|||||||
groupMap.get(key)!.push(entry.value);
|
groupMap.get(key)!.push(entry.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(groupMap.entries()).map(([time, values]) => ({
|
return Array.from(groupMap.entries()).map(([time, values]) => {
|
||||||
time,
|
if (agg === 'sum') {
|
||||||
value: values.reduce((sum, v) => sum + v, 0),
|
const sum = values.reduce((a, b) => a + b, 0);
|
||||||
}));
|
return { time, value: sum };
|
||||||
}
|
}
|
||||||
|
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
||||||
|
const max = values.reduce((a, b) => (b > a ? b : a), -Infinity);
|
||||||
|
return { time, value: agg === 'max' ? max : mean };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
||||||
const chartRef = useRef<any>(null);
|
const chartRef = useRef<any>(null);
|
||||||
@ -85,6 +135,94 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
|||||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||||
const [forecast, setForecast] = useState<TimeSeriesEntry[]>([]);
|
const [forecast, setForecast] = useState<TimeSeriesEntry[]>([]);
|
||||||
|
|
||||||
|
const LIVE_REFRESH_MS = 300000; // 5min when viewing a single day
|
||||||
|
const SLOW_REFRESH_MS = 600000; // 10min for weekly/monthly/yearly
|
||||||
|
|
||||||
|
|
||||||
|
const fetchAndSet = React.useCallback(async () => {
|
||||||
|
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();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetchPowerTimeseries(siteId, isoStart, isoEnd);
|
||||||
|
setConsumption(res.consumption);
|
||||||
|
setGeneration(res.generation);
|
||||||
|
|
||||||
|
// Forecast only needs updating for the selected day
|
||||||
|
const forecastData = await fetchForecast(3.15, 101.7, 37, 0, 25.67);
|
||||||
|
const selectedDateStr = selectedDate.toISOString().split('T')[0];
|
||||||
|
setForecast(
|
||||||
|
forecastData
|
||||||
|
.filter(({ time }: any) => time.startsWith(selectedDateStr))
|
||||||
|
.map(({ time, forecast }: any) => ({ time, value: forecast }))
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch energy timeseries:', error);
|
||||||
|
}
|
||||||
|
}, [siteId, viewMode, selectedDate]);
|
||||||
|
|
||||||
|
// 3) Auto-refresh effect: initial load + interval (pauses when tab hidden)
|
||||||
|
useEffect(() => {
|
||||||
|
let timer: number | undefined;
|
||||||
|
|
||||||
|
const tick = async () => {
|
||||||
|
// Avoid wasted calls when the tab is in the background
|
||||||
|
if (!document.hidden) {
|
||||||
|
await fetchAndSet();
|
||||||
|
}
|
||||||
|
const ms = viewMode === 'day' ? LIVE_REFRESH_MS : SLOW_REFRESH_MS;
|
||||||
|
timer = window.setTimeout(tick, ms);
|
||||||
|
};
|
||||||
|
|
||||||
|
// initial load
|
||||||
|
fetchAndSet();
|
||||||
|
|
||||||
|
// schedule next cycles
|
||||||
|
timer = window.setTimeout(tick, viewMode === 'day' ? LIVE_REFRESH_MS : SLOW_REFRESH_MS);
|
||||||
|
|
||||||
|
const onVis = () => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
// kick immediately when user returns
|
||||||
|
clearTimeout(timer);
|
||||||
|
tick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('visibilitychange', onVis);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
document.removeEventListener('visibilitychange', onVis);
|
||||||
|
};
|
||||||
|
}, [fetchAndSet, viewMode]);
|
||||||
|
|
||||||
|
|
||||||
function useIsDarkMode() {
|
function useIsDarkMode() {
|
||||||
const [isDark, setIsDark] = useState(() =>
|
const [isDark, setIsDark] = useState(() =>
|
||||||
typeof document !== 'undefined'
|
typeof document !== 'undefined'
|
||||||
@ -140,7 +278,7 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
|||||||
setGeneration(res.generation);
|
setGeneration(res.generation);
|
||||||
|
|
||||||
// ⬇️ ADD THIS here — fetch forecast
|
// ⬇️ ADD THIS here — fetch forecast
|
||||||
const forecastData = await fetchForecast(3.15, 101.7, 37, 0, 20.67);
|
const forecastData = await fetchForecast(3.15, 101.7, 37, 0, 30.67);
|
||||||
const selectedDateStr = selectedDate.toISOString().split('T')[0];
|
const selectedDateStr = selectedDate.toISOString().split('T')[0];
|
||||||
|
|
||||||
setForecast(
|
setForecast(
|
||||||
@ -160,9 +298,40 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, [siteId, viewMode, selectedDate]);
|
}, [siteId, viewMode, selectedDate]);
|
||||||
|
|
||||||
const groupedConsumption = groupTimeSeries(consumption, viewMode);
|
const isEnergyView = viewMode !== 'day';
|
||||||
const groupedGeneration = groupTimeSeries(generation, viewMode);
|
|
||||||
const groupedForecast = groupTimeSeries(forecast, viewMode);
|
// Convert to energy series for aggregated views
|
||||||
|
const consumptionForGrouping = isEnergyView
|
||||||
|
? powerSeriesToEnergySeries(consumption, 30)
|
||||||
|
: consumption;
|
||||||
|
|
||||||
|
const generationForGrouping = isEnergyView
|
||||||
|
? powerSeriesToEnergySeries(generation, 30)
|
||||||
|
: generation;
|
||||||
|
|
||||||
|
const forecastForGrouping = isEnergyView
|
||||||
|
? powerSeriesToEnergySeries(forecast, 60) // if forecast is hourly, guess 60
|
||||||
|
: forecast;
|
||||||
|
|
||||||
|
// Group: sum for energy views, mean for day view
|
||||||
|
const groupedConsumption = groupTimeSeries(
|
||||||
|
consumptionForGrouping,
|
||||||
|
viewMode,
|
||||||
|
isEnergyView ? 'sum' : 'mean'
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupedGeneration = groupTimeSeries(
|
||||||
|
generationForGrouping,
|
||||||
|
viewMode,
|
||||||
|
isEnergyView ? 'sum' : 'mean'
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupedForecast = groupTimeSeries(
|
||||||
|
forecastForGrouping,
|
||||||
|
viewMode,
|
||||||
|
isEnergyView ? 'sum' : 'mean'
|
||||||
|
);
|
||||||
|
|
||||||
const forecastMap = Object.fromEntries(groupedForecast.map(d => [d.time, d.value]));
|
const forecastMap = Object.fromEntries(groupedForecast.map(d => [d.time, d.value]));
|
||||||
|
|
||||||
|
|
||||||
@ -238,6 +407,8 @@ function areaGradient(ctx: any, hex: string, alphaTop = 0.22, alphaBottom = 0.02
|
|||||||
const consumptionColor = isDark ? '#B80F0A' : '#EF4444'; // Example: Brighter red for dark mode
|
const consumptionColor = isDark ? '#B80F0A' : '#EF4444'; // Example: Brighter red for dark mode
|
||||||
const generationColor = isDark ? '#48A860' : '#22C55E'; // Example: Brighter green for dark mode
|
const generationColor = isDark ? '#48A860' : '#22C55E'; // Example: Brighter green for dark mode
|
||||||
const forecastColor = '#fcd913'; // A golden yellow that works well in both modes
|
const forecastColor = '#fcd913'; // A golden yellow that works well in both modes
|
||||||
|
const yUnit = isEnergyView ? 'kWh' : 'kW';
|
||||||
|
const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)';
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
labels: filteredLabels.map(formatLabel),
|
labels: filteredLabels.map(formatLabel),
|
||||||
@ -300,6 +471,13 @@ const forecastColor = '#fcd913'; // A golden yellow that works well in both mode
|
|||||||
bodyColor: axisColor,
|
bodyColor: axisColor,
|
||||||
borderColor: isDark ? '#444' : '#ccc',
|
borderColor: isDark ? '#444' : '#ccc',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx: any) => {
|
||||||
|
const dsLabel = ctx.dataset.label || '';
|
||||||
|
const val = ctx.parsed.y;
|
||||||
|
return `${dsLabel}: ${val?.toFixed(2)} ${yUnit}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
@ -326,12 +504,7 @@ const forecastColor = '#fcd913'; // A golden yellow that works well in both mode
|
|||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
suggestedMax: yAxisSuggestedMax,
|
suggestedMax: yAxisSuggestedMax,
|
||||||
title: {
|
title: { display: true, text: yTitle, color: axisColor, font: { weight: 'normal' as const } },
|
||||||
display: true,
|
|
||||||
text: 'Power (kW)',
|
|
||||||
color: axisColor,
|
|
||||||
font: { weight: 'normal' as const },
|
|
||||||
},
|
|
||||||
ticks: {
|
ticks: {
|
||||||
color: axisColor,
|
color: axisColor,
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
BarChart,
|
BarChart,
|
||||||
Bar,
|
Bar,
|
||||||
@ -9,45 +9,24 @@ import {
|
|||||||
Legend,
|
Legend,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { fetchPowerTimeseries } from '@/app/utils/api';
|
import { fetchMonthlyKpi, type MonthlyKPI } from '@/app/utils/api';
|
||||||
|
|
||||||
|
|
||||||
interface MonthlyBarChartProps {
|
interface MonthlyBarChartProps {
|
||||||
siteId: string;
|
siteId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TimeSeriesEntry {
|
const getLastNMonthKeys = (n: number): string[] => {
|
||||||
time: string;
|
const out: string[] = [];
|
||||||
value: number;
|
const now = new Date();
|
||||||
|
// include current month, go back n-1 months
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const d = new Date(now.getFullYear(), now.getMonth() - (n - 1 - i), 1);
|
||||||
|
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; // YYYY-MM
|
||||||
|
out.push(key);
|
||||||
}
|
}
|
||||||
|
return out;
|
||||||
const groupTimeSeries = (
|
|
||||||
data: TimeSeriesEntry[],
|
|
||||||
mode: 'monthly'
|
|
||||||
): TimeSeriesEntry[] => {
|
|
||||||
const groupMap = new Map<string, number[]>();
|
|
||||||
|
|
||||||
for (const entry of data) {
|
|
||||||
const date = new Date(entry.time);
|
|
||||||
const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
||||||
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 MonthlyBarChart: React.FC<MonthlyBarChartProps> = ({ siteId }) => {
|
|
||||||
const [chartData, setChartData] = useState<
|
|
||||||
{ month: string; consumption: number; generation: number }[]
|
|
||||||
>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
function useIsDarkMode() {
|
function useIsDarkMode() {
|
||||||
const [isDark, setIsDark] = useState(() =>
|
const [isDark, setIsDark] = useState(() =>
|
||||||
typeof document !== 'undefined'
|
typeof document !== 'undefined'
|
||||||
@ -58,71 +37,77 @@ const MonthlyBarChart: React.FC<MonthlyBarChartProps> = ({ siteId }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const check = () => setIsDark(document.body.classList.contains('dark'));
|
const check = () => setIsDark(document.body.classList.contains('dark'));
|
||||||
check();
|
check();
|
||||||
|
|
||||||
// Listen for class changes on <body>
|
|
||||||
const observer = new MutationObserver(check);
|
const observer = new MutationObserver(check);
|
||||||
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||||
|
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return isDark;
|
return isDark;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDark = useIsDarkMode();
|
const MonthlyBarChart: React.FC<MonthlyBarChartProps> = ({ siteId }) => {
|
||||||
|
const [chartData, setChartData] = useState<
|
||||||
|
{ month: string; consumption: number; generation: number }[]
|
||||||
|
>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const isDark = useIsDarkMode();
|
||||||
const consumptionColor = isDark ? '#ba8e23' : '#003049';
|
const consumptionColor = isDark ? '#ba8e23' : '#003049';
|
||||||
const generationColor = isDark ? '#fcd913' : '#669bbc';
|
const generationColor = isDark ? '#fcd913' : '#669bbc';
|
||||||
|
|
||||||
|
const monthKeys = useMemo(() => getLastNMonthKeys(6), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!siteId) return;
|
if (!siteId) return;
|
||||||
|
|
||||||
const fetchMonthlyData = async () => {
|
const load = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const start = '2025-01-01T00:00:00+08:00';
|
|
||||||
const end = '2025-12-31T23:59:59+08:00';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetchPowerTimeseries(siteId, start, end);
|
// Fetch all 6 months in parallel
|
||||||
|
const results: MonthlyKPI[] = await Promise.all(
|
||||||
|
monthKeys.map((month) =>
|
||||||
|
fetchMonthlyKpi({
|
||||||
|
site: siteId,
|
||||||
|
month,
|
||||||
|
// consumption_topic: '...', // optional if your API needs it
|
||||||
|
// generation_topic: '...', // optional if your API needs it
|
||||||
|
}).catch((e) => {
|
||||||
|
// normalize failures to an error-shaped record so the chart can still render other months
|
||||||
|
return {
|
||||||
|
site: siteId,
|
||||||
|
month,
|
||||||
|
yield_kwh: null,
|
||||||
|
consumption_kwh: null,
|
||||||
|
grid_draw_kwh: null,
|
||||||
|
efficiency: null,
|
||||||
|
peak_demand_kw: null,
|
||||||
|
avg_power_factor: null,
|
||||||
|
load_factor: null,
|
||||||
|
error: String(e),
|
||||||
|
} as MonthlyKPI;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const groupedConsumption = groupTimeSeries(res.consumption, 'monthly');
|
// Map to chart rows; default nulls to 0 for stacking/tooltip friendliness
|
||||||
const groupedGeneration = groupTimeSeries(res.generation, 'monthly');
|
const rows = results.map((kpi) => {
|
||||||
|
const monthLabel = format(new Date(`${kpi.month}-01`), 'MMM');
|
||||||
|
return {
|
||||||
|
month: monthLabel,
|
||||||
|
consumption: kpi.consumption_kwh ?? 0,
|
||||||
|
generation: kpi.yield_kwh ?? 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const monthMap = new Map<string, { consumption: number; generation: number }>();
|
setChartData(rows);
|
||||||
|
|
||||||
for (const entry of groupedConsumption) {
|
|
||||||
if (!monthMap.has(entry.time)) {
|
|
||||||
monthMap.set(entry.time, { consumption: 0, generation: 0 });
|
|
||||||
}
|
|
||||||
monthMap.get(entry.time)!.consumption = entry.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of groupedGeneration) {
|
|
||||||
if (!monthMap.has(entry.time)) {
|
|
||||||
monthMap.set(entry.time, { consumption: 0, generation: 0 });
|
|
||||||
}
|
|
||||||
monthMap.get(entry.time)!.generation = entry.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatted = Array.from(monthMap.entries())
|
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
|
||||||
.map(([key, val]) => ({
|
|
||||||
month: format(new Date(`${key}-01`), 'MMM'),
|
|
||||||
consumption: val.consumption,
|
|
||||||
generation: val.generation,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setChartData(formatted.slice(-6)); // last 6 months
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch monthly power data:', error);
|
|
||||||
setChartData([]);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMonthlyData();
|
load();
|
||||||
}, [siteId]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [siteId]); // monthKeys are stable via useMemo
|
||||||
|
|
||||||
if (loading || !siteId || chartData.length === 0) {
|
if (loading || !siteId || chartData.length === 0) {
|
||||||
return (
|
return (
|
||||||
@ -152,9 +137,9 @@ const generationColor = isDark ? '#fcd913' : '#669bbc';
|
|||||||
axisLine={{ stroke: isDark ? '#fff' : '#222' }}
|
axisLine={{ stroke: isDark ? '#fff' : '#222' }}
|
||||||
tickLine={{ stroke: isDark ? '#fff' : '#222' }}
|
tickLine={{ stroke: isDark ? '#fff' : '#222' }}
|
||||||
label={{
|
label={{
|
||||||
value: 'Power (kW)', // <-- Y-axis label
|
value: 'Energy (kWh)', // fixed: units are kWh
|
||||||
angle: -90, // Vertical text
|
angle: -90,
|
||||||
position: 'insideLeft', // Position inside the chart area
|
position: 'insideLeft',
|
||||||
style: {
|
style: {
|
||||||
textAnchor: 'middle',
|
textAnchor: 'middle',
|
||||||
fill: isDark ? '#fff' : '#222',
|
fill: isDark ? '#fff' : '#222',
|
||||||
@ -174,15 +159,11 @@ const generationColor = isDark ? '#fcd913' : '#669bbc';
|
|||||||
color: isDark ? '#fff' : '#222',
|
color: isDark ? '#fff' : '#222',
|
||||||
}}
|
}}
|
||||||
cursor={{
|
cursor={{
|
||||||
fill: isDark ? '#808080' : '#e0e7ef', // dark mode bg, light mode bg
|
fill: isDark ? '#808080' : '#e0e7ef',
|
||||||
fillOpacity: isDark ? 0.6 : 0.3, // adjust opacity as you like
|
fillOpacity: isDark ? 0.6 : 0.3,
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Legend
|
|
||||||
wrapperStyle={{
|
|
||||||
color: isDark ? '#fff' : '#222',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Legend wrapperStyle={{ color: isDark ? '#fff' : '#222' }} />
|
||||||
<Bar dataKey="consumption" fill={consumptionColor} name="Consumption (kWh)" />
|
<Bar dataKey="consumption" fill={consumptionColor} name="Consumption (kWh)" />
|
||||||
<Bar dataKey="generation" fill={generationColor} name="Generation (kWh)" />
|
<Bar dataKey="generation" fill={generationColor} name="Generation (kWh)" />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
@ -194,3 +175,4 @@ const generationColor = isDark ? '#fcd913' : '#669bbc';
|
|||||||
|
|
||||||
export default MonthlyBarChart;
|
export default MonthlyBarChart;
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user