Skip to content

Commit

Permalink
feat: export data to CSV and PNG (#124)
Browse files Browse the repository at this point in the history
* feat: export data to csv
* feat: download charts as PNG
* feat: improve responsivity on small devices
  • Loading branch information
ReidyT authored Dec 22, 2024
1 parent 5e01d54 commit 8b65e10
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 31 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@types/react": "18.3.16",
"@types/react-dom": "18.3.5",
"@types/recharts": "^1.8.29",
"file-saver": "^2.0.5",
"i18next": "^23.9.0",
"immer": "^10.1.1",
"lucide-react": "^0.456.0",
Expand All @@ -35,6 +36,7 @@
"react-i18next": "14.1.3",
"react-toastify": "10.0.6",
"recharts": "^2.15.0",
"recharts-to-png": "^2.4.0",
"three": "^0.169.0",
"typescript": "5.7.2",
"use-debounce": "^10.0.4",
Expand Down Expand Up @@ -74,6 +76,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/file-saver": "^2.0.7",
"@types/i18n": "0.13.12",
"@types/papaparse": "^5.3.15",
"@types/three": "^0",
Expand Down
8 changes: 1 addition & 7 deletions src/context/ChartContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,11 @@ import { undefinedContextErrorFactory } from '@/utils/context';

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

export 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' },
Expand Down
28 changes: 28 additions & 0 deletions src/hooks/useHouseCost.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useMemo } from 'react';

import { useSimulation } from '@/context/SimulationContext';
import { HouseComponent } from '@/types/houseComponent';

export const useHouseCost = (): { wallCost: number } => {
const {
house: { getByType },
} = useSimulation();

const wallCost = useMemo(
() =>
getByType(HouseComponent.Wall).reduce(
(totCost, houseComponent) =>
totCost +
houseComponent.actualArea *
houseComponent.buildingMaterials.reduce(
(componentCost, material) =>
componentCost + material.price * material.thickness,
0,
),
0,
),
[getByType],
);

return { wallCost };
};
13 changes: 11 additions & 2 deletions src/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,19 @@
"SIMULATION_GRAPHICS": {
"HEAT_LOSS": {
"ONE_MONTH": "1 month",
"THREE_MONTHS": "3 months",
"SIX_MONTHS": "6 months",
"ONE_YEAR": "1 year",
"THREE_YEARS": "3 years"
"THREE_YEARS": "3 years",
"PERIOD_LABEL": "graphic period",
"HEAT_LOSS_LABEL": "Heat Loss",
"OUTDOOR_LABEL": "Outdoor"
},
"EXPORT_CSV": {
"DOWNLOAD_BTN_LABEL": "Download CSV"
},
"EXPORT_CHART": {
"DOWNLOAD_BTN_LABEL": "Download charts",
"DOWNLOADING_BTN_LABEL": "Downloading..."
}
},
"INSULATIONS": {
Expand Down
59 changes: 59 additions & 0 deletions src/modules/common/ExportCSVButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useTranslation } from 'react-i18next';

import { Button } from '@mui/material';

import { FileSpreadsheet } from 'lucide-react';
import Papa from 'papaparse';

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

