diff --git a/packages/app/src/components/cms/inline-time-series-charts.tsx b/packages/app/src/components/cms/inline-time-series-charts.tsx index fe41f76931..ed8ca6edf2 100644 --- a/packages/app/src/components/cms/inline-time-series-charts.tsx +++ b/packages/app/src/components/cms/inline-time-series-charts.tsx @@ -1,24 +1,12 @@ -import { - ChartConfiguration, - DataScopeKey, - MetricKeys, - ScopedData, - TimespanAnnotationConfiguration, - TimestampedValue, -} from '@corona-dashboard/common'; +import { ChartConfiguration, DataScopeKey, MetricKeys, ScopedData, TimespanAnnotationConfiguration, TimestampedValue } from '@corona-dashboard/common'; import { get } from 'lodash'; import { useMemo } from 'react'; import useSWRImmutable from 'swr/immutable'; import { isDefined } from 'ts-is-present'; import { ErrorBoundary } from '~/components/error-boundary'; import { TimeSeriesChart } from '~/components/time-series-chart'; -import { - DataOptions, - TimespanAnnotationConfig, -} from '~/components/time-series-chart/logic/common'; +import { DataOptions, TimespanAnnotationConfig } from '~/components/time-series-chart/logic/common'; import { useIntl } from '~/intl'; -import { metricConfigs } from '~/metric-config'; -import { ScopedMetricConfigs } from '~/metric-config/common'; import { getBoundaryDateStartUnix } from '~/utils/get-boundary-date-start-unix'; import { getLowerBoundaryDateStartUnix } from '~/utils/get-lower-boundary-date-start-unix'; import { Metadata } from '../metadata'; @@ -26,42 +14,27 @@ import { InlineLoader } from './inline-loader'; import { getColor } from './logic/get-color'; import { getDataUrl } from './logic/get-data-url'; -interface InlineTimeSeriesChartsProps< - S extends DataScopeKey, - M extends MetricKeys -> { +interface InlineTimeSeriesChartsProps> { startDate?: string; endDate?: string; configuration: ChartConfiguration; } -export function InlineTimeSeriesCharts< - S extends DataScopeKey, - M extends MetricKeys ->(props: InlineTimeSeriesChartsProps) { +export function InlineTimeSeriesCharts>(props: InlineTimeSeriesChartsProps) { const { configuration, startDate, endDate } = props; const { commonTexts } = useIntl(); const dateUrl = getDataUrl(startDate, endDate, configuration); - const { data } = useSWRImmutable(dateUrl, (url: string) => - fetch(url).then((_) => _.json()) - ); - - const scopedMetricConfigs = metricConfigs[configuration.area] as - | ScopedMetricConfigs - | undefined; + const { data } = useSWRImmutable(dateUrl, (url: string) => fetch(url).then((_) => _.json())); const seriesConfig = useMemo(() => { return configuration.metricProperties.map((x) => { - const seriesMetricConfig = - scopedMetricConfigs?.[configuration.metricName]?.[x.propertyName]; const config: any = { type: x.type, metricProperty: x.propertyName, label: get(commonTexts, x.labelKey.split('.'), null), color: getColor(x.color), - minimumRange: seriesMetricConfig?.minimumRange, }; if (isDefined(x.curve) && x.curve.length) { config.curve = x.curve; @@ -70,11 +43,7 @@ export function InlineTimeSeriesCharts< config.fillOpacity = x.fillOpacity; } if (isDefined(x.shortLabelKey) && x.shortLabelKey.length) { - config.shortLabelKey = get( - commonTexts, - x.shortLabelKey.split('.'), - null - ); + config.shortLabelKey = get(commonTexts, x.shortLabelKey.split('.'), null); } if (isDefined(x.strokeWidth)) { config.strokeWidth = x.strokeWidth; @@ -84,12 +53,7 @@ export function InlineTimeSeriesCharts< } return config; }); - }, [ - scopedMetricConfigs, - configuration.metricName, - configuration.metricProperties, - commonTexts, - ]); + }, [configuration.metricProperties, commonTexts]); const dataOptions = useMemo(() => { if (!isDefined(configuration) || !isDefined(data)) { @@ -97,31 +61,19 @@ export function InlineTimeSeriesCharts< } const annotations = configuration.timespanAnnotations ?? []; - const timespanAnnotations = annotations.map( - (x: TimespanAnnotationConfiguration) => ({ - fill: x.fill, - start: calculateStart(x.start, data.values), - end: calculateEnd(x.end, x.start, data.values), - label: isDefined(x.labelKey) - ? get(commonTexts, x.labelKey.split('.'), null) - : undefined, - shortLabel: isDefined(x.shortLabelKey) - ? get(commonTexts, x.shortLabelKey.split('.'), null) - : undefined, - }) - ); + const timespanAnnotations = annotations.map((x: TimespanAnnotationConfiguration) => ({ + fill: x.fill, + start: calculateStart(x.start, data.values), + end: calculateEnd(x.end, x.start, data.values), + label: isDefined(x.labelKey) ? get(commonTexts, x.labelKey.split('.'), null) : undefined, + shortLabel: isDefined(x.shortLabelKey) ? get(commonTexts, x.shortLabelKey.split('.'), null) : undefined, + })); return { forcedMaximumValue: configuration.forcedMaximumValue, isPercentage: configuration.isPercentage, renderNullAsZero: configuration.renderNullAsZero, - valueAnnotation: configuration.valueAnnotationKey?.length - ? get( - commonTexts, - configuration.valueAnnotationKey.split('.'), - undefined - ) - : undefined, + valueAnnotation: configuration.valueAnnotationKey?.length ? get(commonTexts, configuration.valueAnnotationKey.split('.'), undefined) : undefined, timespanAnnotations, } as DataOptions; }, [configuration, commonTexts, data]); diff --git a/packages/app/src/components/time-series-chart/logic/scales.ts b/packages/app/src/components/time-series-chart/logic/scales.ts index 1668736cfd..770d1886ff 100644 --- a/packages/app/src/components/time-series-chart/logic/scales.ts +++ b/packages/app/src/components/time-series-chart/logic/scales.ts @@ -1,11 +1,4 @@ -import { - assert, - DateSpanValue, - DAY_IN_SECONDS, - isDateSeries, - isDateSpanSeries, - TimestampedValue, -} from '@corona-dashboard/common'; +import { assert, DateSpanValue, DAY_IN_SECONDS, isDateSeries, isDateSpanSeries, TimestampedValue } from '@corona-dashboard/common'; import { scaleLinear } from '@visx/scale'; import { ScaleLinear } from 'd3-scale'; import { first, isEmpty, last } from 'lodash'; @@ -31,28 +24,14 @@ interface UseScalesResult { hasAllZeroValues: boolean; } -export function useScales(args: { - values: T[]; - maximumValue: number; - minimumValue: number; - bounds: Bounds; - numTicks: number; - minimumRange?: number; -}): UseScalesResult { +export function useScales(args: { values: T[]; maximumValue: number; minimumValue: number; bounds: Bounds; numTicks: number }): UseScalesResult { const today = useCurrentDate(); - const { - maximumValue, - minimumValue, - bounds, - numTicks, - values, - minimumRange = 10, - } = args; + const { maximumValue, minimumValue, bounds, numTicks, values } = args; return useMemo(() => { const [start, end] = getTimeDomain({ values, today, withPadding: true }); const yMin = Math.min(minimumValue, 0); - const yMax = Math.max(maximumValue, minimumRange); + const yMax = Math.max(maximumValue, 0); if (isEmpty(values)) { return { @@ -104,16 +83,7 @@ export function useScales(args: { }; return result; - }, [ - values, - today, - minimumValue, - maximumValue, - bounds.width, - bounds.height, - numTicks, - minimumRange, - ]); + }, [values, today, minimumValue, maximumValue, bounds.width, bounds.height, numTicks]); } /** @@ -126,15 +96,7 @@ export function useScales(args: { * series starts and where the last series ends, and that would remove all * "empty" space on both ends of the chart. */ -export function getTimeDomain({ - values, - today, - withPadding, -}: { - values: T[]; - today: Date; - withPadding: boolean; -}): [start: number, end: number] { +export function getTimeDomain({ values, today, withPadding }: { values: T[]; today: Date; withPadding: boolean }): [start: number, end: number] { /** * Return a sensible default when no values fall within the selected timeframe */ @@ -150,34 +112,24 @@ export function getTimeDomain({ if (isDateSeries(values)) { const start = first(values)?.date_unix; const end = last(values)?.date_unix; - assert( - isDefined(start) && isDefined(end), - `[${getTimeDomain.name}] Missing start or end timestamp in [${start}, ${end}]` - ); + assert(isDefined(start) && isDefined(end), `[${getTimeDomain.name}] Missing start or end timestamp in [${start}, ${end}]`); /** * In cases where we render daily data, it is probably good to add a bit of * time scale "padding" so that the markers and their date span fall nicely * within the "stretched" domain on both ends of the graph. */ - return withPadding - ? [start - DAY_IN_SECONDS / 2, end + DAY_IN_SECONDS / 2] - : [start, end]; + return withPadding ? [start - DAY_IN_SECONDS / 2, end + DAY_IN_SECONDS / 2] : [start, end]; } if (isDateSpanSeries(values)) { const start = first(values)?.date_start_unix; const end = last(values)?.date_end_unix; - assert( - isDefined(start) && isDefined(end), - `[${getTimeDomain.name}] Missing start or end timestamp in [${start}, ${end}]` - ); + assert(isDefined(start) && isDefined(end), `[${getTimeDomain.name}] Missing start or end timestamp in [${start}, ${end}]`); return [start, end]; } - throw new Error( - `Invalid timestamped values, shaped like: ${JSON.stringify(values[0])}` - ); + throw new Error(`Invalid timestamped values, shaped like: ${JSON.stringify(values[0])}`); } /** @@ -188,22 +140,14 @@ export function getTimeDomain({ * It also assumes that if we use date_unix it always means one day worth of * data. */ -function getDateSpanWidth( - values: T[], - xScale: ScaleLinear -) { +function getDateSpanWidth(values: T[], xScale: ScaleLinear) { if (isDateSeries(values)) { return xScale(DAY_IN_SECONDS) - xScale(0); } if (isDateSpanSeries(values)) { - return ( - xScale((values[0] as DateSpanValue).date_end_unix) - - xScale((values[0] as DateSpanValue).date_start_unix) - ); + return xScale((values[0] as DateSpanValue).date_end_unix) - xScale((values[0] as DateSpanValue).date_start_unix); } - throw new Error( - `Invalid timestamped values, shaped like: ${JSON.stringify(values[0])}` - ); + throw new Error(`Invalid timestamped values, shaped like: ${JSON.stringify(values[0])}`); } 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 51143f1368..129a261b0e 100644 --- a/packages/app/src/components/time-series-chart/logic/series.ts +++ b/packages/app/src/components/time-series-chart/logic/series.ts @@ -1,10 +1,4 @@ -import { - getValuesInTimeframe, - isDateSeries, - isDateSpanSeries, - TimeframeOption, - TimestampedValue, -} from '@corona-dashboard/common'; +import { getValuesInTimeframe, isDateSeries, isDateSpanSeries, TimeframeOption, TimestampedValue } from '@corona-dashboard/common'; import { Property } from 'csstype'; import { omit } from 'lodash'; import { useMemo } from 'react'; @@ -55,10 +49,6 @@ interface SeriesCommonDefinition { * values), or when using a second series as a backdrop. */ noMarker?: boolean; - /** - * Specifies a different minimum range than the default for this series. - */ - minimumRange?: number; /** * Hide this series in the legend (because it is shown in a custom * legend, for example) @@ -73,8 +63,7 @@ interface SeriesCommonDefinition { yAxisExceptionValues?: number[]; } -export interface GappedLineSeriesDefinition - extends SeriesCommonDefinition { +export interface GappedLineSeriesDefinition extends SeriesCommonDefinition { type: 'gapped-line'; metricProperty: keyof T; label: string; @@ -85,8 +74,7 @@ export interface GappedLineSeriesDefinition curve?: 'linear' | 'step'; } -export interface LineSeriesDefinition - extends SeriesCommonDefinition { +export interface LineSeriesDefinition extends SeriesCommonDefinition { type: 'line'; metricProperty: keyof T; label: string; @@ -97,8 +85,7 @@ export interface LineSeriesDefinition curve?: 'linear' | 'step'; } -export interface ScatterPlotSeriesDefinition - extends SeriesCommonDefinition { +export interface ScatterPlotSeriesDefinition extends SeriesCommonDefinition { type: 'scatter-plot'; metricProperty: keyof T; label: string; @@ -106,8 +93,7 @@ export interface ScatterPlotSeriesDefinition color: string; } -export interface AreaSeriesDefinition - extends SeriesCommonDefinition { +export interface AreaSeriesDefinition extends SeriesCommonDefinition { type: 'area'; metricProperty: keyof T; label: string; @@ -118,8 +104,7 @@ export interface AreaSeriesDefinition curve?: 'linear' | 'step'; } -export interface GappedAreaSeriesDefinition - extends SeriesCommonDefinition { +export interface GappedAreaSeriesDefinition extends SeriesCommonDefinition { type: 'gapped-area'; metricProperty: keyof T; label: string; @@ -130,8 +115,7 @@ export interface GappedAreaSeriesDefinition curve?: 'linear' | 'step'; } -export interface BarSeriesDefinition - extends SeriesCommonDefinition { +export interface BarSeriesDefinition extends SeriesCommonDefinition { type: 'bar'; metricProperty: keyof T; label: string; @@ -140,8 +124,7 @@ export interface BarSeriesDefinition fillOpacity?: number; } -export interface BarOutOfBoundsSeriesDefinition - extends SeriesCommonDefinition { +export interface BarOutOfBoundsSeriesDefinition extends SeriesCommonDefinition { type: 'bar-out-of-bounds'; metricProperty: keyof T; label: string; @@ -150,8 +133,7 @@ export interface BarOutOfBoundsSeriesDefinition outOfBoundsDates?: number[]; } -export interface SplitBarSeriesDefinition - extends SeriesCommonDefinition { +export interface SplitBarSeriesDefinition extends SeriesCommonDefinition { type: 'split-bar'; metricProperty: keyof T; label: string; @@ -160,8 +142,7 @@ export interface SplitBarSeriesDefinition splitPoints: SplitPoint[]; } -export interface RangeSeriesDefinition - extends SeriesCommonDefinition { +export interface RangeSeriesDefinition extends SeriesCommonDefinition { type: 'range'; metricPropertyLow: keyof T; metricPropertyHigh: keyof T; @@ -172,8 +153,7 @@ export interface RangeSeriesDefinition fillOpacity?: number; } -export interface StackedAreaSeriesDefinition - extends SeriesCommonDefinition { +export interface StackedAreaSeriesDefinition extends SeriesCommonDefinition { type: 'stacked-area'; metricProperty: keyof T; label: string; @@ -185,8 +165,7 @@ export interface StackedAreaSeriesDefinition mixBlendMode?: Property.MixBlendMode; } -export interface GappedStackedAreaSeriesDefinition - extends SeriesCommonDefinition { +export interface GappedStackedAreaSeriesDefinition extends SeriesCommonDefinition { type: 'gapped-stacked-area'; metricProperty: keyof T; label: string; @@ -207,8 +186,7 @@ export interface GappedStackedAreaSeriesDefinition * If the amount of changes for the chart are limited we could maybe merge it in * completely. */ -export interface SplitAreaSeriesDefinition - extends SeriesCommonDefinition { +export interface SplitAreaSeriesDefinition extends SeriesCommonDefinition { type: 'split-area'; metricProperty: keyof T; label: string; @@ -227,8 +205,7 @@ export interface SplitAreaSeriesDefinition * This can be used for example to show a total count at the bottom, or the * percentage counterpart of an absolute value. */ -export interface InvisibleSeriesDefinition - extends SeriesCommonDefinition { +export interface InvisibleSeriesDefinition extends SeriesCommonDefinition { type: 'invisible'; metricProperty: keyof T; label: string; @@ -251,34 +228,17 @@ type CutValuesConfig = { * present in the chart. This is a reverse type guard that you can use in a * filter and TS will understand what comes after is only the others. */ -export function isVisible( - def: SeriesConfig[number] -): def is Exclude> { +export function isVisible(def: SeriesConfig[number]): def is Exclude> { return def.type !== 'invisible'; } -export function useSeriesList( - values: T[], - seriesConfig: SeriesConfig, - cutValuesConfig?: CutValuesConfig[], - dataOptions?: DataOptions -) { - return useMemo( - () => getSeriesList(values, seriesConfig, cutValuesConfig, dataOptions), - [values, seriesConfig, cutValuesConfig, dataOptions] - ); +export function useSeriesList(values: T[], seriesConfig: SeriesConfig, cutValuesConfig?: CutValuesConfig[], dataOptions?: DataOptions) { + return useMemo(() => getSeriesList(values, seriesConfig, cutValuesConfig, dataOptions), [values, seriesConfig, cutValuesConfig, dataOptions]); } -export function useValuesInTimeframe( - values: T[], - timeframe: TimeframeOption, - endDate?: Date -) { +export function useValuesInTimeframe(values: T[], timeframe: TimeframeOption, endDate?: Date) { const today = useCurrentDate(); - return useMemo( - () => getValuesInTimeframe(values, timeframe, endDate ?? today), - [values, timeframe, endDate, today] - ); + return useMemo(() => getValuesInTimeframe(values, timeframe, endDate ?? today), [values, timeframe, endDate, today]); } /** @@ -287,15 +247,8 @@ export function useValuesInTimeframe( * render lines, so that the axis scales with whatever key contains the highest * values. */ -export function calculateSeriesMaximum( - seriesList: SeriesList, - seriesConfig: SeriesConfig, - benchmarkValue = -Infinity -) { - const filterSeries = ( - seriesConfig: SeriesConfigSingle, - series: SeriesValue[] - ): SeriesValue[] => { +export function calculateSeriesMaximum(seriesList: SeriesList, seriesConfig: SeriesConfig, benchmarkValue = -Infinity) { + const filterSeries = (seriesConfig: SeriesConfigSingle, series: SeriesValue[]): SeriesValue[] => { const yAxisExceptionValues = seriesConfig.yAxisExceptionValues; if (!yAxisExceptionValues || yAxisExceptionValues.length === 0) { return series; @@ -309,26 +262,17 @@ export function calculateSeriesMaximum( // Because the frontend application is serverside rendered, 'the middle of the day' depends on the locale settings of the server. // Despite having the actual backend data, we can't reliably know what the middle of the day will be. (don't start on daylight saving time) // This is circumvented by only looking at the day itself, ignoring the time-part, and assuming that will not match too broadly. - return ( - new Date(exceptionValue * 1000).toDateString() === - new Date(value.__date_unix * 1000).toDateString() - ); + return new Date(exceptionValue * 1000).toDateString() === new Date(value.__date_unix * 1000).toDateString(); }); }); }; - const filteredSeriesList = seriesList.map((series, index) => - filterSeries(seriesConfig[index], series) - ); + const filteredSeriesList = seriesList.map((series, index) => filterSeries(seriesConfig[index], series)); const values = filteredSeriesList .filter((_, index) => isVisible(seriesConfig[index])) .flatMap((series) => - series.flatMap((seriesItem: SeriesSingleValue | SeriesDoubleValue) => - isSeriesSingleValue(seriesItem) - ? seriesItem.__value - : [seriesItem.__value_a, seriesItem.__value_b] - ) + series.flatMap((seriesItem: SeriesSingleValue | SeriesDoubleValue) => (isSeriesSingleValue(seriesItem) ? seriesItem.__value : [seriesItem.__value_a, seriesItem.__value_b])) ) .filter(isDefined); @@ -339,8 +283,7 @@ export function calculateSeriesMaximum( * sure the signaalwaarde floats in the middle */ - const artificialMax = - overallMaximum < benchmarkValue ? benchmarkValue * 2 : 0; + const artificialMax = overallMaximum < benchmarkValue ? benchmarkValue * 2 : 0; const maximumValue = Math.max(overallMaximum, artificialMax); @@ -352,26 +295,15 @@ export function calculateSeriesMaximum( * scale the y-axis. We need to do this for each of the keys that are used to * render lines, so that the axis scales with whatever key contains the lowest. */ -export function calculateSeriesMinimum( - seriesList: SeriesList, - seriesConfig: SeriesConfig, - benchmarkValue = -Infinity -) { +export function calculateSeriesMinimum(seriesList: SeriesList, seriesConfig: SeriesConfig, benchmarkValue = -Infinity) { const values = seriesList .filter((_, index) => isVisible(seriesConfig[index])) - .flatMap((series) => - series.flatMap((x: SeriesSingleValue | SeriesDoubleValue) => - isSeriesSingleValue(x) ? x.__value : [x.__value_a, x.__value_b] - ) - ) + .flatMap((series) => series.flatMap((x: SeriesSingleValue | SeriesDoubleValue) => (isSeriesSingleValue(x) ? x.__value : [x.__value_a, x.__value_b]))) .filter(isDefined); const overallMinimum = Math.min(...values); - const artificialMin = - overallMinimum > benchmarkValue - ? benchmarkValue - Math.abs(benchmarkValue) - : 0; + const artificialMin = overallMinimum > benchmarkValue ? benchmarkValue - Math.abs(benchmarkValue) : 0; const minimumValue = Math.max(overallMinimum, artificialMin); @@ -392,21 +324,15 @@ export interface SeriesDoubleValue extends SeriesItem { __value_b?: number; } -export function isBarOutOfBounds( - value: SeriesConfigSingle -): value is BarOutOfBoundsSeriesDefinition { +export function isBarOutOfBounds(value: SeriesConfigSingle): value is BarOutOfBoundsSeriesDefinition { return isDefined((value as any).outOfBoundsDates); } -export function isSeriesSingleValue( - value: SeriesValue -): value is SeriesSingleValue { +export function isSeriesSingleValue(value: SeriesValue): value is SeriesSingleValue { return isDefined((value as any).__value); } -export function isSeriesMissingValue( - value: SeriesValue -): value is SeriesMissingValue { +export function isSeriesMissingValue(value: SeriesValue): value is SeriesMissingValue { return isDefined(value) && isDefined((value as any).__hasMissing); } @@ -416,34 +342,17 @@ export function isSeriesMissingValue( * with TimestampedValue as the LineChart because types got simplified in other * places. */ -export type SeriesValue = - | SeriesSingleValue - | SeriesDoubleValue - | SeriesMissingValue; +export type SeriesValue = SeriesSingleValue | SeriesDoubleValue | SeriesMissingValue; export type SeriesList = SeriesValue[][]; -function getSeriesList( - values: T[], - seriesConfig: SeriesConfig, - cutValuesConfig?: CutValuesConfig[], - dataOptions?: DataOptions -): SeriesList { +function getSeriesList(values: T[], seriesConfig: SeriesConfig, cutValuesConfig?: CutValuesConfig[], dataOptions?: DataOptions): SeriesList { return seriesConfig.filter(isVisible).map((config) => config.type === 'stacked-area' ? getStackedAreaSeriesData(values, config.metricProperty, seriesConfig) : config.type === 'gapped-stacked-area' - ? getGappedStackedAreaSeriesData( - values, - config.metricProperty, - seriesConfig, - dataOptions - ) + ? getGappedStackedAreaSeriesData(values, config.metricProperty, seriesConfig, dataOptions) : config.type === 'range' - ? getRangeSeriesData( - values, - config.metricPropertyLow, - config.metricPropertyHigh - ) + ? getRangeSeriesData(values, config.metricPropertyLow, config.metricPropertyHigh) : /** * Cutting values based on annotation is only supported for single line series */ @@ -451,34 +360,21 @@ function getSeriesList( ); } -function getGappedStackedAreaSeriesData( - values: T[], - metricProperty: keyof T, - seriesConfig: SeriesConfig, - dataOptions?: DataOptions -) { +function getGappedStackedAreaSeriesData(values: T[], metricProperty: keyof T, seriesConfig: SeriesConfig, dataOptions?: DataOptions) { /** * 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', 'gapped-stacked-area' as const) - ); + const stackedAreaDefinitions = seriesConfig.filter(hasValueAtKey('type', 'gapped-stacked-area' as const)); - const seriesBelowCurrentSeries = getSeriesBelowCurrentSeries( - stackedAreaDefinitions, - metricProperty - ); + const seriesBelowCurrentSeries = getSeriesBelowCurrentSeries(stackedAreaDefinitions, metricProperty); const seriesHigh = getSeriesData(values, metricProperty); const seriesLow = getSeriesData(values, metricProperty); seriesLow.forEach((seriesSingleValue, index) => { - if ( - !dataOptions?.renderNullAsZero && - !isPresent(seriesSingleValue.__value) - ) { + if (!dataOptions?.renderNullAsZero && !isPresent(seriesSingleValue.__value)) { return; } @@ -487,20 +383,12 @@ function getGappedStackedAreaSeriesData( * current series, we will sum up all values of the * `seriesBelowCurrentSeries`. */ - seriesSingleValue.__value = sumSeriesValues( - seriesBelowCurrentSeries, - values, - index - ); + seriesSingleValue.__value = sumSeriesValues(seriesBelowCurrentSeries, values, index); }); return seriesLow.map((low, index) => { const valueLow = low.__value; - const valueHigh = isDefined(valueLow) - ? valueLow + (seriesHigh[index].__value ?? 0) - : dataOptions?.renderNullAsZero - ? 0 - : undefined; + const valueHigh = isDefined(valueLow) ? valueLow + (seriesHigh[index].__value ?? 0) : dataOptions?.renderNullAsZero ? 0 : undefined; return { __date_unix: low.__date_unix, @@ -510,11 +398,7 @@ function getGappedStackedAreaSeriesData( }); } -function sumSeriesValues( - seriesBelowCurrentSeries: { metricProperty: keyof T }[], - values: T[], - index: number -): number | undefined { +function sumSeriesValues(seriesBelowCurrentSeries: { metricProperty: keyof T }[], values: T[], index: number): number | undefined { return ( seriesBelowCurrentSeries // for each serie we'll get the value of the current index @@ -524,24 +408,15 @@ function sumSeriesValues( ); } -function getStackedAreaSeriesData( - values: T[], - metricProperty: keyof T, - seriesConfig: SeriesConfig -) { +function getStackedAreaSeriesData(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-area' as const) - ); + const stackedAreaDefinitions = seriesConfig.filter(hasValueAtKey('type', 'stacked-area' as const)); - const seriesBelowCurrentSeries = getSeriesBelowCurrentSeries( - stackedAreaDefinitions, - metricProperty - ); + const seriesBelowCurrentSeries = getSeriesBelowCurrentSeries(stackedAreaDefinitions, metricProperty); const seriesHigh = getSeriesData(values, metricProperty); const seriesLow = getSeriesData(values, metricProperty); @@ -552,11 +427,7 @@ function getStackedAreaSeriesData( * current series, we will sum up all values of the * `seriesBelowCurrentSeries`. */ - seriesSingleValue.__value = sumSeriesValues( - seriesBelowCurrentSeries, - values, - index - ); + seriesSingleValue.__value = sumSeriesValues(seriesBelowCurrentSeries, values, index); }); return seriesLow.map((low, index) => { @@ -571,20 +442,11 @@ function getStackedAreaSeriesData( }); } -function getSeriesBelowCurrentSeries( - definitions: { metricProperty: keyof T }[], - metricProperty: keyof T -) { - return definitions.slice( - definitions.findIndex((x) => x.metricProperty === metricProperty) + 1 - ); +function getSeriesBelowCurrentSeries(definitions: { metricProperty: keyof T }[], metricProperty: keyof T) { + return definitions.slice(definitions.findIndex((x) => x.metricProperty === metricProperty) + 1); } -function getRangeSeriesData( - values: T[], - metricPropertyLow: keyof T, - metricPropertyHigh: keyof T -): SeriesDoubleValue[] { +function getRangeSeriesData(values: T[], metricPropertyLow: keyof T, metricPropertyHigh: keyof T): SeriesDoubleValue[] { const seriesLow = getSeriesData(values, metricPropertyLow); const seriesHigh = getSeriesData(values, metricPropertyHigh); @@ -595,11 +457,7 @@ function getRangeSeriesData( })); } -function getSeriesData( - values: T[], - metricProperty: keyof T, - cutValuesConfig?: CutValuesConfig[] -): SeriesSingleValue[] { +function getSeriesData(values: T[], metricProperty: keyof T, cutValuesConfig?: CutValuesConfig[]): SeriesSingleValue[] { if (values.length === 0) { /** * It could happen that you are using an old dataset and select last week as @@ -609,9 +467,7 @@ function getSeriesData( return []; } - const activeCuts = cutValuesConfig?.filter((x) => - x.metricProperties.includes(metricProperty as string) - ); + const activeCuts = cutValuesConfig?.filter((x) => x.metricProperties.includes(metricProperty as string)); if (isDateSeries(values)) { const uncutValues = values.map((x) => ({ @@ -651,21 +507,14 @@ function getSeriesData( * Cutting values means setting series item.__value to undefined. We still want * them to be fully qualified series items. */ -function cutValues( - values: SeriesSingleValue[], - cuts: { start: number; end: number }[] -): SeriesSingleValue[] { +function cutValues(values: SeriesSingleValue[], cuts: { start: number; end: number }[]): SeriesSingleValue[] { const result = [...values]; // clone because we will mutate this. for (const cut of cuts) { /** * By passing result as the values, we incrementally mutate the input array */ - const [startIndex, endIndex] = getCutIndexStartEnd( - result, - cut.start, - cut.end - ); + const [startIndex, endIndex] = getCutIndexStartEnd(result, cut.start, cut.end); clearValues(result, startIndex, endIndex); } @@ -677,14 +526,8 @@ function cutValues( * Figure out the position and length of the cut to be applied to the values * array, based on start/end timestamps */ -function getCutIndexStartEnd( - values: SeriesSingleValue[], - start: number, - end: number -) { - const startIndex = values.findIndex( - (x) => x.__date_unix >= start && x.__date_unix < end - ); +function getCutIndexStartEnd(values: SeriesSingleValue[], start: number, end: number) { + const startIndex = values.findIndex((x) => x.__date_unix >= start && x.__date_unix < end); if (startIndex === -1) { /** @@ -713,9 +556,7 @@ function getCutIndexStartEnd( * We could just pass down the full annotations type but that would create a bit * of an odd dependency between two mostly independent concepts. */ -export function extractCutValuesConfig( - timespanAnnotations?: TimespanAnnotationConfig[] -) { +export function extractCutValuesConfig(timespanAnnotations?: TimespanAnnotationConfig[]) { return timespanAnnotations ?.map((x) => x.cutValuesForMetricProperties @@ -729,11 +570,7 @@ export function extractCutValuesConfig( .filter(isDefined); } -function clearValues( - values: SeriesSingleValue[], - startIndex: number, - endIndex: number -) { +function clearValues(values: SeriesSingleValue[], startIndex: number, endIndex: number) { for (let index = startIndex; index <= endIndex; ++index) { const originalValue = values[index]; @@ -744,10 +581,7 @@ function clearValues( } } -export function omitValuePropertiesForAnnotation( - value: T, - timespan: TimespanAnnotationConfig -) { +export function omitValuePropertiesForAnnotation(value: T, timespan: TimespanAnnotationConfig) { if (timespan.cutValuesForMetricProperties) { return omit(value, timespan.cutValuesForMetricProperties) as T; } else { diff --git a/packages/app/src/components/time-series-chart/time-series-chart.tsx b/packages/app/src/components/time-series-chart/time-series-chart.tsx index e377290132..5d493123f6 100644 --- a/packages/app/src/components/time-series-chart/time-series-chart.tsx +++ b/packages/app/src/components/time-series-chart/time-series-chart.tsx @@ -11,10 +11,7 @@ import { Legend } from '~/components/legend'; import { ValueAnnotation } from '~/components/value-annotation'; import { useIntl } from '~/intl'; import { useCurrentDate } from '~/utils/current-date-context'; -import { - AccessibilityDefinition, - addAccessibilityFeatures, -} from '~/utils/use-accessibility-annotations'; +import { AccessibilityDefinition, addAccessibilityFeatures } from '~/utils/use-accessibility-annotations'; import { useOnClickOutside } from '~/utils/use-on-click-outside'; import { useResponsiveContainer } from '~/utils/use-responsive-container'; import { useTabInteractiveButton } from '~/utils/use-tab-interactive-button'; @@ -82,10 +79,7 @@ export type { SeriesConfig } from './logic'; * to see if we can use the date_unix timestamps from the data directly * everywhere without unnecessary conversion to and from Date objects. */ -export type TimeSeriesChartProps< - T extends TimestampedValue, - C extends SeriesConfig -> = { +export type TimeSeriesChartProps> = { /** * The mandatory AccessibilityDefinition provides a reference to annotate the * graph with a label and description. @@ -154,10 +148,7 @@ export type TimeSeriesChartProps< isYAxisCollapsed?: boolean; }; -export function TimeSeriesChart< - T extends TimestampedValue, - C extends SeriesConfig ->({ +export function TimeSeriesChart>({ accessibility, values: allValues, endDate, @@ -183,34 +174,14 @@ export function TimeSeriesChart< }: TimeSeriesChartProps) { const { commonTexts } = useIntl(); - const { - tooltipData, - tooltipLeft = 0, - tooltipTop = 0, - showTooltip, - hideTooltip, - tooltipOpen, - } = useTooltip>(); + const { tooltipData, tooltipLeft = 0, tooltipTop = 0, showTooltip, hideTooltip, tooltipOpen } = useTooltip>(); const today = useCurrentDate(); const chartId = useUniqueId(); - const { - valueAnnotation, - isPercentage, - forcedMaximumValue, - benchmark, - timespanAnnotations, - timeAnnotations, - timelineEvents, - } = dataOptions || {}; + const { valueAnnotation, isPercentage, forcedMaximumValue, benchmark, timespanAnnotations, timeAnnotations, timelineEvents } = dataOptions || {}; - const { - ResponsiveContainer, - ref: containerRef, - width, - height, - } = useResponsiveContainer(initialWidth, minHeight); + const { ResponsiveContainer, ref: containerRef, width, height } = useResponsiveContainer(initialWidth, minHeight); const { padding, bounds, leftPaddingRef } = useDimensions({ width, @@ -221,62 +192,29 @@ export function TimeSeriesChart< const values = useValuesInTimeframe(allValues, timeframe, endDate); - const cutValuesConfig = useMemo( - () => extractCutValuesConfig(timespanAnnotations), - [timespanAnnotations] - ); + const cutValuesConfig = useMemo(() => extractCutValuesConfig(timespanAnnotations), [timespanAnnotations]); - const seriesList = useSeriesList( - values, - seriesConfig, - cutValuesConfig, - dataOptions - ); + const seriesList = useSeriesList(values, seriesConfig, cutValuesConfig, dataOptions); /** * The maximum is calculated over all values, because you don't want the * y-axis scaling to change when toggling the timeframe setting. */ const [calculatedSeriesMin, calculatedSeriesMax] = useMemo( - () => [ - calculateSeriesMinimum(seriesList, seriesConfig, benchmark?.value), - calculateSeriesMaximum(seriesList, seriesConfig, benchmark?.value), - ], + () => [calculateSeriesMinimum(seriesList, seriesConfig, benchmark?.value), calculateSeriesMaximum(seriesList, seriesConfig, benchmark?.value)], [seriesList, seriesConfig, benchmark?.value] ); - const calculatedForcedMaximumValue = isFunction(forcedMaximumValue) - ? forcedMaximumValue(calculatedSeriesMax) - : forcedMaximumValue; + const calculatedForcedMaximumValue = isFunction(forcedMaximumValue) ? forcedMaximumValue(calculatedSeriesMax) : forcedMaximumValue; - const seriesMax = - typeof calculatedForcedMaximumValue === 'number' - ? Math.min(calculatedForcedMaximumValue, calculatedSeriesMax) - : calculatedSeriesMax; + const seriesMax = typeof calculatedForcedMaximumValue === 'number' ? Math.min(calculatedForcedMaximumValue, calculatedSeriesMax) : calculatedSeriesMax; - const minimumRanges = seriesConfig - .map((c) => c.minimumRange) - .filter(isDefined); - const minimumRange = minimumRanges.length - ? Math.max(...minimumRanges) - : undefined; - - const { - xScale, - yScale, - getX, - getY, - getY0, - getY1, - dateSpanWidth, - hasAllZeroValues, - } = useScales({ + const { xScale, yScale, getX, getY, getY0, getY1, dateSpanWidth, hasAllZeroValues } = useScales({ values, maximumValue: yTickValues?.[yTickValues.length - 1] || seriesMax, minimumValue: calculatedSeriesMin, bounds, numTicks: yTickValues?.length || numGridLines, - minimumRange, }); const { legendItems, splitLegendGroups } = useLegendItems( @@ -287,18 +225,9 @@ export function TimeSeriesChart< forceLegend ); - const timeDomain = useMemo( - () => - getTimeDomain({ values, today: endDate ?? today, withPadding: false }), - [values, endDate, today] - ); + const timeDomain = useMemo(() => getTimeDomain({ values, today: endDate ?? today, withPadding: false }), [values, endDate, today]); - const { - isTabInteractive, - tabInteractiveButton, - anchorEventHandlers, - setIsTabInteractive, - } = useTabInteractiveButton(commonTexts.accessibility.tab_navigatie_button); + const { isTabInteractive, tabInteractiveButton, anchorEventHandlers, setIsTabInteractive } = useTabInteractiveButton(commonTexts.accessibility.tab_navigatie_button); const timelineState = useTimelineState(timelineEvents, xScale); const [hoverState, chartEventHandlers] = useHoverState({ @@ -315,26 +244,13 @@ export function TimeSeriesChart< setIsTabInteractive, }); - const metricPropertyFormatters = useMetricPropertyFormatters( - seriesConfig, - values - ); + const metricPropertyFormatters = useMetricPropertyFormatters(seriesConfig, values); - const valueMinWidth = useValueWidth( - values, - seriesConfig, - isPercentage, - metricPropertyFormatters - ); + const valueMinWidth = useValueWidth(values, seriesConfig, isPercentage, metricPropertyFormatters); useEffect(() => { if (hoverState) { - const { - nearestPoint, - valuesIndex, - timespanAnnotationIndex, - timelineEventIndex, - } = hoverState; + const { nearestPoint, valuesIndex, timespanAnnotationIndex, timelineEventIndex } = hoverState; showTooltip({ tooltipData: { @@ -349,10 +265,7 @@ export function TimeSeriesChart< */ value: timespanAnnotations && isDefined(timespanAnnotationIndex) - ? omitValuePropertiesForAnnotation( - values[valuesIndex], - timespanAnnotations[timespanAnnotationIndex] - ) + ? omitValuePropertiesForAnnotation(values[valuesIndex], timespanAnnotations[timespanAnnotationIndex]) : values[valuesIndex], config: seriesConfig, configIndex: nearestPoint.seriesConfigIndex, @@ -364,22 +277,13 @@ export function TimeSeriesChart< * dataOptions is already being passed, but it's cumbersome to have to * dig up the annotation from the array in the tooltip logic. */ - timespanAnnotation: - timespanAnnotations && isDefined(timespanAnnotationIndex) - ? timespanAnnotations[timespanAnnotationIndex] - : undefined, - timelineEvent: isDefined(timelineEventIndex) - ? timelineState.events[timelineEventIndex] - : undefined, + timespanAnnotation: timespanAnnotations && isDefined(timespanAnnotationIndex) ? timespanAnnotations[timespanAnnotationIndex] : undefined, + timelineEvent: isDefined(timelineEventIndex) ? timelineState.events[timelineEventIndex] : undefined, valueMinWidth, metricPropertyFormatters, seriesMax, - isOutOfBounds: dataOptions?.outOfBoundsConfig?.checkIsOutofBounds( - values[valuesIndex], - seriesMax, - timeDomain - ), + isOutOfBounds: dataOptions?.outOfBoundsConfig?.checkIsOutofBounds(values[valuesIndex], seriesMax, timeDomain), }, tooltipLeft: nearestPoint.x, tooltipTop: nearestPoint.y, @@ -413,15 +317,10 @@ export function TimeSeriesChart< } }, [onSeriesClick, seriesConfig, tooltipData]); - const isYAxisCollapsed = - defaultIsYAxisCollapsed ?? width < COLLAPSE_Y_AXIS_THRESHOLD; - const timeSeriesAccessibility = addAccessibilityFeatures(accessibility, [ - 'keyboard_time_series_chart', - ]); + const isYAxisCollapsed = defaultIsYAxisCollapsed ?? width < COLLAPSE_Y_AXIS_THRESHOLD; + const timeSeriesAccessibility = addAccessibilityFeatures(accessibility, ['keyboard_time_series_chart']); - const highlightZero = - (first(yScale.domain()) as number) < 0 && - (last(yScale.domain()) as number) > 0; + const highlightZero = (first(yScale.domain()) as number) < 0 && (last(yScale.domain()) as number) > 0; return ( <> @@ -496,24 +395,9 @@ export function TimeSeriesChart< * Highlight 0 on the y-axis when there are positive and * negative values */} - {highlightZero && ( - - )} - - {benchmark && ( - - )} + {highlightZero && } + + {benchmark && } {/** * Timespan annotations are rendered on top of the chart. It is @@ -522,50 +406,22 @@ export function TimeSeriesChart< {timespanAnnotations ?.filter((x) => x.fill !== 'none') .map((x, index) => ( - + ))} {timeAnnotations?.map((x, index) => ( - + ))} - + {tooltipOpen && tooltipData && ( - + )} {hoverState && ( - + @@ -601,14 +455,7 @@ export function TimeSeriesChart< {!disableLegend && splitLegendGroups && ( <> {splitLegendGroups.map((x) => ( - + {x.label && {x.label}:} diff --git a/packages/app/src/domain/tested/reproduction-chart-tile.tsx b/packages/app/src/domain/tested/reproduction-chart-tile.tsx index 1ff5fef186..1223f69454 100644 --- a/packages/app/src/domain/tested/reproduction-chart-tile.tsx +++ b/packages/app/src/domain/tested/reproduction-chart-tile.tsx @@ -6,7 +6,6 @@ import { ChartTile } from '~/components/chart-tile'; import { TimeSeriesChart } from '~/components/time-series-chart'; import { TimelineEventConfig } from '~/components/time-series-chart/components/timeline'; import { SiteText } from '~/locale'; -import { metricConfigs } from '~/metric-config'; interface ReproductionChartTileProps { data: NlReproduction; @@ -62,7 +61,6 @@ export const ReproductionChartTile = ({ label: text.linechart_legend_label, shortLabel: text.linechart_tooltip_label, color: colors.primary, - minimumRange: metricConfigs?.nl?.reproduction?.index_average?.minimumRange, }, ]} dataOptions={{ diff --git a/packages/app/src/metric-config/common.ts b/packages/app/src/metric-config/common.ts index e8cc9782ad..9940a56806 100644 --- a/packages/app/src/metric-config/common.ts +++ b/packages/app/src/metric-config/common.ts @@ -1,8 +1,4 @@ -import { - DataScope, - MetricKeys, - MetricProperty, -} from '@corona-dashboard/common'; +import { DataScope, MetricKeys, MetricProperty } from '@corona-dashboard/common'; /** * These types are placed here to avoid a circular dependency. The nl/vr/gm @@ -19,7 +15,6 @@ export type BarScaleConfig = { export type MetricConfig = { barScale?: BarScaleConfig; - minimumRange?: number; }; /** diff --git a/packages/app/src/metric-config/nl.ts b/packages/app/src/metric-config/nl.ts index 0608b5f3e5..e38676b940 100644 --- a/packages/app/src/metric-config/nl.ts +++ b/packages/app/src/metric-config/nl.ts @@ -37,15 +37,4 @@ export const nl: ScopedMetricConfigs = { }, }, }, - reproduction: { - index_low: { - minimumRange: 1, - }, - index_average: { - minimumRange: 1, - }, - index_high: { - minimumRange: 1, - }, - }, };