Skip to content

Commit

Permalink
Merge pull request #11 from neherlab/plot-tweaks
Browse files Browse the repository at this point in the history
Plot tweaks
  • Loading branch information
ArtPoon authored May 16, 2023
2 parents b2b5b06 + 877f244 commit c6e690d
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 13 deletions.
99 changes: 99 additions & 0 deletions web/src/components/Common/CustomPlotDots.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react'
import { useTheme } from 'styled-components'
import type { Props as DotProps } from 'recharts/types/shape/Dot'

const AREA_FACTOR = 0.6
const CIRCLE_LINEWIDTH = 2

export interface CustomizedDotProps extends DotProps {
value: number
payload: {
counts: Record<string, number>
ranges: Record<string, [number, number]>
totals: Record<string, number>
}
}

/** Line plot dot component which displays a bubble in proportion to frequency */
export function CustomizedDot(props: CustomizedDotProps) {
const theme = useTheme()
const y0 = theme.plot.margin.top

const {
cx,
cy,
stroke,
name,
payload: { counts, totals },
height,
} = props

if (!name || typeof height != 'number' || !cy || totals[name] === 0) {
// variant has not been observed in this region
return null
}

const freq = counts[name] / totals[name] // observed frequency
const cy2 = height * (1 - freq) + y0 // map obs. freq. to plot region
const rad = 1 + AREA_FACTOR * Math.sqrt(counts[name]) // scale area to obs. count

return (
<>
<circle cx={cx} cy={cy2} stroke={stroke} strokeWidth={CIRCLE_LINEWIDTH} fill="#ffffff88" r={rad} />
<line x1={cx} y1={cy} x2={cx} y2={cy < cy2 ? cy2 - rad : cy2 + rad} stroke={stroke} strokeWidth={1} />
</>
// the line segment connects each dot to the linear trend as a color-accessible visual cue of variant id
)
}

/**
* Line plot active (on mouse hover) dot component which displays either a filled bubble in proportion to frequency
* or a confidence line
*/
export function CustomizedActiveDot(props: CustomizedDotProps & { shouldShowDots: boolean }) {
const theme = useTheme()
const y0 = theme.plot.margin.top

const {
cx,
cy,
fill,
name,
payload: { counts, ranges, totals },
value,
shouldShowDots,
} = props

if (!name || !cy || totals[name] === 0) {
return null // no variants observed in this region
}

const freq = counts[name] / totals[name] // observed frequency
// map freq from (0,1) to plot region
const cy2 = value === 1 ? y0 : ((cy - y0) * (1 - freq)) / (1 - value) + y0
// display confidence interval as vertical line segment
const [r1, r2] = ranges[name]

return (
<>
{shouldShowDots && (
<circle
cx={cx}
cy={cy2}
stroke={fill}
strokeWidth={1.5 * CIRCLE_LINEWIDTH}
fill="#ffffff00"
r={1 + AREA_FACTOR * Math.sqrt(counts[name])}
/>
)}
<line
x1={cx}
y1={value === 1 ? y0 : ((cy - y0) * (1 - r2)) / (1 - value) + y0}
x2={cx}
y2={value === 1 ? y0 : ((cy - y0) * (1 - r1)) / (1 - value) + y0}
stroke={fill}
strokeWidth={5}
/>
</>
)
}
24 changes: 18 additions & 6 deletions web/src/components/Regions/RegionsPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import {
} from 'src/helpers/format'
import { calculateTicks } from 'src/helpers/adjustTicks'
import { getCountryColor, getCountryStrokeDashArray, Pathogen, useRegionDataQuery } from 'src/io/getData'
import { shouldShowRangesOnRegionsPlotAtom } from 'src/state/settings.state'
import { shouldShowDotsOnRegionsPlotAtom, shouldShowRangesOnRegionsPlotAtom } from 'src/state/settings.state'
import { variantsAtom } from 'src/state/variants.state'
import { RegionsPlotTooltip } from 'src/components/Regions/RegionsPlotTooltip'
import { CustomizedDot, CustomizedActiveDot } from 'src/components/Common/CustomPlotDots'
import { DateSlider } from 'src/components/Common/DateSlider'
import { localeAtom } from 'src/state/locale.state'

