diff --git a/packages/app/package.json b/packages/app/package.json index db716dbbe3..4a1a0a4b9f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -122,7 +122,7 @@ "next-transpile-modules": "^9.0.0", "node-mocks-http": "^1.11.0", "npm-run-all": "^4.1.5", - "postcss": "^8.4.4", + "postcss": "^8.4.31", "postcss-flexbugs-fixes": "^5.0.2", "postcss-preset-env": "^6.7.0", "react-test-renderer": "^17.0.2", @@ -139,7 +139,7 @@ "bootstrap": "exit 0", "export": "next export", "dev": "yarn workspace @corona-dashboard/icons build && yarn workspace @corona-dashboard/common build && run-p dev:common dev:next dev:lokalize", - "dev:next": "node next-server.js", + "dev:next": "cross-env NODE_OPTIONS='--inspect' node next-server.js", "dev:lokalize": "chokidar \"./src/locale/nl_export.json\" -c \"yarn workspace @corona-dashboard/cms lokalize:generate-types\"", "dev:common": "yarn workspace @corona-dashboard/common build:watch", "build": "cross-env NEXT_TELEMETRY_DISABLED=1 && next build", diff --git a/packages/app/schema/archived_nl/__index.json b/packages/app/schema/archived_nl/__index.json index 7322cb6794..03e8b825f0 100644 --- a/packages/app/schema/archived_nl/__index.json +++ b/packages/app/schema/archived_nl/__index.json @@ -42,6 +42,7 @@ "vaccine_vaccinated_or_support_archived_20230411", "vaccine_delivery_per_supplier_archived_20211101", "vaccine_stock_archived_20211024", + "variants_archived_20231101", "tested_ggd_archived_20230321", "tested_overall_archived_20230331", "tested_per_age_group_archived_20230331", @@ -186,6 +187,9 @@ "vaccine_stock_archived_20211024": { "$ref": "vaccine_stock.json" }, + "variants_archived_20231101": { + "$ref": "variants.json" + }, "repeating_shot_administered_20220713": { "$ref": "repeating_shot_administered.json" }, diff --git a/packages/app/schema/archived_nl/variants.json b/packages/app/schema/archived_nl/variants.json new file mode 100644 index 0000000000..84369cd74a --- /dev/null +++ b/packages/app/schema/archived_nl/variants.json @@ -0,0 +1,86 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "archived_nl_variants", + "required": ["values"], + "additionalProperties": false, + "properties": { + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/variant" + } + } + }, + "definitions": { + "variant": { + "type": "object", + "title": "archived_nl_variants_variant", + "additionalProperties": false, + "required": ["variant_code", "values", "last_value"], + "properties": { + "variant_code": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/value" + } + }, + "last_value": { + "$ref": "#/definitions/value" + } + } + }, + "value": { + "type": "object", + "title": "archived_nl_variants_variant_value", + "additionalProperties": false, + "required": [ + "order", + "occurrence", + "percentage", + "sample_size", + "date_start_unix", + "date_end_unix", + "date_of_report_unix", + "date_of_insertion_unix", + "label_nl", + "label_en" + ], + "properties": { + "order": { + "type": "integer" + }, + "occurrence": { + "type": "integer" + }, + "percentage": { + "type": "number" + }, + "sample_size": { + "type": "integer" + }, + "date_start_unix": { + "type": "integer" + }, + "date_end_unix": { + "type": "integer" + }, + "date_of_insertion_unix": { + "type": "integer" + }, + "date_of_report_unix": { + "type": "integer" + }, + "label_nl": { + "type": "string" + }, + "label_en": { + "type": "string" + } + } + } + } +} diff --git a/packages/app/schema/nl/__difference.json b/packages/app/schema/nl/__difference.json index 0f9a5ff65e..a96261ba71 100644 --- a/packages/app/schema/nl/__difference.json +++ b/packages/app/schema/nl/__difference.json @@ -26,6 +26,9 @@ }, "vulnerable_hospital_admissions": { "$ref": "#/definitions/diff_integer" + }, + "self_test_overall": { + "$ref": "#/definitions/diff_decimal" } }, "required": [ @@ -34,7 +37,8 @@ "infectious_people__estimate", "intensive_care_nice__admissions_on_date_of_reporting_moving_average", "intensive_care_lcps__beds_occupied_covid", - "sewer__average" + "sewer__average", + "self_test_overall" ], "additionalProperties": false, "definitions": { diff --git a/packages/app/schema/nl/__index.json b/packages/app/schema/nl/__index.json index a8ed83c1e1..86c6e693f1 100644 --- a/packages/app/schema/nl/__index.json +++ b/packages/app/schema/nl/__index.json @@ -20,7 +20,8 @@ "infectionradar_symptoms_trend_per_age_group_weekly", "sewer", "vaccine_campaigns", - "vaccine_administered_last_timeframe" + "vaccine_administered_last_timeframe", + "variants" ], "additionalProperties": false, "properties": { diff --git a/packages/app/schema/nl/self_test_overall.json b/packages/app/schema/nl/self_test_overall.json index 6cdc0b9110..51ac345a5f 100644 --- a/packages/app/schema/nl/self_test_overall.json +++ b/packages/app/schema/nl/self_test_overall.json @@ -3,12 +3,15 @@ "value": { "title": "nl_self_test_overall_value", "type": "object", - "required": ["infected_percentage", "date_start_unix", "date_end_unix", "date_of_insertion_unix"], + "required": ["infected_percentage", "n_participants_total_unfiltered", "date_start_unix", "date_end_unix", "date_of_insertion_unix"], "additionalProperties": false, "properties": { "infected_percentage": { "type": ["number", "null"] }, + "n_participants_total_unfiltered": { + "type": ["number"] + }, "date_start_unix": { "type": "integer" }, diff --git a/packages/app/src/components/articles/page-articles-tile.tsx b/packages/app/src/components/articles/page-articles-tile.tsx index ec80b2a0da..2632c76b9c 100644 --- a/packages/app/src/components/articles/page-articles-tile.tsx +++ b/packages/app/src/components/articles/page-articles-tile.tsx @@ -77,7 +77,6 @@ const ArticleCard = styled(Anchor)` display: flex; flex-direction: column; gap: ${space[2]}; - height: 100%; padding: ${space[3]}; ${Text} { diff --git a/packages/app/src/components/interactive-legend.tsx b/packages/app/src/components/interactive-legend.tsx index 1d8731ed73..6d724ccb1a 100644 --- a/packages/app/src/components/interactive-legend.tsx +++ b/packages/app/src/components/interactive-legend.tsx @@ -37,7 +37,7 @@ export function InteractiveLegend({ helpText, selectOptions, selecti const isSelected = selection.includes(item.metricProperty); return ( - + {item.label} {item.shape === 'line' && } {item.shape === 'dashed' && ( @@ -51,7 +51,7 @@ export function InteractiveLegend({ helpText, selectOptions, selecti onToggleItem(item.metricProperty)} aria-label={item.legendAriaLabel} diff --git a/packages/app/src/components/kpi/bordered-kpi-section.tsx b/packages/app/src/components/kpi/bordered-kpi-section.tsx index a5f0d5a9a4..3a254c3162 100644 --- a/packages/app/src/components/kpi/bordered-kpi-section.tsx +++ b/packages/app/src/components/kpi/bordered-kpi-section.tsx @@ -9,10 +9,11 @@ import { KpiContent } from './components/kpi-content'; import { BorderedKpiSectionProps } from './types'; import { Markdown } from '../markdown'; -export const BorderedKpiSection = ({ title, description, source, dateOrRange, tilesData }: BorderedKpiSectionProps) => { +export const BorderedKpiSection = ({ title, description, source, dateOrRange, tilesData, disclaimer }: BorderedKpiSectionProps) => { const metadata: MetadataProps = { date: dateOrRange, source: source, + disclaimer: disclaimer, }; return ( diff --git a/packages/app/src/components/kpi/types.ts b/packages/app/src/components/kpi/types.ts index 8f68774cb0..986f11d303 100644 --- a/packages/app/src/components/kpi/types.ts +++ b/packages/app/src/components/kpi/types.ts @@ -29,6 +29,7 @@ export interface BorderedKpiSectionProps { }; tilesData: [TileData, TileData]; title: string; + disclaimer?: string; } type BarType = { diff --git a/packages/app/src/components/metadata.tsx b/packages/app/src/components/metadata.tsx index 7717c3d9a4..0df2b1ecee 100644 --- a/packages/app/src/components/metadata.tsx +++ b/packages/app/src/components/metadata.tsx @@ -5,6 +5,7 @@ import { space } from '~/style/theme'; import { replaceVariablesInText } from '~/utils/replace-variables-in-text'; import { Box } from './base'; import { InlineText, Text } from './typography'; +import { Markdown } from '~/components/markdown'; type source = { text: string; @@ -25,9 +26,10 @@ export interface MetadataProps extends MarginBottomProps { isTileFooter?: boolean; datumsText?: string; intervalCount?: string; + disclaimer?: string; } -export function Metadata({ date, source, obtainedAt, isTileFooter, datumsText, marginBottom, dataSources, intervalCount }: MetadataProps) { +export function Metadata({ date, source, obtainedAt, isTileFooter, datumsText, marginBottom, dataSources, intervalCount, disclaimer }: MetadataProps) { const { commonTexts, formatDateFromSeconds } = useIntl(); const dateString = @@ -77,6 +79,11 @@ export function Metadata({ date, source, obtainedAt, isTileFooter, datumsText, m }) ) : ( <> + {disclaimer && ( + + + + )} {dateString} {obtainedAt && ` ${replaceVariablesInText(commonTexts.common.metadata.obtained, { diff --git a/packages/app/src/components/sitemap/use-data-sitemap.ts b/packages/app/src/components/sitemap/use-data-sitemap.ts index 3dd5793b7c..afd7ff955d 100644 --- a/packages/app/src/components/sitemap/use-data-sitemap.ts +++ b/packages/app/src/components/sitemap/use-data-sitemap.ts @@ -48,8 +48,8 @@ export function useDataSitemap(base: 'nl' | 'gm', code?: string, data?: Pick = { config: Config[]; valueAnnotation?: string; initialWidth?: number; + disableLegend?: boolean; expectedLabel?: string; formatTooltip?: TooltipFormatter; isPercentage?: boolean; @@ -105,6 +106,7 @@ export function StackedChart(props: StackedChartProp config, initialWidth = 840, isPercentage, + disableLegend, expectedLabel, formatTickValue: formatYTickValue, formatTooltip, @@ -461,9 +463,11 @@ export function StackedChart(props: StackedChartProp )} - - - + {!disableLegend && legendItems && ( + + + + )} ); } diff --git a/packages/app/src/components/time-series-chart/components/series-icon.tsx b/packages/app/src/components/time-series-chart/components/series-icon.tsx index 589f3c3f60..aedc83a125 100644 --- a/packages/app/src/components/time-series-chart/components/series-icon.tsx +++ b/packages/app/src/components/time-series-chart/components/series-icon.tsx @@ -8,6 +8,7 @@ import { ScatterPlotIcon } from './scatter-plot'; import { RangeTrendIcon } from './range-trend'; import { SplitAreaTrendIcon } from './split-area-trend'; import { StackedAreaTrendIcon } from './stacked-area-trend'; +import { StackedBarTrendIcon } from '~/components/time-series-chart/components/stacked-bar-trend'; interface SeriesIconProps { config: SeriesConfig[number]; @@ -20,47 +21,25 @@ interface SeriesIconProps { value?: number | null; } -export function SeriesIcon({ - config, - value, -}: SeriesIconProps) { +export function SeriesIcon({ config, value }: SeriesIconProps) { switch (config.type) { case 'line': case 'gapped-line': - return ( - - ); + return ; case 'scatter-plot': return ; case 'range': - return ( - - ); + return ; case 'area': case 'gapped-area': - return ( - - ); + return ; case 'stacked-area': case 'gapped-stacked-area': - return ( - - ); + return ; case 'bar': - return ( - - ); + return ; + case 'stacked-bar': + return ; case 'split-area': /** * Here we return the icon even if there is no value, because it @@ -71,20 +50,9 @@ export function SeriesIcon({ * * @TODO Possibly we want this behavior for split-bar as well... */ - return ( - - ); + return ; case 'split-bar': - return isPresent(value) ? ( - - ) : null; + return isPresent(value) ? : null; default: return null; } diff --git a/packages/app/src/components/time-series-chart/components/series.tsx b/packages/app/src/components/time-series-chart/components/series.tsx index 4ad196bb1d..c19e3b9e6d 100644 --- a/packages/app/src/components/time-series-chart/components/series.tsx +++ b/packages/app/src/components/time-series-chart/components/series.tsx @@ -2,23 +2,14 @@ import { TimestampedValue } from '@corona-dashboard/common'; import { ScaleLinear } from 'd3-scale'; import { memo } from 'react'; import { AreaTrend, BarTrend, LineTrend, ScatterPlot, RangeTrend } from '.'; -import { - Bounds, - GetX, - GetY, - GetY0, - GetY1, - SeriesConfig, - SeriesDoubleValue, - SeriesList, - SeriesSingleValue, -} from '../logic'; +import { Bounds, GetX, GetY, GetY0, GetY1, SeriesConfig, SeriesDoubleValue, SeriesList, SeriesSingleValue } from '../logic'; import { GappedAreaTrend } from './gapped-area-trend'; import { GappedLinedTrend } from './gapped-line-trend'; import { GappedStackedAreaTrend } from './gapped-stacked-area-trend'; import { SplitAreaTrend } from './split-area-trend'; import { SplitBarTrend } from './split-bar-trend'; import { StackedAreaTrend } from './stacked-area-trend'; +import { StackedBarTrend } from '~/components/time-series-chart/components/stacked-bar-trend'; interface SeriesProps { seriesConfig: SeriesConfig; @@ -42,18 +33,7 @@ interface SeriesProps { export const Series = memo(SeriesUnmemoized) as typeof SeriesUnmemoized; -function SeriesUnmemoized({ - seriesConfig, - seriesList, - getX, - getY, - getY0, - getY1, - yScale, - bounds, - chartId, - seriesMax, -}: SeriesProps) { +function SeriesUnmemoized({ seriesConfig, seriesList, getX, getY, getY0, getY1, yScale, bounds, chartId, seriesMax }: SeriesProps) { return ( <> {seriesList @@ -94,16 +74,7 @@ function SeriesUnmemoized({ /> ); case 'scatter-plot': - return ( - - ); + return ; case 'area': return ( ({ seriesMax={seriesMax} /> ); + case 'stacked-bar': + return ( + + ); case 'split-bar': return ( series.filter((x) => isPresent(x.__value_a) && isPresent(x.__value_b)), [series]); + + const xScale = useMemo( + () => + scaleBand({ + range: [0, bounds.width], + round: true, + domain: series.map(getX), + padding: bandPadding, + }), + [bounds, getX, series, bandPadding] + ); + + /** + * Clip bar width to minimum of 1px otherwise the shape disappears on + * mobile screens. + */ + const barWidth = Math.max(xScale.bandwidth(), 1); + const zeroPosition = getY0({ __value_a: 0, __value_b: 0, __date_unix: 0 }); + + const outOfBoundsItems: SeriesDoubleValue[] = []; + const items: SeriesDoubleValue[] = []; + nonNullSeries.forEach((x) => { + const outOfBounds = undefined !== seriesMax && undefined !== x.__value_a && x.__value_a > seriesMax; + outOfBounds ? outOfBoundsItems.push(x) : items.push(x); + }); + + return ( + <> + {outOfBoundsItems.length && ( + <> + {outOfBoundsItems.map((item, index) => { + const value = { __value: seriesMax, __date_unix: item.__date_unix }; + const x = getX(item) - barWidth / 2; + const y = Math.min(zeroPosition, getY0(value)); + const barHeight = Math.abs(getY0(value) - getY1(value)); + + return ( + + {/* magic-number-alert at the next line the component receives a number as a height and width. + Those are related to the visX library and connot be changed to string/pixel values */} + + + + ); + })} + + )} + + {barWidth > 1 ? ( + <> + {items.map((item, index) => { + const x = getX(item) - barWidth / 2; + const y = Math.min(zeroPosition, getY1(item)); + const barHeight = Math.abs(getY0(item) - getY1(item)); + + return ; + })} + + ) : ( + <> + undefined !== seriesMax && undefined !== x.__value_a && x.__value_a < seriesMax)} + color={color} + fillOpacity={fillOpacity} + strokeWidth={0} + curve="step" + getX={getX} + getY={getY1} + yScale={yScale} + id={id} + /> + + )} + + ); +} + +interface BarTrendIconProps { + color: string; + fillOpacity?: number; + width?: number; + height?: number; +} + +export function StackedBarTrendIcon({ color, fillOpacity = DEFAULT_FILL_OPACITY, width = 15, height = 15 }: BarTrendIconProps) { + const maskId = useUniqueId(); + + return ( + + + + + + + + + ); +} diff --git a/packages/app/src/components/time-series-chart/logic/hover-state.ts b/packages/app/src/components/time-series-chart/logic/hover-state.ts index 67d449a918..09e46dd39c 100644 --- a/packages/app/src/components/time-series-chart/logic/hover-state.ts +++ b/packages/app/src/components/time-series-chart/logic/hover-state.ts @@ -1,35 +1,14 @@ -import { - assert, - endOfDayInSeconds, - isDateSpanValue, - isDateValue, - startOfDayInSeconds, - TimestampedValue, -} from '@corona-dashboard/common'; +import { assert, endOfDayInSeconds, isDateSpanValue, isDateValue, startOfDayInSeconds, TimestampedValue } from '@corona-dashboard/common'; import { localPoint } from '@visx/event'; import { Point } from '@visx/point'; import { bisectCenter } from 'd3-array'; import { ScaleLinear } from 'd3-scale'; import { isEmpty, pick, throttle } from 'lodash'; -import { - Dispatch, - SetStateAction, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { isDefined, isPresent } from 'ts-is-present'; import { TimelineEventConfig } from '../components/timeline'; import { Padding, TimespanAnnotationConfig } from './common'; -import { - isVisible, - SeriesConfig, - SeriesDoubleValue, - SeriesList, - SeriesSingleValue, -} from './series'; +import { isVisible, SeriesConfig, SeriesDoubleValue, SeriesList, SeriesSingleValue } from './series'; import { findSplitPointForValue } from './split'; import { useKeyboardNavigation } from './use-keyboard-navigation'; @@ -60,6 +39,7 @@ interface UseHoverStateArgs { interface HoverState { valuesIndex: number; barPoints: HoveredPoint[]; + stackedBarPoints: HoveredPoint[]; linePoints: HoveredPoint[]; rangePoints: HoveredPoint[]; nearestPoint: HoveredPoint; @@ -84,11 +64,7 @@ export function useHoverState({ }: UseHoverStateArgs) { const [point, setPoint] = useState(); const [valuesIndex, setValuesIndex] = useState(0); - const keyboard = useKeyboardNavigation( - setValuesIndex, - values.length, - setIsTabInteractive - ); + const keyboard = useKeyboardNavigation(setValuesIndex, values.length, setIsTabInteractive); useEffect(() => { isTabInteractive ? keyboard.enable() : keyboard.disable(); @@ -111,9 +87,7 @@ export function useHoverState({ ); const valuesWithInteractiveProperties = useMemo(() => { - return values.filter((x) => - hasSomeFilledProperties(pick(x, interactiveMetricProperties)) - ); + return values.filter((x) => hasSomeFilledProperties(pick(x, interactiveMetricProperties))); }, [values, interactiveMetricProperties]); const interactiveValuesDateUnix = useMemo( @@ -173,23 +147,13 @@ export function useHoverState({ const date_unix = xScale.invert(xPosition); - const index = bisectCenter( - interactiveValuesDateUnix, - date_unix, - 0, - interactiveValuesDateUnix.length - ); + const index = bisectCenter(interactiveValuesDateUnix, date_unix, 0, interactiveValuesDateUnix.length); const timestamp = interactiveValuesDateUnix[index]; - const indexInAllValues = allValuesDateUnix.findIndex( - (x) => x === timestamp - ); + const indexInAllValues = allValuesDateUnix.findIndex((x) => x === timestamp); - assert( - indexInAllValues !== -1, - `[${bisect.name}] Failed to find the values index for interactive value timestamp ${timestamp}` - ); + assert(indexInAllValues !== -1, `[${bisect.name}] Failed to find the values index for interactive value timestamp ${timestamp}`); return indexInAllValues; }, @@ -266,9 +230,7 @@ export function useHoverState({ .filter(isVisible) .filter((x) => !x.nonInteractive) .map((config, index) => { - const seriesValue = seriesList[index][valuesIndex] as - | SeriesSingleValue - | undefined; + const seriesValue = seriesList[index][valuesIndex] as SeriesSingleValue | undefined; if (!isPresent(seriesValue)) { return; @@ -307,13 +269,47 @@ export function useHoverState({ }) .filter(isDefined); + const stackedBarPoints: HoveredPoint[] = seriesConfig + .filter(isVisible) + .filter((x) => !x.nonInteractive) + .map((config, index) => { + const seriesValue = seriesList[index][valuesIndex] as SeriesDoubleValue | undefined; + + if (!isPresent(seriesValue)) { + return; + } + + const xValue = seriesValue.__date_unix; + const yValueA = seriesValue.__value_a; + const yValueB = seriesValue.__value_b; + + /** + * Filter series without Y value on the current valuesIndex + */ + if (!isPresent(yValueA) || !isPresent(yValueB)) { + return; + } + + switch (config.type) { + case 'stacked-bar': + return { + seriesValue, + x: xScale(xValue), + y: isPresent(yValueB) ? yScale(yValueB) : 0, + color: config.color, + metricProperty: config.metricProperty, + seriesConfigIndex: index, + }; + } + }) + .filter(isDefined) + .filter((x) => isPresent(x.seriesValue.__value_a) && isPresent(x.seriesValue.__value_b)); + const linePoints: HoveredPoint[] = seriesConfig .filter(isVisible) .filter((x) => !x.nonInteractive) .map((config, index) => { - const seriesValue = seriesList[index][valuesIndex] as - | SeriesSingleValue - | undefined; + const seriesValue = seriesList[index][valuesIndex] as SeriesSingleValue | undefined; if (!isPresent(seriesValue)) { return; @@ -370,9 +366,7 @@ export function useHoverState({ .filter(isVisible) .filter((x) => !x.nonInteractive) .flatMap((config, index) => { - const seriesValue = seriesList[index][valuesIndex] as - | SeriesDoubleValue - | undefined; + const seriesValue = seriesList[index][valuesIndex] as SeriesDoubleValue | undefined; if (!isPresent(seriesValue)) { return; @@ -385,10 +379,7 @@ export function useHoverState({ /** * Filter series without Y value on the current valuesIndex */ - if ( - (!isPresent(yValueA) || !isPresent(yValueB)) && - config.type !== 'gapped-stacked-area' - ) { + if ((!isPresent(yValueA) || !isPresent(yValueB)) && config.type !== 'gapped-stacked-area') { return; } @@ -427,20 +418,16 @@ export function useHoverState({ } }) .filter(isDefined) - .filter( - (x) => - isPresent(x.seriesValue.__value_a) && - isPresent(x.seriesValue.__value_b) - ); + .filter((x) => isPresent(x.seriesValue.__value_a) && isPresent(x.seriesValue.__value_b)); /** * For nearest point calculation we only need to look at the y component of * the mouse, since all series originate from the same original value and * are thus aligned with the same timestamp. */ - const nearestPoint = [...linePoints, ...rangePoints, ...barPoints].sort( - (a, b) => Math.abs(a.y - pointY) - Math.abs(b.y - pointY) - )[0] as HoveredPoint | undefined; + const nearestPoint = [...linePoints, ...rangePoints, ...barPoints, ...stackedBarPoints].sort((a, b) => Math.abs(a.y - pointY) - Math.abs(b.y - pointY))[0] as + | HoveredPoint + | undefined; /** * Empty hoverstate when there's no nearest point detected @@ -449,60 +436,31 @@ export function useHoverState({ return; } - const timespanAnnotationIndex = timespanAnnotations - ? findActiveTimespanAnnotationIndex( - values[valuesIndex], - timespanAnnotations - ) - : undefined; + const timespanAnnotationIndex = timespanAnnotations ? findActiveTimespanAnnotationIndex(values[valuesIndex], timespanAnnotations) : undefined; - const timelineEventIndex = timelineEvents - ? findActiveTimelineEventIndex(values[valuesIndex], timelineEvents) - : undefined; + const timelineEventIndex = timelineEvents ? findActiveTimelineEventIndex(values[valuesIndex], timelineEvents) : undefined; const hoverState: HoverState = { valuesIndex, barPoints, - linePoints: markNearestPointOnly - ? linePoints.filter((x) => x === nearestPoint) - : linePoints, - rangePoints: markNearestPointOnly - ? rangePoints.filter((x) => x === nearestPoint) - : rangePoints, + stackedBarPoints, + linePoints: markNearestPointOnly ? linePoints.filter((x) => x === nearestPoint) : linePoints, + rangePoints: markNearestPointOnly ? rangePoints.filter((x) => x === nearestPoint) : rangePoints, nearestPoint, timespanAnnotationIndex, timelineEventIndex, }; return hoverState; - }, [ - point, - isTabInteractive, - seriesConfig, - timespanAnnotations, - timelineEvents, - values, - valuesIndex, - markNearestPointOnly, - seriesList, - xScale, - yScale, - ]); + }, [point, isTabInteractive, seriesConfig, timespanAnnotations, timelineEvents, values, valuesIndex, markNearestPointOnly, seriesList, xScale, yScale]); return [hoverState, { handleHover }] as const; } -function findActiveTimespanAnnotationIndex( - hoveredValue: TimestampedValue, - timespanAnnotations: TimespanAnnotationConfig[] -) { - const valueSpanStart = isDateValue(hoveredValue) - ? hoveredValue.date_unix - : hoveredValue.date_start_unix; +function findActiveTimespanAnnotationIndex(hoveredValue: TimestampedValue, timespanAnnotations: TimespanAnnotationConfig[]) { + const valueSpanStart = isDateValue(hoveredValue) ? hoveredValue.date_unix : hoveredValue.date_start_unix; - const valueSpanEnd = isDateValue(hoveredValue) - ? hoveredValue.date_unix - : hoveredValue.date_end_unix; + const valueSpanEnd = isDateValue(hoveredValue) ? hoveredValue.date_unix : hoveredValue.date_end_unix; /** * Loop over the annotations and see if the hovered value falls within its @@ -519,21 +477,10 @@ function findActiveTimespanAnnotationIndex( } } -function findActiveTimelineEventIndex( - hoveredValue: TimestampedValue, - timelineEvents: TimelineEventConfig[] -) { - const valueSpanStartOfDay = startOfDayInSeconds( - isDateValue(hoveredValue) - ? hoveredValue.date_unix - : hoveredValue.date_start_unix - ); +function findActiveTimelineEventIndex(hoveredValue: TimestampedValue, timelineEvents: TimelineEventConfig[]) { + const valueSpanStartOfDay = startOfDayInSeconds(isDateValue(hoveredValue) ? hoveredValue.date_unix : hoveredValue.date_start_unix); - const valueSpanEndOfDay = endOfDayInSeconds( - isDateValue(hoveredValue) - ? hoveredValue.date_unix - : hoveredValue.date_end_unix - ); + const valueSpanEndOfDay = endOfDayInSeconds(isDateValue(hoveredValue) ? hoveredValue.date_unix : hoveredValue.date_end_unix); /** * Loop over the timeline events and see if the hovered value falls within its diff --git a/packages/app/src/components/time-series-chart/logic/series.ts b/packages/app/src/components/time-series-chart/logic/series.ts index 129a261b0e..284021bbee 100644 --- a/packages/app/src/components/time-series-chart/logic/series.ts +++ b/packages/app/src/components/time-series-chart/logic/series.ts @@ -13,6 +13,7 @@ type SeriesConfigSingle = | RangeSeriesDefinition | AreaSeriesDefinition | StackedAreaSeriesDefinition + | StackedBarSeriesDefinition | BarSeriesDefinition | BarOutOfBoundsSeriesDefinition | SplitBarSeriesDefinition @@ -177,6 +178,15 @@ export interface GappedStackedAreaSeriesDefinition e mixBlendMode?: Property.MixBlendMode; } +export interface StackedBarSeriesDefinition extends SeriesCommonDefinition { + type: 'stacked-bar'; + metricProperty: keyof T; + label: string; + shortLabel?: string; + color: string; + fillOpacity?: number; +} + /** * Adding the split series definition here even though it might not end up as * part of this chart. For starters this makes it easier because then we can @@ -353,6 +363,8 @@ function getSeriesList(values: T[], seriesConfig: Se ? getGappedStackedAreaSeriesData(values, config.metricProperty, seriesConfig, dataOptions) : config.type === 'range' ? getRangeSeriesData(values, config.metricPropertyLow, config.metricPropertyHigh) + : config.type === 'stacked-bar' + ? getStackedBarSeriesData(values, config.metricProperty, seriesConfig) : /** * Cutting values based on annotation is only supported for single line series */ @@ -442,6 +454,40 @@ function getStackedAreaSeriesData(values: T[], metri }); } +function getStackedBarSeriesData(values: T[], metricProperty: keyof T, seriesConfig: SeriesConfig) { + /** + * Stacked area series are rendered from top to bottom. The sum of a Y-value + * of all series below the current series equals the low value of a current + * series's Y-value. + */ + const stackedAreaDefinitions = seriesConfig.filter(hasValueAtKey('type', 'stacked-bar' as const)); + + const seriesBelowCurrentSeries = getSeriesBelowCurrentSeries(stackedAreaDefinitions, metricProperty); + + const seriesHigh = getSeriesData(values, metricProperty); + const seriesLow = getSeriesData(values, metricProperty); + + seriesLow.forEach((seriesSingleValue, index) => { + /** + * The series are rendered from top to bottom. To get the low value of the + * current series, we will sum up all values of the + * `seriesBelowCurrentSeries`. + */ + seriesSingleValue.__value = sumSeriesValues(seriesBelowCurrentSeries, values, index); + }); + + return seriesLow.map((low, index) => { + const valueLow = low.__value ?? 0; + const valueHigh = valueLow + (seriesHigh[index].__value ?? 0); + + return { + __date_unix: low.__date_unix, + __value_a: valueLow, + __value_b: valueHigh, + }; + }); +} + function getSeriesBelowCurrentSeries(definitions: { metricProperty: keyof T }[], metricProperty: keyof T) { return definitions.slice(definitions.findIndex((x) => x.metricProperty === metricProperty) + 1); } diff --git a/packages/app/src/domain/layout/gm-layout.tsx b/packages/app/src/domain/layout/gm-layout.tsx index 320b3cd93c..999782c413 100644 --- a/packages/app/src/domain/layout/gm-layout.tsx +++ b/packages/app/src/domain/layout/gm-layout.tsx @@ -61,7 +61,7 @@ export function GmLayout(props: GmLayoutProps) { map: [ ['development_of_the_virus', ['sewage_measurement']], ['consequences_for_healthcare', ['hospital_admissions']], - ['actions_to_take', ['vaccinations']], + ['actions_to_take', ['the_corona_vaccine']], ['archived_metrics', ['positive_tests', 'mortality']], ], }); diff --git a/packages/app/src/domain/layout/logic/types.ts b/packages/app/src/domain/layout/logic/types.ts index d7ab1b4d10..7cf00f3417 100644 --- a/packages/app/src/domain/layout/logic/types.ts +++ b/packages/app/src/domain/layout/logic/types.ts @@ -2,7 +2,7 @@ export type Layout = 'nl' | 'gm' | 'custom'; type SharedCategoryKeys = 'development_of_the_virus' | 'consequences_for_healthcare' | 'actions_to_take'; -export type GmItemKeys = 'hospital_admissions' | 'mortality' | 'positive_tests' | 'sewage_measurement' | 'vaccinations'; +export type GmItemKeys = 'hospital_admissions' | 'mortality' | 'positive_tests' | 'sewage_measurement' | 'the_corona_vaccine'; export type GmCategoryKeys = SharedCategoryKeys | 'archived_metrics'; @@ -22,7 +22,7 @@ export type NlItemKeys = | 'reproduction_number' | 'sewage_measurement' | 'infection_radar' - | 'vaccinations' + | 'the_corona_vaccine' | 'variants'; export type NlCategoryKeys = SharedCategoryKeys | 'archived_metrics'; diff --git a/packages/app/src/domain/layout/logic/use-sidebar.tsx b/packages/app/src/domain/layout/logic/use-sidebar.tsx index 32a0eeecc4..ab7c14b54e 100644 --- a/packages/app/src/domain/layout/logic/use-sidebar.tsx +++ b/packages/app/src/domain/layout/logic/use-sidebar.tsx @@ -32,7 +32,7 @@ const mapKeysToReverseRouter = { reproduction_number: 'reproductiegetal', sewage_measurement: 'rioolwater', infection_radar: 'infectieradar', - vaccinations: 'vaccinaties', + the_corona_vaccine: 'deCoronaprik', variants: 'varianten', } as const; diff --git a/packages/app/src/domain/layout/nl-layout.tsx b/packages/app/src/domain/layout/nl-layout.tsx index a59df60d8d..47991fe3c0 100644 --- a/packages/app/src/domain/layout/nl-layout.tsx +++ b/packages/app/src/domain/layout/nl-layout.tsx @@ -39,7 +39,7 @@ export function NlLayout(props: NlLayoutProps) { map: [ ['development_of_the_virus', ['sewage_measurement', 'infection_radar', 'variants', 'mortality']], ['consequences_for_healthcare', ['hospitals_and_care', 'patients']], - ['actions_to_take', ['vaccinations']], + ['actions_to_take', ['the_corona_vaccine']], [ 'archived_metrics', [ diff --git a/packages/app/src/domain/vaccine/vaccine-campaigns-tile/vaccine-campaigns-tile.tsx b/packages/app/src/domain/vaccine/vaccine-campaigns-tile/vaccine-campaigns-tile.tsx index b07689951a..31b96bf566 100644 --- a/packages/app/src/domain/vaccine/vaccine-campaigns-tile/vaccine-campaigns-tile.tsx +++ b/packages/app/src/domain/vaccine/vaccine-campaigns-tile/vaccine-campaigns-tile.tsx @@ -1,16 +1,11 @@ import { useBreakpoints } from '~/utils/use-breakpoints'; -import { ChartTile, Markdown, MetadataProps } from '~/components'; -import { Text } from '~/components/typography'; +import { ChartTile, MetadataProps } from '~/components'; import { NarrowVaccineCampaignTable } from './components/narrow-vaccine-campaign-table'; import { WideVaccineCampaignTable } from './components/wide-vaccine-campaign-table'; import { VaccineCampaign, VaccineCampaignDescriptions, VaccineCampaignHeaders, VaccineCampaignOptions } from './types'; -import { Box } from '~/components/base'; -import { space } from '~/style/theme'; - interface VaccineCampaignsTileProps { title: string; description: string; - descriptionFooter: string; metadata: MetadataProps; headers: VaccineCampaignHeaders; campaigns: VaccineCampaign[]; @@ -18,7 +13,7 @@ interface VaccineCampaignsTileProps { campaignOptions?: VaccineCampaignOptions; } -export const VaccineCampaignsTile = ({ title, headers, campaigns, campaignDescriptions, description, descriptionFooter, metadata, campaignOptions }: VaccineCampaignsTileProps) => { +export const VaccineCampaignsTile = ({ title, headers, campaigns, campaignDescriptions, description, metadata, campaignOptions }: VaccineCampaignsTileProps) => { const breakpoints = useBreakpoints(); // Display only the campaigns that are not hidden in the campaignOptions prop @@ -36,11 +31,6 @@ export const VaccineCampaignsTile = ({ title, headers, campaigns, campaignDescri ) : ( )} - - - - - ); diff --git a/packages/app/src/domain/variants/static-props/get-variant-chart-data.ts b/packages/app/src/domain/variants/data-selection/get-archived-variant-chart-data.ts similarity index 77% rename from packages/app/src/domain/variants/static-props/get-variant-chart-data.ts rename to packages/app/src/domain/variants/data-selection/get-archived-variant-chart-data.ts index 9943c5ef49..a70db0a7d1 100644 --- a/packages/app/src/domain/variants/static-props/get-variant-chart-data.ts +++ b/packages/app/src/domain/variants/data-selection/get-archived-variant-chart-data.ts @@ -1,17 +1,10 @@ -import { NlVariants } from '@corona-dashboard/common'; +import { ArchivedNlVariants } from '@corona-dashboard/common'; import { isDefined } from 'ts-is-present'; - -export type VariantCode = string; - -export type VariantChartValue = { - date_start_unix: number; - date_end_unix: number; - is_reliable: boolean; -} & Record; +import { VariantChartValue } from '~/domain/variants/data-selection/types'; const EMPTY_VALUES = { - variantChart: null, - dates: { + archivedVariantChart: null, + archivedDates: { date_of_report_unix: 0, date_start_unix: 0, date_end_unix: 0, @@ -22,7 +15,7 @@ const EMPTY_VALUES = { * Returns values for variant timeseries chart * @param variants */ -export function getVariantChartData(variants: NlVariants | undefined) { +export function getArchivedVariantChartData(variants: ArchivedNlVariants | undefined) { if (!isDefined(variants) || !isDefined(variants.values)) { return EMPTY_VALUES; } @@ -51,8 +44,8 @@ export function getVariantChartData(variants: NlVariants | undefined) { }); return { - variantChart: values, - dates: { + archivedVariantChart: values, + archivedDates: { date_of_report_unix: firstVariantInList.last_value.date_of_report_unix, date_start_unix: firstVariantInList.last_value.date_start_unix, date_end_unix: firstVariantInList.last_value.date_end_unix, diff --git a/packages/app/src/domain/variants/data-selection/get-variant-bar-chart-data.ts b/packages/app/src/domain/variants/data-selection/get-variant-bar-chart-data.ts new file mode 100644 index 0000000000..64cd45edcb --- /dev/null +++ b/packages/app/src/domain/variants/data-selection/get-variant-bar-chart-data.ts @@ -0,0 +1,54 @@ +import { NlVariants } from '@corona-dashboard/common'; +import { isDefined } from 'ts-is-present'; +import { VariantChartValue } from '~/domain/variants/data-selection/types'; + +const EMPTY_VALUES = { + variantChart: null, + dates: { + date_of_report_unix: 0, + date_start_unix: 0, + date_end_unix: 0, + }, +} as const; + +/** + * Returns values for variant timeseries chart + * @param variants + */ +export function getVariantBarChartData(variants: NlVariants) { + if (!isDefined(variants) || !isDefined(variants.values)) { + return EMPTY_VALUES; + } + + const sortedVariants = variants.values.sort((a, b) => b.last_value.order - a.last_value.order); + + const firstVariantInList = sortedVariants.shift(); + + if (!isDefined(firstVariantInList)) { + return EMPTY_VALUES; + } + + const values: VariantChartValue[] = firstVariantInList.values.map((value, index) => { + const item: VariantChartValue = { + is_reliable: true, + date_start_unix: value.date_start_unix, + date_end_unix: value.date_end_unix, + [`${firstVariantInList.variant_code}_occurrence`]: value.occurrence, + } as VariantChartValue; + + sortedVariants.forEach((variant) => { + (item as unknown as Record)[`${variant.variant_code}_occurrence`] = variant.values[index].occurrence; + }); + + return item; + }); + + return { + variantChart: values, + dates: { + date_of_report_unix: firstVariantInList.last_value.date_of_report_unix, + date_start_unix: firstVariantInList.last_value.date_start_unix, + date_end_unix: firstVariantInList.last_value.date_end_unix, + }, + } as const; +} diff --git a/packages/app/src/domain/variants/static-props/get-variant-order-colors.ts b/packages/app/src/domain/variants/data-selection/get-variant-order-colors.ts similarity index 88% rename from packages/app/src/domain/variants/static-props/get-variant-order-colors.ts rename to packages/app/src/domain/variants/data-selection/get-variant-order-colors.ts index b100539ff3..42c0fd5b15 100644 --- a/packages/app/src/domain/variants/static-props/get-variant-order-colors.ts +++ b/packages/app/src/domain/variants/data-selection/get-variant-order-colors.ts @@ -1,11 +1,6 @@ import { NlVariants, colors } from '@corona-dashboard/common'; import { isDefined } from 'ts-is-present'; -import { VariantCode } from './'; - -export type ColorMatch = { - variant: VariantCode; - color: string; -}; +import { ColorMatch, VariantCode } from '~/domain/variants/data-selection/types'; const getColorForVariant = (variantCode: VariantCode, index: number): string => { if (variantCode === 'other_variants') return colors.gray5; diff --git a/packages/app/src/domain/variants/static-props/get-variant-table-data.ts b/packages/app/src/domain/variants/data-selection/get-variant-table-data.ts similarity index 83% rename from packages/app/src/domain/variants/static-props/get-variant-table-data.ts rename to packages/app/src/domain/variants/data-selection/get-variant-table-data.ts index de5947f6ed..cacdbfac67 100644 --- a/packages/app/src/domain/variants/static-props/get-variant-table-data.ts +++ b/packages/app/src/domain/variants/data-selection/get-variant-table-data.ts @@ -1,18 +1,7 @@ -import { colors, NlNamedDifference, NlVariants, NlVariantsVariant, NamedDifferenceDecimal } from '@corona-dashboard/common'; +import { colors, NlNamedDifference, NlVariants, NlVariantsVariant } from '@corona-dashboard/common'; import { first } from 'lodash'; import { isDefined } from 'ts-is-present'; -import { ColorMatch } from './get-variant-order-colors'; -import { VariantCode } from '../static-props'; - -export type VariantRow = { - variantCode: VariantCode; - order: number; - percentage: number | null; - difference?: NamedDifferenceDecimal | null; - color: string; -}; - -export type VariantTableData = ReturnType; +import { ColorMatch, VariantRow } from '~/domain/variants/data-selection/types'; /** * Return values to populate the variants table diff --git a/packages/app/src/domain/variants/data-selection/index.ts b/packages/app/src/domain/variants/data-selection/index.ts new file mode 100644 index 0000000000..7589614493 --- /dev/null +++ b/packages/app/src/domain/variants/data-selection/index.ts @@ -0,0 +1,4 @@ +export * from './get-variant-bar-chart-data'; +export * from './get-archived-variant-chart-data'; +export * from './get-variant-order-colors'; +export * from './get-variant-table-data'; diff --git a/packages/app/src/domain/variants/data-selection/types.ts b/packages/app/src/domain/variants/data-selection/types.ts new file mode 100644 index 0000000000..682bbc6651 --- /dev/null +++ b/packages/app/src/domain/variants/data-selection/types.ts @@ -0,0 +1,40 @@ +import { SiteText } from '~/locale'; +import { NamedDifferenceDecimal, TimestampedValue } from '@corona-dashboard/common'; +import { getVariantTableData } from '~/domain/variants/data-selection/get-variant-table-data'; + +export type VariantCode = string; + +export type ColorMatch = { + variant: VariantCode; + color: string; +}; + +export type VariantTableData = ReturnType; + +export type VariantChartValue = { + date_start_unix: number; + date_end_unix: number; + is_reliable: boolean; +} & Record; + +export type VariantRow = { + variantCode: VariantCode; + order: number; + percentage: number | null; + difference?: NamedDifferenceDecimal | null; + color: string; +}; + +export type VariantDynamicLabels = Record; + +export type VariantsOverTimeGraphText = SiteText['pages']['variants_page']['nl']['varianten_over_tijd_grafiek']; + +export type VariantsStackedAreaTileText = { + variantCodes: VariantDynamicLabels; +} & SiteText['pages']['variants_page']['nl']['varianten_over_tijd_grafiek']; + +export type StackedBarConfig = { + metricProperty: keyof T; + label: string; + color: string; +}; diff --git a/packages/app/src/domain/variants/index.ts b/packages/app/src/domain/variants/index.ts new file mode 100644 index 0000000000..270d708310 --- /dev/null +++ b/packages/app/src/domain/variants/index.ts @@ -0,0 +1,4 @@ +export * from './variants-stacked-bar-chart-tile'; +export * from './variants-stacked-area-tile'; +export * from './variants-table-tile'; +export * from './data-selection'; diff --git a/packages/app/src/domain/variants/logic/reorder-and-filter.ts b/packages/app/src/domain/variants/logic/reorder-and-filter.ts new file mode 100644 index 0000000000..983bb63249 --- /dev/null +++ b/packages/app/src/domain/variants/logic/reorder-and-filter.ts @@ -0,0 +1,35 @@ +import { isDefined, isPresent } from 'ts-is-present'; +import { VariantChartValue } from '~/domain/variants/data-selection/types'; +import { TooltipData } from '~/components/time-series-chart/components'; + +/** + * Check if the key metricProperty exists + * @param config + */ +const hasMetricProperty = (config: any): config is { metricProperty: string } => { + return 'metricProperty' in config; +}; + +/** + * Only variants that have a greater occurrence than 0 must be shown in the tooltip, except when the user narrows down + * the total amount of visible variants by selecting one or more from the legend + * @param context - Tooltip data context + * @param selectionOptions - Currently selected variants + */ +export const reorderAndFilter = (context: TooltipData, selectionOptions: P[]) => { + const hasSelectedMetrics = context.config.length !== selectionOptions.length; // Check whether the user has selected any variants from the interactive legend. + + /* Filter out any variants that have an occcurrence value of 0 */ + const filteredValues = Object.fromEntries( + Object.entries(context.value).filter(([key, value]) => (key.includes('occurrence') ? value !== 0 && isPresent(value) && !isNaN(Number(value)) : value)) + ) as VariantChartValue; + + /* Rebuild tooltip data context with filtered values */ + const reorderContext = { + ...context, + config: [...context.config.filter((value) => !hasMetricProperty(value) || filteredValues[value.metricProperty] || hasSelectedMetrics)].filter(isDefined), + value: !hasSelectedMetrics ? filteredValues : context.value, + }; + + return reorderContext as TooltipData; +}; diff --git a/packages/app/src/domain/variants/logic/use-bar-config.ts b/packages/app/src/domain/variants/logic/use-bar-config.ts new file mode 100644 index 0000000000..966d672936 --- /dev/null +++ b/packages/app/src/domain/variants/logic/use-bar-config.ts @@ -0,0 +1,73 @@ +import { ColorMatch, VariantChartValue, VariantDynamicLabels, VariantsOverTimeGraphText } from '~/domain/variants/data-selection/types'; +import { useMemo } from 'react'; +import { getValuesInTimeframe, TimeframeOption } from '@corona-dashboard/common'; +import { isPresent } from 'ts-is-present'; +import { StackedBarSeriesDefinition } from '~/components/time-series-chart/logic'; + +const extractVariantNamesFromValues = (values: VariantChartValue[]) => { + return values + .flatMap((variantChartValue) => Object.keys(variantChartValue)) + .filter((keyName, index, array) => array.indexOf(keyName) === index) + .filter((keyName) => keyName.endsWith('_occurrence')); +}; + +/** + * Create configuration labels for interactive legend + * @param values - Chart data + * @param variantLabels - Mnemonic labels for variants + * @param tooltipLabels - SiteText for other variants + * @param colors - Colors for variants + * @param timeframe - Selected timeframe + * @param today - Date of today + */ +export const useBarConfig = ( + values: VariantChartValue[], + variantLabels: VariantDynamicLabels, + tooltipLabels: VariantsOverTimeGraphText, + colors: ColorMatch[], + timeframe: TimeframeOption, + today: Date +) => { + return useMemo(() => { + const valuesInTimeframe: VariantChartValue[] = getValuesInTimeframe(values, timeframe, today); + + const activeVariantsInTimeframeValues: VariantChartValue[] = valuesInTimeframe.map((val) => { + return Object.fromEntries( + Object.entries(val).filter(([key, value]) => (key.includes('occurrence') ? value !== 0 && isPresent(value) && !isNaN(Number(value)) : value)) + ) as VariantChartValue; + }); + + const activeVariantsInTimeframeNames: string[] = extractVariantNamesFromValues(activeVariantsInTimeframeValues); + + const listOfVariantCodes: string[] = extractVariantNamesFromValues(valuesInTimeframe) + .filter((keyName) => activeVariantsInTimeframeNames.includes(keyName)) + .reverse(); + + const barChartConfig: StackedBarSeriesDefinition[] = []; + + listOfVariantCodes.forEach((variantKey) => { + const variantCodeName = variantKey.split('_').slice(0, -1).join('_'); + + const variantMetricPropertyName = variantCodeName.concat('_occurrence'); + + const variantDynamicLabel = variantLabels[variantCodeName]; + + const color = colors.find((variantColors) => variantColors.variant === variantCodeName)?.color; + + if (variantDynamicLabel) { + const barChartConfigEntry = { + type: 'stacked-bar', + metricProperty: variantMetricPropertyName, + color: color, + label: variantDynamicLabel, + fillOpacity: 1, + shape: 'gapped-area', + }; + + barChartConfig.push(barChartConfigEntry as StackedBarSeriesDefinition); + } + }); + + return barChartConfig; + }, [values, tooltipLabels.tooltip_labels.other_percentage, variantLabels, colors, timeframe, today]); +}; diff --git a/packages/app/src/domain/variants/logic/use-series-config.ts b/packages/app/src/domain/variants/logic/use-series-config.ts new file mode 100644 index 0000000000..aa51e324ec --- /dev/null +++ b/packages/app/src/domain/variants/logic/use-series-config.ts @@ -0,0 +1,60 @@ +import { ColorMatch, VariantChartValue, VariantsStackedAreaTileText } from '~/domain/variants/data-selection/types'; +import { useMemo } from 'react'; +import { GappedAreaSeriesDefinition } from '~/components/time-series-chart/logic'; + +/** + * Create a configuration with appropriate mnemonic label (e.g. "Alpha", "Delta", "Omikron", etc.) and colour for all variants + * present in data. + * @param text + * @param values + * @param colors + */ +export const useSeriesConfig = ( + text: VariantsStackedAreaTileText, + values: VariantChartValue[], + colors: ColorMatch[] +): readonly [GappedAreaSeriesDefinition[], GappedAreaSeriesDefinition[]] => { + return useMemo(() => { + const baseVariantsFiltered = values + .flatMap((x) => Object.keys(x)) // Get all key names + .filter((x, index, array) => array.indexOf(x) === index) // De-dupe keys + .filter((x) => x.endsWith('_percentage')) // Filter out any keys that don't end in '_percentage' + .reverse(); // Reverse to be in alphabetical order + + /* Enrich config with dynamic data / locale */ + const seriesConfig: GappedAreaSeriesDefinition[] = []; + + baseVariantsFiltered.forEach((variantKey) => { + // Remove _percentage from variant key name + const variantCodeFragments = variantKey.split('_'); + variantCodeFragments.pop(); + const variantCode = variantCodeFragments.join('_'); + + // Match mnenonic variant name in lokalize to code-based variant name + const variantDynamicLabel = text.variantCodes[variantCode]; + + // Match appropriate variant color + const color = colors.find((variantColors) => variantColors.variant === variantCode)?.color; + + // Create a variant label configuration and push into array + if (variantDynamicLabel) { + const variantConfig = { + type: 'gapped-area', + metricProperty: variantKey as keyof VariantChartValue, + color, + label: variantDynamicLabel, + strokeWidth: 2, + fillOpacity: 0.2, + shape: 'gapped-area', + mixBlendMode: 'multiply', + }; + + seriesConfig.push(variantConfig as GappedAreaSeriesDefinition); + } + }); + + const selectOptions: GappedAreaSeriesDefinition[] = [...seriesConfig]; + + return [seriesConfig, selectOptions] as const; + }, [values, text.tooltip_labels.other_percentage, text.variantCodes, colors]); +}; diff --git a/packages/app/src/domain/variants/variants-stacked-area-tile/logic/use-unreliable-data-annotations.ts b/packages/app/src/domain/variants/logic/use-unreliable-data-annotations.ts similarity index 78% rename from packages/app/src/domain/variants/variants-stacked-area-tile/logic/use-unreliable-data-annotations.ts rename to packages/app/src/domain/variants/logic/use-unreliable-data-annotations.ts index b2e36614ad..603e6c4949 100644 --- a/packages/app/src/domain/variants/variants-stacked-area-tile/logic/use-unreliable-data-annotations.ts +++ b/packages/app/src/domain/variants/logic/use-unreliable-data-annotations.ts @@ -4,19 +4,14 @@ import { useMemo } from 'react'; import { isDefined } from 'ts-is-present'; import { TimespanAnnotationConfig } from '~/components/time-series-chart/logic'; -export function useUnreliableDataAnnotations( - values: (DateSpanValue & { is_reliable: boolean })[], - label: string -) { +export function useUnreliableDataAnnotations(values: (DateSpanValue & { is_reliable: boolean })[], label: string) { return useMemo( () => values .reduce( (acc, x) => { if (!x.is_reliable) { - const annotation = - last(acc) ?? - ({ label, fill: 'dotted' } as TimespanAnnotationConfig); + const annotation = last(acc) ?? ({ label, fill: 'dotted' } as TimespanAnnotationConfig); if (!isDefined(annotation.start)) { annotation.start = x.date_start_unix; annotation.end = x.date_end_unix; diff --git a/packages/app/src/domain/variants/static-props/index.ts b/packages/app/src/domain/variants/static-props/index.ts deleted file mode 100644 index c455be43bc..0000000000 --- a/packages/app/src/domain/variants/static-props/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './get-variant-chart-data'; -export * from './get-variant-order-colors'; -export * from './get-variant-table-data'; diff --git a/packages/app/src/domain/variants/variants-stacked-area-tile.tsx b/packages/app/src/domain/variants/variants-stacked-area-tile.tsx new file mode 100644 index 0000000000..1ba3a953bb --- /dev/null +++ b/packages/app/src/domain/variants/variants-stacked-area-tile.tsx @@ -0,0 +1,91 @@ +import { TimeframeOption, TimeframeOptionsList } from '@corona-dashboard/common'; +import { useMemo, useState } from 'react'; +import { Spacer } from '~/components/base'; +import { ChartTile } from '~/components/chart-tile'; +import { InteractiveLegend } from '~/components/interactive-legend'; +import { Legend, LegendItem } from '~/components/legend'; +import { MetadataProps } from '~/components/metadata'; +import { TimeSeriesChart } from '~/components/time-series-chart'; +import { TooltipSeriesList } from '~/components/time-series-chart/components/tooltip/tooltip-series-list'; +import { GappedAreaSeriesDefinition } from '~/components/time-series-chart/logic'; +import { useList } from '~/utils/use-list'; +import { space } from '~/style/theme'; +import { useUnreliableDataAnnotations } from './logic/use-unreliable-data-annotations'; +import { ColorMatch, VariantChartValue, VariantsStackedAreaTileText } from '~/domain/variants/data-selection/types'; +import { useSeriesConfig } from '~/domain/variants/logic/use-series-config'; +import { reorderAndFilter } from '~/domain/variants/logic/reorder-and-filter'; + +const alwaysEnabled: (keyof VariantChartValue)[] = []; + +interface VariantsStackedAreaTileProps { + text: VariantsStackedAreaTileText; + values: VariantChartValue[]; + metadata: MetadataProps; + variantColors: ColorMatch[]; +} + +export const VariantsStackedAreaTile = ({ text, values, variantColors, metadata }: VariantsStackedAreaTileProps) => { + const [variantTimeframe, setVariantTimeframe] = useState(TimeframeOption.THREE_MONTHS); + + const { list, toggle, clear } = useList(alwaysEnabled); + + const [seriesConfig, selectOptions] = useSeriesConfig(text, values, variantColors); + + const filteredConfig = useFilteredSeriesConfig(seriesConfig, list); + + /* Static legend contains only the inaccurate item */ + const staticLegendItems: LegendItem[] = []; + + const timespanAnnotations = useUnreliableDataAnnotations(values, text.lagere_betrouwbaarheid); + + const hasTwoColumns = list.length === 0 || list.length > 4; + + if (timespanAnnotations.length) { + staticLegendItems.push({ + shape: 'dotted-square', + color: 'black', + label: text.lagere_betrouwbaarheid, + }); + } + + return ( + + + + ( + >(data, selectOptions)} hasTwoColumns={hasTwoColumns} /> + )} + numGridLines={0} + tickValues={[0, 25, 50, 75, 100]} + /> + + + ); +}; + +const useFilteredSeriesConfig = (seriesConfig: GappedAreaSeriesDefinition[], compareList: (keyof VariantChartValue)[]) => { + return useMemo(() => { + return seriesConfig.filter((item) => compareList.includes(item.metricProperty) || compareList.length === alwaysEnabled.length); + }, [seriesConfig, compareList]); +}; diff --git a/packages/app/src/domain/variants/variants-stacked-area-tile/index.ts b/packages/app/src/domain/variants/variants-stacked-area-tile/index.ts deleted file mode 100644 index efc8c20500..0000000000 --- a/packages/app/src/domain/variants/variants-stacked-area-tile/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { VariantsStackedAreaTile } from './variants-stacked-area-tile'; diff --git a/packages/app/src/domain/variants/variants-stacked-area-tile/variants-stacked-area-tile.tsx b/packages/app/src/domain/variants/variants-stacked-area-tile/variants-stacked-area-tile.tsx deleted file mode 100644 index 753222a673..0000000000 --- a/packages/app/src/domain/variants/variants-stacked-area-tile/variants-stacked-area-tile.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { TimeframeOption, TimeframeOptionsList } from '@corona-dashboard/common'; -import { useMemo, useState } from 'react'; -import { isDefined, isPresent } from 'ts-is-present'; -import { Spacer } from '~/components/base'; -import { ChartTile } from '~/components/chart-tile'; -import { InteractiveLegend } from '~/components/interactive-legend'; -import { Legend, LegendItem } from '~/components/legend'; -import { MetadataProps } from '~/components/metadata'; -import { TimeSeriesChart } from '~/components/time-series-chart'; -import { TooltipSeriesList } from '~/components/time-series-chart/components/tooltip/tooltip-series-list'; -import { GappedAreaSeriesDefinition } from '~/components/time-series-chart/logic'; -import { VariantChartValue } from '~/domain/variants/static-props'; -import { SiteText } from '~/locale'; -import { useList } from '~/utils/use-list'; -import { ColorMatch } from '~/domain/variants/static-props'; -import { useUnreliableDataAnnotations } from './logic/use-unreliable-data-annotations'; -import { space } from '~/style/theme'; -import { VariantDynamicLabels } from '../variants-table-tile/types'; - -type VariantsStackedAreaTileText = { - variantCodes: VariantDynamicLabels; -} & SiteText['pages']['variants_page']['nl']['varianten_over_tijd_grafiek']; - -const alwaysEnabled: (keyof VariantChartValue)[] = []; - -interface VariantsStackedAreaTileProps { - text: VariantsStackedAreaTileText; - values: VariantChartValue[]; - metadata: MetadataProps; - variantColors: ColorMatch[]; -} - -export const VariantsStackedAreaTile = ({ text, values, variantColors, metadata }: VariantsStackedAreaTileProps) => { - const [variantTimeframe, setVariantTimeframe] = useState(TimeframeOption.THREE_MONTHS); - - const { list, toggle, clear } = useList(alwaysEnabled); - - const [seriesConfig, selectOptions] = useSeriesConfig(text, values, variantColors); - - const filteredConfig = useFilteredSeriesConfig(seriesConfig, list); - - /* Static legend contains only the inaccurate item */ - const staticLegendItems: LegendItem[] = []; - - const timespanAnnotations = useUnreliableDataAnnotations(values, text.lagere_betrouwbaarheid); - - if (timespanAnnotations.length) { - staticLegendItems.push({ - shape: 'dotted-square', - color: 'black', - label: text.lagere_betrouwbaarheid, - }); - } - - return ( - - - - { - /** - * Filter out zero values in value object, so it will be invisible in the tooltip. - * When a selection has been made, the zero values will be shown in the tooltip. - */ - const metricAmount = context.config.length; - const totalMetricAmount = seriesConfig.length; - const hasSelectedMetrics = metricAmount !== totalMetricAmount; - - const filteredValues = Object.fromEntries( - Object.entries(context.value).filter(([key, value]) => (key.includes('percentage') ? value !== 0 && isPresent(value) && !isNaN(Number(value)) : value)) - ) as VariantChartValue; - - const reorderContext = { - ...context, - config: [ - // Destructuring so as to not interact with the object directly and eliminate the possibility of introducing inconsistencies - ...context.config.filter((value) => !hasMetricProperty(value) || filteredValues[value.metricProperty] || hasSelectedMetrics), - ].filter(isDefined), - value: !hasSelectedMetrics ? filteredValues : context.value, - }; - - const percentageValuesAmount = Object.keys(reorderContext.value).filter((key) => key.includes('percentage')).length; - - const hasTwoColumns = !hasSelectedMetrics ? percentageValuesAmount > 4 : metricAmount > 4; - - return ; - }} - numGridLines={0} - tickValues={[0, 25, 50, 75, 100]} - /> - - - ); -}; - -const hasMetricProperty = (config: any): config is { metricProperty: string } => { - return 'metricProperty' in config; -}; - -const useFilteredSeriesConfig = (seriesConfig: GappedAreaSeriesDefinition[], compareList: (keyof VariantChartValue)[]) => { - return useMemo(() => { - return seriesConfig.filter((item) => compareList.includes(item.metricProperty) || compareList.length === alwaysEnabled.length); - }, [seriesConfig, compareList]); -}; - -const useSeriesConfig = (text: VariantsStackedAreaTileText, values: VariantChartValue[], variantColors: ColorMatch[]) => { - return useMemo(() => { - const baseVariantsFiltered = values - .flatMap((x) => Object.keys(x)) - .filter((x, index, array) => array.indexOf(x) === index) // de-dupe - .filter((x) => x.endsWith('_percentage')) - .reverse(); // Reverse to be in an alphabetical order - - /* Enrich config with dynamic data / locale */ - const seriesConfig: GappedAreaSeriesDefinition[] = []; - baseVariantsFiltered.forEach((variantKey) => { - const variantCodeFragments = variantKey.split('_'); - variantCodeFragments.pop(); - const variantCode = variantCodeFragments.join('_'); - - const variantDynamicLabel = text.variantCodes[variantCode]; - - const color = variantColors.find((variantColors) => variantColors.variant === variantCode)?.color; - - if (variantDynamicLabel) { - const newConfig = { - type: 'gapped-area', - metricProperty: variantKey as keyof VariantChartValue, - color, - label: variantDynamicLabel, - strokeWidth: 2, - fillOpacity: 0.2, - shape: 'gapped-area', - mixBlendMode: 'multiply', - }; - - seriesConfig.push(newConfig as GappedAreaSeriesDefinition); - } - }); - - const selectOptions = [...seriesConfig]; - - return [seriesConfig, selectOptions] as const; - }, [values, text.tooltip_labels.other_percentage, text.variantCodes, variantColors]); -}; diff --git a/packages/app/src/domain/variants/variants-stacked-bar-chart-tile.tsx b/packages/app/src/domain/variants/variants-stacked-bar-chart-tile.tsx new file mode 100644 index 0000000000..85116b018c --- /dev/null +++ b/packages/app/src/domain/variants/variants-stacked-bar-chart-tile.tsx @@ -0,0 +1,76 @@ +import { ChartTile, MetadataProps, TimeSeriesChart } from '~/components'; +import { Spacer } from '~/components/base'; +import { TimeframeOption, TimeframeOptionsList } from '@corona-dashboard/common'; +import { useState } from 'react'; +import { ColorMatch, VariantChartValue, VariantDynamicLabels, VariantsOverTimeGraphText } from '~/domain/variants/data-selection/types'; +import { useBarConfig } from '~/domain/variants/logic/use-bar-config'; +import { InteractiveLegend, SelectOption } from '~/components/interactive-legend'; +import { useList } from '~/utils/use-list'; +import { TooltipSeriesList } from '~/components/time-series-chart/components/tooltip/tooltip-series-list'; +import { space } from '~/style/theme'; +import { useCurrentDate } from '~/utils/current-date-context'; +import { reorderAndFilter } from '~/domain/variants/logic/reorder-and-filter'; +import { useIntl } from '~/intl'; + +interface VariantsStackedBarChartTileProps { + title: string; + description: string; + values: VariantChartValue[]; + tooltipLabels: VariantsOverTimeGraphText; + variantLabels: VariantDynamicLabels; + variantColors: ColorMatch[]; + metadata: MetadataProps; +} + +const alwaysEnabled: (keyof VariantChartValue)[] = []; + +/** + * Variant bar chart component + * @param title - Graph title + * @param description - Graph description text + * @param helpText - Explainer text above the interactive legend + * @param values - Data + * @param variantLabels - Mnemonic names for variants + * @param variantColors - Colors for variants + * @param metadata - Metadata block + * @constructor + */ +export const VariantsStackedBarChartTile = ({ title, description, tooltipLabels, values, variantLabels, variantColors, metadata }: VariantsStackedBarChartTileProps) => { + const today = useCurrentDate(); + const { commonTexts } = useIntl(); + const { list, toggle, clear } = useList(alwaysEnabled); + const [variantTimeFrame, setVariantTimeFrame] = useState(TimeframeOption.THIRTY_DAYS); + const barSeriesConfig = useBarConfig(values, variantLabels, tooltipLabels, variantColors, variantTimeFrame, today); + + const text = commonTexts.variants_page; + + const interactiveLegendOptions: SelectOption[] = barSeriesConfig; + + const filteredBarConfig = barSeriesConfig.filter((configItem) => list.includes(configItem.metricProperty) || list.length === 0); + + const hasTwoColumns = list.length === 0 || list.length > 4; + + return ( + + + + (data, interactiveLegendOptions)} hasTwoColumns={hasTwoColumns} />} + /> + + ); +}; diff --git a/packages/app/src/domain/variants/variants-table-tile/variants-table-tile.tsx b/packages/app/src/domain/variants/variants-table-tile.tsx similarity index 60% rename from packages/app/src/domain/variants/variants-table-tile/variants-table-tile.tsx rename to packages/app/src/domain/variants/variants-table-tile.tsx index df622a463c..bd2eb9f760 100644 --- a/packages/app/src/domain/variants/variants-table-tile/variants-table-tile.tsx +++ b/packages/app/src/domain/variants/variants-table-tile.tsx @@ -8,24 +8,19 @@ import { FullscreenChartTile } from '~/components/fullscreen-chart-tile'; import { Markdown } from '~/components/markdown'; import { MetadataProps } from '~/components/metadata'; import { Heading } from '~/components/typography'; -import { VariantRow } from '~/domain/variants/static-props'; import { useIntl } from '~/intl'; -import { space } from '~/style/theme'; +import { fontSizes, space } from '~/style/theme'; import { replaceVariablesInText } from '~/utils/replace-variables-in-text'; -import { VariantsTable } from './components/variants-table'; -import { TableText } from './types'; +import { VariantsTable } from './variants-table-tile/components/variants-table'; +import { TableText } from './variants-table-tile/types'; +import { Tile } from '~/components'; +import { VariantRow } from '~/domain/variants/data-selection/types'; -export function VariantsTableTile({ - text, - noDataMessage = '', - source, - data, - dates, - children = null, -}: { +interface VariantsTableTileProps { text: TableText; noDataMessage?: ReactNode; data?: VariantRow[] | null; + sampleThresholdPassed: boolean; source: { download: string; href: string; @@ -37,7 +32,9 @@ export function VariantsTableTile({ date_of_report_unix: number; }; children?: ReactNode; -}) { +} + +export function VariantsTableTile({ text, noDataMessage = '', sampleThresholdPassed, source, data, dates, children = null }: VariantsTableTileProps) { if (!isPresent(data) || !isPresent(dates)) { return ( @@ -56,21 +53,16 @@ export function VariantsTableTile({ } return ( - + {children} ); } -function VariantsTableTileWithData({ - text, - source, - data, - dates, - children = null, -}: { +interface VariantsTableTileWithDataProps { text: TableText; data: VariantRow[]; + sampleThresholdPassed: boolean; source: { download: string; href: string; @@ -82,7 +74,9 @@ function VariantsTableTileWithData({ date_of_report_unix: number; }; children?: ReactNode; -}) { +} + +function VariantsTableTileWithData({ text, sampleThresholdPassed, source, data, dates, children = null }: VariantsTableTileWithDataProps) { const { formatDateSpan } = useIntl(); const metadata: MetadataProps = { @@ -99,12 +93,25 @@ function VariantsTableTileWithData({ }); return ( - - {children} - - - - + <> + {sampleThresholdPassed ? ( + + {children} + + + + + ) : ( + + + {text.titel} + + + + + + )} + ); } diff --git a/packages/app/src/domain/variants/variants-table-tile/components/narrow-variants-table.tsx b/packages/app/src/domain/variants/variants-table-tile/components/narrow-variants-table.tsx index 46591cdbbc..5dd8740668 100644 --- a/packages/app/src/domain/variants/variants-table-tile/components/narrow-variants-table.tsx +++ b/packages/app/src/domain/variants/variants-table-tile/components/narrow-variants-table.tsx @@ -4,7 +4,6 @@ import styled from 'styled-components'; import { isPresent } from 'ts-is-present'; import { Box } from '~/components/base'; import { InlineText } from '~/components/typography'; -import { VariantRow } from '~/domain/variants/static-props'; import { useIntl } from '~/intl'; import { space } from '~/style/theme'; import { getMaximumNumberOfDecimals } from '~/utils/get-maximum-number-of-decimals'; @@ -12,6 +11,7 @@ import { useCollapsible } from '~/utils/use-collapsible'; import { Cell, HeaderCell, PercentageBarWithNumber, StyledTable, VariantDifference, VariantNameCell } from '.'; import { TableText } from '../types'; import { NoPercentageData } from './no-percentage-data'; +import { VariantRow } from '~/domain/variants/data-selection/types'; interface NarrowVariantsTableProps { rows: VariantRow[]; diff --git a/packages/app/src/domain/variants/variants-table-tile/components/variant-difference.tsx b/packages/app/src/domain/variants/variants-table-tile/components/variant-difference.tsx index 3124720e76..c8e5d50314 100644 --- a/packages/app/src/domain/variants/variants-table-tile/components/variant-difference.tsx +++ b/packages/app/src/domain/variants/variants-table-tile/components/variant-difference.tsx @@ -33,7 +33,7 @@ export const VariantDifference = ({ value, text, isWideTable }: VariantDifferenc { condition: value?.difference > 0, renderingValue: ( - + {formatPercentage(value.difference, options)} {text.verschil.meer} @@ -42,7 +42,7 @@ export const VariantDifference = ({ value, text, isWideTable }: VariantDifferenc { condition: value?.difference < 0, renderingValue: ( - + {formatPercentage(-value.difference, options)} {text.verschil.minder} diff --git a/packages/app/src/domain/variants/variants-table-tile/components/variant-name-cell.tsx b/packages/app/src/domain/variants/variants-table-tile/components/variant-name-cell.tsx index 59567e7993..a58ef0a04b 100644 --- a/packages/app/src/domain/variants/variants-table-tile/components/variant-name-cell.tsx +++ b/packages/app/src/domain/variants/variants-table-tile/components/variant-name-cell.tsx @@ -1,7 +1,7 @@ import { BoldText } from '~/components/typography'; import { Cell } from '.'; import { TableText } from '../types'; -import { VariantCode } from '../../static-props'; +import { VariantCode } from '~/domain/variants/data-selection/types'; type VariantNameCellProps = { variantCode: VariantCode; diff --git a/packages/app/src/domain/variants/variants-table-tile/components/variants-table.tsx b/packages/app/src/domain/variants/variants-table-tile/components/variants-table.tsx index 79143c1368..2f1560a73d 100644 --- a/packages/app/src/domain/variants/variants-table-tile/components/variants-table.tsx +++ b/packages/app/src/domain/variants/variants-table-tile/components/variants-table.tsx @@ -1,8 +1,8 @@ -import { VariantRow } from '~/domain/variants/static-props'; import { useBreakpoints } from '~/utils/use-breakpoints'; import { TableText } from '../types'; import { NarrowVariantsTable } from './narrow-variants-table'; import { WideVariantsTable } from './wide-variants-table'; +import { VariantRow } from '~/domain/variants/data-selection/types'; type VariantsTableProps = { rows: VariantRow[]; @@ -12,13 +12,5 @@ type VariantsTableProps = { export function VariantsTable({ rows, text }: VariantsTableProps) { const breakpoints = useBreakpoints(); - return ( - <> - {breakpoints.sm ? ( - - ) : ( - - )} - - ); + return <>{breakpoints.sm ? : }; } diff --git a/packages/app/src/domain/variants/variants-table-tile/components/wide-variants-table.tsx b/packages/app/src/domain/variants/variants-table-tile/components/wide-variants-table.tsx index 2f53067af4..c9fb40f998 100644 --- a/packages/app/src/domain/variants/variants-table-tile/components/wide-variants-table.tsx +++ b/packages/app/src/domain/variants/variants-table-tile/components/wide-variants-table.tsx @@ -2,12 +2,12 @@ import { DifferenceDecimal } from '@corona-dashboard/common'; import { useMemo } from 'react'; import { isPresent } from 'ts-is-present'; import { Box } from '~/components/base'; -import { VariantRow } from '~/domain/variants/static-props'; import { useIntl } from '~/intl'; import { getMaximumNumberOfDecimals } from '~/utils/get-maximum-number-of-decimals'; import { Cell, HeaderCell, PercentageBarWithNumber, StyledTable, VariantDifference, VariantNameCell } from '.'; import { TableText } from '../types'; import { NoPercentageData } from './no-percentage-data'; +import { VariantRow } from '~/domain/variants/data-selection/types'; const columnKeys = ['variant_titel', 'percentage', 'vorige_meting'] as const; diff --git a/packages/app/src/domain/variants/variants-table-tile/index.ts b/packages/app/src/domain/variants/variants-table-tile/index.ts deleted file mode 100644 index 292e200747..0000000000 --- a/packages/app/src/domain/variants/variants-table-tile/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './variants-table-tile'; diff --git a/packages/app/src/domain/variants/variants-table-tile/types.ts b/packages/app/src/domain/variants/variants-table-tile/types.ts index 3f19cd5577..9132720e62 100644 --- a/packages/app/src/domain/variants/variants-table-tile/types.ts +++ b/packages/app/src/domain/variants/variants-table-tile/types.ts @@ -1,4 +1,4 @@ -export type VariantDynamicLabels = Record; +import { VariantDynamicLabels } from '~/domain/variants/data-selection/types'; export type TableText = { anderen_tooltip: string; diff --git a/packages/app/src/next-config/redirects/redirects.js b/packages/app/src/next-config/redirects/redirects.js index 4c738c90bf..90ef7ac550 100644 --- a/packages/app/src/next-config/redirects/redirects.js +++ b/packages/app/src/next-config/redirects/redirects.js @@ -110,6 +110,12 @@ async function redirects() { destination: '/landelijk/infectieradar', permanent: true, }, + // Redirect for COR-1830 + { + source: '/landelijk/vaccinaties', + destination: '/landelijk/de-coronaprik', + permanent: true, + }, ]; } diff --git a/packages/app/src/pages/gemeente/[code]/vaccinaties.tsx b/packages/app/src/pages/gemeente/[code]/de-coronaprik.tsx similarity index 98% rename from packages/app/src/pages/gemeente/[code]/vaccinaties.tsx rename to packages/app/src/pages/gemeente/[code]/de-coronaprik.tsx index b644a01b25..35f209e4bf 100644 --- a/packages/app/src/pages/gemeente/[code]/vaccinaties.tsx +++ b/packages/app/src/pages/gemeente/[code]/de-coronaprik.tsx @@ -181,7 +181,7 @@ export const VaccinationsGmPage = (props: StaticProps) => /> ) => )} ) { reverseRouter.gm.vaccinaties(gmcode), isPercentage: true }} + dataOptions={{ getLink: (gmcode) => reverseRouter.gm.deCoronaprik(gmcode), isPercentage: true }} text={{ title: commonTexts.choropleth.choropleth_vaccination_coverage.nl.archived.fully_vaccinated.title, description: commonTexts.choropleth.choropleth_vaccination_coverage.nl.archived.fully_vaccinated.description, @@ -352,7 +352,6 @@ function VaccinationPage(props: StaticProps) { ) { datumsText: textNl.dates, date: archivedData.vaccine_campaigns_archived_20231004.date_unix, source: textNl.vaccine_campaigns.bronnen.rivm, + disclaimer: textNl.vaccine_campaigns.description_footer, }} /> reverseRouter.gm.vaccinaties(gmcode), isPercentage: true }} + dataOptions={{ getLink: (gmcode) => reverseRouter.gm.deCoronaprik(gmcode), isPercentage: true }} text={{ title: commonTexts.choropleth.choropleth_vaccination_coverage.nl.archived.autumn_2022.title, description: commonTexts.choropleth.choropleth_vaccination_coverage.nl.archived.autumn_2022.description, @@ -427,7 +427,6 @@ function VaccinationPage(props: StaticProps) { ) { datumsText: textNl.dates, date: archivedData.vaccine_campaigns_archived_20220908.date_unix, source: textNl.vaccine_campaigns.bronnen.rivm, + disclaimer: textNl.vaccine_campaigns.description_footer, }} /> diff --git a/packages/app/src/pages/landelijk/infectieradar.tsx b/packages/app/src/pages/landelijk/infectieradar.tsx index d36eb46400..ebc68fda33 100644 --- a/packages/app/src/pages/landelijk/infectieradar.tsx +++ b/packages/app/src/pages/landelijk/infectieradar.tsx @@ -22,6 +22,8 @@ import { ArticleParts, PagePartQueryResult } from '~/types/cms'; import { useDynamicLokalizeTexts } from '~/utils/cms/use-dynamic-lokalize-texts'; import { getLastInsertionDateOfPage } from '~/utils/get-last-insertion-date-of-page'; import { getPageInformationHeaderContent } from '~/utils/get-page-information-header-content'; +import { KpiTile, KpiValue, TwoKpiSection } from '~/components'; +import { replaceVariablesInText } from '~/utils'; const pageMetrics = ['self_test_overall', 'infection_radar_symptoms_per_age_group']; @@ -35,7 +37,7 @@ type LokalizeTexts = ReturnType; export const getStaticProps = createGetStaticProps( ({ locale }: { locale: keyof Languages }) => getLokalizeTexts(selectLokalizeTexts, locale), getLastGeneratedDate, - selectNlData('self_test_overall', 'infectionradar_symptoms_trend_per_age_group_weekly'), + selectNlData('difference.self_test_overall', 'self_test_overall', 'infectionradar_symptoms_trend_per_age_group_weekly'), async (context: GetStaticPropsContext) => { const { content } = await createGetContent<{ parts: PagePartQueryResult; @@ -69,6 +71,8 @@ const InfectionRadar = (props: StaticProps) => { const { metadataTexts, textNl } = useDynamicLokalizeTexts(pageText, selectLokalizeTexts); + const totalInfectedPercentage = data.self_test_overall.last_value.infected_percentage ? data.self_test_overall.last_value.infected_percentage : 0; + const metadata = { ...metadataTexts, title: textNl.metadata.title, @@ -102,6 +106,31 @@ const InfectionRadar = (props: StaticProps) => { })} /> + + + + + + + + + ; export const getStaticProps = createGetStaticProps( ({ locale }: { locale: keyof Languages }) => getLokalizeTexts(selectLokalizeTexts, locale), selectNlData('variants', 'named_difference'), + selectArchivedNlData('variants_archived_20231101'), getLastGeneratedDate, () => { const data = selectNlData('variants', 'named_difference')(); + const archivedData = selectArchivedNlData('variants_archived_20231101')(); const { selectedNlData: { variants }, } = data; + const { + selectedArchivedNlData: { variants_archived_20231101 }, + } = archivedData; + const variantColors = getVariantOrderColors(variants); return { ...getVariantTableData(variants, data.selectedNlData.named_difference, variantColors), - ...getVariantChartData(variants), + ...getVariantBarChartData(variants), + ...getArchivedVariantChartData(variants_archived_20231101), variantColors, }; }, @@ -64,10 +73,22 @@ export const getStaticProps = createGetStaticProps( ); export default function CovidVariantenPage(props: StaticProps) { - const { pageText, selectedNlData: data, lastGenerated, content, variantTable, variantChart, variantColors, dates } = props; + const { + pageText, + selectedNlData: data, + selectedArchivedNlData: archivedData, + lastGenerated, + content, + variantTable, + variantChart, + archivedVariantChart, + variantColors, + dates, + } = props; const { commonTexts, locale } = useIntl(); const { metadataTexts, textNl } = useDynamicLokalizeTexts(pageText, selectLokalizeTexts); + const [isArchivedContentShown, setIsArchivedContentShown] = useState(false); const metadata = { ...metadataTexts, @@ -77,8 +98,20 @@ export default function CovidVariantenPage(props: StaticProps + currentVariant.last_value.occurrence > 0 && currentVariant.variant_code !== 'other_variants' ? 1 + accumulator : accumulator, + 0 + ) + : NaN; + + const sampleThresholdPassed = data.variants ? data.variants!.values[0].last_value.sample_size > 100 : false; + const variantLabels: VariantDynamicLabels = {}; + const variantenTableDescription = sampleThresholdPassed ? textNl.varianten_omschrijving : textNl.varianten_tabel.omschrijving_te_weinig_samples; + data.variants?.values.forEach((variant) => { variantLabels[`${variant.variant_code}`] = locale === 'nl' ? variant.values[0].label_nl : variant.values[0].label_en; }); @@ -96,8 +129,8 @@ export default function CovidVariantenPage(props: StaticProps + + {variantChart && variantLabels && ( - )} + + setIsArchivedContentShown(!isArchivedContentShown)} + /> + + {isArchivedContentShown && ( + <> + {archivedVariantChart && variantLabels && ( + + )} + + )} diff --git a/packages/app/src/pages/landelijk/ziekenhuizen-in-beeld.tsx b/packages/app/src/pages/landelijk/ziekenhuizen-in-beeld.tsx index c98f0d2fa0..cb1bf25a23 100644 --- a/packages/app/src/pages/landelijk/ziekenhuizen-in-beeld.tsx +++ b/packages/app/src/pages/landelijk/ziekenhuizen-in-beeld.tsx @@ -106,7 +106,8 @@ const HospitalsAndCarePage = (props: StaticProps) => { const hospitalLastValue = getLastFilledValue(data.hospital_lcps); const icuLastValue = getLastFilledValue(data.intensive_care_lcps); - const valuesWithoutDateRange = data.hospital_lcps.values.map((value) => ({ ...value, date_end_unix: undefined, date_start_unix: undefined })); + const lcpsHospitalWithoutRange = data.hospital_lcps.values.map((value) => ({ ...value, date_end_unix: undefined, date_start_unix: undefined })); + const lcpsICWithoutRange = data.intensive_care_lcps.values.map((value) => ({ ...value, date_end_unix: undefined, date_start_unix: undefined })); const lastInsertionDateOfPage = getLastInsertionDateOfPage(data, pageMetrics); @@ -173,7 +174,7 @@ const HospitalsAndCarePage = (props: StaticProps) => { accessibility={{ key: 'hospital_beds_occupied_over_time_chart', }} - values={valuesWithoutDateRange} + values={lcpsHospitalWithoutRange} timeframe={hospitalBedsOccupiedOverTimeTimeframe} forceLegend seriesConfig={[ @@ -225,7 +226,7 @@ const HospitalsAndCarePage = (props: StaticProps) => { accessibility={{ key: 'intensive_care_beds_occupied_over_time_chart', }} - values={data.intensive_care_lcps.values} + values={lcpsICWithoutRange} timeframe={intensiveCareBedsTimeframe} forceLegend seriesConfig={[ @@ -254,6 +255,7 @@ const HospitalsAndCarePage = (props: StaticProps) => { }, ], timelineEvents: getTimelineEvents(content.elements.timeSeries, 'intensive_care_lcps', 'beds_occupied_covid'), + useDatesAsRange: false, }} /> @@ -296,7 +298,7 @@ const HospitalsAndCarePage = (props: StaticProps) => { accessibility={{ key: 'hospital_patient_influx_over_time_chart', }} - values={trimLeadingNullValues(valuesWithoutDateRange, 'influx_covid_patients')} + values={trimLeadingNullValues(lcpsHospitalWithoutRange, 'influx_covid_patients')} timeframe={hospitalPatientInfluxOverTimeTimeframe} seriesConfig={[ { @@ -337,7 +339,7 @@ const HospitalsAndCarePage = (props: StaticProps) => { accessibility={{ key: 'intensive_care_patient_influx_over_time_chart', }} - values={trimLeadingNullValues(data.intensive_care_lcps.values, 'influx_covid_patients')} + values={trimLeadingNullValues(lcpsICWithoutRange, 'influx_covid_patients')} timeframe={intensiveCarePatientInfluxOverTimeTimeframe} seriesConfig={[ { diff --git a/packages/app/src/utils/__tests__/use-reverse-router.spec.tsx b/packages/app/src/utils/__tests__/use-reverse-router.spec.tsx index 2b5b38be59..712ed87f35 100644 --- a/packages/app/src/utils/__tests__/use-reverse-router.spec.tsx +++ b/packages/app/src/utils/__tests__/use-reverse-router.spec.tsx @@ -78,8 +78,8 @@ UseReverseRouter("indexes should 'redirect' to child pages", () => { const nlDiv = result.getByTestId('nl'); const gmDiv = result.getByTestId('gm'); - assert.equal(nlDiv.textContent?.endsWith('/vaccinaties'), true); - assert.equal(gmDiv.textContent?.endsWith('/vaccinaties'), true); + assert.equal(nlDiv.textContent?.endsWith('/deCoronaprik'), true); + assert.equal(gmDiv.textContent?.endsWith('/deCoronaprik'), true); }); UseReverseRouter('GM routes should have the GM code in them', () => { diff --git a/packages/cms/src/lokalize/key-mutations.csv b/packages/cms/src/lokalize/key-mutations.csv index a7c6419a8a..4aefeb342d 100644 --- a/packages/cms/src/lokalize/key-mutations.csv +++ b/packages/cms/src/lokalize/key-mutations.csv @@ -1 +1,27 @@ timestamp,action,key,document_id,move_to +2023-10-16T15:02:56.856Z,add,pages.vaccinations_page.nl.vaccine_campaigns.campaigns.autumn_round_corona_vaccination_2022_description,tWOr2ZtVUADiKBNT0r4Txl,__ +2023-10-16T15:02:56.857Z,delete,pages.vaccinations_page.nl.vaccine_campaigns.campaigns.repeat_vaccination_against_corona_description,DNO5adeD4mWNc516AbctlW,__ +2023-10-20T09:31:18.217Z,add,pages.variants_page.nl.section_archived.title,093FZrW2Ae4fRiIdZYDcnH,__ +2023-10-20T09:31:19.500Z,add,pages.variants_page.nl.section_archived.description,hT5k3RDQ7JafeiQP6wRLNE,__ +2023-10-20T09:31:20.726Z,add,pages.variants_page.nl.varianten_barchart.titel,093FZrW2Ae4fRiIdZYDd0v,__ +2023-10-20T09:31:21.806Z,add,pages.variants_page.nl.varianten_barchart.description,ZkwHqMQjnsmR1ekP50NJ35,__ +2023-10-20T09:31:22.794Z,add,pages.variants_page.nl.kpi_amount_of_samples.kpi_tile_title,ZkwHqMQjnsmR1ekP50NJ65,__ +2023-10-20T09:31:23.837Z,add,pages.variants_page.nl.kpi_amount_of_samples.kpi_tile_description,093FZrW2Ae4fRiIdZYDdEZ,__ +2023-10-20T09:31:24.874Z,add,pages.variants_page.nl.kpi_amount_of_samples.tile_total_samples.title,ZkwHqMQjnsmR1ekP50NJ95,__ +2023-10-20T09:31:25.881Z,add,pages.variants_page.nl.kpi_amount_of_samples.tile_total_samples.description,hT5k3RDQ7JafeiQP6wRLY1,__ +2023-10-20T09:31:26.938Z,add,pages.variants_page.nl.kpi_amount_of_samples.tile_total_variants.title,pmfpKotscgjR5sJqbeu52i,__ +2023-10-20T09:31:28.284Z,add,pages.variants_page.nl.kpi_amount_of_samples.tile_total_variants.description,hT5k3RDQ7JafeiQP6wRLio,__ +2023-10-20T09:31:29.643Z,add,pages.variants_page.nl.kpi_amount_of_samples.disclaimer,w5vHLm19hF0S5wj1e5Ryjx,__ +2023-10-20T09:31:30.622Z,add,pages.variants_page.nl.varianten_tabel.omschrijving_te_weinig_samples,w5vHLm19hF0S5wj1e5RyoG,__ +2023-10-23T15:10:02.545Z,add,common.sidebar.metrics.the_corona_vaccine.title,ZkwHqMQjnsmR1ekP50kkn5,__ +2023-10-23T15:10:02.545Z,delete,common.sidebar.metrics.vaccinations.title,wzQp83DAUqyqV3ewG9xYUn,__ +2023-10-24T11:27:36.857Z,add,common.variants_page.legend_help_text,HZiiX43HLlkWORF4EMynAL,__ +2023-10-24T11:27:37.696Z,add,common.variants_page.bar_chart_legend_inaccurate,HZiiX43HLlkWORF4EMynLf,__ +2023-10-24T11:27:38.726Z,add,common.variants_page.tooltip_labels.innacurate,3Jkha9OztzmUWKfWeflWKk,__ +2023-10-24T11:27:38.726Z,delete,common.test_key:,itix6KXAqiuTrWiucCRIit,__ +2023-10-24T11:27:38.727Z,delete,__root.test_123,0hrRKce5hYl5O3WpoZ6oAl,__ +2023-10-24T11:27:38.727Z,delete,__root.test_key_345,0hrRKce5hYl5O3WpoZ6nXX,__ +2023-10-27T14:07:39.563Z,add,pages.infectie_radar_page.nl.kpi_tile.infected_participants_percentage.title,5if6OpiB3iy3C5t2bwoaWw,__ +2023-10-27T14:07:40.526Z,add,pages.infectie_radar_page.nl.kpi_tile.infected_participants_percentage.description,pCa5dcpJ9eiic3eFSI81Ea,__ +2023-10-27T14:07:41.538Z,add,pages.infectie_radar_page.nl.kpi_tile.total_participants.title,XnRo9vzZlQx15CHPE0aA2y,__ +2023-10-27T14:07:42.530Z,add,pages.infectie_radar_page.nl.kpi_tile.total_participants.description,5if6OpiB3iy3C5t2bwoaba,__ diff --git a/packages/cms/src/studio/data/data-structure.ts b/packages/cms/src/studio/data/data-structure.ts index 3c2c8b2b04..9a2ffa395b 100644 --- a/packages/cms/src/studio/data/data-structure.ts +++ b/packages/cms/src/studio/data/data-structure.ts @@ -261,6 +261,7 @@ export const dataStructure = { 'janssen_not_available', 'janssen_total', ], + variants_archived_20231101: ['variant_code', 'values', 'last_value'], repeating_shot_administered_20220713: ['ggd_administered_total'], corona_melder_app_warning_archived_20220421: ['count'], corona_melder_app_download_archived_20220421: ['count'], diff --git a/packages/common/src/data-sorting.ts b/packages/common/src/data-sorting.ts index 6e167d259a..7198de5eb5 100644 --- a/packages/common/src/data-sorting.ts +++ b/packages/common/src/data-sorting.ts @@ -1,5 +1,5 @@ import { isDefined } from 'ts-is-present'; -import { GmSewerPerInstallationValue, NlVariantsVariantValue } from './types'; +import { ArchivedNlVariantsVariantValue, GmSewerPerInstallationValue, NlVariantsVariantValue } from './types'; export type UnknownObject = Record; @@ -84,40 +84,49 @@ export function sortTimeSeriesInDataInPlace(data: T, { setDatesToMiddleOfDay * The variants data is structured similarly to sewer_per_installation as * shown above. @TODO unify/clean up validation of both. */ - if (isDefined((data as UnknownObject).variants)) { - const nestedSeries = (data as UnknownObject).variants as VariantsData; + if (isDefined((data as UnknownObject).variants) || isDefined((data as UnknownObject).variants_archived_20231101)) { + let nestedSeries; - if (!nestedSeries.values) { - /** - * It can happen that we get incomplete json data and assuming that values - * exists here might crash the app - */ - console.error('variants.values does not exist'); - return; + if (isDefined((data as UnknownObject).variants)) { + nestedSeries = (data as UnknownObject).variants as VariantsData; + } + if (isDefined((data as UnknownObject).variants_archived_20231101)) { + nestedSeries = (data as UnknownObject).variants_archived_20231101 as VariantsData; } - nestedSeries.values = nestedSeries.values.map((x, index) => { - if (!x.values) { + if (nestedSeries) { + if (!nestedSeries.values) { /** * It can happen that we get incomplete json data and assuming that values * exists here might crash the app */ - console.error(`variants.nestedSeries.values[${index}].values does not exist`); - return x; + console.error('variants.values does not exist'); + return; } - x.values = sortTimeSeriesValues(x.values) as NlVariantsVariantValue[]; + nestedSeries.values = nestedSeries.values.map((x, index) => { + if (!x.values) { + /** + * It can happen that we get incomplete json data and assuming that values + * exists here might crash the app + */ + console.error(`variants.nestedSeries.values[${index}].values does not exist`); + return x; + } - if (setDatesToMiddleOfDay) { - x.values = x.values.map(setValueDatesToMiddleOfDay); + x.values = sortTimeSeriesValues(x.values) as ArchivedNlVariantsVariantValue[]; - if (x.last_value) { - x.last_value = setValueDatesToMiddleOfDay(x.last_value); + if (setDatesToMiddleOfDay) { + x.values = x.values.map(setValueDatesToMiddleOfDay); + + if (x.last_value) { + x.last_value = setValueDatesToMiddleOfDay(x.last_value); + } } - } - return x; - }); + return x; + }); + } } } diff --git a/packages/common/src/data/reverse-router.ts b/packages/common/src/data/reverse-router.ts index a784920999..74caf6a4dc 100644 --- a/packages/common/src/data/reverse-router.ts +++ b/packages/common/src/data/reverse-router.ts @@ -16,7 +16,7 @@ export function getReverseRouter(isMobile: boolean) { nl: { index: () => (isMobile ? '/landelijk' : reverseRouter.nl.rioolwater()), - vaccinaties: () => '/landelijk/vaccinaties', + deCoronaprik: () => '/landelijk/de-coronaprik', positieveTesten: () => '/landelijk/positieve-testen', infectieradar: () => '/landelijk/infectieradar', besmettelijkeMensen: () => '/landelijk/besmettelijke-mensen', @@ -43,7 +43,7 @@ export function getReverseRouter(isMobile: boolean) { sterfte: (code: string) => `/gemeente/${code}/sterfte`, ziekenhuisopnames: (code: string) => `/gemeente/${code}/ziekenhuis-opnames`, rioolwater: (code: string) => `/gemeente/${code}/rioolwater`, - vaccinaties: (code: string) => `/gemeente/${code}/vaccinaties`, + deCoronaprik: (code: string) => `/gemeente/${code}/de-coronaprik`, }, } as const; diff --git a/packages/common/src/types/data.ts b/packages/common/src/types/data.ts index bda448e74c..44a4c409f1 100644 --- a/packages/common/src/types/data.ts +++ b/packages/common/src/types/data.ts @@ -231,6 +231,7 @@ export interface ArchivedNl { vaccine_coverage_per_age_group_estimated_fully_vaccinated_archived_20231004: NlVaccineCoveragePerAgeGroupEstimatedFullyVaccinatedValue; vaccine_delivery_per_supplier_archived_20211101: ArchivedNlVaccineDeliveryPerSupplier; vaccine_stock_archived_20211024: ArchivedNlVaccineStock; + variants_archived_20231101: ArchivedNlVariants; repeating_shot_administered_20220713: ArchivedNlRepeatingShotAdministered; corona_melder_app_warning_archived_20220421: ArchivedNlCoronaMelderAppWarning; corona_melder_app_download_archived_20220421: ArchivedNlCoronaMelderAppDownload; @@ -833,6 +834,26 @@ export interface ArchivedNlVaccineStockValue { date_of_insertion_unix: number; date_unix: number; } +export interface ArchivedNlVariants { + values: ArchivedNlVariantsVariant[]; +} +export interface ArchivedNlVariantsVariant { + variant_code: string; + values: ArchivedNlVariantsVariantValue[]; + last_value: ArchivedNlVariantsVariantValue; +} +export interface ArchivedNlVariantsVariantValue { + order: number; + occurrence: number; + percentage: number; + sample_size: number; + date_start_unix: number; + date_end_unix: number; + date_of_insertion_unix: number; + date_of_report_unix: number; + label_nl: string; + label_en: string; +} export interface ArchivedNlRepeatingShotAdministered { values: ArchivedNlRepeatingShotAdministeredValue[]; last_value: ArchivedNlRepeatingShotAdministeredValue; @@ -1031,7 +1052,7 @@ export interface Nl { deceased_cbs: NlDeceasedCbs; vaccine_administered_last_timeframe: NlVaccineAdministeredLastTimeframe; vaccine_campaigns: NlVaccineCampaign; - variants?: NlVariants; + variants: NlVariants; self_test_overall: NlSelfTestOverall; infectionradar_symptoms_trend_per_age_group_weekly: NlInfectionradarSymptomsTrendPerAgeGroupWeekly; } @@ -1044,6 +1065,7 @@ export interface NlDifference { sewer__average: DifferenceInteger; reproduction__index_average?: DifferenceDecimal; vulnerable_hospital_admissions?: DifferenceInteger; + self_test_overall: DifferenceDecimal; } export interface DifferenceInteger { old_value: number; @@ -1231,6 +1253,7 @@ export interface NlSelfTestOverall { } export interface NlSelfTestOverallValue { infected_percentage: number | null; + n_participants_total_unfiltered: number; date_start_unix: number; date_end_unix: number; date_of_insertion_unix: number; diff --git a/yarn.lock b/yarn.lock index e7f23b7991..99c9d593cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -90,6 +90,16 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.22.13": + version: 7.22.13 + resolution: "@babel/code-frame@npm:7.22.13" + dependencies: + "@babel/highlight": ^7.22.13 + chalk: ^2.4.2 + checksum: 22e342c8077c8b77eeb11f554ecca2ba14153f707b85294fcf6070b6f6150aae88a7b7436dd88d8c9289970585f3fe5b9b941c5aa3aa26a6d5a8ef3f292da058 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.13.11, @babel/compat-data@npm:^7.15.0": version: 7.15.0 resolution: "@babel/compat-data@npm:7.15.0" @@ -215,7 +225,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.15.4, @babel/generator@npm:^7.15.8": +"@babel/generator@npm:^7.15.8": version: 7.15.8 resolution: "@babel/generator@npm:7.15.8" dependencies: @@ -249,6 +259,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/generator@npm:7.23.0" + dependencies: + "@babel/types": ^7.23.0 + "@jridgewell/gen-mapping": ^0.3.2 + "@jridgewell/trace-mapping": ^0.3.17 + jsesc: ^2.5.1 + checksum: 8efe24adad34300f1f8ea2add420b28171a646edc70f2a1b3e1683842f23b8b7ffa7e35ef0119294e1901f45bfea5b3dc70abe1f10a1917ccdfb41bed69be5f1 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.14.5, @babel/helper-annotate-as-pure@npm:^7.15.4": version: 7.15.4 resolution: "@babel/helper-annotate-as-pure@npm:7.15.4" @@ -495,6 +517,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-environment-visitor@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-environment-visitor@npm:7.22.20" + checksum: d80ee98ff66f41e233f36ca1921774c37e88a803b2f7dca3db7c057a5fea0473804db9fb6729e5dbfd07f4bed722d60f7852035c2c739382e84c335661590b69 + languageName: node + linkType: hard + "@babel/helper-explode-assignable-expression@npm:^7.15.4": version: 7.15.4 resolution: "@babel/helper-explode-assignable-expression@npm:7.15.4" @@ -545,6 +574,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-function-name@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-function-name@npm:7.23.0" + dependencies: + "@babel/template": ^7.22.15 + "@babel/types": ^7.23.0 + checksum: e44542257b2d4634a1f979244eb2a4ad8e6d75eb6761b4cfceb56b562f7db150d134bc538c8e6adca3783e3bc31be949071527aa8e3aab7867d1ad2d84a26e10 + languageName: node + linkType: hard + "@babel/helper-get-function-arity@npm:^7.15.4": version: 7.15.4 resolution: "@babel/helper-get-function-arity@npm:7.15.4" @@ -590,6 +629,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-hoist-variables@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-hoist-variables@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: 394ca191b4ac908a76e7c50ab52102669efe3a1c277033e49467913c7ed6f7c64d7eacbeabf3bed39ea1f41731e22993f763b1edce0f74ff8563fd1f380d92cc + languageName: node + linkType: hard + "@babel/helper-member-expression-to-functions@npm:^7.15.4": version: 7.15.4 resolution: "@babel/helper-member-expression-to-functions@npm:7.15.4" @@ -899,6 +947,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-split-export-declaration@npm:^7.22.6": + version: 7.22.6 + resolution: "@babel/helper-split-export-declaration@npm:7.22.6" + dependencies: + "@babel/types": ^7.22.5 + checksum: e141cace583b19d9195f9c2b8e17a3ae913b7ee9b8120246d0f9ca349ca6f03cb2c001fd5ec57488c544347c0bb584afec66c936511e447fd20a360e591ac921 + languageName: node + linkType: hard + "@babel/helper-string-parser@npm:^7.21.5": version: 7.21.5 resolution: "@babel/helper-string-parser@npm:7.21.5" @@ -906,6 +963,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-string-parser@npm:7.22.5" + checksum: 836851ca5ec813077bbb303acc992d75a360267aa3b5de7134d220411c852a6f17de7c0d0b8c8dcc0f567f67874c00f4528672b2a4f1bc978a3ada64c8c78467 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.14.5, @babel/helper-validator-identifier@npm:^7.14.9, @babel/helper-validator-identifier@npm:^7.15.7": version: 7.15.7 resolution: "@babel/helper-validator-identifier@npm:7.15.7" @@ -920,6 +984,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-validator-identifier@npm:7.22.20" + checksum: 136412784d9428266bcdd4d91c32bcf9ff0e8d25534a9d94b044f77fe76bc50f941a90319b05aafd1ec04f7d127cd57a179a3716009ff7f3412ef835ada95bdc + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-validator-option@npm:7.14.5" @@ -1036,6 +1107,17 @@ __metadata: languageName: node linkType: hard +"@babel/highlight@npm:^7.22.13": + version: 7.22.20 + resolution: "@babel/highlight@npm:7.22.20" + dependencies: + "@babel/helper-validator-identifier": ^7.22.20 + chalk: ^2.4.2 + js-tokens: ^4.0.0 + checksum: 84bd034dca309a5e680083cd827a766780ca63cef37308404f17653d32366ea76262bd2364b2d38776232f2d01b649f26721417d507e8b4b6da3e4e739f6d134 + languageName: node + linkType: hard + "@babel/parser@npm:^7.10.3, @babel/parser@npm:^7.15.4, @babel/parser@npm:^7.15.8": version: 7.15.8 resolution: "@babel/parser@npm:7.15.8" @@ -1045,7 +1127,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.16.0, @babel/parser@npm:^7.16.3": +"@babel/parser@npm:^7.16.0": version: 7.16.3 resolution: "@babel/parser@npm:7.16.3" bin: @@ -1054,7 +1136,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.5, @babel/parser@npm:^7.21.8": +"@babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.8": version: 7.21.8 resolution: "@babel/parser@npm:7.21.8" bin: @@ -1063,6 +1145,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/parser@npm:7.23.0" + bin: + parser: ./bin/babel-parser.js + checksum: 453fdf8b9e2c2b7d7b02139e0ce003d1af21947bbc03eb350fb248ee335c9b85e4ab41697ddbdd97079698de825a265e45a0846bb2ed47a2c7c1df833f42a354 + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.16.2": version: 7.16.2 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.16.2" @@ -3643,55 +3734,32 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.10.3, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.15.4, @babel/traverse@npm:^7.4.5": - version: 7.15.4 - resolution: "@babel/traverse@npm:7.15.4" +"@babel/template@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/template@npm:7.22.15" dependencies: - "@babel/code-frame": ^7.14.5 - "@babel/generator": ^7.15.4 - "@babel/helper-function-name": ^7.15.4 - "@babel/helper-hoist-variables": ^7.15.4 - "@babel/helper-split-export-declaration": ^7.15.4 - "@babel/parser": ^7.15.4 - "@babel/types": ^7.15.4 - debug: ^4.1.0 - globals: ^11.1.0 - checksum: 831506a92c8ed76dc60504de37663bf5a553d7b1b009a94defc082cddb6c380c5487a1aa9438bcd7b9891a2a72758a63e4f878154aa70699d09b388b1445d774 + "@babel/code-frame": ^7.22.13 + "@babel/parser": ^7.22.15 + "@babel/types": ^7.22.15 + checksum: 1f3e7dcd6c44f5904c184b3f7fe280394b191f2fed819919ffa1e529c259d5b197da8981b6ca491c235aee8dbad4a50b7e31304aa531271cb823a4a24a0dd8fd languageName: node linkType: hard -"@babel/traverse@npm:^7.16.0, @babel/traverse@npm:^7.16.3": - version: 7.16.3 - resolution: "@babel/traverse@npm:7.16.3" +"@babel/traverse@npm:^7.10.3, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.15.4, @babel/traverse@npm:^7.16.0, @babel/traverse@npm:^7.16.3, @babel/traverse@npm:^7.19.0, @babel/traverse@npm:^7.20.5, @babel/traverse@npm:^7.21.5, @babel/traverse@npm:^7.4.5": + version: 7.23.2 + resolution: "@babel/traverse@npm:7.23.2" dependencies: - "@babel/code-frame": ^7.16.0 - "@babel/generator": ^7.16.0 - "@babel/helper-function-name": ^7.16.0 - "@babel/helper-hoist-variables": ^7.16.0 - "@babel/helper-split-export-declaration": ^7.16.0 - "@babel/parser": ^7.16.3 - "@babel/types": ^7.16.0 + "@babel/code-frame": ^7.22.13 + "@babel/generator": ^7.23.0 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-function-name": ^7.23.0 + "@babel/helper-hoist-variables": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.6 + "@babel/parser": ^7.23.0 + "@babel/types": ^7.23.0 debug: ^4.1.0 globals: ^11.1.0 - checksum: abb14857b1104c73124612954865e28f95a86eb6741f35851369b4f9eabc17e394c9aa6f21fba6ce23813592353090d409772be828717cbe5154a5e981a753c1 - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.19.0, @babel/traverse@npm:^7.20.5, @babel/traverse@npm:^7.21.5": - version: 7.21.5 - resolution: "@babel/traverse@npm:7.21.5" - dependencies: - "@babel/code-frame": ^7.21.4 - "@babel/generator": ^7.21.5 - "@babel/helper-environment-visitor": ^7.21.5 - "@babel/helper-function-name": ^7.21.0 - "@babel/helper-hoist-variables": ^7.18.6 - "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/parser": ^7.21.5 - "@babel/types": ^7.21.5 - debug: ^4.1.0 - globals: ^11.1.0 - checksum: b403733fa7d858f0c8e224f0434a6ade641bc469a4f92975363391e796629d5bf53e544761dfe85039aab92d5389ebe7721edb309d7a5bb7df2bf74f37bf9f47 + checksum: 26a1eea0dde41ab99dde8b9773a013a0dc50324e5110a049f5d634e721ff08afffd54940b3974a20308d7952085ac769689369e9127dea655f868c0f6e1ab35d languageName: node linkType: hard @@ -3726,6 +3794,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/types@npm:7.23.0" + dependencies: + "@babel/helper-string-parser": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.20 + to-fast-properties: ^2.0.0 + checksum: 215fe04bd7feef79eeb4d33374b39909ce9cad1611c4135a4f7fdf41fe3280594105af6d7094354751514625ea92d0875aba355f53e86a92600f290e77b0e604 + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -3954,7 +4033,7 @@ __metadata: node-mocks-http: ^1.11.0 npm-run-all: ^4.1.5 polished: ^4.1.3 - postcss: ^8.4.4 + postcss: ^8.4.31 postcss-flexbugs-fixes: ^5.0.2 postcss-preset-env: ^6.7.0 react: ^17.0.2 @@ -13148,13 +13227,20 @@ __metadata: languageName: node linkType: hard -"bn.js@npm:^5.0.0, bn.js@npm:^5.1.1": +"bn.js@npm:^5.0.0": version: 5.2.0 resolution: "bn.js@npm:5.2.0" checksum: 6117170393200f68b35a061ecbf55d01dd989302e7b3c798a3012354fa638d124f0b2f79e63f77be5556be80322a09c40339eda6413ba7468524c0b6d4b4cb7a languageName: node linkType: hard +"bn.js@npm:^5.2.1": + version: 5.2.1 + resolution: "bn.js@npm:5.2.1" + checksum: 3dd8c8d38055fedfa95c1d5fc3c99f8dd547b36287b37768db0abab3c239711f88ff58d18d155dd8ad902b0b0cee973747b7ae20ea12a09473272b0201c9edd3 + languageName: node + linkType: hard + "body-parser@npm:1.19.0": version: 1.19.0 resolution: "body-parser@npm:1.19.0" @@ -13338,7 +13424,7 @@ __metadata: languageName: node linkType: hard -"browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.0.1": +"browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.1.0": version: 4.1.0 resolution: "browserify-rsa@npm:4.1.0" dependencies: @@ -13349,19 +13435,19 @@ __metadata: linkType: hard "browserify-sign@npm:^4.0.0": - version: 4.2.1 - resolution: "browserify-sign@npm:4.2.1" + version: 4.2.2 + resolution: "browserify-sign@npm:4.2.2" dependencies: - bn.js: ^5.1.1 - browserify-rsa: ^4.0.1 + bn.js: ^5.2.1 + browserify-rsa: ^4.1.0 create-hash: ^1.2.0 create-hmac: ^1.1.7 - elliptic: ^6.5.3 + elliptic: ^6.5.4 inherits: ^2.0.4 - parse-asn1: ^5.1.5 - readable-stream: ^3.6.0 - safe-buffer: ^5.2.0 - checksum: 0221f190e3f5b2d40183fa51621be7e838d9caa329fe1ba773406b7637855f37b30f5d83e52ff8f244ed12ffe6278dd9983638609ed88c841ce547e603855707 + parse-asn1: ^5.1.6 + readable-stream: ^3.6.2 + safe-buffer: ^5.2.1 + checksum: b622730c0fc183328c3a1c9fdaaaa5118821ed6822b266fa6b0375db7e20061ebec87301d61931d79b9da9a96ada1cab317fce3c68f233e5e93ed02dbb35544c languageName: node linkType: hard @@ -16392,7 +16478,7 @@ __metadata: languageName: node linkType: hard -"elliptic@npm:^6.5.3": +"elliptic@npm:^6.5.3, elliptic@npm:^6.5.4": version: 6.5.4 resolution: "elliptic@npm:6.5.4" dependencies: @@ -24567,7 +24653,7 @@ __metadata: languageName: node linkType: hard -"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.5": +"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.6": version: 5.1.6 resolution: "parse-asn1@npm:5.1.6" dependencies: @@ -26339,14 +26425,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.4": - version: 8.4.4 - resolution: "postcss@npm:8.4.4" +"postcss@npm:^8.4.31": + version: 8.4.31 + resolution: "postcss@npm:8.4.31" dependencies: - nanoid: ^3.1.30 + nanoid: ^3.3.6 picocolors: ^1.0.0 - source-map-js: ^1.0.1 - checksum: 6cf3fe0ecdf5a0d2aeb5e8404938c7eab968704e2e29dc5421e90b4014eb1975c1c0ad828425f2428807ef6e3fcfadd71f988ab55cb06c28ac2866f22403255b + source-map-js: ^1.0.2 + checksum: 1d8611341b073143ad90486fcdfeab49edd243377b1f51834dc4f6d028e82ce5190e4f11bb2633276864503654fb7cab28e67abdc0fbf9d1f88cad4a0ff0beea languageName: node linkType: hard @@ -27891,6 +27977,17 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^3.6.2": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: ^2.0.3 + string_decoder: ^1.1.1 + util-deprecate: ^1.0.1 + checksum: bdcbe6c22e846b6af075e32cf8f4751c2576238c5043169a1c221c92ee2878458a816a4ea33f4c67623c0b6827c8a400409bfb3cf0bf3381392d0b1dfb52ac8d + languageName: node + linkType: hard + "readdir-glob@npm:^1.0.0": version: 1.1.1 resolution: "readdir-glob@npm:1.1.1" @@ -28826,7 +28923,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491