aboutsummaryrefslogtreecommitdiff
path: root/src/reports/ChartReportPage.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/reports/ChartReportPage.jsx')
-rw-r--r--src/reports/ChartReportPage.jsx152
1 files changed, 152 insertions, 0 deletions
diff --git a/src/reports/ChartReportPage.jsx b/src/reports/ChartReportPage.jsx
new file mode 100644
index 00000000..6175e1d8
--- /dev/null
+++ b/src/reports/ChartReportPage.jsx
@@ -0,0 +1,152 @@
+import dayjs from 'dayjs';
+import React, { useState } from 'react';
+import {
+ FormControl, InputLabel, Select, MenuItem,
+} from '@mui/material';
+import {
+ CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis,
+} from 'recharts';
+import ReportFilter from './components/ReportFilter';
+import { formatTime } from '../common/util/formatter';
+import { useTranslation } from '../common/components/LocalizationProvider';
+import PageLayout from '../common/components/PageLayout';
+import ReportsMenu from './components/ReportsMenu';
+import usePositionAttributes from '../common/attributes/usePositionAttributes';
+import { useCatch } from '../reactHelper';
+import { useAttributePreference, usePreference } from '../common/util/preferences';
+import {
+ altitudeFromMeters, distanceFromMeters, speedFromKnots, volumeFromLiters,
+} from '../common/util/converter';
+import useReportStyles from './common/useReportStyles';
+
+const ChartReportPage = () => {
+ const classes = useReportStyles();
+ const t = useTranslation();
+
+ const positionAttributes = usePositionAttributes(t);
+
+ const distanceUnit = useAttributePreference('distanceUnit');
+ const altitudeUnit = useAttributePreference('altitudeUnit');
+ const speedUnit = useAttributePreference('speedUnit');
+ const volumeUnit = useAttributePreference('volumeUnit');
+ const hours12 = usePreference('twelveHourFormat');
+
+ const [items, setItems] = useState([]);
+ const [types, setTypes] = useState(['speed']);
+ const [type, setType] = useState('speed');
+
+ const values = items.map((it) => it[type]);
+ const minValue = Math.min(...values);
+ const maxValue = Math.max(...values);
+ const valueRange = maxValue - minValue;
+
+ const handleSubmit = useCatch(async ({ deviceId, from, to }) => {
+ const query = new URLSearchParams({ deviceId, from, to });
+ const response = await fetch(`/api/reports/route?${query.toString()}`, {
+ headers: { Accept: 'application/json' },
+ });
+ if (response.ok) {
+ const positions = await response.json();
+ const keySet = new Set();
+ const keyList = [];
+ const formattedPositions = positions.map((position) => {
+ const data = { ...position, ...position.attributes };
+ const formatted = {};
+ formatted.fixTime = dayjs(position.fixTime).valueOf();
+ Object.keys(data).filter((key) => !['id', 'deviceId'].includes(key)).forEach((key) => {
+ const value = data[key];
+ if (typeof value === 'number') {
+ keySet.add(key);
+ const definition = positionAttributes[key] || {};
+ switch (definition.dataType) {
+ case 'speed':
+ formatted[key] = speedFromKnots(value, speedUnit).toFixed(2);
+ break;
+ case 'altitude':
+ formatted[key] = altitudeFromMeters(value, altitudeUnit).toFixed(2);
+ break;
+ case 'distance':
+ formatted[key] = distanceFromMeters(value, distanceUnit).toFixed(2);
+ break;
+ case 'volume':
+ formatted[key] = volumeFromLiters(value, volumeUnit).toFixed(2);
+ break;
+ case 'hours':
+ formatted[key] = (value / 1000).toFixed(2);
+ break;
+ default:
+ formatted[key] = value;
+ break;
+ }
+ }
+ });
+ return formatted;
+ });
+ Object.keys(positionAttributes).forEach((key) => {
+ if (keySet.has(key)) {
+ keyList.push(key);
+ keySet.delete(key);
+ }
+ });
+ setTypes([...keyList, ...keySet]);
+ setItems(formattedPositions);
+ } else {
+ throw Error(await response.text());
+ }
+ });
+
+ return (
+ <PageLayout menu={<ReportsMenu />} breadcrumbs={['reportTitle', 'reportChart']}>
+ <ReportFilter handleSubmit={handleSubmit} showOnly>
+ <div className={classes.filterItem}>
+ <FormControl fullWidth>
+ <InputLabel>{t('reportChartType')}</InputLabel>
+ <Select
+ label={t('reportChartType')}
+ value={type}
+ onChange={(e) => setType(e.target.value)}
+ disabled={!items.length}
+ >
+ {types.map((key) => (
+ <MenuItem key={key} value={key}>{positionAttributes[key]?.name || key}</MenuItem>
+ ))}
+ </Select>
+ </FormControl>
+ </div>
+ </ReportFilter>
+ {items.length > 0 && (
+ <div className={classes.chart}>
+ <ResponsiveContainer>
+ <LineChart
+ data={items}
+ margin={{
+ top: 10, right: 40, left: 0, bottom: 10,
+ }}
+ >
+ <XAxis
+ dataKey="fixTime"
+ type="number"
+ tickFormatter={(value) => formatTime(value, 'time', hours12)}
+ domain={['dataMin', 'dataMax']}
+ scale="time"
+ />
+ <YAxis
+ type="number"
+ tickFormatter={(value) => value.toFixed(2)}
+ domain={[minValue - valueRange / 5, maxValue + valueRange / 5]}
+ />
+ <CartesianGrid strokeDasharray="3 3" />
+ <Tooltip
+ formatter={(value, key) => [value, positionAttributes[key]?.name || key]}
+ labelFormatter={(value) => formatTime(value, 'seconds', hours12)}
+ />
+ <Line type="monotone" dataKey={type} />
+ </LineChart>
+ </ResponsiveContainer>
+ </div>
+ )}
+ </PageLayout>
+ );
+};
+
+export default ChartReportPage;