From 2de44575daa02a55f046afe381ef7b35c3eacc3c Mon Sep 17 00:00:00 2001 From: M <120020483+VWSCoronaDashboard30@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:00:47 +0100 Subject: [PATCH] feat(COR-1953): Added corrections for rioolwater NL and infectieradar pages (#5005) --- packages/app/src/components/chart-tile.tsx | 10 ++-- packages/app/src/components/metadata.tsx | 55 +++++++---------- .../time-series-chart/time-series-chart.tsx | 60 ++++++++++++++----- .../infection-radar-per-age-group.tsx | 5 +- .../domain/sewer/sewer-chart/sewer-chart.tsx | 41 +++++++++---- .../app/src/pages/landelijk/infectieradar.tsx | 17 ++++++ .../app/src/utils/current-date-context.tsx | 36 ++--------- 7 files changed, 128 insertions(+), 96 deletions(-) diff --git a/packages/app/src/components/chart-tile.tsx b/packages/app/src/components/chart-tile.tsx index 957f11c632..202c20df91 100644 --- a/packages/app/src/components/chart-tile.tsx +++ b/packages/app/src/components/chart-tile.tsx @@ -13,28 +13,28 @@ import { Heading } from './typography'; interface ChartTileProps { children: ReactNode; - title: string; description?: string; disableFullscreen?: boolean; id?: string; metadata?: MetadataProps; + onSelectTimeframe?: (timeframe: TimeframeOption) => void; timeframeInitialValue?: TimeframeOption; timeframeOptions?: TimeframeOption[]; + title: string; toggle?: ChartTileToggleProps; - onSelectTimeframe?: (timeframe: TimeframeOption) => void; } export const ChartTile = ({ children, - title, description, disableFullscreen, id, metadata, + onSelectTimeframe, timeframeInitialValue, - toggle, timeframeOptions, - onSelectTimeframe, + title, + toggle, }: ChartTileProps) => { const [timeframe, setTimeframe] = useState(timeframeInitialValue || TimeframeOption.ALL); diff --git a/packages/app/src/components/metadata.tsx b/packages/app/src/components/metadata.tsx index 2f959b9c25..02afd3ee52 100644 --- a/packages/app/src/components/metadata.tsx +++ b/packages/app/src/components/metadata.tsx @@ -1,16 +1,16 @@ -import { MarginBottomProps } from 'styled-system'; -import { ExternalLink } from '~/components/external-link'; -import { useIntl } from '~/intl'; -import { space } from '~/style/theme'; -import { replaceVariablesInText } from '~/utils/replace-variables-in-text'; import { Box } from './base'; +import { Calendar, Clock, Database, External as ExternalLinkIcon } from '@corona-dashboard/icons'; +import { colors } from '@corona-dashboard/common'; +import { ExternalLink } from '~/components/external-link'; import { InlineText, Text } from './typography'; +import { MarginBottomProps } from 'styled-system'; import { Markdown } from '~/components/markdown'; -import { Calendar, Clock, Database, External as ExternalLinkIcon } from '@corona-dashboard/icons'; +import { replaceVariablesInText } from '~/utils/replace-variables-in-text'; +import { space } from '~/style/theme'; +import { useIntl } from '~/intl'; +import css from '@styled-system/css'; import React from 'react'; import styled from 'styled-components'; -import { colors } from '@corona-dashboard/common'; -import css from '@styled-system/css'; type source = { text: string; @@ -40,18 +40,18 @@ export interface MetadataProps extends MarginBottomProps { } export function Metadata({ + dataSources, date, - source, - obtainedAt, - isTileFooter, + dateOfInsertion, + datePeriod, datumsText, - marginBottom, - dataSources, - intervalCount, disclaimer, - datePeriod, - dateOfInsertion, + intervalCount, isArchivedGraph = false, + isTileFooter, + marginBottom, + obtainedAt, + source, }: MetadataProps) { const { commonTexts, formatDateFromSeconds } = useIntl(); @@ -117,26 +117,17 @@ export function Metadata({ )} - {dateOfInsertion && !isArchivedGraph && ( - + {dateOfInsertion && ( + - {replaceVariablesInText(commonTexts.common.metadata.last_insertion_date, { dateOfInsertion: formatDateFromSeconds(dateOfInsertion, 'weekday-long') })} - - - )} - - {dateOfInsertion && isArchivedGraph && ( - - - - - - {replaceVariablesInText(commonTexts.common.metadata.last_insertion_date_archived, { - dateOfInsertion: formatDateFromSeconds(dateOfInsertion, 'weekday-long'), - })} + {isArchivedGraph + ? replaceVariablesInText(commonTexts.common.metadata.last_insertion_date_archived, { + dateOfInsertion: formatDateFromSeconds(dateOfInsertion, 'weekday-long'), + }) + : replaceVariablesInText(commonTexts.common.metadata.last_insertion_date, { dateOfInsertion: formatDateFromSeconds(dateOfInsertion, 'weekday-long') })} )} diff --git a/packages/app/src/components/time-series-chart/time-series-chart.tsx b/packages/app/src/components/time-series-chart/time-series-chart.tsx index f362a52f20..7572f2b4a2 100644 --- a/packages/app/src/components/time-series-chart/time-series-chart.tsx +++ b/packages/app/src/components/time-series-chart/time-series-chart.tsx @@ -1,4 +1,4 @@ -import { TimeframeOption, TimestampedValue } from '@corona-dashboard/common'; +import { TimeframeOption, TimestampedValue, isDateSeries, isDateSpanSeries } from '@corona-dashboard/common'; import css from '@styled-system/css'; import { TickFormatter } from '@visx/axis'; import { useTooltip } from '@visx/tooltip'; @@ -53,6 +53,7 @@ import { useValuesInTimeframe, useValueWidth, } from './logic'; +import { DateRange } from '../metadata'; export type { SeriesConfig } from './logic'; /** @@ -129,6 +130,19 @@ export type TimeSeriesChartProps void; + /** + * These handlers are responsible for managing the parent to child to parent relation. + * The dashboard is not currently adapted to having the CurrentDateContext available + * at page level. In order to connect the state values to the context, we either extract + * all the timeseries instances into separate components or pass the handlers as functions + * from the parent component. onHandleTimeIntervalChange is responsible for handling the + * metadata interval for the X axis. onHandleDateOfInsertion is responsible for handling + * the last insertion date metadata for the interval. + * @param value: DateRange | number + * @returns + */ + onHandleTimeIntervalChange?: (value: DateRange) => void; + /** * By default markers for all series are displayed on hover, also the tooltip * will display all series of the selected X-value. The `markNearestPointOnly` @@ -151,27 +165,28 @@ export type TimeSeriesChartProps>({ accessibility, - values: allValues, + dataOptions, + disableLegend, + displayTooltipValueOnly, endDate, - seriesConfig, + forceLegend = false, + formatTickValue: formatYTickValue, + formatTooltip, initialWidth = 840, + isYAxisCollapsed: defaultIsYAxisCollapsed, + markNearestPointOnly, minHeight = 250, - timeframe = TimeframeOption.ALL, - formatTooltip, - dataOptions, - showWeekNumbers, numGridLines = 4, - tickValues: yTickValues, - xTickNumber, - formatTickValue: formatYTickValue, + onHandleTimeIntervalChange, + onSeriesClick, paddingLeft, + seriesConfig, + showWeekNumbers, + tickValues: yTickValues, + timeframe = TimeframeOption.ALL, tooltipTitle, - disableLegend, - forceLegend = false, - onSeriesClick, - markNearestPointOnly, - displayTooltipValueOnly, - isYAxisCollapsed: defaultIsYAxisCollapsed, + values: allValues, + xTickNumber, }: TimeSeriesChartProps) { const { commonTexts } = useIntl(); @@ -193,6 +208,19 @@ export function TimeSeriesChart { + if (onHandleTimeIntervalChange) { + if (isDateSpanSeries(values)) { + onHandleTimeIntervalChange({ + start: values[0] ? values[0].date_start_unix : 0, + end: values[values.length - 1] ? values[values.length - 1].date_end_unix : 0, + }); + } else if (isDateSeries(values)) { + onHandleTimeIntervalChange({ start: values[0] ? values[0].date_unix : 0, end: values[values.length - 1] ? values[values.length - 1].date_unix : 0 }); + } + } + }, [values, timeframe, onHandleTimeIntervalChange]); + const cutValuesConfig = useMemo(() => extractCutValuesConfig(timespanAnnotations), [timespanAnnotations]); const seriesList = useSeriesList(values, seriesConfig, cutValuesConfig, dataOptions); diff --git a/packages/app/src/domain/infection_radar/infection-radar-per-age-group.tsx b/packages/app/src/domain/infection_radar/infection-radar-per-age-group.tsx index 7e26b66163..284e41a0c5 100644 --- a/packages/app/src/domain/infection_radar/infection-radar-per-age-group.tsx +++ b/packages/app/src/domain/infection_radar/infection-radar-per-age-group.tsx @@ -14,6 +14,7 @@ import { AccessibilityDefinition } from '~/utils/use-accessibility-annotations'; import { useBreakpoints } from '~/utils/use-breakpoints'; import { useList } from '~/utils/use-list'; import { BASE_SERIES_CONFIG } from './series-config'; +import { DateRange } from '~/components/metadata'; interface InfectionRadarSymptomsPerAgeGroup { /** @@ -25,9 +26,10 @@ interface InfectionRadarSymptomsPerAgeGroup { timeframe: TimeframeOption; timelineEvents?: TimelineEventConfig[]; text: SiteText['pages']['infectie_radar_page']['nl']; + onHandleTimeIntervalChange?: (value: DateRange) => void; } -export function InfectionRadarSymptomsPerAgeGroup({ values, timeframe, accessibility, timelineEvents, text }: InfectionRadarSymptomsPerAgeGroup) { +export function InfectionRadarSymptomsPerAgeGroup({ values, timeframe, accessibility, timelineEvents, text, onHandleTimeIntervalChange }: InfectionRadarSymptomsPerAgeGroup) { const { commonTexts } = useIntl(); const { list, toggle, clear } = useList(); const breakpoints = useBreakpoints(true); @@ -89,6 +91,7 @@ export function InfectionRadarSymptomsPerAgeGroup({ values, timeframe, accessibi valueAnnotation: text.infection_radar_infected_per_age_group.value_annotation, timelineEvents, }} + onHandleTimeIntervalChange={onHandleTimeIntervalChange} /> ); diff --git a/packages/app/src/domain/sewer/sewer-chart/sewer-chart.tsx b/packages/app/src/domain/sewer/sewer-chart/sewer-chart.tsx index e6acb05976..f2c0d8568f 100644 --- a/packages/app/src/domain/sewer/sewer-chart/sewer-chart.tsx +++ b/packages/app/src/domain/sewer/sewer-chart/sewer-chart.tsx @@ -2,21 +2,23 @@ import { AccessibilityDefinition } from '~/utils/use-accessibility-annotations'; import { Box } from '~/components/base'; import { ChartTile } from '~/components/chart-tile'; import { ChartTimeControls } from '~/components/chart-time-controls'; -import { colors, NlSewer, SewerPerInstallationData, TimeframeOption, TimeframeOptionsList } from '@corona-dashboard/common'; +import { colors, isDateSpanSeries, NlSewer, SewerPerInstallationData, TimeframeOption, TimeframeOptionsList } from '@corona-dashboard/common'; +import { DateRange } from '~/components/metadata'; import { isPresent } from 'ts-is-present'; import { mediaQueries, space } from '~/style/theme'; import { mergeData, useSewerStationSelectPropsSimplified } from './logic'; import { RichContentSelect } from '~/components/rich-content-select'; -import styled from 'styled-components'; import { Text } from '~/components/typography'; import { TimelineEventConfig } from '~/components/time-series-chart/components/timeline'; import { TimeSeriesChart } from '~/components/time-series-chart'; -import { useRouter } from 'next/router'; import { useEffect, useMemo, useState } from 'react'; -import { useScopedWarning } from '~/utils/use-scoped-warning'; import { useIntl } from '~/intl'; +import { useRouter } from 'next/router'; +import { useScopedWarning } from '~/utils/use-scoped-warning'; +import { useValuesInTimeframe } from '~/components/time-series-chart/logic'; import { Warning } from '@corona-dashboard/icons'; import { WarningTile } from '~/components/warning-tile'; +import styled from 'styled-components'; interface SewerChartProps { /** @@ -82,8 +84,19 @@ export const SewerChart = ({ accessibility, dataAverages, dataPerInstallation, t } as SewerPerInstallationData), text.rwziSelectDropdown?.select_none_label || '' ); + const { commonTexts } = useIntl(); + + const [timeframe, setTimeframe] = useState(TimeframeOption.ALL); + const [sewerTimeframe, setSewerTimeframe] = useState(TimeframeOption.ALL); const router = useRouter(); + const values = useValuesInTimeframe(dataAverages.values, timeframe); + + const [metadataTimeInterval, setMetadataTimeInterval] = useState({ start: 0, end: 0 }); + const metadataLastInsertion = values[values.length - 1] ? values[values.length - 1].date_of_insertion_unix : 1; // Weird behavior if set to 0 + + const scopedGmName = commonTexts.gemeente_index.municipality_warning; + const scopedWarning = useScopedWarning(vrNameOrGmName || '', warning || ''); useEffect(() => { const routeChangeHandler = () => onChange(undefined); @@ -91,14 +104,16 @@ export const SewerChart = ({ accessibility, dataAverages, dataPerInstallation, t return () => router.events.off('routeChangeStart', routeChangeHandler); }, [onChange, router.events]); - const [sewerTimeframe, setSewerTimeframe] = useState(TimeframeOption.ALL); - - const { commonTexts } = useIntl(); - const scopedGmName = commonTexts.gemeente_index.municipality_warning; - - const scopedWarning = useScopedWarning(vrNameOrGmName || '', warning || ''); - - const [timeframe, setTimeframe] = useState(TimeframeOption.ALL); + useEffect(() => { + if (isDateSpanSeries(values)) { + setMetadataTimeInterval({ + start: values[0] ? values[0].date_start_unix : 0, + end: values[values.length - 1] ? values[values.length - 1].date_end_unix : 0, + }); + } else { + setMetadataTimeInterval({ start: values[0] ? values[0].date_unix : 0, end: values[values.length - 1] ? values[values.length - 1].date_unix : 0 }); + } + }, [timeframe, values, setMetadataTimeInterval]); useEffect(() => { setSewerTimeframe(timeframe); @@ -142,6 +157,8 @@ export const SewerChart = ({ accessibility, dataAverages, dataPerInstallation, t title={text.title} metadata={{ source: text.source, + datePeriod: metadataTimeInterval, + dateOfInsertion: metadataLastInsertion, }} description={text.description} > diff --git a/packages/app/src/pages/landelijk/infectieradar.tsx b/packages/app/src/pages/landelijk/infectieradar.tsx index 2d637b4d80..8f3730ead6 100644 --- a/packages/app/src/pages/landelijk/infectieradar.tsx +++ b/packages/app/src/pages/landelijk/infectieradar.tsx @@ -24,6 +24,7 @@ import { TimeSeriesChart } from '~/components/time-series-chart'; import { useDynamicLokalizeTexts } from '~/utils/cms/use-dynamic-lokalize-texts'; import { useIntl } from '~/intl'; import { useState } from 'react'; +import { DateRange } from '~/components/metadata'; const pageMetrics = ['self_test_overall', 'infection_radar_symptoms_per_age_group']; @@ -67,8 +68,10 @@ const InfectionRadar = (props: StaticProps) => { const reverseRouter = useReverseRouter(); const [confirmedCasesSelfTestedTimeframe, setConfirmedCasesSelfTestedTimeframe] = useState(TimeframeOption.SIX_MONTHS); + const [confirmedCasesSelfTestedTimeInterval, setConfirmedCasesSelfTestedTimeInterval] = useState({ start: 0, end: 0 }); const [confirmedCasesCovidSymptomsPerAgeTimeFrame, setConfirmedCasesCovidSymptomsPerAgeTimeFrame] = useState(TimeframeOption.THREE_MONTHS); + const [confirmedCasesCovidSymptomsPerAgeTimeInterval, setConfirmedCasesCovidSymptomsPerAgeTimeInterval] = useState({ start: 0, end: 0 }); const { commonTexts } = useIntl(); @@ -82,6 +85,14 @@ const InfectionRadar = (props: StaticProps) => { description: textNl.metadata.description, }; + const handleSetConfirmedCasesSelfTestedTimeInterval = (value: DateRange) => { + setConfirmedCasesSelfTestedTimeInterval(value); + }; + + const handleSetConfirmedCasesCovidSymptomsPerAgeTimeInterval = (value: DateRange) => { + setConfirmedCasesCovidSymptomsPerAgeTimeInterval(value); + }; + const lastInsertionDateOfPage = getLastInsertionDateOfPage(data, pageMetrics); return ( @@ -140,6 +151,8 @@ const InfectionRadar = (props: StaticProps) => { description={textNl.chart_self_tests.description} metadata={{ source: textNl.sources.self_test, + datePeriod: confirmedCasesSelfTestedTimeInterval, + dateOfInsertion: data.self_test_overall.last_value.date_of_insertion_unix, }} timeframeOptions={TimeframeOptionsList} timeframeInitialValue={confirmedCasesSelfTestedTimeframe} @@ -164,6 +177,7 @@ const InfectionRadar = (props: StaticProps) => { timelineEvents: getTimelineEvents(content.elements.timeSeries, 'self_test_overall'), }} forceLegend + onHandleTimeIntervalChange={handleSetConfirmedCasesSelfTestedTimeInterval} /> @@ -174,6 +188,8 @@ const InfectionRadar = (props: StaticProps) => { timeframeInitialValue={confirmedCasesCovidSymptomsPerAgeTimeFrame} metadata={{ source: textNl.chart_infection_radar_age_groups.source.rivm, + datePeriod: confirmedCasesCovidSymptomsPerAgeTimeInterval, + dateOfInsertion: data.infectionradar_symptoms_trend_per_age_group_weekly.last_value.date_of_insertion_unix, }} onSelectTimeframe={setConfirmedCasesCovidSymptomsPerAgeTimeFrame} > @@ -185,6 +201,7 @@ const InfectionRadar = (props: StaticProps) => { timeframe={confirmedCasesCovidSymptomsPerAgeTimeFrame} timelineEvents={getTimelineEvents(content.elements.timeSeries, 'infectionradar_symptoms_trend_per_age_group_weekly')} text={textNl} + onHandleTimeIntervalChange={handleSetConfirmedCasesCovidSymptomsPerAgeTimeInterval} /> diff --git a/packages/app/src/utils/current-date-context.tsx b/packages/app/src/utils/current-date-context.tsx index 2842ee8c85..806105c3ea 100644 --- a/packages/app/src/utils/current-date-context.tsx +++ b/packages/app/src/utils/current-date-context.tsx @@ -1,11 +1,5 @@ import { assert, endOfDayInSeconds } from '@corona-dashboard/common'; -import { - createContext, - ReactNode, - useContext, - useEffect, - useState, -} from 'react'; +import { createContext, ReactNode, useContext, useEffect, useState } from 'react'; /** * Dates shouldn't be created during render because the server-side date can be @@ -24,26 +18,11 @@ import { */ const CurrentDateContext = createContext(undefined); -export function CurrentDateProvider({ - dateInSeconds, - children, -}: { - dateInSeconds: number; - children: ReactNode; -}) { - const [date, setDate] = useState( - new Date(endOfDayInSeconds(dateInSeconds) * 1000) - ); - useEffect( - () => setDate(new Date(endOfDayInSeconds(Date.now() / 1000) * 1000)), - [] - ); +export function CurrentDateProvider({ dateInSeconds, children }: { dateInSeconds: number; children: ReactNode }) { + const [date, setDate] = useState(new Date(endOfDayInSeconds(dateInSeconds) * 1000)); + useEffect(() => setDate(new Date(endOfDayInSeconds(Date.now() / 1000) * 1000)), []); - return ( - - {children} - - ); + return {children}; } /** @@ -51,10 +30,7 @@ export function CurrentDateProvider({ */ export function useCurrentDate() { const currentDate = useContext(CurrentDateContext); - assert( - currentDate, - `[${useCurrentDate.name}] Missing CurrentDateProvider in component tree` - ); + assert(currentDate, `[${useCurrentDate.name}] Missing CurrentDateProvider in component tree`); return currentDate; }