From 726ee16fa2a597496c8b7a5d8a232deceb40c3dd Mon Sep 17 00:00:00 2001 From: Arjun Puri Date: Fri, 4 Nov 2022 12:13:37 +0100 Subject: [PATCH 01/13] WIP --- .../tooltips/choropleth-tooltip.tsx | 79 ++++++------------- .../choropleth/tooltips/tooltip-content.tsx | 16 +--- .../tooltips/tooltip-notification.tsx | 17 ++++ .../choropleth/tooltips/tooltip-subject.tsx | 56 ++++++------- .../choropleth/tooltips/tooltip.tsx | 46 ++--------- 5 files changed, 75 insertions(+), 139 deletions(-) create mode 100644 packages/app/src/components/choropleth/tooltips/tooltip-notification.tsx diff --git a/packages/app/src/components/choropleth/tooltips/choropleth-tooltip.tsx b/packages/app/src/components/choropleth/tooltips/choropleth-tooltip.tsx index c2281a4448..721b2e79d2 100644 --- a/packages/app/src/components/choropleth/tooltips/choropleth-tooltip.tsx +++ b/packages/app/src/components/choropleth/tooltips/choropleth-tooltip.tsx @@ -7,6 +7,7 @@ import { replaceVariablesInText } from '~/utils/replace-variables-in-text'; import { ChoroplethDataItem } from '../logic'; import { TooltipContent } from './tooltip-content'; import { TooltipSubject } from './tooltip-subject'; +import { TooltipNotification } from './tooltip-notification'; import { TooltipData } from './types'; type ChoroplethDataItemProps = { @@ -14,61 +15,29 @@ type ChoroplethDataItemProps = { dataFormatters?: Partial string>>; }; -export function ChoroplethTooltip( - props: ChoroplethDataItemProps -) { +export function ChoroplethTooltip(props: ChoroplethDataItemProps) { const { data, dataFormatters } = props; - const { - commonTexts, - formatNumber, - formatPercentage, - formatDate, - formatDateFromSeconds, - formatDateFromMilliseconds, - formatRelativeDate, - formatDateSpan, - } = useIntl(); + const { commonTexts, formatNumber, formatPercentage, formatDate, formatDateFromSeconds, formatDateFromMilliseconds, formatRelativeDate, formatDateSpan } = useIntl(); const text = commonTexts.choropleth_tooltip; - const subject = ( - text as unknown as Record>> - )[data.map]?.[data.dataConfig.metricProperty as string]?.subject; - assert( - isDefined(subject), - `[${ - ChoroplethTooltip.name - }] No tooltip subject found in siteText.choropleth_tooltip.${ - data.map - }.${data.dataConfig.metricProperty.toString()}` - ); + const subject = (text as unknown as Record>>)[data.map]?.[data.dataConfig.metricProperty as string]?.subject; + assert(isDefined(subject), `[${ChoroplethTooltip.name}] No tooltip subject found in siteText.choropleth_tooltip.${data.map}.${data.dataConfig.metricProperty.toString()}`); - const tooltipContent = ( - text as unknown as Record>> - )[data.map]?.[data.dataConfig.metricProperty as string]?.content; - assert( - isDefined(tooltipContent), - `[${ - ChoroplethTooltip.name - }] No tooltip content found in siteText.choropleth_tooltip.${ - data.map - }.${data.dataConfig.metricProperty.toString()}` - ); + const tooltipContent = (text as unknown as Record>>)[data.map]?.[data.dataConfig.metricProperty as string]?.content; + assert(isDefined(tooltipContent), `[${ChoroplethTooltip.name}] No tooltip content found in siteText.choropleth_tooltip.${data.map}.${data.dataConfig.metricProperty.toString()}`); const tooltipVars = { ...data.dataItem, ...data.dataOptions.tooltipVariables, } as Record; - const formattedTooltipVars = Object.entries(dataFormatters || {}).reduce( - (acc, [key, formatter]) => { - return { - ...acc, - [key]: formatter(tooltipVars[key]), - }; - }, - tooltipVars - ); + const formattedTooltipVars = Object.entries(dataFormatters || {}).reduce((acc, [key, formatter]) => { + return { + ...acc, + [key]: formatter(tooltipVars[key]), + }; + }, tooltipVars); const content = replaceVariablesInText(tooltipContent, formattedTooltipVars, { formatNumber, @@ -86,24 +55,20 @@ export function ChoroplethTooltip( const filterBelow = typeof dataItem === 'number' ? dataItem : null; return ( - - + + {ariaContent} + + {/* Setting to true for the timebeing, it should eventually come from tooltip.data.dataItem (see index.tsx -> ToolTip), can get rid of the isOutDated Prop */} + {true && ( + + + + )} ); } diff --git a/packages/app/src/components/choropleth/tooltips/tooltip-content.tsx b/packages/app/src/components/choropleth/tooltips/tooltip-content.tsx index 2d71593db3..315c76d322 100644 --- a/packages/app/src/components/choropleth/tooltips/tooltip-content.tsx +++ b/packages/app/src/components/choropleth/tooltips/tooltip-content.tsx @@ -17,11 +17,7 @@ export function TooltipContent(props: IProps) { const isTouch = useIsTouchDevice(); return ( - + @@ -47,13 +43,7 @@ const StyledTooltipContent = styled.div<{ isInteractive: boolean }>((x) => }) ); -function TooltipHeader({ - href, - children, -}: { - href?: string; - children: ReactNode; -}) { +function TooltipHeader({ href, children }: { href?: string; children: ReactNode }) { if (href) { return ( @@ -96,8 +86,6 @@ const TooltipInfo = styled.div( cursor: 'pointer', borderTop: '1px solid', borderTopColor: 'gray3', - py: 2, - px: 3, }) ); diff --git a/packages/app/src/components/choropleth/tooltips/tooltip-notification.tsx b/packages/app/src/components/choropleth/tooltips/tooltip-notification.tsx new file mode 100644 index 0000000000..88eae631fc --- /dev/null +++ b/packages/app/src/components/choropleth/tooltips/tooltip-notification.tsx @@ -0,0 +1,17 @@ +import { Box } from '~/components/base'; +import { colors } from '@corona-dashboard/common'; +import { space } from '~/style/theme'; + +interface TooltipNotificationProps { + children: React.ReactNode; +} + +export const TooltipNotification = (props: TooltipNotificationProps): React.ReactElement => { + const { children } = props; + + return ( + + {children} + + ); +}; diff --git a/packages/app/src/components/choropleth/tooltips/tooltip-subject.tsx b/packages/app/src/components/choropleth/tooltips/tooltip-subject.tsx index e1ef6ff5a7..dd95ad472b 100644 --- a/packages/app/src/components/choropleth/tooltips/tooltip-subject.tsx +++ b/packages/app/src/components/choropleth/tooltips/tooltip-subject.tsx @@ -5,6 +5,7 @@ import { isDefined, isPresent } from 'ts-is-present'; import { Box } from '~/components/base'; import { BoldText } from '~/components/typography'; import { getThresholdValue } from '~/utils/get-threshold-value'; +import styled from 'styled-components'; interface TooltipSubjectProps { subject?: string; @@ -14,13 +15,7 @@ interface TooltipSubjectProps { noDataFillColor?: string; } -export function TooltipSubject({ - subject, - thresholdValues, - filterBelow, - children, - noDataFillColor, -}: TooltipSubjectProps) { +export function TooltipSubject({ subject, thresholdValues, filterBelow, children, noDataFillColor }: TooltipSubjectProps) { const color = !isPresent(filterBelow) && isDefined(thresholdValues) ? noDataFillColor || getThresholdValue(thresholdValues, 0).color @@ -29,30 +24,31 @@ export function TooltipSubject({ : noDataFillColor; return ( - - {subject && {subject}} - - {children} + + +

HELLO

+ {subject && {subject}} + m={0} + spacingHorizontal={2} + css={css({ + display: 'flex', + alignItems: 'center', + flexWrap: 'nowrap', + whiteSpace: 'pre-wrap', + })} + > + {children} + + -
+
); } + +const StyledTooltipSubject = styled.div( + css({ + py: 2, + px: 3, + }) +); diff --git a/packages/app/src/components/choropleth/tooltips/tooltip.tsx b/packages/app/src/components/choropleth/tooltips/tooltip.tsx index 7e5d771716..82f78a7df6 100644 --- a/packages/app/src/components/choropleth/tooltips/tooltip.tsx +++ b/packages/app/src/components/choropleth/tooltips/tooltip.tsx @@ -28,25 +28,14 @@ const padding = { top: 12, }; -export function Tooltip({ - left, - top, - formatTooltip, - data, - dataFormatters, - placement = 'bottom-right', -}: TTooltipProps) { +export function Tooltip({ left, top, formatTooltip, data, dataFormatters, placement = 'bottom-right' }: TTooltipProps) { const viewportSize = useViewport(); const isMounted = useIsMounted({ delayMs: 10 }); const [ref, { height = 0 }] = useResizeObserver(); const [boundingBox, boundingBoxRef] = useBoundingBox(); const isTouch = useIsTouchDevice(); - const content = isDefined(formatTooltip) ? ( - formatTooltip(data) - ) : ( - - ); + const content = isDefined(formatTooltip) ? formatTooltip(data) : ; if (!content) return null; @@ -56,25 +45,16 @@ export function Tooltip({ const maxx = boundingBox?.width ?? 400; const maxy = viewportSize.height ?? 480; - const t = ( - placement: ChoroplethTooltipPlacement, - top: number, - left: number - ): string => { + const t = (placement: ChoroplethTooltipPlacement, top: number, left: number): string => { switch (placement) { case 'top-center': { - const xt = (current: number) => - Math.round((100 * (current - minx)) / (maxx - minx)); + const xt = (current: number) => Math.round((100 * (current - minx)) / (maxx - minx)); return `translateX(-${xt(left)}%)`; } case 'bottom-right': default: { - const xt = (current: number) => - Math.round((100 * (current - minx)) / (maxx - minx)); - const yt = (current: number) => - Math.round( - current > maxy / 2 ? -(height + padding.top) : padding.top - ); + const xt = (current: number) => Math.round((100 * (current - minx)) / (maxx - minx)); + const yt = (current: number) => Math.round(current > maxy / 2 ? -(height + padding.top) : padding.top); const bboxTop = boundingBox?.top ?? 0; return `translate(-${xt(left)}%, ${yt(bboxTop + top)}px)`; } @@ -82,18 +62,8 @@ export function Tooltip({ }; return ( - - + + Date: Mon, 7 Nov 2022 17:47:12 +0100 Subject: [PATCH 02/13] feat(sewer-choropleth): Implement tooltip notification for outdated data --- .../tooltips/choropleth-tooltip.tsx | 24 ++++++++++++++++--- .../choropleth/tooltips/tooltip-subject.tsx | 1 + 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/choropleth/tooltips/choropleth-tooltip.tsx b/packages/app/src/components/choropleth/tooltips/choropleth-tooltip.tsx index 721b2e79d2..67edde04d3 100644 --- a/packages/app/src/components/choropleth/tooltips/choropleth-tooltip.tsx +++ b/packages/app/src/components/choropleth/tooltips/choropleth-tooltip.tsx @@ -18,6 +18,10 @@ type ChoroplethDataItemProps = { export function ChoroplethTooltip(props: ChoroplethDataItemProps) { const { data, dataFormatters } = props; const { commonTexts, formatNumber, formatPercentage, formatDate, formatDateFromSeconds, formatDateFromMilliseconds, formatRelativeDate, formatDateSpan } = useIntl(); + const isSewerMap = data.dataConfig.metricName === 'sewer'; + let tooltipNotification; + let showNotification; + let outdatedDataDate; const text = commonTexts.choropleth_tooltip; @@ -27,6 +31,15 @@ export function ChoroplethTooltip(props: Choroplet const tooltipContent = (text as unknown as Record>>)[data.map]?.[data.dataConfig.metricProperty as string]?.content; assert(isDefined(tooltipContent), `[${ChoroplethTooltip.name}] No tooltip content found in siteText.choropleth_tooltip.${data.map}.${data.dataConfig.metricProperty.toString()}`); + if (isSewerMap) { + tooltipNotification = (text as unknown as Record>>)[data.map]?.[data.dataConfig.metricProperty as string] + ?.outdated_data_notification; + assert( + isDefined(tooltipNotification), + `[${ChoroplethTooltip.name}] No tooltip notification found in siteText.choropleth_tooltip.${data.map}.${data.dataConfig.metricProperty.toString()}` + ); + } + const tooltipVars = { ...data.dataItem, ...data.dataOptions.tooltipVariables, @@ -54,6 +67,12 @@ export function ChoroplethTooltip(props: Choroplet const dataItem = data.dataItem[data.dataConfig.metricProperty]; const filterBelow = typeof dataItem === 'number' ? dataItem : null; + if (isSewerMap) { + // TODO:Arjun - Setting to true for the timebeing, it should eventually come from tooltip.data.dataItem (see index.tsx -> ToolTip) + showNotification = true || tooltipVars.hasOutDatedData; + outdatedDataDate = formatDateFromSeconds(tooltipVars['date_of_insertion_unix'] as number, 'medium'); // TODO:Arjun - Not sure which date to use yet + } + return ( @@ -63,10 +82,9 @@ export function ChoroplethTooltip(props: Choroplet
- {/* Setting to true for the timebeing, it should eventually come from tooltip.data.dataItem (see index.tsx -> ToolTip), can get rid of the isOutDated Prop */} - {true && ( + {showNotification && outdatedDataDate && tooltipNotification && ( - + )}
diff --git a/packages/app/src/components/choropleth/tooltips/tooltip-subject.tsx b/packages/app/src/components/choropleth/tooltips/tooltip-subject.tsx index dd95ad472b..b0734636c2 100644 --- a/packages/app/src/components/choropleth/tooltips/tooltip-subject.tsx +++ b/packages/app/src/components/choropleth/tooltips/tooltip-subject.tsx @@ -26,6 +26,7 @@ export function TooltipSubject({ subject, thresholdValues, filterBelow, children return ( + {/* TODO:Arjun - remove before committing */}

HELLO

{subject && {subject}} Date: Fri, 11 Nov 2022 10:04:11 +0100 Subject: [PATCH 03/13] feat(sewer-choropleth): Implement yellow color on map when data for that area is outdated --- .../choropleth/logic/use-fill-color.ts | 83 +++++++++---------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/packages/app/src/components/choropleth/logic/use-fill-color.ts b/packages/app/src/components/choropleth/logic/use-fill-color.ts index 68088f29c0..662e8839fe 100644 --- a/packages/app/src/components/choropleth/logic/use-fill-color.ts +++ b/packages/app/src/components/choropleth/logic/use-fill-color.ts @@ -1,4 +1,4 @@ -import { assert, ChoroplethThresholdsValue } from '@corona-dashboard/common'; +import { assert, ChoroplethThresholdsValue, colors } from '@corona-dashboard/common'; import { scaleThreshold } from 'd3-scale'; import { useMemo } from 'react'; import { isDefined, isPresent } from 'ts-is-present'; @@ -17,70 +17,73 @@ import { isCodedValueType } from './utils'; * @param dataConfig * @returns */ -export function useFillColor( - data: T[], - map: MapType, - dataConfig: DataConfig, - thresholdMap?: MapType -) { +export function useFillColor(data: T[], map: MapType, dataConfig: DataConfig, thresholdMap?: MapType) { const codeType = mapToCodeType[map]; const { metricProperty, noDataFillColor } = dataConfig; - const getValueByCode = useMemo( - () => createGetValueByCode(metricProperty, codeType, data), - [metricProperty, codeType, data] - ); + const getValueByCode = useMemo(() => createGetValueByCode(metricProperty, codeType, data), [metricProperty, codeType, data]); + + const getIsOutdatedByCode = useMemo(() => createIsOutdatedByCode(codeType, data), [codeType, data]); const threshold = thresholds[thresholdMap || map][metricProperty as string]; - assert( - isDefined(threshold), - `[${ - useFillColor.name - }] No threshold configured for map type ${map} and metric property ${metricProperty.toString()}` - ); + assert(isDefined(threshold), `[${useFillColor.name}] No threshold configured for map type ${map} and metric property ${metricProperty.toString()}`); const colorScale = useMemo(() => createColorScale(threshold), [threshold]); - return useMemo( - () => createGetFillColor(getValueByCode, colorScale, noDataFillColor), - [getValueByCode, colorScale, noDataFillColor] - ); + return useMemo(() => createGetFillColor(getValueByCode, getIsOutdatedByCode, colorScale, noDataFillColor), [getValueByCode, getIsOutdatedByCode, colorScale, noDataFillColor]); } -export function getFillColor( - data: T[], - map: MapType, - dataConfig: DataConfig, - thresholdMap?: MapType -) { +export function getFillColor(data: T[], map: MapType, dataConfig: DataConfig, thresholdMap?: MapType) { const codeType = mapToCodeType[map]; const { metricProperty, noDataFillColor } = dataConfig; const getValueByCode = createGetValueByCode(metricProperty, codeType, data); + const getIsOutdatedByCode = createIsOutdatedByCode(codeType, data); + const threshold = thresholds[thresholdMap || map][metricProperty as string]; - assert( - isDefined(threshold), - `[${ - getFillColor.name - }] No threshold configured for map type ${map} and metric property ${metricProperty.toString()}` - ); + assert(isDefined(threshold), `[${getFillColor.name}] No threshold configured for map type ${map} and metric property ${metricProperty.toString()}`); const colorScale = createColorScale(threshold); - return createGetFillColor(getValueByCode, colorScale, noDataFillColor); + return createGetFillColor(getValueByCode, getIsOutdatedByCode, colorScale, noDataFillColor); } function createGetFillColor( getValueByCode: ReturnType, + getIsOutdatedByCode: ReturnType, colorScale: ReturnType, noDataFillColor: string ) { return (code: string) => { const value = getValueByCode(code); - const result = isPresent(value) ? colorScale(value) : noDataFillColor; + const shouldOverrideDefaultColor = getIsOutdatedByCode(code); + let result = noDataFillColor; + + if (isPresent(value)) result = colorScale(value); + + // Override the default color scale when a datapoint contains outdated data. + if (shouldOverrideDefaultColor) result = colors.yellow1; + return result; }; } +// Returns a function which returns a boolean when the datapoint (matching the code passed in) contains outdated data. +function createIsOutdatedByCode(codeType: CodeProp, data: T[]): (code: string) => boolean | undefined { + return (code: string) => { + const dataPointByCode = data.find((dataPoint) => { + if ((dataPoint as unknown as Record)[codeType] === code) { + return dataPoint; + } + }); + + if (dataPointByCode && 'data_is_outdated' in dataPointByCode) { + return dataPointByCode.data_is_outdated; + } + + return false; + }; +} + function createColorScale(threshold: ChoroplethThresholdsValue[]) { const domain = threshold.map((t) => t.threshold); domain.shift(); @@ -91,11 +94,7 @@ function createColorScale(threshold: ChoroplethThresholdsValue[]) { return color; } -function createGetValueByCode( - metricProperty: keyof T, - codeType: CodeProp, - data: T[] -) { +function createGetValueByCode(metricProperty: keyof T, codeType: CodeProp, data: T[]) { return (code: string) => { const item = data .filter((x) => { @@ -104,8 +103,6 @@ function createGetValueByCode( }) .find((x) => (x as unknown as Record)[codeType] === code); - return isDefined(item) && isPresent(item[metricProperty]) - ? Number(item[metricProperty]) - : undefined; + return isDefined(item) && isPresent(item[metricProperty]) ? Number(item[metricProperty]) : undefined; }; } From ae770be725b006b28dab18401ef6eb1b67312527 Mon Sep 17 00:00:00 2001 From: Arjun Puri Date: Fri, 11 Nov 2022 10:08:13 +0100 Subject: [PATCH 04/13] feat(sewer-choropleth): Implements new legend item for outdated data, exports font-sizes. --- .../app/src/components/choropleth-legenda.tsx | 54 +++++++-------- .../app/src/components/choropleth-tile.tsx | 51 ++++---------- .../app/src/pages/landelijk/rioolwater.tsx | 67 +++++-------------- packages/app/src/style/theme.ts | 2 +- 4 files changed, 55 insertions(+), 119 deletions(-) diff --git a/packages/app/src/components/choropleth-legenda.tsx b/packages/app/src/components/choropleth-legenda.tsx index 75efeb0ce8..d9bbc8e2bb 100644 --- a/packages/app/src/components/choropleth-legenda.tsx +++ b/packages/app/src/components/choropleth-legenda.tsx @@ -1,28 +1,26 @@ -import { ChoroplethThresholdsValue } from '@corona-dashboard/common'; +import { ChoroplethThresholdsValue, colors } from '@corona-dashboard/common'; import { css, SystemStyleObject } from '@styled-system/css'; import styled from 'styled-components'; import { ValueAnnotation } from '~/components/value-annotation'; import { useIntl } from '~/intl'; +import { space, fontSizes } from '~/style/theme'; import { replaceVariablesInText } from '~/utils'; import { useBreakpoints } from '~/utils/use-breakpoints'; import { useResizeObserver } from '~/utils/use-resize-observer'; import { Box } from './base'; import { Legend, LegendItem } from './legend'; -import { Text } from './typography'; +import { InlineText, Text } from './typography'; interface ChoroplethLegendaProps { title: string; thresholds: ChoroplethThresholdsValue[]; valueAnnotation?: string; type?: 'default' | 'bar'; + pageType?: string; + outdatedDataLabel?: string; } -export function ChoroplethLegenda({ - title, - thresholds, - valueAnnotation, - type = 'bar', -}: ChoroplethLegendaProps) { +export function ChoroplethLegenda({ title, thresholds, valueAnnotation, type = 'bar', pageType, outdatedDataLabel }: ChoroplethLegendaProps) { const [itemRef, itemSize] = useResizeObserver(); const [endLabelRef, endLabelSize] = useResizeObserver(); const { commonTexts, formatNumber } = useIntl(); @@ -44,13 +42,18 @@ export function ChoroplethLegenda({ ); return ( -