export const ExportCSVButton = (): JSX.Element => {
const { t } = useTranslation('SIMULATION_GRAPHICS', {
keyPrefix: 'EXPORT_CSV',
});

const {
simulation: {
days: { simulationDays },
},
temperatures: { indoor, outdoor },
electricity: { pricekWh },
} = useSimulation();

const { wallCost } = useHouseCost();

const handleClick = (): void => {
const data = simulationDays.map((d) => ({
'date (UTC)': d.date.toUTCString(),
'indoor (°C)': indoor,
'outdoor (°C)': outdoor.value,
'heat loss (W)': d.heatLoss.global,
'total heat loss (W)': d.totalHeatLoss,
'price kWh (CHF)': pricekWh,
'total electricity cost (CHF)': d.totalElectricityCost,
'wall cost (CHF)': wallCost,
}));

const csv = Papa.unparse(data);
const csvData = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const csvURL = URL.createObjectURL(csvData);

const link = document.createElement('a');
link.href = csvURL;
link.download = `Insulation_Simulator_Data${new Date().getTime()}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};

return (
<Button
startIcon={<FileSpreadsheet />}
variant="outlined"
onClick={handleClick}
>
{t('DOWNLOAD_BTN_LABEL')}
</Button>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Flower, Leaf, Snowflake, Sun } from 'lucide-react';

import { useSeason } from '@/context/SeasonContext';
import { useSimulation } from '@/context/SimulationContext';
import { useHouseCost } from '@/hooks/useHouseCost';
import { FormattedHeatLoss } from '@/types/heatLoss';
import { HouseComponent } from '@/types/houseComponent';
import { Season, Seasons } from '@/types/seasons';
Expand Down Expand Up @@ -32,33 +33,19 @@ export const useSimulationInformations =

const {
heatLoss: { global: heatLoss },
house: { getByType, getFirstOfType },
house: { getFirstOfType },
} = useSimulation();

const wallComponent = useMemo(
() => getFirstOfType(HouseComponent.Wall),
[getFirstOfType],
);

const wallPrices = useMemo(
() =>
getByType(HouseComponent.Wall).reduce(
(totCost, houseComponent) =>
totCost +
houseComponent.actualArea *
houseComponent.buildingMaterials.reduce(
(componentCost, material) =>
componentCost + material.price * material.thickness,
0,
),
0,
),
[getByType],
);
const { wallCost } = useHouseCost();

return {
heatLoss: formatHeatLossRate(heatLoss),
wallsPrice: Math.round(wallPrices),
wallsPrice: Math.round(wallCost),
seasonIcon: iconsBySeason[season],
formattedWallSize: wallComponent
? formatComponentSize({
Expand Down
41 changes: 37 additions & 4 deletions src/modules/common/charts/HeatLossCharts.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';

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

import { saveAs } from 'file-saver';
import { FileChartLine } from 'lucide-react';
import {
CartesianGrid,
Legend,
Expand All @@ -17,20 +20,28 @@ import {
XAxis,
YAxis,
} from 'recharts';
import { useCurrentPng } from 'recharts-to-png';

import { PERIODS, useChart } from '@/context/ChartContext';
import { useSimulation } from '@/context/SimulationContext';

import { ExportCSVButton } from '../ExportCSVButton';

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

const [getPng, { ref, isLoading }] = useCurrentPng();

const chartData = useMemo(
() =>
simulationDays.map((d) => ({
Expand All @@ -46,13 +57,21 @@ export const HeatLossCharts = ({ width }: Props): JSX.Element => {
currentIdx + 1,
);

const handleDownload = useCallback(async () => {
const png = await getPng();

if (png) {
saveAs(png, 'simulation_heatloss_chart.png');
}
}, [getPng]);

return (
<Stack spacing={2} alignItems="center">
<ToggleButtonGroup
value={period.numberOfDays}
exclusive
onChange={(_, v) => updatePeriod(v)}
aria-label="graphic period"
aria-label={t('PERIOD_LABEL')}
>
{PERIODS.map((p) => (
<ToggleButton
Expand All @@ -65,6 +84,7 @@ export const HeatLossCharts = ({ width }: Props): JSX.Element => {
))}
</ToggleButtonGroup>
<LineChart
ref={ref}
width={width}
height={300}
data={data}
Expand All @@ -85,7 +105,7 @@ export const HeatLossCharts = ({ width }: Props): JSX.Element => {
dataKey="hl"
stroke="#8884d8"
activeDot={{ r: 8 }}
name="Heat Loss"
name={t('HEAT_LOSS_LABEL')}
unit=" kilowatt"
dot={false}
animationDuration={0}
Expand All @@ -94,11 +114,24 @@ export const HeatLossCharts = ({ width }: Props): JSX.Element => {
<Line
dataKey="outdoor"
unit=" °C"
name="Outdoor"
name={t('OUTDOOR_LABEL')}
dot={false}
animationDuration={0}
/>
</LineChart>

<Stack direction="row" spacing={1}>
<Button
variant="outlined"
startIcon={<FileChartLine />}
onClick={handleDownload}
>
{isLoading
? tExport('DOWNLOADING_BTN_LABEL')
: tExport('DOWNLOAD_BTN_LABEL')}
</Button>
<ExportCSVButton />
</Stack>
</Stack>
);
};
2 changes: 1 addition & 1 deletion src/modules/main/PlayerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { SimulationSettingsPanel } from '../common/SimulationSettingsPanel/Simul
import { HeatLossCharts } from '../common/charts/HeatLossCharts';
import { Tabs } from '../common/tabs/Tabs';

const SM = { width: 375, height: 400 };
const SM = { width: 350, height: 475 };
const MD = { width: 500, height: 500 };

const PlayerViewComponent = (): JSX.Element => {
Expand Down
Loading

0 comments on commit 8b65e10

Please sign in to comment.