Expand All @@ -37,7 +38,8 @@ function RegionsPlotImpl<T>({ width, height, data, minDate, maxDate, pathogen, c
const theme = useTheme()
const locale = useRecoilValue(localeAtom)
const shouldShowRanges = useRecoilValue(shouldShowRangesOnRegionsPlotAtom)
const variants = useRecoilValue(variantsAtom(pathogen.name))
const shouldShowDots = useRecoilValue(shouldShowDotsOnRegionsPlotAtom)
const variants = useRecoilValue(variantsAtom(pathogen.name)) // name, color and lineStyle
const {
variantsData: { variantsStyles },
} = useRegionDataQuery(pathogen.name, countryName)
Expand All @@ -48,25 +50,33 @@ function RegionsPlotImpl<T>({ width, height, data, minDate, maxDate, pathogen, c
)

const { lines, ranges } = useMemo(() => {
// draw trendlines
const lines = variants
.map(
({ name, enabled }) =>
enabled && (
<Line
key={`line-${name}`}
type="monotone"
name={name}
type="linear"
name={name} // variant name
dataKey={(d) => get(d.avgs, name)} // eslint-disable-line react-perf/jsx-no-new-function-as-prop
stroke={getCountryColor(variantsStyles, name)}
strokeWidth={theme.plot.line.strokeWidth}
strokeDasharray={getCountryStrokeDashArray(variantsStyles, name)}
dot={false}
// HACK: this is not type safe. These components rely on props, which are not included in Recharts typings
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
dot={shouldShowDots ? <CustomizedDot /> : false} // eslint-disable-line react-perf/jsx-no-jsx-as-prop
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
activeDot={<CustomizedActiveDot name={name} shouldShowDots={shouldShowDots} />} // eslint-disable-line react-perf/jsx-no-jsx-as-prop
isAnimationActive={false}
/>
),
)
.filter(Boolean)

// confidence intervals as shaded polygons
const ranges = variants
.map(
({ name, enabled }) =>
Expand All @@ -79,14 +89,15 @@ function RegionsPlotImpl<T>({ width, height, data, minDate, maxDate, pathogen, c
fill={getCountryColor(variantsStyles, name)}
fillOpacity={0.1}
isAnimationActive={false}
activeDot={false}
display={!shouldShowRanges ? 'none' : undefined}
/>
),
)
.filter(Boolean)

return { lines, ranges }
}, [shouldShowRanges, theme.plot.line.strokeWidth, variants, variantsStyles])
}, [shouldShowRanges, shouldShowDots, theme.plot.line.strokeWidth, variants, variantsStyles])

const metadata = useMemo(() => ({ pathogenName: pathogen.name, countryName }), [countryName, pathogen.name])

Expand Down Expand Up @@ -155,6 +166,7 @@ export function RegionsPlot({ pathogen, countryName }: RegionsPlotProps) {
}, [])

const { data, minDate, maxDate } = useMemo(() => {
// subset data (avgs, counts, date, ranges, totals) to date range
const data = regionData.values
.filter(({ date }) => {
const ts = ymdToTimestamp(date)
Expand Down
11 changes: 9 additions & 2 deletions web/src/components/Regions/RegionsPlotSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ import React, { useMemo } from 'react'
import { CheckboxWithText } from 'src/components/Common/Checkbox'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import { useRecoilToggle } from 'src/hooks/useToggle'
import { shouldShowRangesOnRegionsPlotAtom } from 'src/state/settings.state'
import { shouldShowDotsOnRegionsPlotAtom, shouldShowRangesOnRegionsPlotAtom } from 'src/state/settings.state'

export function RegionsPlotSettings() {
const { t } = useTranslationSafe()
const { state: shouldShowRanges, setState: setShouldShowRanges } = useRecoilToggle(shouldShowRangesOnRegionsPlotAtom)
const { state: shouldShowDots, setState: setShouldShowDots } = useRecoilToggle(shouldShowDotsOnRegionsPlotAtom)
const text = useMemo(() => t('Show confidence intervals'), [t])
return <CheckboxWithText title={text} label={text} checked={shouldShowRanges} setChecked={setShouldShowRanges} />
const text2 = useMemo(() => t('Show observed counts'), [t])
return (
<>
<CheckboxWithText title={text} label={text} checked={shouldShowRanges} setChecked={setShouldShowRanges} />
<CheckboxWithText title={text2} label={text2} checked={shouldShowDots} setChecked={setShouldShowDots} />
</>
)
}
17 changes: 13 additions & 4 deletions web/src/components/Variants/VariantsPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
import { calculateTicks } from 'src/helpers/adjustTicks'
import { getCountryColor, getCountryStrokeDashArray, Pathogen, useVariantDataQuery } from 'src/io/getData'
import { continentsAtom, countriesAtom } from 'src/state/geography.state'
import { shouldShowRangesOnVariantsPlotAtom } from 'src/state/settings.state'
import { shouldShowDotsOnVariantsPlotAtom, shouldShowRangesOnVariantsPlotAtom } from 'src/state/settings.state'
import { VariantsPlotTooltip } from 'src/components/Variants/VariantsPlotTooltip'
import { CustomizedDot, CustomizedActiveDot } from 'src/components/Common/CustomPlotDots'
import { DateSlider } from 'src/components/Common/DateSlider'
import { localeAtom } from 'src/state/locale.state'

Expand All @@ -37,6 +38,7 @@ function LinePlot<T>({ width, height, data, minDate, maxDate, pathogen, variantN
const theme = useTheme()
const locale = useRecoilValue(localeAtom)
const shouldShowRanges = useRecoilValue(shouldShowRangesOnVariantsPlotAtom)
const shouldShowDots = useRecoilValue(shouldShowDotsOnVariantsPlotAtom)
const regions = useRecoilValue(continentsAtom(pathogen.name))
const countries = useRecoilValue(countriesAtom(pathogen.name))
const {
Expand All @@ -57,13 +59,19 @@ function LinePlot<T>({ width, height, data, minDate, maxDate, pathogen, variantN
enabled && (
<Line
key={`line-${name}`}
type="monotone"
type="linear"
name={name}
dataKey={(d) => get(d.avgs, name)} // eslint-disable-line react-perf/jsx-no-new-function-as-prop
stroke={getCountryColor(geographyStyles, name)}
strokeWidth={theme.plot.line.strokeWidth}
strokeDasharray={getCountryStrokeDashArray(geographyStyles, name)}
dot={false}
// HACK: this is not type safe. These components rely on props, which are not included in Recharts typings
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
dot={shouldShowDots ? <CustomizedDot /> : false} // eslint-disable-line react-perf/jsx-no-jsx-as-prop
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
activeDot={<CustomizedActiveDot name={name} shouldShowDots={shouldShowDots} />} // eslint-disable-line react-perf/jsx-no-jsx-as-prop
isAnimationActive={false}
/>
),
Expand All @@ -82,14 +90,15 @@ function LinePlot<T>({ width, height, data, minDate, maxDate, pathogen, variantN
fill={getCountryColor(geographyStyles, name)}
fillOpacity={0.1}
isAnimationActive={false}
activeDot={false}
display={!shouldShowRanges ? 'none' : undefined}
/>
),
)
.filter(Boolean)

return { lines, ranges }
}, [regions, countries, geographyStyles, theme.plot.line.strokeWidth, shouldShowRanges])
}, [regions, countries, geographyStyles, theme.plot.line.strokeWidth, shouldShowRanges, shouldShowDots])

const metadata = useMemo(() => ({ pathogenName: pathogen.name, variantName }), [pathogen.name, variantName])

Expand Down
5 changes: 4 additions & 1 deletion web/src/components/Variants/VariantsPlotSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import { Form as FormBase } from 'reactstrap'
import { CheckboxWithText } from 'src/components/Common/Checkbox'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import { useRecoilToggle } from 'src/hooks/useToggle'
import { shouldShowRangesOnVariantsPlotAtom } from 'src/state/settings.state'
import { shouldShowDotsOnVariantsPlotAtom, shouldShowRangesOnVariantsPlotAtom } from 'src/state/settings.state'

export function VariantsPlotSettings() {
const { t } = useTranslationSafe()
const { state: shouldShowRanges, setState: setShouldShowRanges } = useRecoilToggle(shouldShowRangesOnVariantsPlotAtom)
const { state: shouldShowDots, setState: setShouldShowDots } = useRecoilToggle(shouldShowDotsOnVariantsPlotAtom)
const text = useMemo(() => t('Show confidence intervals'), [t])
const text2 = useMemo(() => t('Show observed counts'), [t])
return (
<Form>
<CheckboxWithText title={text} label={text} checked={shouldShowRanges} setChecked={setShouldShowRanges} />
<CheckboxWithText title={text2} label={text2} checked={shouldShowDots} setChecked={setShouldShowDots} />
</Form>
)
}
Expand Down
12 changes: 12 additions & 0 deletions web/src/state/settings.state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,24 @@ export const shouldShowRangesOnVariantsPlotAtom = atom({
effects: [persistAtom],
})

export const shouldShowDotsOnVariantsPlotAtom = atom({
key: 'shouldShowDotsOnVariantsPlotAtom',
default: true,
effects: [persistAtom],
})

export const shouldShowRangesOnRegionsPlotAtom = atom({
key: 'shouldShowRangesOnRegionsPlotAtom',
default: false,
effects: [persistAtom],
})

export const shouldShowDotsOnRegionsPlotAtom = atom({
key: 'shouldShowDotsOnRegionsPlotAtom',
default: true,
effects: [persistAtom],
})

export const isSidebarSettingsCollapsedAtom = atom({
key: 'isSidebarSettingsCollapsedAtom',
default: false,
Expand Down

0 comments on commit c6e690d

Please sign in to comment.