From c182c9d2d4ba78fce92a6c0db2cd259e1a80721d Mon Sep 17 00:00:00 2001 From: Benedikt Nordhoff Date: Tue, 16 Jan 2024 11:14:39 +0100 Subject: [PATCH] Add recent day chart to sensor tooltip --- .env.example | 2 +- .env.production | 2 +- README.md | 2 +- package-lock.json | 28 +++++++ package.json | 1 + src/components/SensorChart/index.tsx | 100 +++++++++++++++++++++++++ src/components/SensorTooltip/index.tsx | 9 ++- src/hooks/useMoistureData.ts | 5 +- src/hooks/useSensorDetails.ts | 50 +++++++++++++ src/model/models.ts | 11 +++ types/vite.d.ts | 2 +- 11 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 src/components/SensorChart/index.tsx create mode 100644 src/hooks/useSensorDetails.ts diff --git a/.env.example b/.env.example index f35e5ba..67f7b08 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -VITE_MOISTURE_DATA_URL=endpoint for moiture data json +VITE_BACKEND_URL=endpoint for moiture data json diff --git a/.env.production b/.env.production index 86742ae..9af809e 100644 --- a/.env.production +++ b/.env.production @@ -1 +1 @@ -VITE_MOISTURE_DATA_URL=https://api.bodenfeuchte.org/mapData +VITE_BACKEND_URL=https://api.bodenfeuchte.org diff --git a/README.md b/README.md index 18cf2be..647a8b2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Available Scripts -Before running/building the app either create a `.env` file or set the environment variable `VITE_MOISTURE_DATA_URL`. +Before running/building the app either create a `.env` file or set the environment variable `VITE_BACKEND_URL`. In the project directory, you can run: diff --git a/package-lock.json b/package-lock.json index 1f67257..5c51bcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "date-fns": "^2.30.0", "leaflet": "^1.9.4", "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-leaflet": "^4.2.1", "use-http": "^1.0.28" @@ -2992,6 +2993,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", + "peer": true + }, "node_modules/@microsoft/load-themed-styles": { "version": "1.10.295", "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.295.tgz", @@ -4692,6 +4699,18 @@ "url": "https://github.com/chalk/chalk-template?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", + "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=7" + } + }, "node_modules/cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", @@ -8466,6 +8485,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", diff --git a/package.json b/package.json index 763b12e..b32e150 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "date-fns": "^2.30.0", "leaflet": "^1.9.4", "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-leaflet": "^4.2.1", "use-http": "^1.0.28" diff --git a/src/components/SensorChart/index.tsx b/src/components/SensorChart/index.tsx new file mode 100644 index 0000000..d8f061c --- /dev/null +++ b/src/components/SensorChart/index.tsx @@ -0,0 +1,100 @@ +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Tooltip, + Legend, + ChartData, +} from "chart.js"; +import { Chart } from "react-chartjs-2"; +import { SensorDetails, SensorInfo } from "../../model/models"; +import useSensorDetails from "../../hooks/useSensorDetails"; + +// ChartJS setup +ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, BarElement, Tooltip, Legend); + +const recentDaysChartOptions = { + responsive: true, + interaction: { + mode: "index" as const, + intersect: false, + }, + plugins: { + legend: { + position: "top" as const, + labels: { + boxWidth: 20, + boxHeight: 1, + }, + }, + }, + maintainAspectRatio: false, + scales: { + moisture: { + type: "linear" as const, + display: true, + position: "left" as const, + ticks: { + callback: (value: any, index: any, ticks: any) => value + " %", + }, + suggestMin: 0, + suggestMax: 50, + }, + // precipitation: { + // type: 'linear' as const, + // display: true, + // position: 'right' as const, + // suggestedMax: 10, + // grid: { + // drawOnChartArea: false, + // }, + // ticks: { + // callback: (value: any, index: any, ticks: any) => (value + " mm") + // } + // }, + }, +}; + +function extractRecentDaysDataset(details: SensorDetails): ChartData<"bar" | "line", number[], string> { + const colors = { + red: "rgb(255, 99, 132)", + orange: "rgb(255, 159, 64)", + yellow: "rgb(255, 205, 86)", + green: "rgb(75, 192, 192)", + blue: "rgb(54, 162, 235)", + purple: "rgb(153, 102, 255)", + grey: "rgb(201, 203, 207)", + }; + return { + labels: details.measurements.map((d) => d.timestamp.toLocaleDateString()), + datasets: [ + { + label: "Tageswert", + type: "line", + data: details.measurements.map((d) => d.moisture), + borderColor: colors.green, + backgroundColor: colors.green, + yAxisID: "moisture", + }, + { + label: "Globales Mittel", + type: "line", + data: details.peerMeasurements.map((d) => d.moisture), + borderColor: colors.grey, + backgroundColor: colors.grey, + yAxisID: "moisture", + }, + ], + }; +} + +export default function SensorChart({ sensorInfo }: { sensorInfo: SensorInfo }) { + const { details, loading, error } = useSensorDetails(sensorInfo); + if (details) return ; + else if (loading) return "Loading"; + else if (error) return "Error"; + return "No chart"; +} diff --git a/src/components/SensorTooltip/index.tsx b/src/components/SensorTooltip/index.tsx index 6ee5d71..a2b725b 100644 --- a/src/components/SensorTooltip/index.tsx +++ b/src/components/SensorTooltip/index.tsx @@ -1,5 +1,6 @@ import { FontWeights, getTheme, mergeStyleSets, Text } from "@fluentui/react"; import { SensorInfo } from "../../model/models"; +import SensorChart from "../SensorChart"; // Styles const theme = getTheme(); @@ -18,6 +19,9 @@ const styles = mergeStyleSets({ height: "100%", padding: "0 18px 15px", }, + chart: { + minHeight: "200px", + }, subtext: [ theme.fonts.small, { @@ -48,7 +52,7 @@ const SensorTooltip = ({ record }: { record: SensorInfo }) => { - tagesmittel + Tagesmittel letzte @@ -86,6 +90,9 @@ const SensorTooltip = ({ record }: { record: SensorInfo }) => { Letzte Aktualisierung: {record.last_updated.toLocaleString()} +
+ +
); diff --git a/src/hooks/useMoistureData.ts b/src/hooks/useMoistureData.ts index a511216..d18d326 100644 --- a/src/hooks/useMoistureData.ts +++ b/src/hooks/useMoistureData.ts @@ -3,7 +3,8 @@ import { MoistureData, MoistureDataDto } from "../model/models"; import add from "date-fns/add"; import { useCallback, useEffect, useRef, useState } from "react"; -const MOISTURE_DATA_URL = import.meta.env.VITE_MOISTURE_DATA_URL; +const BACKEND_URL = import.meta.env.VITE_BACKEND_URL; +const MOISTURE_DATA_URL = `${BACKEND_URL}/mapData`; const MOCK_DATA: MoistureDataDto = { timestamp: "1970-01-01 00:00:00", @@ -83,7 +84,7 @@ export default function useMoistureData(): MoistureState { }; } - const data = dto ? processData(dto) : undefined; + const data = !error && dto ? processData(dto) : undefined; validTo.current = data ? add(data.timestamp, { days: 1, minutes: 1 }) : add(new Date(), { minutes: 5 }); return { loading, error, data }; diff --git a/src/hooks/useSensorDetails.ts b/src/hooks/useSensorDetails.ts new file mode 100644 index 0000000..fab6c64 --- /dev/null +++ b/src/hooks/useSensorDetails.ts @@ -0,0 +1,50 @@ +import useFetch from "use-http"; +import { MoistureMeasurement, SensorDetails, SensorInfo } from "../model/models"; + +const BACKEND_URL = import.meta.env.VITE_BACKEND_URL; + +interface SensorDetailsDto { + sensor: Array<{ + moisture: number; + time: string; + }>; + peers: Array<{ + moisture: number; + time: string; + }>; +} + +function processData(sensorInfo: SensorInfo, data: SensorDetailsDto): SensorDetails { + function toDate(timestamp: string): Date { + return new Date(/\+|Z/i.test(timestamp) ? timestamp : timestamp + "Z"); + } + function processMeasurement({ moisture, time }: { moisture: number; time: string }): MoistureMeasurement { + return { moisture, timestamp: toDate(time) }; + } + + const measurements = data.sensor.map(processMeasurement); + const peerMeasurements = data.peers.map(processMeasurement); + + return { + measurements, + peerMeasurements, + info: sensorInfo, + }; +} + +interface SensorDetailsState { + loading: Boolean; + error: Error | undefined; + details: SensorDetails | undefined; +} + +export default function useSensorDetails(sensorInfo: SensorInfo): SensorDetailsState { + const url = `${BACKEND_URL}/sensorData/${sensorInfo.device}?records=7&resolution=1d`; + + const fetchResult = useFetch(url, [sensorInfo]); + const { loading, error, data: dto } = fetchResult; + + const details = !error && dto ? processData(sensorInfo, dto) : undefined; + + return { loading, error, details }; +} diff --git a/src/model/models.ts b/src/model/models.ts index c19303f..4d50e4f 100644 --- a/src/model/models.ts +++ b/src/model/models.ts @@ -39,3 +39,14 @@ export interface MoistureData { records: SensorInfo[]; timestamp: Date; } + +export interface MoistureMeasurement { + timestamp: Date; + moisture: number; +} + +export interface SensorDetails { + info: SensorInfo; + measurements: MoistureMeasurement[]; + peerMeasurements: MoistureMeasurement[]; +} diff --git a/types/vite.d.ts b/types/vite.d.ts index 6a86b3e..4cdc0fb 100644 --- a/types/vite.d.ts +++ b/types/vite.d.ts @@ -1,6 +1,6 @@ interface ImportMetaEnv { readonly DEV: boolean; - readonly VITE_MOISTURE_DATA_URL: string; + readonly VITE_BACKEND_URL: string; } interface ImportMeta {