Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

feature/COR-1269-time-series-chart-y-axis-scaling #4551

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 15 additions & 63 deletions packages/app/src/components/cms/inline-time-series-charts.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,40 @@
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';
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<ScopedData[S]>
> {
interface InlineTimeSeriesChartsProps<S extends DataScopeKey, M extends MetricKeys<ScopedData[S]>> {
startDate?: string;
endDate?: string;
configuration: ChartConfiguration<S, M>;
}

export function InlineTimeSeriesCharts<
S extends DataScopeKey,
M extends MetricKeys<ScopedData[S]>
>(props: InlineTimeSeriesChartsProps<S, M>) {
export function InlineTimeSeriesCharts<S extends DataScopeKey, M extends MetricKeys<ScopedData[S]>>(props: InlineTimeSeriesChartsProps<S, M>) {
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<ScopedData[S]>
| 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;
Expand All @@ -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;
Expand All @@ -84,44 +53,27 @@ export function InlineTimeSeriesCharts<
}
return config;
});
}, [
scopedMetricConfigs,
configuration.metricName,
configuration.metricProperties,
commonTexts,
]);
}, [configuration.metricProperties, commonTexts]);

const dataOptions = useMemo(() => {
if (!isDefined(configuration) || !isDefined(data)) {
return undefined;
}

const annotations = configuration.timespanAnnotations ?? [];
const timespanAnnotations = annotations.map<TimespanAnnotationConfig>(
(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<TimespanAnnotationConfig>((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]);
Expand Down
82 changes: 13 additions & 69 deletions packages/app/src/components/time-series-chart/logic/scales.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -31,28 +24,14 @@ interface UseScalesResult {
hasAllZeroValues: boolean;
}

export function useScales<T extends TimestampedValue>(args: {
values: T[];
maximumValue: number;
minimumValue: number;
bounds: Bounds;
numTicks: number;
minimumRange?: number;
}): UseScalesResult {
export function useScales<T extends TimestampedValue>(args: { values: T[]; maximumValue: number; minimumValue: number; bounds: Bounds; numTicks: number }): UseScalesResult {
VWSCoronaDashboard26 marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down Expand Up @@ -104,16 +83,7 @@ export function useScales<T extends TimestampedValue>(args: {
};

return result;
}, [
values,
today,
minimumValue,
maximumValue,
bounds.width,
bounds.height,
numTicks,
minimumRange,
]);
}, [values, today, minimumValue, maximumValue, bounds.width, bounds.height, numTicks]);
}

/**
Expand All @@ -126,15 +96,7 @@ export function useScales<T extends TimestampedValue>(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<T extends TimestampedValue>({
values,
today,
withPadding,
}: {
values: T[];
today: Date;
withPadding: boolean;
}): [start: number, end: number] {
export function getTimeDomain<T extends TimestampedValue>({ 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
*/
Expand All @@ -150,34 +112,24 @@ export function getTimeDomain<T extends TimestampedValue>({
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])}`);
}

/**
Expand All @@ -188,22 +140,14 @@ export function getTimeDomain<T extends TimestampedValue>({
* It also assumes that if we use date_unix it always means one day worth of
* data.
*/
function getDateSpanWidth<T extends TimestampedValue>(
values: T[],
xScale: ScaleLinear<number, number>
) {
function getDateSpanWidth<T extends TimestampedValue>(values: T[], xScale: ScaleLinear<number, number>) {
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])}`);
}
Loading