Skip to content

Commit

Permalink
feat: add heat loss graphic (#117)
Browse files Browse the repository at this point in the history
* fix: solve the accordion issue on mobile

* feat: add first heat loss graphic
  • Loading branch information
ReidyT authored Dec 20, 2024
1 parent 351b1a4 commit 44511ef
Show file tree
Hide file tree
Showing 13 changed files with 603 additions and 18 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@types/node": "20.17.10",
"@types/react": "18.3.16",
"@types/react-dom": "18.3.5",
"@types/recharts": "^1.8.29",
"deep-equal": "^2.2.3",
"i18next": "^23.9.0",
"lucide-react": "^0.456.0",
Expand All @@ -33,6 +34,7 @@
"react-dom": "18.3.1",
"react-i18next": "14.1.3",
"react-toastify": "10.0.6",
"recharts": "^2.15.0",
"three": "^0.169.0",
"typescript": "5.7.2",
"use-debounce": "^10.0.4"
Expand Down
13 changes: 9 additions & 4 deletions src/context/SimulationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ import {
} from '@/hooks/useHouseComponents';
import { HouseComponentsConfigurator } from '@/models/HouseComponentsConfigurator';
import {
SimulationDay,
createDefault,
simulationHistory,
} from '@/reducer/simulationHistoryReducer';
import { FormattedHeatLoss } from '@/types/heatLoss';
import { HeatLossPerComponent } from '@/types/houseComponent';
import { SimulationStatus } from '@/types/simulation';
import { TemperatureRow, UserOutdoorTemperature } from '@/types/temperatures';
import { NonEmptyArray } from '@/types/utils';
import { WindowScaleSize, WindowSizeType } from '@/types/window';
import { undefinedContextErrorFactory } from '@/utils/context';
import { formatHeatLossRate } from '@/utils/heatLoss';
Expand Down Expand Up @@ -59,6 +61,7 @@ type SimulationContextType = {
days: {
total: number;
currentIdx: number;
simulationDays: NonEmptyArray<SimulationDay>;
goToDay: (idx: number) => void;
getDateOf: (idx: number) => Date;
};
Expand Down Expand Up @@ -157,7 +160,7 @@ export const SimulationProvider = ({
temperatures.current = rows;
dispatchHistory({
type: 'load',
temperatureRows: temperatures.current,
temperatureRows: rows,
});
setSimulationStatus(SimulationStatus.LOADING);
});
Expand Down Expand Up @@ -322,16 +325,17 @@ export const SimulationProvider = ({
status: simulationStatus,
start: startSimulation,
pause: pauseSimulation,
date: new Date(temperatures.current[currentDayIdx]?.time),
date: currentDay.date,
duration: {
years: simulationDurationInYears,
update: updateSimulationDuration,
},
days: {
total: numberOfDays,
currentIdx: currentDayIdx,
simulationDays,
goToDay,
getDateOf: (idx: number) => new Date(temperatures.current[idx]?.time),
getDateOf: (idx: number) => simulationDays[idx].date,
},
speed: {
current: SPEED_STATES[simulationSpeedIdx].text,
Expand Down Expand Up @@ -379,10 +383,10 @@ export const SimulationProvider = ({
simulationStatus,
startSimulation,
pauseSimulation,
currentDayIdx,
simulationDurationInYears,
updateSimulationDuration,
numberOfDays,
currentDayIdx,
goToDay,
simulationSpeedIdx,
updatePricekWh,
Expand All @@ -391,6 +395,7 @@ export const SimulationProvider = ({
updateWindowSize,
updateNumberOfFloors,
houseComponentsHook,
simulationDays,
]);

return (
Expand Down
13 changes: 13 additions & 0 deletions src/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@
"OUTDOOR_TEMPERATURE_OVERRIDE_LABEL": "Override Temperature"
}
},
"SIMULATION_TABS": {
"VISUALIZE": "Visualize",
"ANALYZE": "Analyze"
},
"SIMULATION_GRAPHICS": {
"HEAT_LOSS": {
"ONE_MONTH": "1 month",
"THREE_MONTHS": "3 months",
"SIX_MONTHS": "6 months",
"ONE_YEAR": "1 year",
"THREE_YEARS": "3 years"
}
},
"INSULATIONS": {
"Brick": "Brick",
"Aerogel": "Aerogel",
Expand Down
13 changes: 5 additions & 8 deletions src/modules/common/SimulationCanvas.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { useMediaQuery, useTheme } from '@mui/material';

import { OrbitControls } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';

Expand All @@ -10,17 +8,16 @@ import { Garden } from '../models/Garden';
import { ResidentialHouse } from '../models/House/ResidentialHouse/ResidentialHouse';
import { Tree } from '../models/Tree/Tree';

export const SimulationCanvas = (): JSX.Element => {
const { numberOfFloors } = useSimulation('house');
type Props = { size: string | number };

const theme = useTheme();
const md = useMediaQuery(theme.breakpoints.up('sm'));
export const SimulationCanvas = ({ size }: Props): JSX.Element => {
const { numberOfFloors } = useSimulation('house');

return (
<Canvas
style={{
height: md ? '500px' : '375px',
width: md ? '500px' : '375px',
height: size,
width: size,
}}
camera={{ position: [0, 0, -35.5], fov: 30 }}
>
Expand Down
119 changes: 119 additions & 0 deletions src/modules/common/charts/HeatLossCharts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';

import {
Stack,
ToggleButton,
ToggleButtonGroup,
Typography,
} from '@mui/material';

import {
CartesianGrid,
Legend,
Line,
LineChart,
Tooltip,
XAxis,
YAxis,
} from 'recharts';

import { useSimulation } from '@/context/SimulationContext';

type Period = {
numberOfDays: number;
labelKey:
| 'ONE_MONTH'
| 'THREE_MONTHS'
| 'SIX_MONTHS'
| 'ONE_YEAR'
| 'THREE_YEARS';
};

const PERIODS: Period[] = [
{ numberOfDays: 30, labelKey: 'ONE_MONTH' },
{ numberOfDays: 90, labelKey: 'THREE_MONTHS' },
{ numberOfDays: 180, labelKey: 'SIX_MONTHS' },
{ numberOfDays: 365, labelKey: 'ONE_YEAR' },
{ numberOfDays: 1_095, labelKey: 'THREE_YEARS' },
] as const;

type Props = { width: number };
export const HeatLossCharts = ({ width }: Props): JSX.Element => {
const { t } = useTranslation('SIMULATION_GRAPHICS', {
keyPrefix: 'HEAT_LOSS',
});
const {
days: { simulationDays, currentIdx },
} = useSimulation('simulation');

const [period, setPeriod] = useState(PERIODS[0]);

const data = simulationDays
.slice(Math.max(currentIdx - period.numberOfDays, 0), currentIdx + 1)
.map((d) => ({
name: d.date.toLocaleDateString(),
hl: Number.parseFloat((d.heatLoss.global / 1000).toFixed(1)),
outdoor: Math.round(d.weatherTemperature),
}));

const handlePeriodChange = (value: number): void => {
setPeriod(PERIODS.find((p) => value === p.numberOfDays) ?? PERIODS[0]);
};

return (
<Stack spacing={2} alignItems="center">
<ToggleButtonGroup
value={period.numberOfDays}
exclusive
onChange={(_, v) => handlePeriodChange(v)}
aria-label="graphic period"
>
{PERIODS.map((p) => (
<ToggleButton
key={p.labelKey}
value={p.numberOfDays}
aria-label={t(p.labelKey)}
>
<Typography>{t(p.labelKey)}</Typography>
</ToggleButton>
))}
</ToggleButtonGroup>
<LineChart
width={width}
height={300}
data={data}
margin={{
top: 10,
right: 5,
left: 10,
bottom: 10,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis domain={[0.5, 30]} />
<Tooltip />
<Legend />
<Line
type="monotone"
dataKey="hl"
stroke="#8884d8"
activeDot={{ r: 8 }}
name="Heat Loss"
unit=" kilowatt"
dot={false}
animationDuration={0}
/>

<Line
dataKey="outdoor"
unit=" °C"
name="Outdoor"
dot={false}
animationDuration={0}
/>
</LineChart>
</Stack>
);
};
55 changes: 55 additions & 0 deletions src/modules/common/tabs/TabPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useEffect, useState } from 'react';

import { useTabContext } from '@mui/lab';

type Props = {
children: JSX.Element;
value: string;
unmountOnExit?: boolean;
};

/**
* This component allow to sue the MUI Tabs without the conditional rendering.
*
* Original code source: https://github.com/mui/material-ui/issues/21250.
* @param unmountOnExit indicates if the children must be unmount on tab changed.
*/
export const TabPanel = ({
children,
value: id,
unmountOnExit = false,
}: Props): JSX.Element | null => {
const context = useTabContext();

if (context === null) {
throw new TypeError('No TabContext provided');
}
const tabId = context.value;
const isCurrent = id === tabId;

const [visited, setVisited] = useState(false);
useEffect(() => {
if (isCurrent) {
setVisited(true);
}
}, [isCurrent]);

return (unmountOnExit && isCurrent) || !unmountOnExit ? (
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
margin: 'auto',
visibility: isCurrent ? 'visible' : 'hidden',
}}
>
{visited && children}
</div>
) : null;
};
54 changes: 54 additions & 0 deletions src/modules/common/tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useState } from 'react';

import { TabContext, TabList } from '@mui/lab';
import { Stack, Tab } from '@mui/material';

import { TabPanel } from './TabPanel';

export type DataTab = {
label: string;
icon?: JSX.Element;
unmountOnExit?: boolean;
element: JSX.Element;
};

type Props = {
height: string | number;
width: string | number;
tabs: DataTab[];
};

export const Tabs = ({ height, width, tabs }: Props): JSX.Element => {
const [tab, setTab] = useState('0');

return (
<Stack>
<TabContext value={tab}>
<Stack sx={{ borderBottom: 1, borderColor: 'divider' }}>
<TabList onChange={(_, v) => setTab(v)}>
{tabs.map(({ icon, label }, idx) => (
<Tab
key={label}
icon={icon}
iconPosition="start"
label={label}
value={idx.toString()}
/>
))}
</TabList>
</Stack>
<Stack position="relative" height={height} width={width}>
{tabs.map(({ element, label, unmountOnExit }, idx) => (
<TabPanel
key={label}
value={idx.toString()}
unmountOnExit={unmountOnExit}
>
{element}
</TabPanel>
))}
</Stack>
</TabContext>
</Stack>
);
};
Loading

0 comments on commit 44511ef

Please sign in to comment.