From 62a6cdd0ddfec6755b442bd0c34f8bd006f1b0d2 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Mon, 16 Sep 2024 14:14:27 +0200 Subject: [PATCH 01/37] :tada: Added a new hook and use that hook to be able to add an axis range on a specific value of y-axis --- .../LineChartComponents/LineChart.tsx | 47 +++++++++++++++++ .../shared/LineChart/ValueAxisRange.ts | 50 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 frontend/src/components/shared/LineChart/ValueAxisRange.ts diff --git a/frontend/src/components/LineChartComponents/LineChart.tsx b/frontend/src/components/LineChartComponents/LineChart.tsx index 4dbdab11..1f44c5ba 100644 --- a/frontend/src/components/LineChartComponents/LineChart.tsx +++ b/frontend/src/components/LineChartComponents/LineChart.tsx @@ -25,6 +25,7 @@ import useDateAxis from 'components/shared/LineChart/DateAxis'; import useValueAxis from 'components/shared/LineChart/ValueAxis'; import {useDateSelectorFilter} from 'components/shared/LineChart/Filter'; import useDateAxisRange from 'components/shared/LineChart/AxisRange'; +import useValueAxisRange from 'components/shared/LineChart/ValueAxisRange'; import {useLineSeriesList} from 'components/shared/LineChart/LineSeries'; import {LineSeries} from '@amcharts/amcharts5/.internal/charts/xy/series/LineSeries'; import {LineChartData} from 'types/lineChart'; @@ -65,6 +66,9 @@ interface LineChartProps { /** Optional localization settings for the chart, including number formatting and language overrides. */ localization?: Localization; + + /** Optional horizontal limit for the Y-axis. Defaults to 0. */ + horizontalYLimit?: number; } /** * React Component to render the Linechart Section @@ -82,6 +86,7 @@ export default function LineChart({ exportedFileName = 'Data', yAxisLabel, localization, + horizontalYLimit = 50000, }: LineChartProps): JSX.Element { const {t: defaultT, i18n} = useTranslation(); @@ -237,6 +242,48 @@ export default function LineChart({ useDateAxisRange(selectedDateRangeSettings, root, chart, xAxis); + // a horizontal line to limit the y-axis + const targetLineSettings = useMemo(() => { + if (!root || (!horizontalYLimit || horizontalYLimit === 0)) { + return {}; + } + + // TODO: how to get the max value of the y-axis? + return { + data: { + value: horizontalYLimit, + endValue: 110000, // max value of the y-axis + }, + grid: { + stroke: color(theme.palette.error.main), + strokeOpacity: 1, + strokeWidth: 2, + visible: true, + location: 0, + }, + axisFill: { + fill: color(theme.palette.error.main), + fillOpacity: 0.3, + visible: true, + }, + label: { + fill: color(theme.palette.divider), + text: `Target: ${horizontalYLimit}`, + location: 0, + background: RoundedRectangle.new(root, { + fill: color(theme.palette.secondary.main), + }), + + // Put Label to the topmost layer to make sure it is drawn on top of the axis tick labels + layer: Number.MAX_VALUE, + } + } + + }, [root, horizontalYLimit, theme.palette.divider]); + + // Effect to add horizontal line to chart + useValueAxisRange(targetLineSettings, root, chart, yAxis); + // Effect to change localization of chart if language changes useLayoutEffect( () => { diff --git a/frontend/src/components/shared/LineChart/ValueAxisRange.ts b/frontend/src/components/shared/LineChart/ValueAxisRange.ts new file mode 100644 index 00000000..62bdcea4 --- /dev/null +++ b/frontend/src/components/shared/LineChart/ValueAxisRange.ts @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: Apache-2.0 + +import {useLayoutEffect} from 'react'; +import {Root} from '@amcharts/amcharts5/.internal/core/Root'; +import {XYChart} from '@amcharts/amcharts5/.internal/charts/xy/XYChart'; +import {ValueAxis} from '@amcharts/amcharts5/.internal/charts/xy/axes/ValueAxis'; +import {AxisRenderer} from '@amcharts/amcharts5/.internal/charts/xy/axes/AxisRenderer'; +import {IValueAxisDataItem} from '@amcharts/amcharts5/.internal/charts/xy/axes/ValueAxis'; +import {IGridSettings} from '@amcharts/amcharts5/.internal/charts/xy/axes/Grid'; +import {IGraphicsSettings} from '@amcharts/amcharts5/.internal/core/render/Graphics'; +import {IAxisLabelSettings} from '@amcharts/amcharts5/.internal/charts/xy/axes/AxisLabel'; + +// This hook is used to create a value axis range for the chart. +export default function useValueAxisRange( + settings: { + data?: IValueAxisDataItem; + grid?: Partial; + axisFill?: Partial; + label?: Partial; + }, + root: Root | null, + chart: XYChart | null, + yAxis: ValueAxis | null +) { + useLayoutEffect(() => { + if (!chart || !root || !yAxis || !settings.data) { + return; + } + + const rangeDataItem = yAxis.makeDataItem(settings.data); + const range = yAxis.createAxisRange(rangeDataItem); + + if (settings.grid) { + range.get('grid')?.setAll(settings.grid); + } + + if (settings.axisFill) { + range.get('axisFill')?.setAll(settings.axisFill); + } + + if (settings.label) { + range.get('label')?.setAll(settings.label); + } + + return () => { + yAxis?.axisRanges.removeValue(range); + }; + }, [chart, root, settings.axisFill, settings.data, settings.grid, settings.label, yAxis]); +} From 3eecb4a9cdb1ba2db08b7dcb57a26faf633d3ab7 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Tue, 17 Sep 2024 18:25:11 +0200 Subject: [PATCH 02/37] :wrench: Refactor LineChart component to use horizontalYAxisThreshold instead of horizontalYLimit --- .../LineChartComponents/LineChart.tsx | 93 ++++++++++--------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/frontend/src/components/LineChartComponents/LineChart.tsx b/frontend/src/components/LineChartComponents/LineChart.tsx index 1f44c5ba..34eed868 100644 --- a/frontend/src/components/LineChartComponents/LineChart.tsx +++ b/frontend/src/components/LineChartComponents/LineChart.tsx @@ -68,7 +68,7 @@ interface LineChartProps { localization?: Localization; /** Optional horizontal limit for the Y-axis. Defaults to 0. */ - horizontalYLimit?: number; + horizontalYAxisThreshold?: number; } /** * React Component to render the Linechart Section @@ -86,7 +86,7 @@ export default function LineChart({ exportedFileName = 'Data', yAxisLabel, localization, - horizontalYLimit = 50000, + horizontalYAxisThreshold = 0, }: LineChartProps): JSX.Element { const {t: defaultT, i18n} = useTranslation(); @@ -242,48 +242,6 @@ export default function LineChart({ useDateAxisRange(selectedDateRangeSettings, root, chart, xAxis); - // a horizontal line to limit the y-axis - const targetLineSettings = useMemo(() => { - if (!root || (!horizontalYLimit || horizontalYLimit === 0)) { - return {}; - } - - // TODO: how to get the max value of the y-axis? - return { - data: { - value: horizontalYLimit, - endValue: 110000, // max value of the y-axis - }, - grid: { - stroke: color(theme.palette.error.main), - strokeOpacity: 1, - strokeWidth: 2, - visible: true, - location: 0, - }, - axisFill: { - fill: color(theme.palette.error.main), - fillOpacity: 0.3, - visible: true, - }, - label: { - fill: color(theme.palette.divider), - text: `Target: ${horizontalYLimit}`, - location: 0, - background: RoundedRectangle.new(root, { - fill: color(theme.palette.secondary.main), - }), - - // Put Label to the topmost layer to make sure it is drawn on top of the axis tick labels - layer: Number.MAX_VALUE, - } - } - - }, [root, horizontalYLimit, theme.palette.divider]); - - // Effect to add horizontal line to chart - useValueAxisRange(targetLineSettings, root, chart, yAxis); - // Effect to change localization of chart if language changes useLayoutEffect( () => { @@ -429,6 +387,53 @@ export default function LineChart({ ) ); + // a horizontal line to limit the y-axis + const targetLineSettings = useMemo(() => { + if (!root || !yAxis || !horizontalYAxisThreshold || horizontalYAxisThreshold === 0) { + return {}; + } + + return { + data: { + value: horizontalYAxisThreshold, + endValue: 1e6, // Adjust max value as needed + above: true, + }, + grid: { + stroke: color(theme.palette.error.main), // Use dynamic stroke color based on the threshold + strokeOpacity: 1, + strokeWidth: 2, + visible: true, + location: 0, + }, + axisFill: { + fill: color(theme.palette.error.main), // Use dynamic fill color based on the threshold + fillOpacity: 0.5, + visible: true, + }, + label: { + fill: color(theme.palette.divider), + text: `Threshold: ${horizontalYAxisThreshold}`, + location: 0, + background: RoundedRectangle.new(root, { + fill: color(theme.palette.primary.main), // Apply dynamic background color for the label + }), + centerY: 1, + layer: Number.MAX_VALUE, + }, + }; + }, [ + root, + yAxis, + horizontalYAxisThreshold, + theme.palette.divider, + theme.palette.error.main, + theme.palette.primary.main, + ]); + + // Add horizontal line to limit the y-axis + useValueAxisRange(targetLineSettings, root, chart, yAxis); + // Effect to update data in series useEffect(() => { // Skip effect if chart is not initialized yet From 8d0c16531231fe6043fcbe788d5ce078498c8d26 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Tue, 17 Sep 2024 18:26:57 +0200 Subject: [PATCH 03/37] :tada: Added settings button to lineChartContainer, now horizontalYAxisThreshold is saved in UserPreferenceSlice.ts and could be adjusted through LineChartSettings --- .../LineChartComponents/LineChartSettings.tsx | 91 +++++++++++++++++++ .../src/components/LineChartContainer.tsx | 4 + frontend/src/store/UserPreferenceSlice.ts | 10 +- 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/LineChartComponents/LineChartSettings.tsx diff --git a/frontend/src/components/LineChartComponents/LineChartSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettings.tsx new file mode 100644 index 00000000..2d556669 --- /dev/null +++ b/frontend/src/components/LineChartComponents/LineChartSettings.tsx @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: Apache-2.0 + +import SettingsIcon from '@mui/icons-material/Settings'; +import {Button, Grid, Popover, Typography} from '@mui/material'; +import Box from '@mui/material/Box'; +import {useAppSelector, useAppDispatch} from '../../store/hooks'; +import {setHorizontalYAxisThreshold} from '../../store/UserPreferenceSlice'; +import IconButton from '@mui/material/IconButton'; +import CloseIcon from '@mui/icons-material/Close'; +import TextField from '@mui/material/TextField'; + +import React, {useState} from 'react'; + +export function LineChartSettings() { + const dispatch = useAppDispatch(); + const [anchorEl, setAnchorEl] = useState(null); + const [showPopover, setShowPopover] = useState(false); + + const horizontalYAxisThreshold = useAppSelector((state) => state.userPreference.horizontalYAxisThreshold); + + const handlePopoverOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + setShowPopover(true); + }; + + const handlePopoverClose = () => { + setAnchorEl(null); + setShowPopover(false); + }; + + const handleThresholdChange = (event: React.ChangeEvent) => { + const newThreshold = Number(event.target.value); + + // update redux state + dispatch(setHorizontalYAxisThreshold(newThreshold)); + }; + + return ( + + + + + + + + Chart Settings + + + Horizontal Y-Threshold + + + + + + + + + ); +} diff --git a/frontend/src/components/LineChartContainer.tsx b/frontend/src/components/LineChartContainer.tsx index b53e725c..97b299c3 100644 --- a/frontend/src/components/LineChartContainer.tsx +++ b/frontend/src/components/LineChartContainer.tsx @@ -10,6 +10,7 @@ import {useAppDispatch, useAppSelector} from 'store/hooks'; import {selectDate} from 'store/DataSelectionSlice'; import {setReferenceDayBottom} from 'store/LayoutSlice'; import {useTranslation} from 'react-i18next'; +import {LineChartSettings} from './LineChartComponents/LineChartSettings'; export default function LineChartContainer() { const {t} = useTranslation('backend'); @@ -20,6 +21,7 @@ export default function LineChartContainer() { const selectedCompartment = useAppSelector((state) => state.dataSelection.compartment); const selectedDateInStore = useAppSelector((state) => state.dataSelection.date); + const horizontalYAxisThreshold = useAppSelector((state) => state.userPreference.horizontalYAxisThreshold); const referenceDay = useAppSelector((state) => state.dataSelection.simulationStart); const minDate = useAppSelector((state) => state.dataSelection.minDate); const maxDate = useAppSelector((state) => state.dataSelection.maxDate); @@ -66,7 +68,9 @@ export default function LineChartContainer() { maxDate={maxDate} referenceDay={referenceDay} yAxisLabel={yAxisLabel} + horizontalYAxisThreshold={horizontalYAxisThreshold} /> + ); } diff --git a/frontend/src/store/UserPreferenceSlice.ts b/frontend/src/store/UserPreferenceSlice.ts index 7459ca91..cc60d0ef 100644 --- a/frontend/src/store/UserPreferenceSlice.ts +++ b/frontend/src/store/UserPreferenceSlice.ts @@ -8,6 +8,7 @@ export interface UserPreference { selectedHeatmap: HeatmapLegend; selectedTab?: string; isInitialVisit: boolean; + horizontalYAxisThreshold?: number; } const initialState: UserPreference = { @@ -22,6 +23,7 @@ const initialState: UserPreference = { }, selectedTab: '1', isInitialVisit: true, + horizontalYAxisThreshold: undefined, }; /** @@ -42,8 +44,14 @@ export const UserPreferenceSlice = createSlice({ setInitialVisit(state, action: PayloadAction) { state.isInitialVisit = action.payload; }, + + /** Set the horizontal Y-Axis Threshold */ + setHorizontalYAxisThreshold(state, action: PayloadAction) { + state.horizontalYAxisThreshold = action.payload; + }, }, }); -export const {selectHeatmapLegend, selectTab, setInitialVisit} = UserPreferenceSlice.actions; +export const {selectHeatmapLegend, selectTab, setInitialVisit, setHorizontalYAxisThreshold} = + UserPreferenceSlice.actions; export default UserPreferenceSlice.reducer; From 5b2fbd354a8a053e5543e3527f7448b2d538bbdb Mon Sep 17 00:00:00 2001 From: kunkoala Date: Wed, 18 Sep 2024 14:18:29 +0200 Subject: [PATCH 04/37] :tada: added a new hook for creating series range, and use it with useLineSeries to create all ranges (wonder if this approach worked) --- .../LineChartComponents/LineChart.tsx | 72 +++++++++++-------- .../shared/LineChart/SeriesRange.ts | 61 ++++++++++++++++ 2 files changed, 103 insertions(+), 30 deletions(-) create mode 100644 frontend/src/components/shared/LineChart/SeriesRange.ts diff --git a/frontend/src/components/LineChartComponents/LineChart.tsx b/frontend/src/components/LineChartComponents/LineChart.tsx index 34eed868..e81de7a9 100644 --- a/frontend/src/components/LineChartComponents/LineChart.tsx +++ b/frontend/src/components/LineChartComponents/LineChart.tsx @@ -26,9 +26,8 @@ import useValueAxis from 'components/shared/LineChart/ValueAxis'; import {useDateSelectorFilter} from 'components/shared/LineChart/Filter'; import useDateAxisRange from 'components/shared/LineChart/AxisRange'; import useValueAxisRange from 'components/shared/LineChart/ValueAxisRange'; -import {useLineSeriesList} from 'components/shared/LineChart/LineSeries'; -import {LineSeries} from '@amcharts/amcharts5/.internal/charts/xy/series/LineSeries'; import {LineChartData} from 'types/lineChart'; +import {useSeriesRange} from 'components/shared/LineChart/SeriesRange'; interface LineChartProps { /** Optional unique identifier for the chart. Defaults to 'chartdiv'. */ @@ -86,7 +85,7 @@ export default function LineChart({ exportedFileName = 'Data', yAxisLabel, localization, - horizontalYAxisThreshold = 0, + horizontalYAxisThreshold = undefined, }: LineChartProps): JSX.Element { const {t: defaultT, i18n} = useTranslation(); @@ -364,28 +363,46 @@ export default function LineChart({ }); }, [lineChartData, root, xAxis, yAxis, chartId]); - useLineSeriesList( - root, - chart, - lineChartDataSettings, - useCallback( - (series: LineSeries) => { - if (!lineChartData) return; - const seriesSettings = lineChartData.find((line) => line.serieId === series.get('id')?.split('_')[1]); - series.strokes.template.setAll({ - strokeWidth: seriesSettings?.stroke.strokeWidth ?? 2, - strokeDasharray: seriesSettings?.stroke.strokeDasharray ?? undefined, - }); - if (seriesSettings?.fill) { - series.fills.template.setAll({ - fillOpacity: seriesSettings.fillOpacity ?? 1, - visible: true, - }); - } - }, - [lineChartData] - ) - ); + // useLineSeriesList( + // root, + // chart, + // lineChartDataSettings, + // useCallback( + // (series: LineSeries) => { + // if (!lineChartData) return; + + // const seriesSettings = lineChartData.find((line) => line.serieId === series.get('id')?.split('_')[1]); + + // series.strokes.template.setAll({ + // strokeWidth: seriesSettings?.stroke.strokeWidth ?? 2, + // strokeDasharray: seriesSettings?.stroke.strokeDasharray ?? undefined, + // }); + // if (seriesSettings?.fill) { + // series.fills.template.setAll({ + // fillOpacity: seriesSettings.fillOpacity ?? 1, + // visible: true, + // }); + // } + // }, + // [lineChartData] + // ) + // ); + + // Effect to add series range above threshold to chart + useSeriesRange(root, chart, lineChartDataSettings, yAxis, { + threshold: horizontalYAxisThreshold ?? 0, + fills: { + fill: color(theme.palette.error.main), + fillOpacity: 0.3, + visible: true, + }, + strokes: { + stroke: color(theme.palette.error.main), + strokeWidth: 2, + strokeOpacity: 1, + visible: true, + }, + }); // a horizontal line to limit the y-axis const targetLineSettings = useMemo(() => { @@ -406,11 +423,6 @@ export default function LineChart({ visible: true, location: 0, }, - axisFill: { - fill: color(theme.palette.error.main), // Use dynamic fill color based on the threshold - fillOpacity: 0.5, - visible: true, - }, label: { fill: color(theme.palette.divider), text: `Threshold: ${horizontalYAxisThreshold}`, diff --git a/frontend/src/components/shared/LineChart/SeriesRange.ts b/frontend/src/components/shared/LineChart/SeriesRange.ts new file mode 100644 index 00000000..d8debcd5 --- /dev/null +++ b/frontend/src/components/shared/LineChart/SeriesRange.ts @@ -0,0 +1,61 @@ +import {XYChart} from '@amcharts/amcharts5/.internal/charts/xy/XYChart'; +import {Root} from '@amcharts/amcharts5/.internal/core/Root'; +import {ILineSeriesSettings, LineSeries} from '@amcharts/amcharts5/.internal/charts/xy/series/LineSeries'; +import {useLineSeriesList} from './LineSeries'; +import {useLayoutEffect} from 'react'; +import {AxisRenderer, ValueAxis} from '@amcharts/amcharts5/xy'; +import {IGraphicsSettings} from '@amcharts/amcharts5'; +import {ILineSeriesAxisRange} from '@amcharts/amcharts5/.internal/charts/xy/series/LineSeries'; + +export function useSeriesRange( + root: Root | null, + chart: XYChart | null, + settings: Array, + yAxis: ValueAxis | null, // The yAxis for creating range + rangeSettings: { + threshold: number; // The threshold for the series range + fills: Partial; // Fill color for the range + strokes: Partial; // Stroke color for the range + }, + initializer?: (series: LineSeries, i: number) => void +) { + // Use the existing `useLineSeriesList` hook to create the series + const seriesList = useLineSeriesList(root, chart, settings, initializer); + + // Use `useLayoutEffect` to apply the series range logic after the series are created + useLayoutEffect(() => { + if (!seriesList || !yAxis || seriesList.length === 0) return; + + // Iterate over each series to create and apply the series range + seriesList.forEach((series: LineSeries) => { + const seriesRangeDataItem = yAxis.makeDataItem({ + value: rangeSettings.threshold, // Start of the range + endValue: 1e6, // End value of the range (adjust as needed) + }); + + const seriesRange = series.createAxisRange(seriesRangeDataItem); + + // Set the fill and stroke properties for the range + if (rangeSettings.fills) { + seriesRange.fills?.template.setAll(rangeSettings.fills); + } + + if (rangeSettings.strokes) { + seriesRange.strokes?.template.setAll(rangeSettings.strokes); + } + }); + + return () => { + // Dispose of the ranges when the component unmounts + seriesList.forEach((series: LineSeries) => { + series.axisRanges.each((range: ILineSeriesAxisRange) => { + if (series.axisRanges.contains(range)) { + series.axisRanges.removeValue(range); + } + }); + }); + }; + }, [seriesList, rangeSettings, yAxis]); + + return seriesList ?? null; +} From 4e56f95a6b154067c8a5bb5b1dddc4d69f6a660e Mon Sep 17 00:00:00 2001 From: kunkoala Date: Fri, 27 Sep 2024 11:26:07 +0200 Subject: [PATCH 05/37] :wrench: :hammer: make a seriesRangeSettings to use for the series range, and updated the seriesRange settings to accept optional parameter so that the horizontal line is undefined when there's no horizontalYAxis --- .../LineChartComponents/LineChart.tsx | 37 ++++++++++++------- .../shared/LineChart/SeriesRange.ts | 6 +-- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/LineChartComponents/LineChart.tsx b/frontend/src/components/LineChartComponents/LineChart.tsx index e81de7a9..0a2fd67b 100644 --- a/frontend/src/components/LineChartComponents/LineChart.tsx +++ b/frontend/src/components/LineChartComponents/LineChart.tsx @@ -389,20 +389,29 @@ export default function LineChart({ // ); // Effect to add series range above threshold to chart - useSeriesRange(root, chart, lineChartDataSettings, yAxis, { - threshold: horizontalYAxisThreshold ?? 0, - fills: { - fill: color(theme.palette.error.main), - fillOpacity: 0.3, - visible: true, - }, - strokes: { - stroke: color(theme.palette.error.main), - strokeWidth: 2, - strokeOpacity: 1, - visible: true, - }, - }); + + const seriesRangeSettings = useMemo(() => { + if (!root || !horizontalYAxisThreshold || horizontalYAxisThreshold === 0) { + return {}; + } + + return { + threshold: horizontalYAxisThreshold, + fills: { + fill: color(theme.palette.error.main), + fillOpacity: 0.3, + visible: true, + }, + strokes: { + stroke: color(theme.palette.error.main), + strokeWidth: 2, + strokeOpacity: 1, + visible: true, + }, + }; + }, [root, horizontalYAxisThreshold, theme.palette.error.main]); + + useSeriesRange(root, chart, lineChartDataSettings, yAxis, seriesRangeSettings); // a horizontal line to limit the y-axis const targetLineSettings = useMemo(() => { diff --git a/frontend/src/components/shared/LineChart/SeriesRange.ts b/frontend/src/components/shared/LineChart/SeriesRange.ts index d8debcd5..2f292ac7 100644 --- a/frontend/src/components/shared/LineChart/SeriesRange.ts +++ b/frontend/src/components/shared/LineChart/SeriesRange.ts @@ -13,9 +13,9 @@ export function useSeriesRange( settings: Array, yAxis: ValueAxis | null, // The yAxis for creating range rangeSettings: { - threshold: number; // The threshold for the series range - fills: Partial; // Fill color for the range - strokes: Partial; // Stroke color for the range + threshold?: number; // The threshold for the series range + fills?: Partial; // Fill color for the range + strokes?: Partial; // Stroke color for the range }, initializer?: (series: LineSeries, i: number) => void ) { From 6a469f4a5a6e666399c5c0e8d548eb5bef4556ab Mon Sep 17 00:00:00 2001 From: kunkoala Date: Fri, 27 Sep 2024 15:04:24 +0200 Subject: [PATCH 06/37] :wrench: :hammer: Update LineChartSettings to use selected district and compartment for horizontal Y-axis threshold --- .../LineChartComponents/LineChartSettings.tsx | 12 ++++++++++-- frontend/src/components/LineChartContainer.tsx | 3 ++- frontend/src/store/UserPreferenceSlice.ts | 16 +++++++++++----- frontend/src/types/horizontalThreshold.ts | 13 +++++++++++++ 4 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 frontend/src/types/horizontalThreshold.ts diff --git a/frontend/src/components/LineChartComponents/LineChartSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettings.tsx index 2d556669..cde5a1f9 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettings.tsx @@ -17,7 +17,11 @@ export function LineChartSettings() { const [anchorEl, setAnchorEl] = useState(null); const [showPopover, setShowPopover] = useState(false); - const horizontalYAxisThreshold = useAppSelector((state) => state.userPreference.horizontalYAxisThreshold); + const selectedDistrict = useAppSelector((state) => state.dataSelection.district.ags); + const selectedCompartment = useAppSelector((state) => state.dataSelection.compartment); + const horizontalYAxisThreshold = useAppSelector( + (state) => state.userPreference.horizontalYAxisThresholds?.[`${selectedDistrict}-${selectedCompartment}`]?.threshold + ); const handlePopoverOpen = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -33,7 +37,11 @@ export function LineChartSettings() { const newThreshold = Number(event.target.value); // update redux state - dispatch(setHorizontalYAxisThreshold(newThreshold)); + dispatch(setHorizontalYAxisThreshold({ + district: selectedDistrict, + compartment: selectedCompartment ?? '', + threshold: newThreshold, + })); }; return ( diff --git a/frontend/src/components/LineChartContainer.tsx b/frontend/src/components/LineChartContainer.tsx index 97b299c3..8c0617f3 100644 --- a/frontend/src/components/LineChartContainer.tsx +++ b/frontend/src/components/LineChartContainer.tsx @@ -20,8 +20,9 @@ export default function LineChartContainer() { const {isChartDataFetching, chartData} = useContext(DataContext); const selectedCompartment = useAppSelector((state) => state.dataSelection.compartment); + const selectedDistrict = useAppSelector((state) => state.dataSelection.district.ags); const selectedDateInStore = useAppSelector((state) => state.dataSelection.date); - const horizontalYAxisThreshold = useAppSelector((state) => state.userPreference.horizontalYAxisThreshold); + const horizontalYAxisThreshold = useAppSelector((state) => state.userPreference.horizontalYAxisThresholds?.[`${selectedDistrict}-${selectedCompartment}`]?.threshold); const referenceDay = useAppSelector((state) => state.dataSelection.simulationStart); const minDate = useAppSelector((state) => state.dataSelection.minDate); const maxDate = useAppSelector((state) => state.dataSelection.maxDate); diff --git a/frontend/src/store/UserPreferenceSlice.ts b/frontend/src/store/UserPreferenceSlice.ts index cc60d0ef..02204e03 100644 --- a/frontend/src/store/UserPreferenceSlice.ts +++ b/frontend/src/store/UserPreferenceSlice.ts @@ -3,12 +3,14 @@ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; import {HeatmapLegend} from '../types/heatmapLegend'; +import {HorizontalThreshold} from 'types/horizontalThreshold'; +import {Dictionary} from 'util/util'; export interface UserPreference { selectedHeatmap: HeatmapLegend; selectedTab?: string; isInitialVisit: boolean; - horizontalYAxisThreshold?: number; + horizontalYAxisThresholds?: Dictionary; } const initialState: UserPreference = { @@ -23,7 +25,7 @@ const initialState: UserPreference = { }, selectedTab: '1', isInitialVisit: true, - horizontalYAxisThreshold: undefined, + horizontalYAxisThresholds: {}, }; /** @@ -45,9 +47,13 @@ export const UserPreferenceSlice = createSlice({ state.isInitialVisit = action.payload; }, - /** Set the horizontal Y-Axis Threshold */ - setHorizontalYAxisThreshold(state, action: PayloadAction) { - state.horizontalYAxisThreshold = action.payload; + /** Set the horizontal Y-Axis Threshold for a specific district and compartment */ + setHorizontalYAxisThreshold(state, action: PayloadAction) { + if (!state.horizontalYAxisThresholds) { + state.horizontalYAxisThresholds = {}; + } + + state.horizontalYAxisThresholds[`${action.payload.district}-${action.payload.compartment}`] = action.payload; }, }, }); diff --git a/frontend/src/types/horizontalThreshold.ts b/frontend/src/types/horizontalThreshold.ts new file mode 100644 index 00000000..1259793c --- /dev/null +++ b/frontend/src/types/horizontalThreshold.ts @@ -0,0 +1,13 @@ +/** + * Represents the horizontal threshold for a specific district and compartment. + */ +export interface HorizontalThreshold { + /** The district for which the threshold applies (AGS). */ + district: string; + + /** The compartment for which the threshold applies. */ + compartment: string; + + /** The actual threshold value for the Y-axis. */ + threshold: number; +} From 4be3f678cc9da1b14e2ad180303756aae5a759b8 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Fri, 27 Sep 2024 15:52:50 +0200 Subject: [PATCH 07/37] :wrench: small code refactors --- .../LineChartComponents/LineChart.tsx | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/LineChartComponents/LineChart.tsx b/frontend/src/components/LineChartComponents/LineChart.tsx index 0a2fd67b..cd9a2ced 100644 --- a/frontend/src/components/LineChartComponents/LineChart.tsx +++ b/frontend/src/components/LineChartComponents/LineChart.tsx @@ -27,6 +27,7 @@ import {useDateSelectorFilter} from 'components/shared/LineChart/Filter'; import useDateAxisRange from 'components/shared/LineChart/AxisRange'; import useValueAxisRange from 'components/shared/LineChart/ValueAxisRange'; import {LineChartData} from 'types/lineChart'; +import {LineSeries} from '@amcharts/amcharts5/xy'; import {useSeriesRange} from 'components/shared/LineChart/SeriesRange'; interface LineChartProps { @@ -363,33 +364,7 @@ export default function LineChart({ }); }, [lineChartData, root, xAxis, yAxis, chartId]); - // useLineSeriesList( - // root, - // chart, - // lineChartDataSettings, - // useCallback( - // (series: LineSeries) => { - // if (!lineChartData) return; - - // const seriesSettings = lineChartData.find((line) => line.serieId === series.get('id')?.split('_')[1]); - - // series.strokes.template.setAll({ - // strokeWidth: seriesSettings?.stroke.strokeWidth ?? 2, - // strokeDasharray: seriesSettings?.stroke.strokeDasharray ?? undefined, - // }); - // if (seriesSettings?.fill) { - // series.fills.template.setAll({ - // fillOpacity: seriesSettings.fillOpacity ?? 1, - // visible: true, - // }); - // } - // }, - // [lineChartData] - // ) - // ); - // Effect to add series range above threshold to chart - const seriesRangeSettings = useMemo(() => { if (!root || !horizontalYAxisThreshold || horizontalYAxisThreshold === 0) { return {}; @@ -406,12 +381,38 @@ export default function LineChart({ stroke: color(theme.palette.error.main), strokeWidth: 2, strokeOpacity: 1, + strokeDasharray: [6, 4], visible: true, }, }; }, [root, horizontalYAxisThreshold, theme.palette.error.main]); - useSeriesRange(root, chart, lineChartDataSettings, yAxis, seriesRangeSettings); + useSeriesRange( + root, + chart, + lineChartDataSettings, + yAxis, + seriesRangeSettings, + useCallback( + (series: LineSeries) => { + if (!lineChartData) return; + + const seriesSettings = lineChartData.find((line) => line.serieId === series.get('id')?.split('_')[1]); + console.log(seriesSettings); + series.strokes.template.setAll({ + strokeWidth: seriesSettings?.stroke.strokeWidth ?? 2, + strokeDasharray: seriesSettings?.stroke.strokeDasharray ?? undefined, + }); + if (seriesSettings?.fill) { + series.fills.template.setAll({ + fillOpacity: seriesSettings.fillOpacity ?? 1, + visible: true, + }); + } + }, + [lineChartData] + ) + ); // a horizontal line to limit the y-axis const targetLineSettings = useMemo(() => { From df759974fc5f0cfd75fa36bb4409551a92931813 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Fri, 27 Sep 2024 16:58:28 +0200 Subject: [PATCH 08/37] :sparkles: Fixed formatting issues --- .../LineChartComponents/LineChartSettings.tsx | 12 +++++++----- frontend/src/components/LineChartContainer.tsx | 4 +++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/LineChartComponents/LineChartSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettings.tsx index cde5a1f9..11c7e904 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettings.tsx @@ -37,11 +37,13 @@ export function LineChartSettings() { const newThreshold = Number(event.target.value); // update redux state - dispatch(setHorizontalYAxisThreshold({ - district: selectedDistrict, - compartment: selectedCompartment ?? '', - threshold: newThreshold, - })); + dispatch( + setHorizontalYAxisThreshold({ + district: selectedDistrict, + compartment: selectedCompartment ?? '', + threshold: newThreshold, + }) + ); }; return ( diff --git a/frontend/src/components/LineChartContainer.tsx b/frontend/src/components/LineChartContainer.tsx index 8c0617f3..bc50556d 100644 --- a/frontend/src/components/LineChartContainer.tsx +++ b/frontend/src/components/LineChartContainer.tsx @@ -22,7 +22,9 @@ export default function LineChartContainer() { const selectedCompartment = useAppSelector((state) => state.dataSelection.compartment); const selectedDistrict = useAppSelector((state) => state.dataSelection.district.ags); const selectedDateInStore = useAppSelector((state) => state.dataSelection.date); - const horizontalYAxisThreshold = useAppSelector((state) => state.userPreference.horizontalYAxisThresholds?.[`${selectedDistrict}-${selectedCompartment}`]?.threshold); + const horizontalYAxisThreshold = useAppSelector( + (state) => state.userPreference.horizontalYAxisThresholds?.[`${selectedDistrict}-${selectedCompartment}`]?.threshold + ); const referenceDay = useAppSelector((state) => state.dataSelection.simulationStart); const minDate = useAppSelector((state) => state.dataSelection.minDate); const maxDate = useAppSelector((state) => state.dataSelection.maxDate); From b775110d7da10f5784d467b15d853bf443cd2afd Mon Sep 17 00:00:00 2001 From: kunkoala Date: Mon, 30 Sep 2024 17:50:40 +0200 Subject: [PATCH 09/37] :sparkles: added license --- frontend/src/components/shared/LineChart/SeriesRange.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/components/shared/LineChart/SeriesRange.ts b/frontend/src/components/shared/LineChart/SeriesRange.ts index 2f292ac7..c1aeff8a 100644 --- a/frontend/src/components/shared/LineChart/SeriesRange.ts +++ b/frontend/src/components/shared/LineChart/SeriesRange.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: Apache-2.0 + import {XYChart} from '@amcharts/amcharts5/.internal/charts/xy/XYChart'; import {Root} from '@amcharts/amcharts5/.internal/core/Root'; import {ILineSeriesSettings, LineSeries} from '@amcharts/amcharts5/.internal/charts/xy/series/LineSeries'; From 0b1e1e883a435c762cf93f722c5d8e7f4279dc63 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Mon, 30 Sep 2024 17:52:01 +0200 Subject: [PATCH 10/37] :wrench: :hammer: refactored district data type and use it on different files --- frontend/src/store/DataSelectionSlice.ts | 15 ++------------- frontend/src/types/district.ts | 20 ++++++++++++++++++++ frontend/src/types/horizontalThreshold.ts | 4 +++- 3 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 frontend/src/types/district.ts diff --git a/frontend/src/store/DataSelectionSlice.ts b/frontend/src/store/DataSelectionSlice.ts index d8f11f24..f23475a8 100644 --- a/frontend/src/store/DataSelectionSlice.ts +++ b/frontend/src/store/DataSelectionSlice.ts @@ -4,14 +4,7 @@ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; import {dateToISOString, Dictionary} from '../util/util'; import {GroupFilter} from 'types/group'; - -/** - * AGS is the abbreviation for "Amtlicher Gemeindeschlüssel" in German, which are IDs of areas in Germany. The AGS have - * a structure to them that describes a hierarchy from a state level to a district level (and even smaller). Since we - * are only interested in districts, our AGS are always of length 5. We dedicate the AGS of '00000' to the whole of - * Germany, in case no AGS is selected. - */ -export type AGS = string; +import {AGS, District} from 'types/district'; /** * This contains all the state, that the user can configure directly. @@ -19,11 +12,7 @@ export type AGS = string; * IMPORTANT: ALL NEW ADDITIONS MUST BE NULLABLE TO ENSURE EXISTING CACHES DOESN'T BREAK ON UPDATES! */ export interface DataSelection { - district: { - ags: AGS; - name: string; - type: string; - }; + district: District; /** The current date in the store. Must be an ISO 8601 date cutoff at time (YYYY-MM-DD) */ date: string | null; scenario: number | null; diff --git a/frontend/src/types/district.ts b/frontend/src/types/district.ts new file mode 100644 index 00000000..1a19a7bd --- /dev/null +++ b/frontend/src/types/district.ts @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: Apache-2.0 + +/** + * AGS is the abbreviation for "Amtlicher Gemeindeschlüssel" in German, which are IDs of areas in Germany. The AGS have + * a structure to them that describes a hierarchy from a state level to a district level (and even smaller). Since we + * are only interested in districts, our AGS are always of length 5. We dedicate the AGS of '00000' to the whole of + * Germany, in case no AGS is selected. + */ +export type AGS = string; + +/** + * This interface describes a district in Germany. It contains the AGS, the name of the district and the type of the + * district. + */ +export interface District { + ags: AGS; + name: string; + type: string; +} diff --git a/frontend/src/types/horizontalThreshold.ts b/frontend/src/types/horizontalThreshold.ts index 1259793c..357dfd51 100644 --- a/frontend/src/types/horizontalThreshold.ts +++ b/frontend/src/types/horizontalThreshold.ts @@ -1,9 +1,11 @@ +import {District} from './district'; + /** * Represents the horizontal threshold for a specific district and compartment. */ export interface HorizontalThreshold { /** The district for which the threshold applies (AGS). */ - district: string; + district: District; /** The compartment for which the threshold applies. */ compartment: string; From 7f5a75180f076dd52bfe162c7445a59d639caf4c Mon Sep 17 00:00:00 2001 From: kunkoala Date: Mon, 30 Sep 2024 17:53:11 +0200 Subject: [PATCH 11/37] :wrench: edited some functions for horizontal threshold slice in UserPreferenceSlice --- frontend/src/store/UserPreferenceSlice.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/frontend/src/store/UserPreferenceSlice.ts b/frontend/src/store/UserPreferenceSlice.ts index 02204e03..cbb30329 100644 --- a/frontend/src/store/UserPreferenceSlice.ts +++ b/frontend/src/store/UserPreferenceSlice.ts @@ -10,7 +10,7 @@ export interface UserPreference { selectedHeatmap: HeatmapLegend; selectedTab?: string; isInitialVisit: boolean; - horizontalYAxisThresholds?: Dictionary; + horizontalYAxisThresholds: Dictionary; } const initialState: UserPreference = { @@ -47,17 +47,26 @@ export const UserPreferenceSlice = createSlice({ state.isInitialVisit = action.payload; }, + setHorizontalYAxisThresholds(state, action: PayloadAction>) { + state.horizontalYAxisThresholds = action.payload; + }, + /** Set the horizontal Y-Axis Threshold for a specific district and compartment */ setHorizontalYAxisThreshold(state, action: PayloadAction) { if (!state.horizontalYAxisThresholds) { state.horizontalYAxisThresholds = {}; } - state.horizontalYAxisThresholds[`${action.payload.district}-${action.payload.compartment}`] = action.payload; + state.horizontalYAxisThresholds[`${action.payload.district.ags}-${action.payload.compartment}`] = action.payload; }, }, }); -export const {selectHeatmapLegend, selectTab, setInitialVisit, setHorizontalYAxisThreshold} = - UserPreferenceSlice.actions; +export const { + selectHeatmapLegend, + selectTab, + setInitialVisit, + setHorizontalYAxisThresholds, + setHorizontalYAxisThreshold, +} = UserPreferenceSlice.actions; export default UserPreferenceSlice.reducer; From ef94f5da7bacc72c4d73e614b40f8958944ead42 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Mon, 7 Oct 2024 11:45:20 +0200 Subject: [PATCH 12/37] :tada: :wrench: :hammer: refactored some file structures, added line chart settings menu to the bottom left that is customizable and able to add new menus such as filters, added a horizontal line settings menu that is able to do add, update, delete on horizontal line threshold that is shown on the list. --- .../LineChartComponents/LineChart.tsx | 12 +- .../LineChartContainer.tsx | 27 ++- .../LineChartComponents/LineChartSettings.tsx | 101 --------- .../HorizontalThresholdList.tsx | 177 ++++++++++++++++ .../HorizontalThresholdSettings.tsx | 200 ++++++++++++++++++ .../LineChartSettings.tsx | 156 ++++++++++++++ frontend/src/components/MainContentTabs.tsx | 2 +- 7 files changed, 558 insertions(+), 117 deletions(-) rename frontend/src/components/{ => LineChartComponents}/LineChartContainer.tsx (73%) delete mode 100644 frontend/src/components/LineChartComponents/LineChartSettings.tsx create mode 100644 frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx create mode 100644 frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx create mode 100644 frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx diff --git a/frontend/src/components/LineChartComponents/LineChart.tsx b/frontend/src/components/LineChartComponents/LineChart.tsx index cd9a2ced..606b7cd9 100644 --- a/frontend/src/components/LineChartComponents/LineChart.tsx +++ b/frontend/src/components/LineChartComponents/LineChart.tsx @@ -428,19 +428,21 @@ export default function LineChart({ }, grid: { stroke: color(theme.palette.error.main), // Use dynamic stroke color based on the threshold - strokeOpacity: 1, - strokeWidth: 2, + strokeOpacity: 0.8, + strokeWidth: 1.5, visible: true, location: 0, }, label: { - fill: color(theme.palette.divider), + fill: color(theme.palette.primary.contrastText), text: `Threshold: ${horizontalYAxisThreshold}`, location: 0, + centerY: -3, + centerX: 0, + inside: true, background: RoundedRectangle.new(root, { - fill: color(theme.palette.primary.main), // Apply dynamic background color for the label + fill: color(theme.palette.error.main), }), - centerY: 1, layer: Number.MAX_VALUE, }, }; diff --git a/frontend/src/components/LineChartContainer.tsx b/frontend/src/components/LineChartComponents/LineChartContainer.tsx similarity index 73% rename from frontend/src/components/LineChartContainer.tsx rename to frontend/src/components/LineChartComponents/LineChartContainer.tsx index bc50556d..b7c35385 100644 --- a/frontend/src/components/LineChartContainer.tsx +++ b/frontend/src/components/LineChartComponents/LineChartContainer.tsx @@ -2,15 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 import React, {useContext, useEffect, useMemo, useState} from 'react'; -import LineChart from './LineChartComponents/LineChart'; -import LoadingContainer from './shared/LoadingContainer'; +import LineChart from './LineChart'; +import LoadingContainer from '../shared/LoadingContainer'; import {useTheme} from '@mui/material'; -import {DataContext} from '../DataContext'; +import {DataContext} from '../../DataContext'; import {useAppDispatch, useAppSelector} from 'store/hooks'; import {selectDate} from 'store/DataSelectionSlice'; import {setReferenceDayBottom} from 'store/LayoutSlice'; import {useTranslation} from 'react-i18next'; -import {LineChartSettings} from './LineChartComponents/LineChartSettings'; +import {LineChartSettings} from './LineChartSettingsComponents/LineChartSettings'; +import {Dictionary} from 'util/util'; +import {HorizontalThreshold} from 'types/horizontalThreshold'; export default function LineChartContainer() { const {t} = useTranslation('backend'); @@ -20,17 +22,17 @@ export default function LineChartContainer() { const {isChartDataFetching, chartData} = useContext(DataContext); const selectedCompartment = useAppSelector((state) => state.dataSelection.compartment); - const selectedDistrict = useAppSelector((state) => state.dataSelection.district.ags); + const selectedDistrict = useAppSelector((state) => state.dataSelection.district); const selectedDateInStore = useAppSelector((state) => state.dataSelection.date); - const horizontalYAxisThreshold = useAppSelector( - (state) => state.userPreference.horizontalYAxisThresholds?.[`${selectedDistrict}-${selectedCompartment}`]?.threshold - ); + const storeHorizontalThresholds = useAppSelector((state) => state.userPreference.horizontalYAxisThresholds); const referenceDay = useAppSelector((state) => state.dataSelection.simulationStart); const minDate = useAppSelector((state) => state.dataSelection.minDate); const maxDate = useAppSelector((state) => state.dataSelection.maxDate); const [selectedDate, setSelectedDate] = useState(selectedDateInStore ?? '2024-08-07'); const [referenceDayBottomPosition, setReferenceDayBottomPosition] = useState(0); + const [horizontalThresholds, setHhorizontalThresholds] = + useState>(storeHorizontalThresholds); const yAxisLabel = useMemo(() => { return t(`infection-states.${selectedCompartment}`); @@ -71,9 +73,14 @@ export default function LineChartContainer() { maxDate={maxDate} referenceDay={referenceDay} yAxisLabel={yAxisLabel} - horizontalYAxisThreshold={horizontalYAxisThreshold} + horizontalYAxisThreshold={horizontalThresholds[`${selectedDistrict.ags}-${selectedCompartment}`]?.threshold} + /> + - ); } diff --git a/frontend/src/components/LineChartComponents/LineChartSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettings.tsx deleted file mode 100644 index 11c7e904..00000000 --- a/frontend/src/components/LineChartComponents/LineChartSettings.tsx +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) -// SPDX-License-Identifier: Apache-2.0 - -import SettingsIcon from '@mui/icons-material/Settings'; -import {Button, Grid, Popover, Typography} from '@mui/material'; -import Box from '@mui/material/Box'; -import {useAppSelector, useAppDispatch} from '../../store/hooks'; -import {setHorizontalYAxisThreshold} from '../../store/UserPreferenceSlice'; -import IconButton from '@mui/material/IconButton'; -import CloseIcon from '@mui/icons-material/Close'; -import TextField from '@mui/material/TextField'; - -import React, {useState} from 'react'; - -export function LineChartSettings() { - const dispatch = useAppDispatch(); - const [anchorEl, setAnchorEl] = useState(null); - const [showPopover, setShowPopover] = useState(false); - - const selectedDistrict = useAppSelector((state) => state.dataSelection.district.ags); - const selectedCompartment = useAppSelector((state) => state.dataSelection.compartment); - const horizontalYAxisThreshold = useAppSelector( - (state) => state.userPreference.horizontalYAxisThresholds?.[`${selectedDistrict}-${selectedCompartment}`]?.threshold - ); - - const handlePopoverOpen = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - setShowPopover(true); - }; - - const handlePopoverClose = () => { - setAnchorEl(null); - setShowPopover(false); - }; - - const handleThresholdChange = (event: React.ChangeEvent) => { - const newThreshold = Number(event.target.value); - - // update redux state - dispatch( - setHorizontalYAxisThreshold({ - district: selectedDistrict, - compartment: selectedCompartment ?? '', - threshold: newThreshold, - }) - ); - }; - - return ( - - - - - - - - Chart Settings - - - Horizontal Y-Threshold - - - - - - - - - ); -} diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx new file mode 100644 index 00000000..da385304 --- /dev/null +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx @@ -0,0 +1,177 @@ +import React, {useState} from 'react'; +import {Grid, IconButton, TextField, Typography, Divider, Box} from '@mui/material'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import EditIcon from '@mui/icons-material/Edit'; +import CheckIcon from '@mui/icons-material/Check'; +import CancelIcon from '@mui/icons-material/Cancel'; +import {useTheme} from '@mui/material/styles'; +import {HorizontalThreshold} from 'types/horizontalThreshold'; +import {Dictionary} from 'util/util'; +import type {District} from 'types/district'; + +export interface HorizontalThresholdListProps { + /** The list of horizontal thresholds to display */ + horizontalThresholds: Dictionary; + + /** Callback to handle the deletion of a threshold */ + handleDeleteThreshold: (district: District, compartment: string) => void; + + /** Callback to handle changes to an existing threshold value */ + handleUpdateThreshold: (key: string, value: number) => void; + + /** Current edited key of the threshold */ + editingThresholdKey: string | null; + + /** Callback to set the current edited key of the threshold */ + setEditingThresholdKey: React.Dispatch>; + + /** A boolean state to see whether a threshold is currently being added */ + isAddingThreshold: boolean; +} + +export const HorizontalThresholdList = ({ + horizontalThresholds, + handleDeleteThreshold, + handleUpdateThreshold, + editingThresholdKey, + setEditingThresholdKey, + isAddingThreshold, +}: HorizontalThresholdListProps) => { + const [localThreshold, setLocalThreshold] = useState(0); + const theme = useTheme(); + + const handleEditThreshold = (key: string, threshold: number) => { + setEditingThresholdKey(key); + setLocalThreshold(threshold); + }; + + const updateThreshold = (key: string, newThresholdValue: number | null) => { + if (newThresholdValue === null || newThresholdValue < 0) { + return; + } + handleUpdateThreshold(key, newThresholdValue); + setEditingThresholdKey(null); + }; + + return ( + + {Object.entries(horizontalThresholds ?? {}).length === 0 && !isAddingThreshold ? ( + + No thresholds set + + ) : ( + Object.entries(horizontalThresholds ?? {}).map(([key, threshold]) => ( + + + + {threshold.district.name} + {threshold.compartment} + + + + {editingThresholdKey === key ? ( + + setLocalThreshold(Number(e.target.value))} + /> + + updateThreshold(key, localThreshold)} + > + + + setEditingThresholdKey(null)} + sx={{ + color: theme.palette.error.main, + }} + > + + + + + ) : ( + + Threshold: {threshold.threshold} + + handleEditThreshold(key, threshold.threshold)}> + + + handleDeleteThreshold(threshold.district, threshold.compartment)} + > + + + + + )} + + + + )) + )} + + ); +}; diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx new file mode 100644 index 00000000..13170c33 --- /dev/null +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx @@ -0,0 +1,200 @@ +import React, {useState} from 'react'; +import {Grid, Box, IconButton, TextField, Typography, Divider} from '@mui/material'; +import {useTheme} from '@mui/material/styles'; +import AddBoxIcon from '@mui/icons-material/AddBox'; +import CheckIcon from '@mui/icons-material/Check'; +import CancelIcon from '@mui/icons-material/Cancel'; +import {Dictionary} from 'util/util'; +import type {District} from 'types/district'; +import type {HorizontalThreshold} from 'types/horizontalThreshold'; +import {HorizontalThresholdList} from './HorizontalThresholdList'; + +export interface HorizontalThresholdSettingsProps { + /** The district to which the settings apply. */ + selectedDistrict: District; + + /** The compartment to which the settings apply. */ + selectedCompartment: string; + + /** The horizontal thresholds for the y-axis. */ + horizontalThresholds: Dictionary; + + /** A function that sets the horizontal thresholds for the y-axis. */ + setHorizontalThresholds: React.Dispatch>>; +} + +export function HorizontalThresholdSettings({ + selectedDistrict, + selectedCompartment, + horizontalThresholds, + setHorizontalThresholds, +}: HorizontalThresholdSettingsProps) { + const [localYAxisThreshold, setLocalYAxisThreshold] = useState(0); + const [editingThresholdKey, setEditingThresholdKey] = useState(null); + const [isAddingThreshold, setIsAddingThreshold] = useState(false); + + const theme = useTheme(); + + // function to handle adding a new threshold + const handleAddThreshold = () => { + if (localYAxisThreshold === null || localYAxisThreshold < 0) { + // TODO: Show error message + return; + } + + const key = `${selectedDistrict.ags}-${selectedCompartment}`; + + const existingThreshold = horizontalThresholds[key]; + + if (existingThreshold) { + console.log('Threshold already exists'); + + // handle error here, maybe show modal + return; + } + + const newThreshold: HorizontalThreshold = { + district: selectedDistrict, + compartment: selectedCompartment ?? '', + threshold: localYAxisThreshold, + }; + + const newThresholds = {...horizontalThresholds, [key]: newThreshold}; + + setHorizontalThresholds(newThresholds); + setLocalYAxisThreshold(null); + setIsAddingThreshold(false); + }; + + // function to handle updating an existing threshold + const handleUpdateThreshold = (key: string, updatedThresholdValue: number) => { + const existingThreshold = horizontalThresholds[key]; + + if (existingThreshold) { + const updatedThreshold: HorizontalThreshold = { + ...existingThreshold, + threshold: updatedThresholdValue, + }; + + const newThresholds = {...horizontalThresholds, [key]: updatedThreshold}; + + setHorizontalThresholds(newThresholds); + setLocalYAxisThreshold(null); + } + }; + + // function to handle deleting a threshold + const handleDeleteThreshold = (district: District, compartment: string) => { + const newThresholds = {...horizontalThresholds}; + delete newThresholds[`${district.ags}-${compartment}`]; + + setHorizontalThresholds(newThresholds); + }; + + return ( + + + {isAddingThreshold ? ( + + + {selectedDistrict.name} + {selectedCompartment} + + + + setLocalYAxisThreshold(Number(e.target.value))} + /> + + + + + setIsAddingThreshold(false)}> + + + + + + ) : ( + setIsAddingThreshold(true)} + > + + + + + )} + + ); +} diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx new file mode 100644 index 00000000..e64fc646 --- /dev/null +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: Apache-2.0 + +import React, {useState} from 'react'; +import SettingsIcon from '@mui/icons-material/Settings'; +import {Button, Divider, Popover, Typography} from '@mui/material'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import CloseIcon from '@mui/icons-material/Close'; +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule'; +import {Dictionary} from 'util/util'; +import type {HorizontalThreshold} from 'types/horizontalThreshold'; +import type {District} from 'types/district'; + +import {HorizontalThresholdSettings} from './HorizontalThresholdSettings/HorizontalThresholdSettings'; + +/** + * The different views that can be displayed in the settings popover. + * You can add more views here if you want to add more settings. + */ +type SettingsView = 'settingsMenu' | 'horizontalThresholdSettings' | 'filters'; + +/** + * The settings menu for the line chart. Each item in the menu has a label, a view, and an icon. + */ +const SETTINGS_MENU = [ + {label: 'Horizontal Threshold Settings', view: 'horizontalThresholdSettings', icon: }, +]; + +export interface LineChartSettingsProps { + /** The district to which the settings apply. */ + selectedDistrict: District; + + /** The compartment to which the settings apply. */ + selectedCompartment: string; + + /** The horizontal thresholds for the y-axis. */ + horizontalThresholds: Dictionary; + + /** A function that sets the horizontal thresholds for the y-axis. */ + setHorizontalThresholds: React.Dispatch>>; +} + +/** + * LineChartSettings component displays a button that opens a popover with settings for the line chart. + * The settings include the ability to set horizontal thresholds for the y-axis. + * The settings is also expandable to include more settings in the future. + */ +export function LineChartSettings({ + selectedDistrict, + selectedCompartment, + horizontalThresholds, + setHorizontalThresholds, +}: LineChartSettingsProps) { + const [currentView, setCurrentView] = useState('settingsMenu'); + const [anchorEl, setAnchorEl] = useState(null); + const [showPopover, setShowPopover] = useState(false); + + const handlePopoverOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + setShowPopover(true); + }; + + const handlePopoverClose = () => { + setAnchorEl(null); + setShowPopover(false); + }; + + const handleNavigate = (view: SettingsView) => { + setCurrentView(view); + }; + + const handleBackButton = () => { + setCurrentView('settingsMenu'); + }; + + const renderHeader = (title: string) => ( + + handleBackButton()} disabled={currentView === 'settingsMenu'}> + + + {title} + + + + + ); + + return ( + + + + {currentView === 'settingsMenu' && ( + + {renderHeader('Line Chart Settings')} + + + {SETTINGS_MENU.map((item) => ( + + ))} + + + )} + {currentView === 'horizontalThresholdSettings' && ( + + {renderHeader('Horizontal Threshold Settings')} + + + + )} + + + ); +} diff --git a/frontend/src/components/MainContentTabs.tsx b/frontend/src/components/MainContentTabs.tsx index 8e4fff7d..f856c866 100644 --- a/frontend/src/components/MainContentTabs.tsx +++ b/frontend/src/components/MainContentTabs.tsx @@ -20,7 +20,7 @@ import {useTheme} from '@mui/material/styles'; /* * Lazy loading the components to improve the performance. */ -const SimulationChart = React.lazy(() => import('./LineChartContainer')); +const SimulationChart = React.lazy(() => import('./LineChartComponents/LineChartContainer')); const ParameterEditor = React.lazy(() => import('./ParameterEditor')); /** From d7ac19ca25e3722302ffdd865c825258d80dc7eb Mon Sep 17 00:00:00 2001 From: kunkoala Date: Mon, 7 Oct 2024 12:49:57 +0200 Subject: [PATCH 13/37] :wrench: :sparkles: added some error handlings and for adding a new threshold and some refactors --- .../LineChartComponents/LineChart.tsx | 9 +--- .../HorizontalThresholdList.tsx | 3 ++ .../HorizontalThresholdSettings.tsx | 42 ++++++++++++++-- .../LineChartSettings.tsx | 48 +++++++++++-------- 4 files changed, 69 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/LineChartComponents/LineChart.tsx b/frontend/src/components/LineChartComponents/LineChart.tsx index 606b7cd9..2698be18 100644 --- a/frontend/src/components/LineChartComponents/LineChart.tsx +++ b/frontend/src/components/LineChartComponents/LineChart.tsx @@ -446,14 +446,7 @@ export default function LineChart({ layer: Number.MAX_VALUE, }, }; - }, [ - root, - yAxis, - horizontalYAxisThreshold, - theme.palette.divider, - theme.palette.error.main, - theme.palette.primary.main, - ]); + }, [root, yAxis, horizontalYAxisThreshold, theme.palette.error.main, theme.palette.primary.contrastText]); // Add horizontal line to limit the y-axis useValueAxisRange(targetLineSettings, root, chart, yAxis); diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx index da385304..25dcf964 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: CC0-1.0 + import React, {useState} from 'react'; import {Grid, IconButton, TextField, Typography, Divider, Box} from '@mui/material'; import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx index 13170c33..291aafb5 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx @@ -1,4 +1,7 @@ -import React, {useState} from 'react'; +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: CC0-1.0 + +import React, {useState, useEffect} from 'react'; import {Grid, Box, IconButton, TextField, Typography, Divider} from '@mui/material'; import {useTheme} from '@mui/material/styles'; import AddBoxIcon from '@mui/icons-material/AddBox'; @@ -29,16 +32,44 @@ export function HorizontalThresholdSettings({ horizontalThresholds, setHorizontalThresholds, }: HorizontalThresholdSettingsProps) { - const [localYAxisThreshold, setLocalYAxisThreshold] = useState(0); + const [localYAxisThreshold, setLocalYAxisThreshold] = useState(null); const [editingThresholdKey, setEditingThresholdKey] = useState(null); const [isAddingThreshold, setIsAddingThreshold] = useState(false); + const [isValid, setIsValid] = useState(localYAxisThreshold !== null && localYAxisThreshold >= 0); + const [ableToAddThreshold, setAbleToAddThreshold] = useState(false); + + // Checks if the user has entered a valid threshold value + useEffect(() => { + setIsValid(localYAxisThreshold !== null && localYAxisThreshold >= 0); + }, [localYAxisThreshold]); + + // Checks if the user is able to add a threshold + useEffect(() => { + const key = `${selectedDistrict.ags}-${selectedCompartment}`; + const existingThreshold = horizontalThresholds[key]; + if (existingThreshold) { + setAbleToAddThreshold(false); + return; + } + setAbleToAddThreshold(true); + }, [selectedDistrict, selectedCompartment, horizontalThresholds]); const theme = useTheme(); + const handleIsAddingThreshold = (value: boolean) => { + const key = `${selectedDistrict.ags}-${selectedCompartment}`; + const existingThreshold = horizontalThresholds[key]; + + if (existingThreshold) { + // handle error here, maybe show modal + return; + } + setIsAddingThreshold(value); + }; + // function to handle adding a new threshold const handleAddThreshold = () => { if (localYAxisThreshold === null || localYAxisThreshold < 0) { - // TODO: Show error message return; } @@ -143,6 +174,7 @@ export function HorizontalThresholdSettings({ }} size='small' value={localYAxisThreshold ?? 0} + error={!isValid} onChange={(e) => setLocalYAxisThreshold(Number(e.target.value))} /> setIsAddingThreshold(true)} + onClick={() => handleIsAddingThreshold(true)} > diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx index e64fc646..1adb770d 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx @@ -24,9 +24,13 @@ type SettingsView = 'settingsMenu' | 'horizontalThresholdSettings' | 'filters'; /** * The settings menu for the line chart. Each item in the menu has a label, a view, and an icon. */ -const SETTINGS_MENU = [ - {label: 'Horizontal Threshold Settings', view: 'horizontalThresholdSettings', icon: }, -]; +const SETTINGS_MENU = { + HORIZONTAL_THRESHOLD: { + label: 'Horizontal Threshold Settings', + view: 'horizontalThresholdSettings', + icon: , + }, +}; export interface LineChartSettingsProps { /** The district to which the settings apply. */ @@ -117,24 +121,25 @@ export function LineChartSettings({ {renderHeader('Line Chart Settings')} - {SETTINGS_MENU.map((item) => ( - - ))} + )} @@ -142,6 +147,7 @@ export function LineChartSettings({ {renderHeader('Horizontal Threshold Settings')} + Date: Mon, 7 Oct 2024 14:06:53 +0200 Subject: [PATCH 14/37] :wrench: Set horizontal thresholds in store --- .../components/LineChartComponents/LineChartContainer.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/components/LineChartComponents/LineChartContainer.tsx b/frontend/src/components/LineChartComponents/LineChartContainer.tsx index b7c35385..f7486571 100644 --- a/frontend/src/components/LineChartComponents/LineChartContainer.tsx +++ b/frontend/src/components/LineChartComponents/LineChartContainer.tsx @@ -13,6 +13,7 @@ import {useTranslation} from 'react-i18next'; import {LineChartSettings} from './LineChartSettingsComponents/LineChartSettings'; import {Dictionary} from 'util/util'; import {HorizontalThreshold} from 'types/horizontalThreshold'; +import {setHorizontalYAxisThresholds} from 'store/UserPreferenceSlice'; export default function LineChartContainer() { const {t} = useTranslation('backend'); @@ -38,6 +39,11 @@ export default function LineChartContainer() { return t(`infection-states.${selectedCompartment}`); }, [selectedCompartment, t]); + // Set horizontal thresholds in store + useEffect(() => { + dispatch(setHorizontalYAxisThresholds(horizontalThresholds)); + }, [horizontalThresholds, dispatch]); + // Set selected date in store useEffect(() => { dispatch(selectDate(selectedDate)); From 16be2a5cc8db19ce4d5e7da98708c15fca7b1ddb Mon Sep 17 00:00:00 2001 From: kunkoala Date: Mon, 7 Oct 2024 14:49:09 +0200 Subject: [PATCH 15/37] :sparkles: add license to type --- frontend/src/types/horizontalThreshold.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/types/horizontalThreshold.ts b/frontend/src/types/horizontalThreshold.ts index 357dfd51..6850749e 100644 --- a/frontend/src/types/horizontalThreshold.ts +++ b/frontend/src/types/horizontalThreshold.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: CC0-1.0 + import {District} from './district'; /** From 37943798f7009b5ba2f3658e73f29f6f31fa5659 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Tue, 8 Oct 2024 15:53:51 +0200 Subject: [PATCH 16/37] :wrench: refactor some state names to adhere to naming conventions --- .../LineChartComponents/LineChartContainer.tsx | 4 ++-- .../LineChartSettings.tsx | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/LineChartComponents/LineChartContainer.tsx b/frontend/src/components/LineChartComponents/LineChartContainer.tsx index f7486571..fb304e8a 100644 --- a/frontend/src/components/LineChartComponents/LineChartContainer.tsx +++ b/frontend/src/components/LineChartComponents/LineChartContainer.tsx @@ -32,7 +32,7 @@ export default function LineChartContainer() { const [selectedDate, setSelectedDate] = useState(selectedDateInStore ?? '2024-08-07'); const [referenceDayBottomPosition, setReferenceDayBottomPosition] = useState(0); - const [horizontalThresholds, setHhorizontalThresholds] = + const [horizontalThresholds, sethorizontalThresholds] = useState>(storeHorizontalThresholds); const yAxisLabel = useMemo(() => { @@ -85,7 +85,7 @@ export default function LineChartContainer() { selectedDistrict={selectedDistrict} selectedCompartment={selectedCompartment ?? ''} horizontalThresholds={horizontalThresholds} - setHorizontalThresholds={setHhorizontalThresholds} + setHorizontalThresholds={sethorizontalThresholds} /> ); diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx index 1adb770d..ade94639 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx @@ -24,8 +24,8 @@ type SettingsView = 'settingsMenu' | 'horizontalThresholdSettings' | 'filters'; /** * The settings menu for the line chart. Each item in the menu has a label, a view, and an icon. */ -const SETTINGS_MENU = { - HORIZONTAL_THRESHOLD: { +const settingsMenu = { + horizontalThreshold: { label: 'Horizontal Threshold Settings', view: 'horizontalThresholdSettings', icon: , @@ -122,8 +122,8 @@ export function LineChartSettings({ From da16f931b0517d3659b22469be98b1c030bdd93b Mon Sep 17 00:00:00 2001 From: kunkoala Date: Tue, 8 Oct 2024 15:55:18 +0200 Subject: [PATCH 17/37] :wrench: :tada: added a useEffect to change the selected map area when the district store changes --- frontend/src/components/Sidebar/SidebarContainer.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frontend/src/components/Sidebar/SidebarContainer.tsx b/frontend/src/components/Sidebar/SidebarContainer.tsx index 26638bff..4c5942d5 100644 --- a/frontend/src/components/Sidebar/SidebarContainer.tsx +++ b/frontend/src/components/Sidebar/SidebarContainer.tsx @@ -95,6 +95,17 @@ export default function MapContainer() { // This effect should only run when the selectedArea changes }, [selectedArea, dispatch]); + // set the selected area when the district in the store changes + useEffect(() => { + if (storeSelectedArea.name != '') { + setSelectedArea({ + RS: storeSelectedArea.ags, + GEN: storeSelectedArea.name, + BEZ: storeSelectedArea.type, + }); + } + }, [storeSelectedArea]); + // Set legend in store useEffect(() => { dispatch(selectHeatmapLegend({legend: legend})); From 2b2e7f43e6f0cd32e2ebd51571a78d7b3ea49f63 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Tue, 8 Oct 2024 16:28:00 +0200 Subject: [PATCH 18/37] :tada: :hammer: refactor HorizontalThresholdList into HorizontalThresholdList- and Item, now highlights selected area based on the current selected district and compartment --- .../HorizontalThresholdItem.tsx | 174 ++++++++++++++++++ .../HorizontalThresholdList.tsx | 168 ++++------------- .../HorizontalThresholdSettings.tsx | 20 +- 3 files changed, 220 insertions(+), 142 deletions(-) create mode 100644 frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx new file mode 100644 index 00000000..954e2f56 --- /dev/null +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx @@ -0,0 +1,174 @@ +import React, {useState} from 'react'; +import {Grid, IconButton, TextField, Typography, Divider, Box} from '@mui/material'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import EditIcon from '@mui/icons-material/Edit'; +import CheckIcon from '@mui/icons-material/Check'; +import CancelIcon from '@mui/icons-material/Cancel'; +import {useTheme} from '@mui/material/styles'; +import {HorizontalThreshold} from 'types/horizontalThreshold'; +import type {District} from 'types/district'; +import {selectDistrict, selectCompartment} from 'store/DataSelectionSlice'; +import {useAppDispatch} from 'store/hooks'; + +export interface HorizontalThresholdItemProps { + /** The threshold item to display */ + threshold: HorizontalThreshold; + + /** The key for the threshold (used for editing and updates) */ + thresholdKey: string; + + /** Callback to handle the deletion of a threshold */ + handleDeleteThreshold: (district: District, compartment: string) => void; + + /** Callback to handle updating the threshold value */ + handleUpdateThreshold: (key: string, value: number) => void; + + /** Current edited key of the threshold */ + editingThresholdKey: string | null; + + /** Callback to set the current edited key of the threshold */ + setEditingThresholdKey: React.Dispatch>; + + /** The to determine whether threshold is selected */ + selected: boolean; + + /** Callback to set the currently selected threshold */ + setSelectedThresholdKey: React.Dispatch>; +} + +export const HorizontalThresholdItem = ({ + threshold, + thresholdKey, + handleDeleteThreshold, + handleUpdateThreshold, + editingThresholdKey, + setEditingThresholdKey, + selected, + setSelectedThresholdKey, +}: HorizontalThresholdItemProps) => { + const [localThreshold, setLocalThreshold] = useState(threshold.threshold); + const theme = useTheme(); + const dispatch = useAppDispatch(); + + const updateThreshold = () => { + if (localThreshold < 0) return; + handleUpdateThreshold(thresholdKey, localThreshold); + setEditingThresholdKey(null); + }; + + const handleEditThreshold = (key: string, threshold: number) => { + setEditingThresholdKey(key); + setLocalThreshold(threshold); + }; + + const handleSelectThreshold = (threshold: HorizontalThreshold) => { + setSelectedThresholdKey(threshold.district.ags + '-' + threshold.compartment); + dispatch(selectDistrict(threshold.district)); + dispatch(selectCompartment(threshold.compartment)); + }; + + return ( + + handleSelectThreshold(threshold)} + > + + {threshold.district.name} + {threshold.compartment} + + + {editingThresholdKey === thresholdKey ? ( + + setLocalThreshold(Number(e.target.value))} + /> + + + + + setEditingThresholdKey(null)} + sx={{ + color: theme.palette.error.main, + }} + > + + + + + ) : ( + + + {threshold.threshold} + + + handleEditThreshold(thresholdKey, threshold.threshold)}> + + + handleDeleteThreshold(threshold.district, threshold.compartment)} + > + + + + + )} + + + + ); +}; diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx index 25dcf964..643fe90f 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx @@ -1,16 +1,12 @@ // SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) -// SPDX-License-Identifier: CC0-1.0 +// SPDX-License-Identifier: Apache-2.0 import React, {useState} from 'react'; -import {Grid, IconButton, TextField, Typography, Divider, Box} from '@mui/material'; -import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; -import EditIcon from '@mui/icons-material/Edit'; -import CheckIcon from '@mui/icons-material/Check'; -import CancelIcon from '@mui/icons-material/Cancel'; -import {useTheme} from '@mui/material/styles'; -import {HorizontalThreshold} from 'types/horizontalThreshold'; +import {Box, Typography} from '@mui/material'; import {Dictionary} from 'util/util'; +import {HorizontalThreshold} from 'types/horizontalThreshold'; import type {District} from 'types/district'; +import {HorizontalThresholdItem} from './HorizontalThresholdItem'; export interface HorizontalThresholdListProps { /** The list of horizontal thresholds to display */ @@ -22,39 +18,31 @@ export interface HorizontalThresholdListProps { /** Callback to handle changes to an existing threshold value */ handleUpdateThreshold: (key: string, value: number) => void; - /** Current edited key of the threshold */ - editingThresholdKey: string | null; - - /** Callback to set the current edited key of the threshold */ - setEditingThresholdKey: React.Dispatch>; - /** A boolean state to see whether a threshold is currently being added */ isAddingThreshold: boolean; + + /** The selected District */ + selectedDistrict: District; + + /** The selected compartment */ + selectedCompartment: string; + + /** The currently selected threshold key */ + selectedThresholdKey: string | null; + + /** Callback to set the currently selected threshold key */ + setSelectedThresholdKey: React.Dispatch>; } export const HorizontalThresholdList = ({ horizontalThresholds, handleDeleteThreshold, handleUpdateThreshold, - editingThresholdKey, - setEditingThresholdKey, isAddingThreshold, + selectedThresholdKey, + setSelectedThresholdKey, }: HorizontalThresholdListProps) => { - const [localThreshold, setLocalThreshold] = useState(0); - const theme = useTheme(); - - const handleEditThreshold = (key: string, threshold: number) => { - setEditingThresholdKey(key); - setLocalThreshold(threshold); - }; - - const updateThreshold = (key: string, newThresholdValue: number | null) => { - if (newThresholdValue === null || newThresholdValue < 0) { - return; - } - handleUpdateThreshold(key, newThresholdValue); - setEditingThresholdKey(null); - }; + const [editingThresholdKey, setEditingThresholdKey] = useState(null); return ( @@ -69,111 +57,21 @@ export const HorizontalThresholdList = ({ No thresholds set ) : ( - Object.entries(horizontalThresholds ?? {}).map(([key, threshold]) => ( - - - - {threshold.district.name} - {threshold.compartment} - - - - {editingThresholdKey === key ? ( - - setLocalThreshold(Number(e.target.value))} - /> - - updateThreshold(key, localThreshold)} - > - - - setEditingThresholdKey(null)} - sx={{ - color: theme.palette.error.main, - }} - > - - - - - ) : ( - - Threshold: {threshold.threshold} - - handleEditThreshold(key, threshold.threshold)}> - - - handleDeleteThreshold(threshold.district, threshold.compartment)} - > - - - - - )} - - { + return ( + - - )) + ); + }) )} ); diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx index 291aafb5..ede1cb69 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx @@ -33,14 +33,14 @@ export function HorizontalThresholdSettings({ setHorizontalThresholds, }: HorizontalThresholdSettingsProps) { const [localYAxisThreshold, setLocalYAxisThreshold] = useState(null); - const [editingThresholdKey, setEditingThresholdKey] = useState(null); + const [selectedThresholdKey, setSelectedThresholdKey] = useState(`${selectedDistrict.ags}-${selectedCompartment}`); const [isAddingThreshold, setIsAddingThreshold] = useState(false); - const [isValid, setIsValid] = useState(localYAxisThreshold !== null && localYAxisThreshold >= 0); + const [isValid, setIsValid] = useState(localYAxisThreshold !== null && localYAxisThreshold > 0); const [ableToAddThreshold, setAbleToAddThreshold] = useState(false); // Checks if the user has entered a valid threshold value useEffect(() => { - setIsValid(localYAxisThreshold !== null && localYAxisThreshold >= 0); + setIsValid(localYAxisThreshold !== null && localYAxisThreshold > 0); }, [localYAxisThreshold]); // Checks if the user is able to add a threshold @@ -93,6 +93,7 @@ export function HorizontalThresholdSettings({ const newThresholds = {...horizontalThresholds, [key]: newThreshold}; setHorizontalThresholds(newThresholds); + setSelectedThresholdKey(key); setLocalYAxisThreshold(null); setIsAddingThreshold(false); }; @@ -134,9 +135,11 @@ export function HorizontalThresholdSettings({ horizontalThresholds={horizontalThresholds} handleDeleteThreshold={handleDeleteThreshold} handleUpdateThreshold={handleUpdateThreshold} - editingThresholdKey={editingThresholdKey} - setEditingThresholdKey={setEditingThresholdKey} isAddingThreshold={isAddingThreshold} + selectedDistrict={selectedDistrict} + selectedCompartment={selectedCompartment} + selectedThresholdKey={selectedThresholdKey} + setSelectedThresholdKey={setSelectedThresholdKey} /> {isAddingThreshold ? ( setLocalYAxisThreshold(Number(e.target.value))} + onChange={(e) => { + const value = e.target.value === '' ? null : Number(e.target.value); + setLocalYAxisThreshold(value); + }} /> Date: Tue, 8 Oct 2024 16:57:35 +0200 Subject: [PATCH 19/37] :wrench: added some conditionals to prevent unnecessary re-renders --- frontend/src/components/Sidebar/SidebarContainer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Sidebar/SidebarContainer.tsx b/frontend/src/components/Sidebar/SidebarContainer.tsx index 4c5942d5..b6d8d144 100644 --- a/frontend/src/components/Sidebar/SidebarContainer.tsx +++ b/frontend/src/components/Sidebar/SidebarContainer.tsx @@ -95,9 +95,9 @@ export default function MapContainer() { // This effect should only run when the selectedArea changes }, [selectedArea, dispatch]); - // set the selected area when the district in the store changes + // Set selected area in state when it changes in store useEffect(() => { - if (storeSelectedArea.name != '') { + if (storeSelectedArea.name !== '' && selectedArea?.RS !== storeSelectedArea.ags) { setSelectedArea({ RS: storeSelectedArea.ags, GEN: storeSelectedArea.name, From 12f9319b7d4270bb5784019935c4867bc4ae9f85 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Tue, 8 Oct 2024 18:29:51 +0200 Subject: [PATCH 20/37] :wrench: :hammer: :tada: :sparkle: distinct styling for selected threshold and logic, refactor threshold input component and code style --- .../HorizontalThresholdItem.tsx | 145 +++++++++--------- .../HorizontalThresholdList.tsx | 18 +-- .../HorizontalThresholdSettings.tsx | 109 +++++-------- .../ThresholdInput.tsx | 71 +++++++++ .../LineChartSettings.tsx | 4 +- 5 files changed, 196 insertions(+), 151 deletions(-) create mode 100644 frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/ThresholdInput.tsx diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx index 954e2f56..d85f0ba0 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx @@ -1,14 +1,14 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: Apache-2.0 + import React, {useState} from 'react'; -import {Grid, IconButton, TextField, Typography, Divider, Box} from '@mui/material'; +import {IconButton, Typography, Divider, Box} from '@mui/material'; import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; import EditIcon from '@mui/icons-material/Edit'; -import CheckIcon from '@mui/icons-material/Check'; -import CancelIcon from '@mui/icons-material/Cancel'; import {useTheme} from '@mui/material/styles'; import {HorizontalThreshold} from 'types/horizontalThreshold'; +import ThresholdInput from './ThresholdInput'; import type {District} from 'types/district'; -import {selectDistrict, selectCompartment} from 'store/DataSelectionSlice'; -import {useAppDispatch} from 'store/hooks'; export interface HorizontalThresholdItemProps { /** The threshold item to display */ @@ -23,6 +23,9 @@ export interface HorizontalThresholdItemProps { /** Callback to handle updating the threshold value */ handleUpdateThreshold: (key: string, value: number) => void; + /** Callback to handle selection of a threshold */ + handleSelectThreshold: (threshold: HorizontalThreshold) => void; + /** Current edited key of the threshold */ editingThresholdKey: string | null; @@ -31,24 +34,21 @@ export interface HorizontalThresholdItemProps { /** The to determine whether threshold is selected */ selected: boolean; - - /** Callback to set the currently selected threshold */ - setSelectedThresholdKey: React.Dispatch>; } -export const HorizontalThresholdItem = ({ +export default function HorizontalThresholdItem({ threshold, thresholdKey, handleDeleteThreshold, handleUpdateThreshold, + handleSelectThreshold, editingThresholdKey, setEditingThresholdKey, selected, - setSelectedThresholdKey, -}: HorizontalThresholdItemProps) => { +}: HorizontalThresholdItemProps) { const [localThreshold, setLocalThreshold] = useState(threshold.threshold); const theme = useTheme(); - const dispatch = useAppDispatch(); + const isValid = localThreshold !== null && localThreshold > 0; const updateThreshold = () => { if (localThreshold < 0) return; @@ -61,101 +61,100 @@ export const HorizontalThresholdItem = ({ setLocalThreshold(threshold); }; - const handleSelectThreshold = (threshold: HorizontalThreshold) => { - setSelectedThresholdKey(threshold.district.ags + '-' + threshold.compartment); - dispatch(selectDistrict(threshold.district)); - dispatch(selectCompartment(threshold.compartment)); - }; - return ( - handleSelectThreshold(threshold)} > - - {threshold.district.name} - {threshold.compartment} + + + {threshold.district.name} + + + {threshold.compartment} + {editingThresholdKey === thresholdKey ? ( + setLocalThreshold(Number(e.target.value))} + onSave={updateThreshold} + onCancel={() => setEditingThresholdKey(null)} + isSaveDisabled={!isValid} + /> + ) : ( - setLocalThreshold(Number(e.target.value))} - /> + - - - + {threshold.threshold} + + + + setEditingThresholdKey(null)} - sx={{ - color: theme.palette.error.main, - }} + aria-label='edit horizontal threshold for given district and compartment' + onClick={() => handleEditThreshold(thresholdKey, threshold.threshold)} > - - - - - ) : ( - - - {threshold.threshold} - - - handleEditThreshold(thresholdKey, threshold.threshold)}> handleDeleteThreshold(threshold.district, threshold.compartment)} > @@ -163,7 +162,7 @@ export const HorizontalThresholdItem = ({ )} - + ); -}; +} diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx index 643fe90f..41252de1 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx @@ -6,7 +6,7 @@ import {Box, Typography} from '@mui/material'; import {Dictionary} from 'util/util'; import {HorizontalThreshold} from 'types/horizontalThreshold'; import type {District} from 'types/district'; -import {HorizontalThresholdItem} from './HorizontalThresholdItem'; +import HorizontalThresholdItem from './HorizontalThresholdItem'; export interface HorizontalThresholdListProps { /** The list of horizontal thresholds to display */ @@ -18,6 +18,9 @@ export interface HorizontalThresholdListProps { /** Callback to handle changes to an existing threshold value */ handleUpdateThreshold: (key: string, value: number) => void; + /** Callback to handle selection of thresholds */ + handleSelectThreshold: (threshold: HorizontalThreshold) => void; + /** A boolean state to see whether a threshold is currently being added */ isAddingThreshold: boolean; @@ -29,19 +32,16 @@ export interface HorizontalThresholdListProps { /** The currently selected threshold key */ selectedThresholdKey: string | null; - - /** Callback to set the currently selected threshold key */ - setSelectedThresholdKey: React.Dispatch>; } -export const HorizontalThresholdList = ({ +export default function HorizontalThresholdList({ horizontalThresholds, handleDeleteThreshold, handleUpdateThreshold, + handleSelectThreshold, isAddingThreshold, selectedThresholdKey, - setSelectedThresholdKey, -}: HorizontalThresholdListProps) => { +}: HorizontalThresholdListProps) { const [editingThresholdKey, setEditingThresholdKey] = useState(null); return ( @@ -68,11 +68,11 @@ export const HorizontalThresholdList = ({ editingThresholdKey={editingThresholdKey} setEditingThresholdKey={setEditingThresholdKey} selected={selectedThresholdKey === key} - setSelectedThresholdKey={setSelectedThresholdKey} + handleSelectThreshold={handleSelectThreshold} /> ); }) )} ); -}; +} diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx index ede1cb69..46b142d1 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx @@ -2,15 +2,16 @@ // SPDX-License-Identifier: CC0-1.0 import React, {useState, useEffect} from 'react'; -import {Grid, Box, IconButton, TextField, Typography, Divider} from '@mui/material'; +import {Grid, Box, IconButton, Typography} from '@mui/material'; import {useTheme} from '@mui/material/styles'; import AddBoxIcon from '@mui/icons-material/AddBox'; -import CheckIcon from '@mui/icons-material/Check'; -import CancelIcon from '@mui/icons-material/Cancel'; import {Dictionary} from 'util/util'; import type {District} from 'types/district'; import type {HorizontalThreshold} from 'types/horizontalThreshold'; -import {HorizontalThresholdList} from './HorizontalThresholdList'; +import HorizontalThresholdList from './HorizontalThresholdList'; +import ThresholdInput from './ThresholdInput'; +import {selectDistrict, selectCompartment} from 'store/DataSelectionSlice'; +import {useAppDispatch} from 'store/hooks'; export interface HorizontalThresholdSettingsProps { /** The district to which the settings apply. */ @@ -26,22 +27,23 @@ export interface HorizontalThresholdSettingsProps { setHorizontalThresholds: React.Dispatch>>; } -export function HorizontalThresholdSettings({ +export default function HorizontalThresholdSettings({ selectedDistrict, selectedCompartment, horizontalThresholds, setHorizontalThresholds, }: HorizontalThresholdSettingsProps) { - const [localYAxisThreshold, setLocalYAxisThreshold] = useState(null); - const [selectedThresholdKey, setSelectedThresholdKey] = useState(`${selectedDistrict.ags}-${selectedCompartment}`); + const dispatch = useAppDispatch(); + const theme = useTheme(); + + const [localThreshold, setLocalThreshold] = useState(null); + const [selectedThresholdKey, setSelectedThresholdKey] = useState( + `${selectedDistrict.ags}-${selectedCompartment}` + ); const [isAddingThreshold, setIsAddingThreshold] = useState(false); - const [isValid, setIsValid] = useState(localYAxisThreshold !== null && localYAxisThreshold > 0); const [ableToAddThreshold, setAbleToAddThreshold] = useState(false); - // Checks if the user has entered a valid threshold value - useEffect(() => { - setIsValid(localYAxisThreshold !== null && localYAxisThreshold > 0); - }, [localYAxisThreshold]); + const isValid = localThreshold !== null && localThreshold > 0; // Checks if the user is able to add a threshold useEffect(() => { @@ -54,8 +56,6 @@ export function HorizontalThresholdSettings({ setAbleToAddThreshold(true); }, [selectedDistrict, selectedCompartment, horizontalThresholds]); - const theme = useTheme(); - const handleIsAddingThreshold = (value: boolean) => { const key = `${selectedDistrict.ags}-${selectedCompartment}`; const existingThreshold = horizontalThresholds[key]; @@ -69,7 +69,7 @@ export function HorizontalThresholdSettings({ // function to handle adding a new threshold const handleAddThreshold = () => { - if (localYAxisThreshold === null || localYAxisThreshold < 0) { + if (localThreshold === null || localThreshold < 0) { return; } @@ -87,14 +87,14 @@ export function HorizontalThresholdSettings({ const newThreshold: HorizontalThreshold = { district: selectedDistrict, compartment: selectedCompartment ?? '', - threshold: localYAxisThreshold, + threshold: localThreshold, }; const newThresholds = {...horizontalThresholds, [key]: newThreshold}; setHorizontalThresholds(newThresholds); setSelectedThresholdKey(key); - setLocalYAxisThreshold(null); + setLocalThreshold(null); setIsAddingThreshold(false); }; @@ -111,7 +111,7 @@ export function HorizontalThresholdSettings({ const newThresholds = {...horizontalThresholds, [key]: updatedThreshold}; setHorizontalThresholds(newThresholds); - setLocalYAxisThreshold(null); + setLocalThreshold(null); } }; @@ -123,6 +123,15 @@ export function HorizontalThresholdSettings({ setHorizontalThresholds(newThresholds); }; + const handleSelectThreshold = (threshold: HorizontalThreshold) => { + if (isAddingThreshold) { + return; + } + setSelectedThresholdKey(threshold.district.ags + '-' + threshold.compartment); + dispatch(selectDistrict(threshold.district)); + dispatch(selectCompartment(threshold.compartment)); + }; + return ( {isAddingThreshold ? ( {selectedDistrict.name} {selectedCompartment} - - { + const value = e.target.value === '' ? null : Number(e.target.value); + setLocalThreshold(value); }} - > - { - const value = e.target.value === '' ? null : Number(e.target.value); - setLocalYAxisThreshold(value); - }} - /> - - - - - setIsAddingThreshold(false)}> - - - - + onSave={handleAddThreshold} + onCancel={() => setIsAddingThreshold(false)} + isSaveDisabled={!isValid} + /> ) : ( ) => void; + onSave: () => void; + onCancel: () => void; + isSaveDisabled: boolean; +} + +export default function ThresholdInput({ + id, + value, + error, + onChange, + onSave, + onCancel, + isSaveDisabled, +}: ThresholdInputProps) { + const theme = useTheme(); + return ( + + + + + + + + + + + + ); +} diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx index ade94639..d2358889 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx @@ -13,7 +13,7 @@ import {Dictionary} from 'util/util'; import type {HorizontalThreshold} from 'types/horizontalThreshold'; import type {District} from 'types/district'; -import {HorizontalThresholdSettings} from './HorizontalThresholdSettings/HorizontalThresholdSettings'; +import HorizontalThresholdSettings from './HorizontalThresholdSettings/HorizontalThresholdSettings'; /** * The different views that can be displayed in the settings popover. @@ -112,7 +112,7 @@ export function LineChartSettings({ }} slotProps={{ paper: { - sx: {width: '30%'}, + sx: {minWidth: '30%'}, }, }} > From 0565529efad19b00c5daf0a3a00bcd69e14fc5e0 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Tue, 8 Oct 2024 18:33:59 +0200 Subject: [PATCH 21/37] :wrench: useEffect dependencies --- frontend/src/components/Sidebar/SidebarContainer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Sidebar/SidebarContainer.tsx b/frontend/src/components/Sidebar/SidebarContainer.tsx index b6d8d144..055926b5 100644 --- a/frontend/src/components/Sidebar/SidebarContainer.tsx +++ b/frontend/src/components/Sidebar/SidebarContainer.tsx @@ -104,7 +104,7 @@ export default function MapContainer() { BEZ: storeSelectedArea.type, }); } - }, [storeSelectedArea]); + }, [storeSelectedArea, selectedArea]); // Set legend in store useEffect(() => { From 71564d310e06f0e04ec5d69ecf1ec81837a739e2 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Thu, 10 Oct 2024 14:28:56 +0200 Subject: [PATCH 22/37] :wrench: use useRef for selected area and update the compartments to change when the store changes --- .../ScenarioComponents/ScenarioContainer.tsx | 5 +++++ .../src/components/Sidebar/SidebarContainer.tsx | 13 +++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ScenarioComponents/ScenarioContainer.tsx b/frontend/src/components/ScenarioComponents/ScenarioContainer.tsx index eb927c81..140eedf3 100644 --- a/frontend/src/components/ScenarioComponents/ScenarioContainer.tsx +++ b/frontend/src/components/ScenarioComponents/ScenarioContainer.tsx @@ -209,6 +209,11 @@ export default function ScenarioContainer({minCompartmentsRows = 4, maxCompartme dispatch(selectCompartment(selectedCompartment)); }, [dispatch, selectedCompartment]); + // updates the selected compartment in the state when the compartment in the store changes for use with LineChartSettings + useEffect(() => { + setSelectedCompartment(storeSelectedCompartment ?? 'MildInfections'); // Sync the local state with the store's selected compartment + }, [storeSelectedCompartment]); + // This effect updates the start date in the state whenever the reference day changes. useEffect(() => { dispatch(setStartDate(startDay!)); diff --git a/frontend/src/components/Sidebar/SidebarContainer.tsx b/frontend/src/components/Sidebar/SidebarContainer.tsx index 055926b5..5ea75519 100644 --- a/frontend/src/components/Sidebar/SidebarContainer.tsx +++ b/frontend/src/components/Sidebar/SidebarContainer.tsx @@ -73,6 +73,7 @@ export default function MapContainer() { const [fixedLegendMaxValue, setFixedLegendMaxValue] = useState(null); const legendRef = useRef(null); + const selectedAreaRef = useRef(null); // Set selected area on first load. If language change and selected area is germany, set default value again to update the name useEffect(() => { @@ -97,14 +98,22 @@ export default function MapContainer() { // Set selected area in state when it changes in store useEffect(() => { - if (storeSelectedArea.name !== '' && selectedArea?.RS !== storeSelectedArea.ags) { + // Only update `selectedArea` if `storeSelectedArea` has changed meaningfully + if (storeSelectedArea.name !== '' && selectedAreaRef.current?.RS !== storeSelectedArea.ags) { setSelectedArea({ RS: storeSelectedArea.ags, GEN: storeSelectedArea.name, BEZ: storeSelectedArea.type, }); + + // update the ref with the new selectedArea + selectedAreaRef.current = { + RS: storeSelectedArea.ags, + GEN: storeSelectedArea.name, + BEZ: storeSelectedArea.type, + }; } - }, [storeSelectedArea, selectedArea]); + }, [storeSelectedArea]); // Set legend in store useEffect(() => { From 1f4792d198e5e00da9e6b0a98c90efc7ca9d4ae1 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Fri, 11 Oct 2024 16:13:40 +0200 Subject: [PATCH 23/37] :wrench: :hammer: use MUI Table for the threshold list --- .../HorizontalThresholdItem.tsx | 150 ++++++------- .../HorizontalThresholdList.tsx | 208 +++++++++++++++--- .../HorizontalThresholdSettings.tsx | 74 +------ .../LineChartSettings.tsx | 3 +- 4 files changed, 259 insertions(+), 176 deletions(-) diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx index d85f0ba0..4dcbb6ab 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React, {useState} from 'react'; -import {IconButton, Typography, Divider, Box} from '@mui/material'; +import {IconButton, Typography, Box, TableCell, TableRow} from '@mui/material'; import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; import EditIcon from '@mui/icons-material/Edit'; import {useTheme} from '@mui/material/styles'; @@ -62,50 +62,39 @@ export default function HorizontalThresholdItem({ }; return ( - - handleSelectThreshold(threshold)} - > - handleSelectThreshold(threshold)} + > + + - - {threshold.district.name} - - - {threshold.compartment} - - + {threshold.district.name} + + + + + {threshold.compartment} + + - {editingThresholdKey === thresholdKey ? ( + {editingThresholdKey === thresholdKey ? ( + setEditingThresholdKey(null)} isSaveDisabled={!isValid} /> - ) : ( - + ) : ( + <> + - {threshold.threshold} - - - - handleEditThreshold(thresholdKey, threshold.threshold)} - > - - - handleDeleteThreshold(threshold.district, threshold.compartment)} + - - + handleEditThreshold(thresholdKey, threshold.threshold)} + > + + + handleDeleteThreshold(threshold.district, threshold.compartment)} + > + + + - - )} - - - + + + )} + ); } diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx index 41252de1..19cd2bde 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx @@ -2,11 +2,27 @@ // SPDX-License-Identifier: Apache-2.0 import React, {useState} from 'react'; -import {Box, Typography} from '@mui/material'; +import AddBoxIcon from '@mui/icons-material/AddBox'; +import {useTheme} from '@mui/material/styles'; +import { + Box, + Typography, + Table, + TableHead, + TableRow, + TableCell, + Paper, + IconButton, + TableFooter, + TableBody, +} from '@mui/material'; +import styled from '@mui/material/styles/styled'; +import {tableCellClasses} from '@mui/material/TableCell'; import {Dictionary} from 'util/util'; import {HorizontalThreshold} from 'types/horizontalThreshold'; import type {District} from 'types/district'; import HorizontalThresholdItem from './HorizontalThresholdItem'; +import ThresholdInput from './ThresholdInput'; export interface HorizontalThresholdListProps { /** The list of horizontal thresholds to display */ @@ -21,9 +37,27 @@ export interface HorizontalThresholdListProps { /** Callback to handle selection of thresholds */ handleSelectThreshold: (threshold: HorizontalThreshold) => void; + /** Callback to handle adding a new threshold */ + handleAddThreshold: () => void; + + /** boolean function to handle flow of adding a new threshold */ + handleIsAddingThreshold: (value: boolean) => void; + /** A boolean state to see whether a threshold is currently being added */ isAddingThreshold: boolean; + /** set the isAddingThreshold */ + setIsAddingThreshold: React.Dispatch>; + + /** local value of threshold */ + localThreshold: number | null; + + /** function to set local threshold */ + setLocalThreshold: React.Dispatch>; + + /** boolean whether it's possible to add a threshold */ + ableToAddThreshold: boolean; + /** The selected District */ selectedDistrict: District; @@ -34,45 +68,165 @@ export interface HorizontalThresholdListProps { selectedThresholdKey: string | null; } +const StyledTableCell = styled(TableCell)(({theme}) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.background.default, + border: 0, + color: theme.palette.text.primary, + }, +})); + export default function HorizontalThresholdList({ horizontalThresholds, handleDeleteThreshold, handleUpdateThreshold, handleSelectThreshold, + handleAddThreshold, isAddingThreshold, + handleIsAddingThreshold, + setIsAddingThreshold, + selectedDistrict, + selectedCompartment, + ableToAddThreshold, + localThreshold, + setLocalThreshold, selectedThresholdKey, }: HorizontalThresholdListProps) { + const theme = useTheme(); const [editingThresholdKey, setEditingThresholdKey] = useState(null); + const isValid = localThreshold !== null && localThreshold > 0; return ( - + + + + + District + + + Compartment + + + Threshold + + + {Object.entries(horizontalThresholds ?? {}).length === 0 && !isAddingThreshold ? ( - - No thresholds set - + + + No thresholds set + + ) : ( - Object.entries(horizontalThresholds ?? {}).map(([key, threshold]) => { - return ( - - ); - }) + <> + + {Object.entries(horizontalThresholds ?? {}).map(([key, threshold]) => { + return ( + + ); + })} + + )} - + + {isAddingThreshold ? ( + + + + {selectedDistrict.name} + + + + + {selectedCompartment} + + + + + { + const value = e.target.value === '' ? null : Number(e.target.value); + setLocalThreshold(value); + }} + onSave={handleAddThreshold} + onCancel={() => setIsAddingThreshold(false)} + isSaveDisabled={!isValid} + /> + + + ) : ( + handleIsAddingThreshold(true)} + > + + + + + + + + + )} + +
); } diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx index 46b142d1..92b2fb9d 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx @@ -2,14 +2,11 @@ // SPDX-License-Identifier: CC0-1.0 import React, {useState, useEffect} from 'react'; -import {Grid, Box, IconButton, Typography} from '@mui/material'; -import {useTheme} from '@mui/material/styles'; -import AddBoxIcon from '@mui/icons-material/AddBox'; +import {Box} from '@mui/material'; import {Dictionary} from 'util/util'; import type {District} from 'types/district'; import type {HorizontalThreshold} from 'types/horizontalThreshold'; import HorizontalThresholdList from './HorizontalThresholdList'; -import ThresholdInput from './ThresholdInput'; import {selectDistrict, selectCompartment} from 'store/DataSelectionSlice'; import {useAppDispatch} from 'store/hooks'; @@ -34,7 +31,6 @@ export default function HorizontalThresholdSettings({ setHorizontalThresholds, }: HorizontalThresholdSettingsProps) { const dispatch = useAppDispatch(); - const theme = useTheme(); const [localThreshold, setLocalThreshold] = useState(null); const [selectedThresholdKey, setSelectedThresholdKey] = useState( @@ -43,8 +39,6 @@ export default function HorizontalThresholdSettings({ const [isAddingThreshold, setIsAddingThreshold] = useState(false); const [ableToAddThreshold, setAbleToAddThreshold] = useState(false); - const isValid = localThreshold !== null && localThreshold > 0; - // Checks if the user is able to add a threshold useEffect(() => { const key = `${selectedDistrict.ags}-${selectedCompartment}`; @@ -145,71 +139,17 @@ export default function HorizontalThresholdSettings({ handleDeleteThreshold={handleDeleteThreshold} handleUpdateThreshold={handleUpdateThreshold} handleSelectThreshold={handleSelectThreshold} + handleAddThreshold={handleAddThreshold} + handleIsAddingThreshold={handleIsAddingThreshold} + ableToAddThreshold={ableToAddThreshold} isAddingThreshold={isAddingThreshold} + localThreshold={localThreshold} + setIsAddingThreshold={setIsAddingThreshold} + setLocalThreshold={setLocalThreshold} selectedDistrict={selectedDistrict} selectedCompartment={selectedCompartment} selectedThresholdKey={selectedThresholdKey} /> - {isAddingThreshold ? ( - - - {selectedDistrict.name} - {selectedCompartment} - - - { - const value = e.target.value === '' ? null : Number(e.target.value); - setLocalThreshold(value); - }} - onSave={handleAddThreshold} - onCancel={() => setIsAddingThreshold(false)} - isSaveDisabled={!isValid} - /> - - ) : ( - handleIsAddingThreshold(true)} - > - - - - - )}
); } diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx index d2358889..931d151a 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx @@ -80,7 +80,7 @@ export function LineChartSettings({ }; const renderHeader = (title: string) => ( - + handleBackButton()} disabled={currentView === 'settingsMenu'}> @@ -146,7 +146,6 @@ export function LineChartSettings({ {currentView === 'horizontalThresholdSettings' && ( {renderHeader('Horizontal Threshold Settings')} - Date: Tue, 15 Oct 2024 14:54:49 +0200 Subject: [PATCH 24/37] :heavy_check_mark: add new userPreferenceSlice test to include threshold --- .../store/UserPreferenceSlice.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/frontend/src/__tests__/store/UserPreferenceSlice.test.ts b/frontend/src/__tests__/store/UserPreferenceSlice.test.ts index f8ceaa1a..27aee024 100644 --- a/frontend/src/__tests__/store/UserPreferenceSlice.test.ts +++ b/frontend/src/__tests__/store/UserPreferenceSlice.test.ts @@ -6,10 +6,14 @@ import {describe, test, expect} from 'vitest'; import reducer, { selectHeatmapLegend, selectTab, + setHorizontalYAxisThreshold, + setHorizontalYAxisThresholds, setInitialVisit, UserPreference, } from '../../store/UserPreferenceSlice'; import {HeatmapLegend} from '../../types/heatmapLegend'; +import {District} from 'types/district'; +import {HorizontalThreshold} from 'types/horizontalThreshold'; describe('DataSelectionSlice', () => { const initialState: UserPreference = { @@ -23,6 +27,7 @@ describe('DataSelectionSlice', () => { }, selectedTab: '1', isInitialVisit: true, + horizontalYAxisThresholds: {}, }; test('Initial State', () => { @@ -42,6 +47,7 @@ describe('DataSelectionSlice', () => { selectedHeatmap: legend, selectedTab: '1', isInitialVisit: true, + horizontalYAxisThresholds: {}, }); }); @@ -57,6 +63,7 @@ describe('DataSelectionSlice', () => { }, selectedTab: '2', isInitialVisit: true, + horizontalYAxisThresholds: {}, }); }); @@ -72,6 +79,7 @@ describe('DataSelectionSlice', () => { }, selectedTab: '1', isInitialVisit: true, + horizontalYAxisThresholds: {}, }); }); @@ -87,6 +95,46 @@ describe('DataSelectionSlice', () => { }, selectedTab: '1', isInitialVisit: false, + horizontalYAxisThresholds: {}, + }); + }); + + test('Set horizontal thresholds', () => { + const thresholds: Record = { + '11000-compartment1': { + district: {ags: '12345', name: 'Test District', type: 'Test Type'} as District, + compartment: 'compartment1', + threshold: 50, + }, + }; + + expect(reducer(initialState, setHorizontalYAxisThresholds(thresholds))).toEqual({ + ...initialState, + horizontalYAxisThresholds: thresholds, + }); + }); + + test('Add Horizontal Threshold', () => { + const newThreshold = { + district: {ags: '11111', name: 'district1', type: 'type1'} as District, + compartment: 'compartment1', + threshold: 10, + }; + + expect(reducer(initialState, setHorizontalYAxisThreshold(newThreshold))).toEqual({ + selectedHeatmap: { + name: 'uninitialized', + isNormalized: true, + steps: [ + {color: 'rgb(255,255,255)', value: 0}, + {color: 'rgb(255,255,255)', value: 1}, + ], + }, + selectedTab: '1', + isInitialVisit: true, + horizontalYAxisThresholds: { + '11111-compartment1': newThreshold, + }, }); }); }); From b9ff42d8a114f7ba28c5f4ea4b5de07212372355 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Fri, 18 Oct 2024 13:04:25 +0200 Subject: [PATCH 25/37] :hammer: :sparkle: refactored horizontal threshold components, added test id for testing --- .../HorizontalThresholdItem.tsx | 7 + .../HorizontalThresholdList.tsx | 349 ++++++++++-------- .../HorizontalThresholdSettings.tsx | 131 +------ .../ThresholdInput.tsx | 9 +- .../LineChartSettings.tsx | 11 +- frontend/src/store/UserPreferenceSlice.ts | 2 +- 6 files changed, 232 insertions(+), 277 deletions(-) diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx index 4dcbb6ab..54c9f683 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx @@ -34,6 +34,11 @@ export interface HorizontalThresholdItemProps { /** The to determine whether threshold is selected */ selected: boolean; + + /** + * testId for testing + */ + testId?: string; } export default function HorizontalThresholdItem({ @@ -45,6 +50,7 @@ export default function HorizontalThresholdItem({ editingThresholdKey, setEditingThresholdKey, selected, + testId, }: HorizontalThresholdItemProps) { const [localThreshold, setLocalThreshold] = useState(threshold.threshold); const theme = useTheme(); @@ -71,6 +77,7 @@ export default function HorizontalThresholdItem({ }, }} onClick={() => handleSelectThreshold(threshold)} + data-testid={testId} > ; - /** Callback to handle the deletion of a threshold */ - handleDeleteThreshold: (district: District, compartment: string) => void; - - /** Callback to handle changes to an existing threshold value */ - handleUpdateThreshold: (key: string, value: number) => void; - - /** Callback to handle selection of thresholds */ - handleSelectThreshold: (threshold: HorizontalThreshold) => void; - - /** Callback to handle adding a new threshold */ - handleAddThreshold: () => void; - - /** boolean function to handle flow of adding a new threshold */ - handleIsAddingThreshold: (value: boolean) => void; - - /** A boolean state to see whether a threshold is currently being added */ - isAddingThreshold: boolean; - - /** set the isAddingThreshold */ - setIsAddingThreshold: React.Dispatch>; - - /** local value of threshold */ - localThreshold: number | null; - - /** function to set local threshold */ - setLocalThreshold: React.Dispatch>; - - /** boolean whether it's possible to add a threshold */ - ableToAddThreshold: boolean; + /** A function that sets the horizontal thresholds for the y-axis. */ + setHorizontalThresholds: React.Dispatch>>; /** The selected District */ selectedDistrict: District; /** The selected compartment */ selectedCompartment: string; - - /** The currently selected threshold key */ - selectedThresholdKey: string | null; } const StyledTableCell = styled(TableCell)(({theme}) => ({ @@ -78,58 +51,126 @@ const StyledTableCell = styled(TableCell)(({theme}) => ({ export default function HorizontalThresholdList({ horizontalThresholds, - handleDeleteThreshold, - handleUpdateThreshold, - handleSelectThreshold, - handleAddThreshold, - isAddingThreshold, - handleIsAddingThreshold, - setIsAddingThreshold, + setHorizontalThresholds, selectedDistrict, selectedCompartment, - ableToAddThreshold, - localThreshold, - setLocalThreshold, - selectedThresholdKey, }: HorizontalThresholdListProps) { const theme = useTheme(); + const dispatch = useAppDispatch(); + + const [ableToAddThreshold, setAbleToAddThreshold] = useState(false); + const [localThreshold, setLocalThreshold] = useState(null); + const [selectedThresholdKey, setSelectedThresholdKey] = useState( + `${selectedDistrict.ags}-${selectedCompartment}` + ); + const [isAddingThreshold, setIsAddingThreshold] = useState(false); const [editingThresholdKey, setEditingThresholdKey] = useState(null); const isValid = localThreshold !== null && localThreshold > 0; + // Checks if the user is able to add a threshold + useEffect(() => { + const key = `${selectedDistrict.ags}-${selectedCompartment}`; + const existingThreshold = horizontalThresholds[key]; + if (existingThreshold) { + setAbleToAddThreshold(false); + return; + } + setAbleToAddThreshold(true); + }, [selectedDistrict, selectedCompartment, horizontalThresholds]); + + // function to handle adding a new threshold + const handleAddThreshold = () => { + if (localThreshold === null || localThreshold < 0) return; + const thresholdKey = `${selectedDistrict.ags}-${selectedCompartment}`; + const existingThreshold = horizontalThresholds[thresholdKey]; + + if (existingThreshold) { + return; + } + + const newThreshold: HorizontalThreshold = { + district: selectedDistrict, + compartment: selectedCompartment ?? '', + threshold: localThreshold, + }; + + const newThresholds = {...horizontalThresholds, [thresholdKey]: newThreshold}; + setHorizontalThresholds(newThresholds); + setSelectedThresholdKey(thresholdKey); + setLocalThreshold(null); + setIsAddingThreshold(false); + }; + + // function to handle deleting a threshold + const handleDeleteThreshold = (district: District, compartment: string) => { + const newThresholds = {...horizontalThresholds}; + delete newThresholds[`${district.ags}-${compartment}`]; + + setHorizontalThresholds(newThresholds); + }; + + const handleSelectThreshold = (threshold: HorizontalThreshold) => { + if (isAddingThreshold) { + return; + } + setSelectedThresholdKey(threshold.district.ags + '-' + threshold.compartment); + dispatch(selectDistrict(threshold.district)); + dispatch(selectCompartment(threshold.compartment)); + }; + + const handleUpdateThreshold = (key: string, value: number) => { + const existingThreshold = horizontalThresholds[key]; + + if (existingThreshold) { + if (value < 0) return; + const updatedThreshold: HorizontalThreshold = { + ...existingThreshold, + threshold: value, + }; + + const newThresholds = {...horizontalThresholds, [key]: updatedThreshold}; + + setHorizontalThresholds(newThresholds); + setLocalThreshold(null); + } + }; + return ( - - - - - District - - - Compartment - - - Threshold - - - - {Object.entries(horizontalThresholds ?? {}).length === 0 && !isAddingThreshold ? ( - - - No thresholds set - - - ) : ( - <> + +
+ + + + District + + + Compartment + + + Threshold + + + + {Object.entries(horizontalThresholds ?? {}).length === 0 && !isAddingThreshold ? ( + + + No thresholds set + + + + ) : ( + {Object.entries(horizontalThresholds ?? {}).map(([key, threshold]) => { return ( ); })} - - )} - - {isAddingThreshold ? ( - - - - {selectedDistrict.name} - - - - - {selectedCompartment} - - + )} + + {isAddingThreshold ? ( + + + + {selectedDistrict.name} + + + + + {selectedCompartment} + + - - { - const value = e.target.value === '' ? null : Number(e.target.value); - setLocalThreshold(value); - }} - onSave={handleAddThreshold} - onCancel={() => setIsAddingThreshold(false)} - isSaveDisabled={!isValid} - /> - - - ) : ( - handleIsAddingThreshold(true)} - > - + { + const value = e.target.value === '' ? null : Number(e.target.value); + setLocalThreshold(value); + }} + onSave={handleAddThreshold} + onCancel={() => setIsAddingThreshold(false)} + isSaveDisabled={!isValid} + /> + + + ) : ( + { + const key = `${selectedDistrict.ags}-${selectedCompartment}`; + const existingThreshold = horizontalThresholds[key]; + + if (existingThreshold) { + // handle error here, maybe show modal + return; + } + setIsAddingThreshold(true); + }} + data-testid='add-threshold-testid' > - - - - - - - - )} - -
+ + + +
+ + + )} + + + ); } diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx index 92b2fb9d..f85add0a 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx @@ -1,14 +1,11 @@ // SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) // SPDX-License-Identifier: CC0-1.0 -import React, {useState, useEffect} from 'react'; -import {Box} from '@mui/material'; +import React from 'react'; import {Dictionary} from 'util/util'; import type {District} from 'types/district'; import type {HorizontalThreshold} from 'types/horizontalThreshold'; import HorizontalThresholdList from './HorizontalThresholdList'; -import {selectDistrict, selectCompartment} from 'store/DataSelectionSlice'; -import {useAppDispatch} from 'store/hooks'; export interface HorizontalThresholdSettingsProps { /** The district to which the settings apply. */ @@ -30,126 +27,12 @@ export default function HorizontalThresholdSettings({ horizontalThresholds, setHorizontalThresholds, }: HorizontalThresholdSettingsProps) { - const dispatch = useAppDispatch(); - - const [localThreshold, setLocalThreshold] = useState(null); - const [selectedThresholdKey, setSelectedThresholdKey] = useState( - `${selectedDistrict.ags}-${selectedCompartment}` - ); - const [isAddingThreshold, setIsAddingThreshold] = useState(false); - const [ableToAddThreshold, setAbleToAddThreshold] = useState(false); - - // Checks if the user is able to add a threshold - useEffect(() => { - const key = `${selectedDistrict.ags}-${selectedCompartment}`; - const existingThreshold = horizontalThresholds[key]; - if (existingThreshold) { - setAbleToAddThreshold(false); - return; - } - setAbleToAddThreshold(true); - }, [selectedDistrict, selectedCompartment, horizontalThresholds]); - - const handleIsAddingThreshold = (value: boolean) => { - const key = `${selectedDistrict.ags}-${selectedCompartment}`; - const existingThreshold = horizontalThresholds[key]; - - if (existingThreshold) { - // handle error here, maybe show modal - return; - } - setIsAddingThreshold(value); - }; - - // function to handle adding a new threshold - const handleAddThreshold = () => { - if (localThreshold === null || localThreshold < 0) { - return; - } - - const key = `${selectedDistrict.ags}-${selectedCompartment}`; - - const existingThreshold = horizontalThresholds[key]; - - if (existingThreshold) { - console.log('Threshold already exists'); - - // handle error here, maybe show modal - return; - } - - const newThreshold: HorizontalThreshold = { - district: selectedDistrict, - compartment: selectedCompartment ?? '', - threshold: localThreshold, - }; - - const newThresholds = {...horizontalThresholds, [key]: newThreshold}; - - setHorizontalThresholds(newThresholds); - setSelectedThresholdKey(key); - setLocalThreshold(null); - setIsAddingThreshold(false); - }; - - // function to handle updating an existing threshold - const handleUpdateThreshold = (key: string, updatedThresholdValue: number) => { - const existingThreshold = horizontalThresholds[key]; - - if (existingThreshold) { - const updatedThreshold: HorizontalThreshold = { - ...existingThreshold, - threshold: updatedThresholdValue, - }; - - const newThresholds = {...horizontalThresholds, [key]: updatedThreshold}; - - setHorizontalThresholds(newThresholds); - setLocalThreshold(null); - } - }; - - // function to handle deleting a threshold - const handleDeleteThreshold = (district: District, compartment: string) => { - const newThresholds = {...horizontalThresholds}; - delete newThresholds[`${district.ags}-${compartment}`]; - - setHorizontalThresholds(newThresholds); - }; - - const handleSelectThreshold = (threshold: HorizontalThreshold) => { - if (isAddingThreshold) { - return; - } - setSelectedThresholdKey(threshold.district.ags + '-' + threshold.compartment); - dispatch(selectDistrict(threshold.district)); - dispatch(selectCompartment(threshold.compartment)); - }; - return ( - - - + ); } diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/ThresholdInput.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/ThresholdInput.tsx index b684b02b..488c77c3 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/ThresholdInput.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/ThresholdInput.tsx @@ -39,6 +39,7 @@ export default function ThresholdInput({ - +
diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx index 931d151a..530f2fbf 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx @@ -82,10 +82,10 @@ export function LineChartSettings({ const renderHeader = (title: string) => ( handleBackButton()} disabled={currentView === 'settingsMenu'}> - + {title} - + @@ -98,11 +98,16 @@ export function LineChartSettings({ zIndex: 1000, }} > - ; + horizontalYAxisThresholds?: Dictionary; } const initialState: UserPreference = { From 855c2132f9cca1aac235adfd07126979adce6a42 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Tue, 22 Oct 2024 13:51:50 +0200 Subject: [PATCH 26/37] :wrench: improved UX by disabling clicking other items when user is editing / adding a new threshold, improved styling, added some testid --- .../HorizontalThresholdItem.tsx | 38 +++++++++++++++---- .../HorizontalThresholdList.tsx | 11 +++++- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx index 54c9f683..ec007b40 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx @@ -26,7 +26,7 @@ export interface HorizontalThresholdItemProps { /** Callback to handle selection of a threshold */ handleSelectThreshold: (threshold: HorizontalThreshold) => void; - /** Current edited key of the threshold */ + /** The current edited key of the threshold */ editingThresholdKey: string | null; /** Callback to set the current edited key of the threshold */ @@ -35,9 +35,13 @@ export interface HorizontalThresholdItemProps { /** The to determine whether threshold is selected */ selected: boolean; - /** - * testId for testing - */ + /** Boolean to determine if the threshold is being edited */ + isEditingThreshold: boolean; + + /** Boolean to determine if the threshold is being added */ + isAddingThreshold: boolean; + + /** testId for testing */ testId?: string; } @@ -50,14 +54,16 @@ export default function HorizontalThresholdItem({ editingThresholdKey, setEditingThresholdKey, selected, + isEditingThreshold, + isAddingThreshold, testId, }: HorizontalThresholdItemProps) { - const [localThreshold, setLocalThreshold] = useState(threshold.threshold); + const [localThreshold, setLocalThreshold] = useState(threshold.threshold); const theme = useTheme(); const isValid = localThreshold !== null && localThreshold > 0; const updateThreshold = () => { - if (localThreshold < 0) return; + if (localThreshold === null || localThreshold < 0) return; handleUpdateThreshold(thresholdKey, localThreshold); setEditingThresholdKey(null); }; @@ -67,16 +73,24 @@ export default function HorizontalThresholdItem({ setLocalThreshold(threshold); }; + const isDisabled = (isEditingThreshold && editingThresholdKey !== thresholdKey) || isAddingThreshold; + return ( { + if (!isDisabled) { + handleSelectThreshold(threshold); + } }} - onClick={() => handleSelectThreshold(threshold)} data-testid={testId} > @@ -84,6 +98,7 @@ export default function HorizontalThresholdItem({ variant='body1' sx={{ fontSize: theme.typography.listElement.fontSize, + color: isDisabled ? theme.palette.text.disabled : theme.palette.text.primary, }} > {threshold.district.name} @@ -94,6 +109,7 @@ export default function HorizontalThresholdItem({ variant='body1' sx={{ fontSize: theme.typography.listElement.fontSize, + color: isDisabled ? theme.palette.text.disabled : theme.palette.text.primary, }} > {threshold.compartment} @@ -106,7 +122,10 @@ export default function HorizontalThresholdItem({ id='horizontal-y-threshold-input' value={localThreshold} error={!isValid} - onChange={(e) => setLocalThreshold(Number(e.target.value))} + onChange={(e) => { + const value = e.target.value === '' ? null : Number(e.target.value); + setLocalThreshold(value); + }} onSave={updateThreshold} onCancel={() => setEditingThresholdKey(null)} isSaveDisabled={!isValid} @@ -133,6 +152,7 @@ export default function HorizontalThresholdItem({ variant='body1' sx={{ fontSize: theme.typography.listElement.fontSize, + color: isDisabled ? theme.palette.text.disabled : theme.palette.text.primary, }} > {threshold.threshold} @@ -146,6 +166,7 @@ export default function HorizontalThresholdItem({ > handleEditThreshold(thresholdKey, threshold.threshold)} > handleDeleteThreshold(threshold.district, threshold.compartment)} > diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx index 5806fc2d..262e11f2 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx @@ -110,7 +110,7 @@ export default function HorizontalThresholdList({ }; const handleSelectThreshold = (threshold: HorizontalThreshold) => { - if (isAddingThreshold) { + if (isAddingThreshold || editingThresholdKey !== null) { return; } setSelectedThresholdKey(threshold.district.ags + '-' + threshold.compartment); @@ -183,6 +183,8 @@ export default function HorizontalThresholdList({ editingThresholdKey={editingThresholdKey} setEditingThresholdKey={setEditingThresholdKey} selected={selectedThresholdKey === key} + isEditingThreshold={editingThresholdKey !== null} + isAddingThreshold={isAddingThreshold} testId={`threshold-item-${key}`} /> ); @@ -191,7 +193,12 @@ export default function HorizontalThresholdList({ )} {isAddingThreshold ? ( - + Date: Tue, 22 Oct 2024 13:52:44 +0200 Subject: [PATCH 27/37] :heavy_check_mark: created tests for HorizontalThreshold components and UserPreferenceSlice --- .../HorizontalThresholdItem.test.tsx | 218 ++++++++++++++++ .../HorizontalThresholdList.test.tsx | 239 ++++++++++++++++++ .../LineChartSettings.test.tsx | 112 ++++++++ .../store/UserPreferenceSlice.test.ts | 29 +++ 4 files changed, 598 insertions(+) create mode 100644 frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdItem.test.tsx create mode 100644 frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdList.test.tsx create mode 100644 frontend/src/__tests__/components/LineChartSettings/LineChartSettings.test.tsx diff --git a/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdItem.test.tsx b/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdItem.test.tsx new file mode 100644 index 00000000..580de379 --- /dev/null +++ b/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdItem.test.tsx @@ -0,0 +1,218 @@ +import {render, screen, waitFor} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import {describe, test, expect} from 'vitest'; +import React, {useState} from 'react'; +import {ThemeProvider} from '@mui/system'; +import Theme from 'util/Theme'; +import {Provider} from 'react-redux'; +import {Store} from 'store'; +import {Dictionary} from 'util/util'; +import {District} from 'types/district'; +import {HorizontalThreshold} from 'types/horizontalThreshold'; +import HorizontalThresholdItem from 'components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem'; + +type HorizontalThresholdItemTestProps = { + isAddingThreshold?: boolean; +}; + +const HorizontalThresholdItemTest: React.FC = ({isAddingThreshold = false}) => { + const currentHorizontalThresholds: Dictionary = { + '00000-Compartment 1': { + threshold: 10, + district: {ags: '00000', name: 'district1', type: 'type1'}, + compartment: 'Compartment 1', + }, + '00000-Compartment 2': { + threshold: 20, + district: {ags: '00000', name: 'district1', type: 'type1'}, + compartment: 'Compartment 2', + }, + '01001-Compartment 1': { + threshold: 40, + district: {ags: '01001', name: 'district2', type: 'type2'}, + compartment: 'Compartment 1', + }, + '01001-Compartment 3': { + threshold: 60, + district: {ags: '01001', name: 'district2', type: 'type2'}, + compartment: 'Compartment 3', + }, + '01059-Compartment 2': { + threshold: 80, + district: {ags: '01059', name: 'district3', type: 'type3'}, + compartment: 'Compartment 2', + }, + '01059-Compartment 3': { + threshold: 90, + district: {ags: '01059', name: 'district3', type: 'type3'}, + compartment: 'Compartment 3', + }, + }; + + const [editingThresholdKey, setEditingThresholdKey] = useState(null); + const [selectedThresholdKey, setSelectedThresholdKey] = useState('00000-Compartment 1'); + const [horizontalThresholds, setHorizontalThresholds] = + useState>(currentHorizontalThresholds); + + const handleSelectThreshold = (threshold: HorizontalThreshold) => { + if (isAddingThreshold || editingThresholdKey !== null) { + return; + } + setSelectedThresholdKey(threshold.district.ags + '-' + threshold.compartment); + }; + + // function to handle deleting a threshold + const handleDeleteThreshold = (district: District, compartment: string) => { + const newThresholds = {...horizontalThresholds}; + delete newThresholds[`${district.ags}-${compartment}`]; + + setHorizontalThresholds(newThresholds); + }; + + const handleUpdateThreshold = (key: string, value: number) => { + const existingThreshold = horizontalThresholds[key]; + + if (existingThreshold) { + if (value < 0) return; + const updatedThreshold: HorizontalThreshold = { + ...existingThreshold, + threshold: value, + }; + + const newThresholds = {...horizontalThresholds, [key]: updatedThreshold}; + + setHorizontalThresholds(newThresholds); + } + }; + + return ( +
+ + + {Object.entries(horizontalThresholds ?? {}).map(([key, threshold]) => { + return ( + + ); + })} + + +
+ ); +}; + +describe('HorizontalThresholdItem Component', () => { + test('correctly selects threshold item', async () => { + render(); + + // initial selected threshold item + const initialThresholdItem = screen.getByTestId('threshold-item-00000-Compartment 1'); + expect(initialThresholdItem).toBeInTheDocument(); + expect(initialThresholdItem).toHaveClass('selected-threshold'); // Initially selected + + // Click on a different threshold item to select it + const targetThresholdItem = screen.getByTestId('threshold-item-01001-Compartment 3'); + await userEvent.click(targetThresholdItem); + + // Verify that the new threshold is selected and no other threshold has 'selected-threshold' class + expect(targetThresholdItem).toHaveClass('selected-threshold'); + expect(initialThresholdItem).not.toHaveClass('selected-threshold'); + }); + + test('clicking on the edit button should show the textfield input', async () => { + render(); + const editThresholdButton = screen.getByTestId('edit-threshold-button-01001-Compartment 3'); + expect(editThresholdButton).toBeInTheDocument(); + await userEvent.click(editThresholdButton); + expect(await screen.findByTestId('threshold-input-testid')).toBeInTheDocument(); + }); + + test('should edit a threshold', async () => { + render(); + + // Click on the threshold item + const thresholdItem = screen.getByTestId('threshold-item-01001-Compartment 3'); + expect(thresholdItem).toBeInTheDocument(); + await userEvent.click(thresholdItem); + + // Click on the edit button + const editThresholdButton = screen.getByTestId('edit-threshold-button-01001-Compartment 3'); + expect(editThresholdButton).toBeInTheDocument(); + expect(await screen.findByText('60')).toBeInTheDocument(); + await userEvent.click(editThresholdButton); + + const thresholdInput = await screen.findByLabelText('Horizontal Threshold'); + const saveButton = screen.getByTestId('save-threshold'); + expect(saveButton).toBeInTheDocument(); + expect(thresholdInput).toBeInTheDocument(); + await userEvent.clear(thresholdInput); + await userEvent.type(thresholdInput, '68123'); + expect(screen.getByDisplayValue('68123')).toBeInTheDocument(); + await userEvent.click(saveButton); + + expect(await screen.findByTestId('threshold-item-01001-Compartment 3')).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.queryByText('68123')).toBeInTheDocument(); + }); + }); + + test('edit a threshold with negative number input', async () => { + render(); + + // Click on the threshold item + const thresholdItem = screen.getByTestId('threshold-item-01001-Compartment 3'); + expect(thresholdItem).toBeInTheDocument(); + await userEvent.click(thresholdItem); + + // Click on the edit button + const editThresholdButton = screen.getByTestId('edit-threshold-button-00000-Compartment 1'); + expect(editThresholdButton).toBeInTheDocument(); + expect(await screen.findByText('10')).toBeInTheDocument(); + await userEvent.click(editThresholdButton); + + // Edit the threshold with a negative number + const thresholdInput = await screen.findByLabelText('Horizontal Threshold'); + const saveButton = screen.getByTestId('save-threshold'); + expect(saveButton).toBeInTheDocument(); + expect(thresholdInput).toBeInTheDocument(); + await userEvent.clear(thresholdInput); + await userEvent.type(thresholdInput, '-10'); + expect(screen.getByDisplayValue('-10')).toBeInTheDocument(); + // disabled save button + expect(saveButton).toBeDisabled(); + + // edit threshold with empty input + await userEvent.clear(thresholdInput); + expect(screen.getByDisplayValue('')).toBeInTheDocument(); + expect(saveButton).toBeDisabled(); + }); + + test('should delete a threshold', async () => { + render(); + const thresholdItem = screen.getByTestId('threshold-item-01001-Compartment 3'); + expect(thresholdItem).toBeInTheDocument(); + await userEvent.click(thresholdItem); + + // Click on the delete button + const deleteThresholdButton = screen.getByTestId('delete-threshold-button-01001-Compartment 3'); + expect(deleteThresholdButton).toBeInTheDocument(); + await userEvent.click(deleteThresholdButton); + + await waitFor(() => { + expect(screen.queryByTestId('threshold-item-01001-Compartment 3')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdList.test.tsx b/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdList.test.tsx new file mode 100644 index 00000000..6a6713cb --- /dev/null +++ b/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdList.test.tsx @@ -0,0 +1,239 @@ +import {render, screen, waitFor} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import {describe, test, expect} from 'vitest'; +import React, {useState} from 'react'; +import {ThemeProvider} from '@mui/system'; +import Theme from 'util/Theme'; +import {Provider} from 'react-redux'; +import {Store} from 'store'; +import {Dictionary} from 'util/util'; +import {District} from 'types/district'; +import {HorizontalThreshold} from 'types/horizontalThreshold'; +import HorizontalThresholdList from 'components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList'; + +type HorizontalThresholdListTestProps = { + selectedDistrict?: District; + selectedCompartment?: string; +}; + +const HorizontalThresholdListTest: React.FC = ({ + selectedDistrict = {ags: '02000', name: 'district4', type: 'type4'}, + selectedCompartment = 'Compartment 4', +}) => { + const currentHorizontalThresholds: Dictionary = { + '00000-Compartment 1': { + threshold: 10, + district: {ags: '00000', name: 'district1', type: 'type1'}, + compartment: 'Compartment 1', + }, + '00000-Compartment 2': { + threshold: 20, + district: {ags: '00000', name: 'district1', type: 'type1'}, + compartment: 'Compartment 2', + }, + '01001-Compartment 2': { + threshold: 50, + district: {ags: '01001', name: 'district2', type: 'type2'}, + compartment: 'Compartment 2', + }, + '01001-Compartment 3': { + threshold: 60, + district: {ags: '01001', name: 'district2', type: 'type2'}, + compartment: 'Compartment 3', + }, + '01059-Compartment 1': { + threshold: 70, + district: {ags: '01059', name: 'district3', type: 'type3'}, + compartment: 'Compartment 1', + }, + '01059-Compartment 3': { + threshold: 90, + district: {ags: '01059', name: 'district3', type: 'type3'}, + compartment: 'Compartment 3', + }, + }; + + const [horizontalThresholds, setHorizontalThresholds] = + useState>(currentHorizontalThresholds); + + return ( +
+ + + + + +
+ ); +}; + +describe('HorizontalThresholdSettingsList Component', () => { + test('should render the HorizontalThresholdList', () => { + render(); + const horizontalThresholdList = screen.getByTestId('horizontal-threshold-list'); + expect(horizontalThresholdList).toBeInTheDocument(); + }); + + const horizontalThresholds: Dictionary = { + '00000-Compartment 1': { + threshold: 10, + district: {ags: '00000', name: 'district1', type: 'type1'}, + compartment: 'Compartment 1', + }, + '00000-Compartment 2': { + threshold: 20, + district: {ags: '00000', name: 'district1', type: 'type1'}, + compartment: 'Compartment 2', + }, + '01001-Compartment 2': { + threshold: 50, + district: {ags: '01001', name: 'district2', type: 'type2'}, + compartment: 'Compartment 2', + }, + '01001-Compartment 3': { + threshold: 60, + district: {ags: '01001', name: 'district2', type: 'type2'}, + compartment: 'Compartment 3', + }, + '01059-Compartment 1': { + threshold: 70, + district: {ags: '01059', name: 'district3', type: 'type3'}, + compartment: 'Compartment 1', + }, + '01059-Compartment 3': { + threshold: 90, + district: {ags: '01059', name: 'district3', type: 'type3'}, + compartment: 'Compartment 3', + }, + }; + + test('should render the HorizontalThresholdList TableBody with the correct number of rows', async () => { + render(); + expect(screen.getByTestId('horizontal-threshold-list')).toBeInTheDocument(); + + await waitFor(() => { + const tableLength = Object.entries(horizontalThresholds).length; + const horizontalThresholdTable = screen.getByTestId('horizontal-table-body-testid'); + expect(horizontalThresholdTable.querySelectorAll('.MuiTableRow-root').length).toBe(tableLength); + }); + }); + + test('should render the table body with correct district, compartment, and threshold values', async () => { + render(); + + await waitFor(() => { + Object.entries(horizontalThresholds).forEach(([key, {district, compartment, threshold}]) => { + // Get the row by the test id + const thresholdRow = screen.getByTestId(`threshold-item-${key}`); + + // Check if the district name is present in the row + expect(thresholdRow).toHaveTextContent(district.name); + + // Check if the compartment name is present in the row + expect(thresholdRow).toHaveTextContent(compartment); + + // Check if the threshold value is present in the row + expect(thresholdRow).toHaveTextContent(threshold.toString()); + }); + }); + }); + + test('should render the HorizontalThresholdList with the correct number of rows including the add row and table header row', async () => { + render(); + + await waitFor(() => { + const totalLength = Object.entries(horizontalThresholds).length + 2; + const horizontalThresholdList = screen.getByTestId('horizontal-threshold-list'); + expect(horizontalThresholdList).toBeInTheDocument(); + expect(horizontalThresholdList.querySelectorAll('.MuiTableRow-root').length).toBe(totalLength); + }); + }); + + test('render the add threshold button when adding', async () => { + render(); + expect(screen.getByTestId('horizontal-threshold-list')).toBeInTheDocument(); + + const addThresholdButton = await screen.findByTestId('add-threshold-testid'); + expect(addThresholdButton).toBeInTheDocument(); + + await userEvent.click(addThresholdButton); + + const addThresholdTableRow = await screen.findByTestId('add-threshold-table-row-testid'); + expect(addThresholdTableRow).toBeInTheDocument(); + expect(await screen.findByTestId('threshold-input-container-testid')).toBeInTheDocument(); + }); + + test('disable the add threshold button when selected district and compartment already has a threshold', () => { + render( + + ); + expect(screen.getByTestId('horizontal-threshold-list')).toBeInTheDocument(); + + const addThresholdButton = screen.getByTestId('add-threshold-button-testid'); + expect(addThresholdButton).toBeDisabled(); + }); + + // similar to selectedThresholdKey + + test('should add threshold row and render it', async () => { + render(); + + expect(screen.getByTestId('horizontal-threshold-list')).toBeInTheDocument(); + + const addThresholdButton = screen.getByTestId('add-threshold-testid'); + expect(addThresholdButton).toBeInTheDocument(); + + await userEvent.click(addThresholdButton); + + const addThresholdTableRow = await screen.findByTestId('add-threshold-table-row-testid'); + expect(addThresholdTableRow).toBeInTheDocument(); + expect(await screen.findByTestId('threshold-input-container-testid')).toBeInTheDocument(); + + const thresholdInput = await screen.findByLabelText('Horizontal Threshold'); + expect(thresholdInput).toBeInTheDocument(); + + await userEvent.clear(thresholdInput); + await userEvent.type(thresholdInput, '12612'); + expect(screen.getByDisplayValue('12612')).toBeInTheDocument(); + + const saveThresholdButton = screen.getByTestId('save-threshold'); + expect(saveThresholdButton).toBeInTheDocument(); + await userEvent.click(saveThresholdButton); + + await waitFor(() => { + const horizontalThresholdTable = screen.getByTestId('horizontal-table-body-testid'); + expect(horizontalThresholdTable.querySelectorAll('.MuiTableRow-root').length).toBe( + Object.entries(horizontalThresholds).length + 1 + ); + }); + + // check whether the new threshold is added to the table + const newThresholdRow = await screen.findByTestId('threshold-item-02000-Compartment 4'); + expect(newThresholdRow).toBeInTheDocument(); + }); + + test('should handle error when adding a threshold that already exists', async () => { + // Render the component with an existing threshold for the selected district and compartment + render( + + ); + + // Attempt to add a new threshold which already exists + const addThresholdButton = screen.getByTestId('add-threshold-testid'); + await userEvent.click(addThresholdButton); + + // setIsAddingThreshold(true) is not called + expect(screen.queryByTestId('add-threshold-table-row-testid')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/components/LineChartSettings/LineChartSettings.test.tsx b/frontend/src/__tests__/components/LineChartSettings/LineChartSettings.test.tsx new file mode 100644 index 00000000..6159f0aa --- /dev/null +++ b/frontend/src/__tests__/components/LineChartSettings/LineChartSettings.test.tsx @@ -0,0 +1,112 @@ +import {render, screen, waitFor} from '@testing-library/react'; +import {describe, test, expect} from 'vitest'; +import React, {useState} from 'react'; +import {ThemeProvider} from '@mui/system'; +import Theme from 'util/Theme'; +import {LineChartSettings} from 'components/LineChartComponents/LineChartSettingsComponents/LineChartSettings'; +import {Provider} from 'react-redux'; +import {Store} from '../../../store'; +import {Dictionary} from 'util/util'; +import {District} from 'types/district'; +import {HorizontalThreshold} from 'types/horizontalThreshold'; +import {userEvent} from '@testing-library/user-event'; + +const LineChartSettingsTest: React.FC = () => { + const selectedDistrict: District = {ags: '00000', name: 'district1', type: 'type1'}; + const selectedCompartment = 'Compartment 1'; + const [horizontalThresholds, setHorizontalThresholds] = useState>({}); + + return ( +
+ + + + + +
+ ); +}; + +describe('LineChartSettings', () => { + test('should render LineChartSettings Popover', () => { + render(); + expect(screen.getByTestId('line-chart-settings')).toBeInTheDocument(); + + const settingsButton = screen.getByTestId('settings-popover-button-testid'); + expect(settingsButton).toBeInTheDocument(); + }); + + test('renders popover on click', async () => { + render(); + const settingsButton = screen.getByTestId('settings-popover-button-testid'); + await userEvent.click(settingsButton); + expect(screen.getByTestId('line-chart-settings-popover-testid')).toBeInTheDocument(); + }); + + test('renders menu items in popover', async () => { + render(); + + const settingsButton = screen.getByTestId('settings-popover-button-testid'); + await userEvent.click(settingsButton); + + expect(screen.getByText('Horizontal Threshold Settings')).toBeInTheDocument(); + }); + + test('Navigates to all menu item in popover', async () => { + render(); + const settingsButton = screen.getByTestId('settings-popover-button-testid'); + await userEvent.click(settingsButton); + + // add more menus here + const menuItems = ['Horizontal Threshold Settings']; + + for (const menuItem of menuItems) { + await userEvent.click(screen.getByText(menuItem)); + expect(screen.getByText(menuItem)).toBeInTheDocument(); + + const backButton = screen.getByTestId('settings-back-button'); + await userEvent.click(backButton); + + // Ensure we're back at the main settings menu + expect(screen.getByText('Line Chart Settings')).toBeInTheDocument(); + } + }); + + test('Displays the settings main menu when back button is clicked from any menu', async () => { + render(); + const settingsButton = screen.getByTestId('settings-popover-button-testid'); + await userEvent.click(settingsButton); + + const thresholdSettingsButton = screen.getByText('Horizontal Threshold Settings'); + await userEvent.click(thresholdSettingsButton); + + const backButton = screen.getByTestId('settings-back-button'); + await userEvent.click(backButton); + + const settingsHeader = screen.getByText('Line Chart Settings'); + expect(settingsHeader).toBeInTheDocument(); + }); + + test('closes the popover when close button is clicked', async () => { + render(); + + // Open the popover + const settingsButton = screen.getByTestId('settings-popover-button-testid'); + await userEvent.click(settingsButton); + expect(screen.getByTestId('line-chart-settings-popover-testid')).toBeInTheDocument(); + + // Find and click the close button + const closeButton = screen.getByTestId('settings-close-button'); + await userEvent.click(closeButton); + + // Assert that the popover is no longer in the document + await waitFor(() => { + expect(screen.queryByTestId('line-chart-settings-popover-testid')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/__tests__/store/UserPreferenceSlice.test.ts b/frontend/src/__tests__/store/UserPreferenceSlice.test.ts index 27aee024..78823f21 100644 --- a/frontend/src/__tests__/store/UserPreferenceSlice.test.ts +++ b/frontend/src/__tests__/store/UserPreferenceSlice.test.ts @@ -137,4 +137,33 @@ describe('DataSelectionSlice', () => { }, }); }); + + test('Add Horizontal Threshold when horizontalYAxisThresholds is undefined', () => { + const stateWithUndefinedThresholds = { + ...initialState, + horizontalYAxisThresholds: undefined, + }; + + const newThreshold = { + district: {ags: '11111', name: 'district1', type: 'type1'} as District, + compartment: 'compartment1', + threshold: 10, + }; + + expect(reducer(stateWithUndefinedThresholds, setHorizontalYAxisThreshold(newThreshold))).toEqual({ + selectedHeatmap: { + name: 'uninitialized', + isNormalized: true, + steps: [ + {color: 'rgb(255,255,255)', value: 0}, + {color: 'rgb(255,255,255)', value: 1}, + ], + }, + selectedTab: '1', + isInitialVisit: true, + horizontalYAxisThresholds: { + '11111-compartment1': newThreshold, + }, + }); + }); }); From 5cf76566747719d298e29c67b274ba3184abf51f Mon Sep 17 00:00:00 2001 From: kunkoala Date: Tue, 22 Oct 2024 13:53:25 +0200 Subject: [PATCH 28/37] :wrench: :beetle: fixed import statement case for FilterValue in DataCard.test.tsx --- .../components/Scenario/CardsComponents/DataCard.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/__tests__/components/Scenario/CardsComponents/DataCard.test.tsx b/frontend/src/__tests__/components/Scenario/CardsComponents/DataCard.test.tsx index 9efe2cff..f1fd439b 100644 --- a/frontend/src/__tests__/components/Scenario/CardsComponents/DataCard.test.tsx +++ b/frontend/src/__tests__/components/Scenario/CardsComponents/DataCard.test.tsx @@ -6,7 +6,7 @@ import {render, screen} from '@testing-library/react'; import {describe, test, expect} from 'vitest'; import {Dictionary} from 'util/util'; import {GroupFilter} from 'types/group'; -import {filterValue} from 'types/card'; +import {FilterValue} from 'types/card'; import Theme from 'util/Theme'; import {ThemeProvider} from '@mui/system'; import DataCard from 'components/ScenarioComponents/CardsComponents/DataCard'; @@ -30,7 +30,7 @@ const DataCardTest = () => { const SelectedScenario = true; const Color = 'primary'; const ActiveScenarios = [0, 1, 2]; - const FilterValues: Dictionary = { + const FilterValues: Dictionary = { '0': [ {filteredTitle: 'Group 1', filteredValues: {'Compartment 1': 10, 'Compartment 2': 20, 'Compartment 3': 30}}, {filteredTitle: 'Group 2', filteredValues: {'Compartment 1': 40, 'Compartment 2': 50, 'Compartment 3': 60}}, From 5866381d55bf43dc18949a44a8b6c60feba213fc Mon Sep 17 00:00:00 2001 From: kunkoala Date: Tue, 22 Oct 2024 13:57:51 +0200 Subject: [PATCH 29/37] :wrench: :sparkles: :green_heart: added missing reuse compliance --- .../HorizontalThresholdItem.test.tsx | 3 +++ .../HorizontalThresholdList.test.tsx | 3 +++ .../components/LineChartSettings/LineChartSettings.test.tsx | 3 +++ 3 files changed, 9 insertions(+) diff --git a/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdItem.test.tsx b/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdItem.test.tsx index 580de379..2036b29c 100644 --- a/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdItem.test.tsx +++ b/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdItem.test.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: Apache-2.0 + import {render, screen, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import {describe, test, expect} from 'vitest'; diff --git a/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdList.test.tsx b/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdList.test.tsx index 6a6713cb..f955516f 100644 --- a/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdList.test.tsx +++ b/frontend/src/__tests__/components/LineChartSettings/HorizontalThresholdSettings/HorizontalThresholdList.test.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: Apache-2.0 + import {render, screen, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import {describe, test, expect} from 'vitest'; diff --git a/frontend/src/__tests__/components/LineChartSettings/LineChartSettings.test.tsx b/frontend/src/__tests__/components/LineChartSettings/LineChartSettings.test.tsx index 6159f0aa..34424923 100644 --- a/frontend/src/__tests__/components/LineChartSettings/LineChartSettings.test.tsx +++ b/frontend/src/__tests__/components/LineChartSettings/LineChartSettings.test.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: Apache-2.0 + import {render, screen, waitFor} from '@testing-library/react'; import {describe, test, expect} from 'vitest'; import React, {useState} from 'react'; From 674bd5a165c83b1c34ffbb37766116c00e17c7e8 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Tue, 29 Oct 2024 13:59:37 +0100 Subject: [PATCH 30/37] :wrench: :beetle: small refactor and bug fix --- .../src/components/LineChartComponents/LineChartContainer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/LineChartComponents/LineChartContainer.tsx b/frontend/src/components/LineChartComponents/LineChartContainer.tsx index fb304e8a..2381ef11 100644 --- a/frontend/src/components/LineChartComponents/LineChartContainer.tsx +++ b/frontend/src/components/LineChartComponents/LineChartContainer.tsx @@ -25,7 +25,7 @@ export default function LineChartContainer() { const selectedCompartment = useAppSelector((state) => state.dataSelection.compartment); const selectedDistrict = useAppSelector((state) => state.dataSelection.district); const selectedDateInStore = useAppSelector((state) => state.dataSelection.date); - const storeHorizontalThresholds = useAppSelector((state) => state.userPreference.horizontalYAxisThresholds); + const storeHorizontalThresholds = useAppSelector((state) => state.userPreference.horizontalYAxisThresholds ?? {}); const referenceDay = useAppSelector((state) => state.dataSelection.simulationStart); const minDate = useAppSelector((state) => state.dataSelection.minDate); const maxDate = useAppSelector((state) => state.dataSelection.maxDate); From 42a8b767ba4b6f347140ec96e9e356d32747f532 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Tue, 29 Oct 2024 14:06:42 +0100 Subject: [PATCH 31/37] :tada: :wrench: added localization for settings menu and horizontal threshold --- frontend/locales/de-settings.json5 | 29 ++++++ frontend/locales/en-settings.json5 | 29 ++++++ .../LineChartContainer.tsx | 2 + .../HorizontalThresholdItem.tsx | 25 +++++- .../HorizontalThresholdList.tsx | 16 +++- .../HorizontalThresholdSettings.tsx | 6 ++ .../LineChartSettings.tsx | 90 ++++++++++++------- frontend/src/util/i18n.ts | 2 + frontend/src/util/localization.ts | 46 ++++++++++ 9 files changed, 205 insertions(+), 40 deletions(-) create mode 100644 frontend/locales/de-settings.json5 create mode 100644 frontend/locales/en-settings.json5 create mode 100644 frontend/src/util/localization.ts diff --git a/frontend/locales/de-settings.json5 b/frontend/locales/de-settings.json5 new file mode 100644 index 00000000..9e3f545e --- /dev/null +++ b/frontend/locales/de-settings.json5 @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: CC0-1.0 + +{ + title: 'Liniendiagramm Einstellungen', + manageGroups: 'Filter', + manageThreshold: 'Threshold', + horizontalThresholds: { + title: 'Schwellen Verwalten', + district: 'Bezirk', + compartment: 'Infektionsstatus', + threshold: 'Schwelle', + noThresholds: 'Keine Schwellenwerte festgelegt.', + }, + 'group-filters': { + title: 'Gruppen Verwalten', + 'nothing-selected': 'Wählen Sie eine Gruppe aus um diese zu bearbeiten oder erstellen Sie eine neue Gruppe.', + 'add-group': 'Neue Gruppe Erstellen', + name: 'Name', + close: 'Abbrechen', + apply: 'Anwenden', + 'confirm-deletion-title': 'Gruppe Löschen', + 'confirm-deletion-text': 'Sind Sie sicher, dass Sie die Gruppe "{{groupName}}" wirklich löschen wollen?', + 'confirm-discard-title': 'Änderungen Verwerfen', + 'confirm-discard-text': 'Sie haben ungespeicherte Änderungen. Wollen sie diese verwerfen?', + discard: 'Verwerfen', + delete: 'Löschen', + }, +} diff --git a/frontend/locales/en-settings.json5 b/frontend/locales/en-settings.json5 new file mode 100644 index 00000000..c24a04d2 --- /dev/null +++ b/frontend/locales/en-settings.json5 @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: CC0-1.0 + +{ + title: 'Line Chart Settings', + manageGroups: 'Filters', + manageThreshold: 'Thresholds', + horizontalThresholds: { + title: 'Manage Threshold', + district: 'District', + compartment: 'Compartment', + threshold: 'Threshold', + noThresholds: 'No thresholds set.', + }, + 'group-filters': { + title: 'Manage Groups', + 'nothing-selected': 'Select a group to edit or create a new one.', + 'add-group': 'Add new group', + name: 'Name', + close: 'Cancel', + apply: 'Apply', + 'confirm-deletion-title': 'Delete Group', + 'confirm-deletion-text': 'Are you sure you want to delete the "{{groupName}}" group?', + 'confirm-discard-title': 'Discard Changes', + 'confirm-discard-text': 'You have unsaved changes. Do you want to discard them?', + discard: 'Discard', + delete: 'Delete', + }, +} diff --git a/frontend/src/components/LineChartComponents/LineChartContainer.tsx b/frontend/src/components/LineChartComponents/LineChartContainer.tsx index 2381ef11..bda75cbb 100644 --- a/frontend/src/components/LineChartComponents/LineChartContainer.tsx +++ b/frontend/src/components/LineChartComponents/LineChartContainer.tsx @@ -14,6 +14,7 @@ import {LineChartSettings} from './LineChartSettingsComponents/LineChartSettings import {Dictionary} from 'util/util'; import {HorizontalThreshold} from 'types/horizontalThreshold'; import {setHorizontalYAxisThresholds} from 'store/UserPreferenceSlice'; +import {useCompartmentLocalization} from 'util/localization'; export default function LineChartContainer() { const {t} = useTranslation('backend'); @@ -86,6 +87,7 @@ export default function LineChartContainer() { selectedCompartment={selectedCompartment ?? ''} horizontalThresholds={horizontalThresholds} setHorizontalThresholds={sethorizontalThresholds} + localization={useCompartmentLocalization()} /> ); diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx index ec007b40..ba5397ff 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdItem.tsx @@ -9,6 +9,8 @@ import {useTheme} from '@mui/material/styles'; import {HorizontalThreshold} from 'types/horizontalThreshold'; import ThresholdInput from './ThresholdInput'; import type {District} from 'types/district'; +import type {Localization} from 'types/localization'; +import {useTranslation} from 'react-i18next'; export interface HorizontalThresholdItemProps { /** The threshold item to display */ @@ -43,6 +45,9 @@ export interface HorizontalThresholdItemProps { /** testId for testing */ testId?: string; + + /** An object containing localization information (translation & number formattation). */ + localization?: Localization; } export default function HorizontalThresholdItem({ @@ -57,9 +62,25 @@ export default function HorizontalThresholdItem({ isEditingThreshold, isAddingThreshold, testId, + localization = {formatNumber: (value: number) => value.toString(), customLang: 'global', overrides: {}}, }: HorizontalThresholdItemProps) { - const [localThreshold, setLocalThreshold] = useState(threshold.threshold); const theme = useTheme(); + const {t: defaultT} = useTranslation(); + const {t: customT} = useTranslation(localization.customLang); + + // Get the translated compartment name + const getTranslatedCompartmentName = (compartment: string): string => { + const overrideKey = `compartments.${compartment}`; + // Check if the translation exists in the overrides + if (localization.overrides?.[overrideKey]) { + return customT(localization.overrides[overrideKey]); // Translate using the custom namespace + } else { + return defaultT(compartment); // Fallback to the default compartment name if no translation is found + } + }; + + const [localThreshold, setLocalThreshold] = useState(threshold.threshold); + const isValid = localThreshold !== null && localThreshold > 0; const updateThreshold = () => { @@ -112,7 +133,7 @@ export default function HorizontalThresholdItem({ color: isDisabled ? theme.palette.text.disabled : theme.palette.text.primary, }} > - {threshold.compartment} + {getTranslatedCompartmentName(threshold.compartment)}
diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx index 262e11f2..0001101c 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdList.tsx @@ -24,8 +24,10 @@ import {tableCellClasses} from '@mui/material/TableCell'; import {Dictionary} from 'util/util'; import {HorizontalThreshold} from 'types/horizontalThreshold'; import type {District} from 'types/district'; +import type {Localization} from 'types/localization'; import HorizontalThresholdItem from './HorizontalThresholdItem'; import ThresholdInput from './ThresholdInput'; +import {useTranslation} from 'react-i18next'; export interface HorizontalThresholdListProps { /** The list of horizontal thresholds to display */ @@ -39,6 +41,9 @@ export interface HorizontalThresholdListProps { /** The selected compartment */ selectedCompartment: string; + + /** An object containing localization information (translation & number formattation). */ + localization?: Localization; } const StyledTableCell = styled(TableCell)(({theme}) => ({ @@ -54,7 +59,9 @@ export default function HorizontalThresholdList({ setHorizontalThresholds, selectedDistrict, selectedCompartment, + localization, }: HorizontalThresholdListProps) { + const {t: tSettings} = useTranslation('settings'); const theme = useTheme(); const dispatch = useAppDispatch(); @@ -141,7 +148,7 @@ export default function HorizontalThresholdList({ - District + {tSettings('horizontalThresholds.district')} - Compartment + {tSettings('horizontalThresholds.compartment')} - Threshold + {tSettings('horizontalThresholds.threshold')} @@ -165,7 +172,7 @@ export default function HorizontalThresholdList({ - No thresholds set + {tSettings('horizontalThresholds.noThresholds')} @@ -186,6 +193,7 @@ export default function HorizontalThresholdList({ isEditingThreshold={editingThresholdKey !== null} isAddingThreshold={isAddingThreshold} testId={`threshold-item-${key}`} + localization={localization} /> ); })} diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx index f85add0a..479d4b2f 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/HorizontalThresholdSettings/HorizontalThresholdSettings.tsx @@ -4,6 +4,7 @@ import React from 'react'; import {Dictionary} from 'util/util'; import type {District} from 'types/district'; +import type {Localization} from 'types/localization'; import type {HorizontalThreshold} from 'types/horizontalThreshold'; import HorizontalThresholdList from './HorizontalThresholdList'; @@ -19,6 +20,9 @@ export interface HorizontalThresholdSettingsProps { /** A function that sets the horizontal thresholds for the y-axis. */ setHorizontalThresholds: React.Dispatch>>; + + /** An object containing localization information (translation & number formattation). */ + localization?: Localization; } export default function HorizontalThresholdSettings({ @@ -26,6 +30,7 @@ export default function HorizontalThresholdSettings({ selectedCompartment, horizontalThresholds, setHorizontalThresholds, + localization, }: HorizontalThresholdSettingsProps) { return ( ); } diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx index 530f2fbf..3c4fe2ca 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx @@ -12,7 +12,8 @@ import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule'; import {Dictionary} from 'util/util'; import type {HorizontalThreshold} from 'types/horizontalThreshold'; import type {District} from 'types/district'; - +import type {Localization} from 'types/localization'; +import {useTranslation} from 'react-i18next'; import HorizontalThresholdSettings from './HorizontalThresholdSettings/HorizontalThresholdSettings'; /** @@ -21,17 +22,13 @@ import HorizontalThresholdSettings from './HorizontalThresholdSettings/Horizonta */ type SettingsView = 'settingsMenu' | 'horizontalThresholdSettings' | 'filters'; -/** - * The settings menu for the line chart. Each item in the menu has a label, a view, and an icon. - */ -const settingsMenu = { - horizontalThreshold: { - label: 'Horizontal Threshold Settings', - view: 'horizontalThresholdSettings', - icon: , - }, +type SettingsMenu = { + [key: string]: { + label: string; + view: string; + icon: JSX.Element; + }; }; - export interface LineChartSettingsProps { /** The district to which the settings apply. */ selectedDistrict: District; @@ -44,6 +41,9 @@ export interface LineChartSettingsProps { /** A function that sets the horizontal thresholds for the y-axis. */ setHorizontalThresholds: React.Dispatch>>; + + /** An object containing localization information (translation & number formattation). */ + localization?: Localization; } /** @@ -56,7 +56,27 @@ export function LineChartSettings({ selectedCompartment, horizontalThresholds, setHorizontalThresholds, + localization, }: LineChartSettingsProps) { + const {t: tSettings} = useTranslation('settings'); + + /** + * The settings menu for the line chart. Each item in the menu has a label, a view, and an icon. + */ + + const settingsMenu: SettingsMenu = { + horizontalThreshold: { + label: tSettings('manageThreshold'), + view: 'horizontalThresholdSettings', + icon: , + }, + // filters: { + // label: tSettings('manageGroups'), + // view: 'filters', + // icon: , + // }, + }; + const [currentView, setCurrentView] = useState('settingsMenu'); const [anchorEl, setAnchorEl] = useState(null); const [showPopover, setShowPopover] = useState(false); @@ -123,40 +143,42 @@ export function LineChartSettings({ > {currentView === 'settingsMenu' && ( - {renderHeader('Line Chart Settings')} - - - - - + + ))}
)} {currentView === 'horizontalThresholdSettings' && ( - {renderHeader('Horizontal Threshold Settings')} + {renderHeader(tSettings('horizontalThresholds.title'))} )} diff --git a/frontend/src/util/i18n.ts b/frontend/src/util/i18n.ts index eb36b771..bfe2294a 100644 --- a/frontend/src/util/i18n.ts +++ b/frontend/src/util/i18n.ts @@ -17,11 +17,13 @@ const translationFiles = [ new URL('../../locales/en-global.json5', import.meta.url).href, new URL('../../locales/en-translation.json5', import.meta.url).href, new URL('../../locales/en-onboarding.json5', import.meta.url).href, + new URL('../../locales/en-settings.json5', import.meta.url).href, new URL('../../locales/de-backend.json5', import.meta.url).href, new URL('../../locales/de-legal.json5', import.meta.url).href, new URL('../../locales/de-global.json5', import.meta.url).href, new URL('../../locales/de-translation.json5', import.meta.url).href, new URL('../../locales/de-onboarding.json5', import.meta.url).href, + new URL('../../locales/de-settings.json5', import.meta.url).href, ] as Array; void i18n diff --git a/frontend/src/util/localization.ts b/frontend/src/util/localization.ts new file mode 100644 index 00000000..a8c683e5 --- /dev/null +++ b/frontend/src/util/localization.ts @@ -0,0 +1,46 @@ +import {useMemo} from 'react'; +import {NumberFormatter} from 'util/hooks'; +import {useTranslation} from 'react-i18next'; + +export const useCompartmentLocalization = () => { + const {i18n} = useTranslation(); + const {formatNumber} = NumberFormatter(i18n.language, 1, 0); + return useMemo( + () => ({ + formatNumber: formatNumber, + customLang: 'backend', + overrides: { + ['compartments.Infected']: 'infection-states.Infected', + ['compartments.MildInfections']: 'infection-states.MildInfections', + ['compartments.Hospitalized']: 'infection-states.Hospitalized', + ['compartments.ICU']: 'infection-states.ICU', + ['compartments.Dead']: 'infection-states.Dead', + ['compartments.DeadV1']: 'infection-states.DeadV1', + ['compartments.DeadV2']: 'infection-states.DeadV2', + ['compartments.Exposed']: 'infection-states.Exposed', + ['compartments.Recovered']: 'infection-states.Recovered', + ['compartments.Carrier']: 'infection-states.Carrier', + ['compartments.Susceptible']: 'infection-states.Susceptible', + ['compartments.InfectedT']: 'infection-states.InfectedT', + ['compartments.InfectedTV1']: 'infection-states.InfectedTV1', + ['compartments.InfectedTV2']: 'infection-states.InfectedTV2', + ['compartments.InfectedV1']: 'infection-states.InfectedV1', + ['compartments.InfectedV2']: 'infection-states.InfectedV2', + ['compartments.HospitalizedV1']: 'infection-states.HospitalizedV1', + ['compartments.HospitalizedV2']: 'infection-states.HospitalizedV2', + ['compartments.ICUV1']: 'infection-states.ICUV1', + ['compartments.ICUV2']: 'infection-states.ICUV2', + ['compartments.ExposedV1']: 'infection-states.ExposedV1', + ['compartments.ExposedV2']: 'infection-states.ExposedV2', + ['compartments.CarrierT']: 'infection-states.CarrierT', + ['compartments.CarrierTV1']: 'infection-states.CarrierTV1', + ['compartments.CarrierTV2']: 'infection-states.CarrierTV2', + ['compartments.CarrierV1']: 'infection-states.CarrierV1', + ['compartments.CarrierV2']: 'infection-states.CarrierV2', + ['compartments.SusceptibleV1']: 'infection-states.SusceptibleV1', + ['compartments.SusceptibleV2']: 'infection-states.SusceptibleV2', + }, + }), + [formatNumber] + ); +}; From e3b5c6061c628f00cddcdb3a29335232e6456495 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Wed, 30 Oct 2024 15:25:57 +0100 Subject: [PATCH 32/37] :green_heart: fix license --- frontend/src/util/localization.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/util/localization.ts b/frontend/src/util/localization.ts index a8c683e5..a75f1124 100644 --- a/frontend/src/util/localization.ts +++ b/frontend/src/util/localization.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) +// SPDX-License-Identifier: Apache-2.0 + import {useMemo} from 'react'; import {NumberFormatter} from 'util/hooks'; import {useTranslation} from 'react-i18next'; From 92c18e39e2d510d4f6e8f01ede9dddebb1cc26f3 Mon Sep 17 00:00:00 2001 From: kunkoala Date: Thu, 21 Nov 2024 11:44:14 +0100 Subject: [PATCH 33/37] :green_heart: :heavy_check_mark: fix lineChartSettings test --- .../LineChartSettings.test.tsx | 34 +++++++------------ .../LineChartSettings.tsx | 13 +++++-- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/frontend/src/__tests__/components/LineChartSettings/LineChartSettings.test.tsx b/frontend/src/__tests__/components/LineChartSettings/LineChartSettings.test.tsx index 34424923..fdba33a7 100644 --- a/frontend/src/__tests__/components/LineChartSettings/LineChartSettings.test.tsx +++ b/frontend/src/__tests__/components/LineChartSettings/LineChartSettings.test.tsx @@ -57,7 +57,7 @@ describe('LineChartSettings', () => { const settingsButton = screen.getByTestId('settings-popover-button-testid'); await userEvent.click(settingsButton); - expect(screen.getByText('Horizontal Threshold Settings')).toBeInTheDocument(); + expect(screen.getByTestId('settings-menu-item-horizontalThreshold')).toBeInTheDocument(); }); test('Navigates to all menu item in popover', async () => { @@ -66,35 +66,25 @@ describe('LineChartSettings', () => { await userEvent.click(settingsButton); // add more menus here - const menuItems = ['Horizontal Threshold Settings']; - - for (const menuItem of menuItems) { - await userEvent.click(screen.getByText(menuItem)); - expect(screen.getByText(menuItem)).toBeInTheDocument(); + const menuItemsTestId = [ + { + settingsMenu: 'settings-menu-item-horizontalThreshold', + menuContainer: 'horizontalThresholdSettings-setting-container', + }, + // add more menus here + ]; + for (const menuItem of menuItemsTestId) { + await userEvent.click(screen.getByTestId(menuItem.settingsMenu)); + expect(screen.getByTestId(menuItem.menuContainer)).toBeInTheDocument(); const backButton = screen.getByTestId('settings-back-button'); await userEvent.click(backButton); // Ensure we're back at the main settings menu - expect(screen.getByText('Line Chart Settings')).toBeInTheDocument(); + expect(screen.getByTestId('main-settings-menu')).toBeInTheDocument(); } }); - test('Displays the settings main menu when back button is clicked from any menu', async () => { - render(); - const settingsButton = screen.getByTestId('settings-popover-button-testid'); - await userEvent.click(settingsButton); - - const thresholdSettingsButton = screen.getByText('Horizontal Threshold Settings'); - await userEvent.click(thresholdSettingsButton); - - const backButton = screen.getByTestId('settings-back-button'); - await userEvent.click(backButton); - - const settingsHeader = screen.getByText('Line Chart Settings'); - expect(settingsHeader).toBeInTheDocument(); - }); - test('closes the popover when close button is clicked', async () => { render(); diff --git a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx index 3c4fe2ca..bb7b2dee 100644 --- a/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx +++ b/frontend/src/components/LineChartComponents/LineChartSettingsComponents/LineChartSettings.tsx @@ -100,7 +100,10 @@ export function LineChartSettings({ }; const renderHeader = (title: string) => ( - + handleBackButton()} disabled={currentView === 'settingsMenu'}> @@ -142,13 +145,17 @@ export function LineChartSettings({ }} > {currentView === 'settingsMenu' && ( - + {renderHeader(tSettings('title'))} {Object.entries(settingsMenu).map(([key, item]) => ( <> -