Skip to content

Commit

Permalink
Merge pull request #471 from hodcroftlab/302-download-data-dynamically
Browse files Browse the repository at this point in the history
302 download data dynamically
  • Loading branch information
AdvancedCodingMonkey authored Jan 15, 2025
2 parents f31ec95 + 08113a5 commit 9d2631e
Show file tree
Hide file tree
Showing 79 changed files with 1,827 additions and 758 deletions.
3 changes: 2 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"typeface-droid-sans-mono": "^0.0.44",
"typeface-open-sans": "^1.1.13",
"url-join": "^5.0.0",
"use-sync-external-store": "^1.2.2"
"zod": "3.24.1"
},
"devDependencies": {
"@babel/core": "^7.26.0",
Expand Down Expand Up @@ -214,6 +214,7 @@
"lodash-webpack-plugin": "^0.11.6",
"map.prototype.tojson": "^0.1.3",
"markdown-loader": "^8.0.0",
"msw": "2.6.9",
"next-transpile-modules": "^10.0.1",
"nodemon": "^3.1.7",
"npm-run-all": "^4.1.5",
Expand Down
5 changes: 2 additions & 3 deletions web/src/components/Acknowledgements/AcknowledgementsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { PageHeading } from '../Common/PageHeading'
import AcknowledgementsContent from './AcknowledgementsContent.md'
import { AcknowledgementsError } from 'src/components/Acknowledgements/AcknowledgementsError'

import { getClusters } from 'src/io/getClusters'
import { useClusters } from 'src/io/getClusters'
import { Layout } from 'src/components/Layout/Layout'
import { AcknowledgementsCard, AcknowledgementsKeysJson } from 'src/components/Acknowledgements/AcknowledgementsCard'

Expand All @@ -19,9 +19,8 @@ export const AcknowledgementsPageContainer = styled(Container)`
padding: 0 0.5rem;
`

const clusters = getClusters()

export function useQueryAcknowledgementsKeys() {
const clusters = useClusters()
return useQuery({
queryKey: ['acknowledgements_keys'],
queryFn: async () => {
Expand Down
36 changes: 36 additions & 0 deletions web/src/components/Cases/CasesComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Row } from 'reactstrap'
import React, { useMemo } from 'react'
import { ColCustom } from 'src/components/Common/ColCustom'
import { useTicks, useTimeDomain } from 'src/io/useParams'
import { CasesPlotCard } from 'src/components/Cases/CasesPlotCard'
import { CountryFlag } from 'src/components/Common/CountryFlag'
import { PerCountryCasesDistribution } from 'src/io/getPerCountryCasesData'

export function CasesComponents({
withClustersFiltered,
enabledClusters,
}: {
withClustersFiltered: PerCountryCasesDistribution[]
enabledClusters: string[]
}) {
const timeDomain = useTimeDomain()
const ticks = useTicks()

const casesComponents = useMemo(
() =>
withClustersFiltered.map(({ country, distribution }) => (
<ColCustom key={country} md={12} lg={6} xl={6} xxl={4}>
<CasesPlotCard
country={country}
distribution={distribution}
cluster_names={enabledClusters}
Icon={CountryFlag}
ticks={ticks}
timeDomain={timeDomain}
/>
</ColCustom>
)),
[enabledClusters, withClustersFiltered, ticks, timeDomain],
)
return <Row className={'gx-0'}>{casesComponents}</Row>
}
59 changes: 24 additions & 35 deletions web/src/components/Cases/CasesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
import React, { useCallback, useMemo } from 'react'
import React, { Suspense, useCallback, useMemo } from 'react'
import { Col, Row } from 'reactstrap'
import { useRecoilState } from 'recoil'
import { CasesPlotCard } from './CasesPlotCard'
import { ErrorBoundary } from 'react-error-boundary'
import { CenteredEditable, Editable } from 'src/components/Common/Editable'
import { ColCustom } from 'src/components/Common/ColCustom'
import { Layout } from 'src/components/Layout/Layout'
import { MainFlex, SidebarFlex, WrapperFlex } from 'src/components/Common/PlotLayout'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import { MdxContent } from 'src/i18n/getMdxContent'
import { getPerCountryCasesData, filterClusters, filterCountries } from 'src/io/getPerCountryCasesData'
import { clustersCasesAtom, disableAllClusters, enableAllClusters, toggleCluster } from 'src/state/ClustersForCaseData'
import {
continentsCasesAtom,
countriesCasesAtom,
disableAllCountries,
enableAllCountries,
toggleContinent,
toggleCountry,
} from 'src/state/PlacesForCaseData'
import { filterClusters, filterCountries, usePerCountryCasesData } from 'src/io/getPerCountryCasesData'
import { clustersCasesAtom } from 'src/state/ClustersForCaseData'
import { continentsCasesAtom, countriesCasesAtom } from 'src/state/PlacesForCaseData'
import { CountryFlag } from 'src/components/Common/CountryFlag'
import { PageHeading } from 'src/components/Common/PageHeading'
import { SharingPanel } from 'src/components/Common/SharingPanel'
import { DistributionSidebar } from 'src/components/DistributionSidebar/DistributionSidebar'
import { CasesComponents } from 'src/components/Cases/CasesComponents'
import { FetchError } from 'src/components/Error/FetchError'
import { LOADING } from 'src/components/Loading/Loading'
import { disableAllClusters, enableAllClusters, toggleCluster } from 'src/state/Clusters'
import { disableAllCountries, enableAllCountries, toggleContinent, toggleCountry } from 'src/state/Places'

const enabledFilters = ['clusters', 'countriesWithIcons']

Expand All @@ -32,7 +29,7 @@ export function CasesPage() {
const [continents, setContinents] = useRecoilState(continentsCasesAtom)
const [clusters, setClusters] = useRecoilState(clustersCasesAtom)

const { perCountryCasesDistributions } = useMemo(() => getPerCountryCasesData(), [])
const { perCountryCasesDistributions } = usePerCountryCasesData()

const { enabledClusters, withClustersFiltered } = useMemo(() => {
const { withCountriesFiltered } = filterCountries(countries, perCountryCasesDistributions)
Expand All @@ -41,21 +38,6 @@ export function CasesPage() {
return { enabledClusters, withClustersFiltered }
}, [countries, perCountryCasesDistributions, clusters])

const casesComponents = useMemo(
() =>
withClustersFiltered.map(({ country, distribution }) => (
<ColCustom key={country} md={12} lg={6} xl={6} xxl={4}>
<CasesPlotCard
country={country}
distribution={distribution}
cluster_names={enabledClusters}
Icon={CountryFlag}
/>
</ColCustom>
)),
[enabledClusters, withClustersFiltered],
)

const handleClusterCheckedChange = useCallback(
(cluster: string) => {
setClusters((oldClusters) => toggleCluster(oldClusters, cluster))
Expand Down Expand Up @@ -95,27 +77,27 @@ export function CasesPage() {

return (
<Layout wide>
<Row noGutters>
<Row className={'gx-0'}>
<Col>
<PageHeading>{t('Estimated Cases by Variant')}</PageHeading>
</Col>
</Row>

<Row noGutters>
<Row className={'gx-0'}>
<Col>
<CenteredEditable githubUrl="tree/master/web/src/content/en/PerCountryCasesIntro.md">
<MdxContent filepath="PerCountryCasesIntro.md" />
</CenteredEditable>
</Col>
</Row>

<Row noGutters>
<Row className={'gx-0'}>
<Col>
<SharingPanel />
</Col>
</Row>

<Row noGutters>
<Row className={'gx-0'}>
<Col>
<Editable githubUrl="blob/master/scripts" text={t('View data generation scripts')}>
<WrapperFlex>
Expand All @@ -140,9 +122,16 @@ export function CasesPage() {
</SidebarFlex>

<MainFlex>
<Row noGutters>
<Row className={'gx-0'}>
<Col>
<Row noGutters>{casesComponents}</Row>
<ErrorBoundary FallbackComponent={FetchError}>
<Suspense fallback={LOADING}>
<CasesComponents
withClustersFiltered={withClustersFiltered}
enabledClusters={enabledClusters}
/>
</Suspense>
</ErrorBoundary>
</Col>
</Row>
</MainFlex>
Expand Down
9 changes: 6 additions & 3 deletions web/src/components/Cases/CasesPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,28 @@ import { useTheme } from 'styled-components'
import { CasesPlotTooltip } from './CasesPlotTooltip'
import type { PerCountryCasesDistributionDatum } from 'src/io/getPerCountryCasesData'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import { ticks, timeDomain } from 'src/io/getParams'
import { CLUSTER_NAME_OTHERS, getClusterColor } from 'src/io/getClusters'
import { CLUSTER_NAME_OTHERS, useClusterColors } from 'src/io/getClusters'
import { formatDateHumanely } from 'src/helpers/format'
import { adjustTicks } from 'src/helpers/adjustTicks'
import { PlotPlaceholder } from 'src/components/Common/PlotPlaceholder'
import { ChartContainer } from 'src/components/Common/ChartContainer'
import { Ticks, TimeDomain } from 'src/io/useParams'

const CHART_MARGIN = { left: 10, top: 12, bottom: 6, right: 12 }
const ALLOW_ESCAPE_VIEW_BOX = { x: false, y: true }

export interface CasesPlotProps {
cluster_names: string[]
distribution: PerCountryCasesDistributionDatum[]
ticks: Ticks
timeDomain: TimeDomain
}

export function CasesPlotComponent({ cluster_names, distribution }: CasesPlotProps) {
export function CasesPlotComponent({ cluster_names, distribution, ticks, timeDomain }: CasesPlotProps) {
const { t } = useTranslationSafe()
const theme = useTheme()
const chartRef = useRef(null)
const getClusterColor = useClusterColors()

const data = distribution.map(({ week, stand_total_cases, stand_estimated_cases }) => {
const weekSec = DateTime.fromFormat(week, 'yyyy-MM-dd').toSeconds()
Expand Down
14 changes: 11 additions & 3 deletions web/src/components/Cases/CasesPlotCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { PlotCardTitle } from 'src/components/Common/PlotCardTitle'
import { CountryFlagProps } from 'src/components/Common/CountryFlag'
import { USStateCodeProps } from 'src/components/Common/USStateCode'
import { CasesPlot } from 'src/components/Cases/CasesPlot'
import { Ticks, TimeDomain } from 'src/io/useParams'

const FlagAlignment = styled.span`
display: flex;
Expand All @@ -21,9 +22,11 @@ export interface CasesPlotCardProps {
distribution: PerCountryCasesDistributionDatum[]
cluster_names: string[]
Icon?: React.ComponentType<CountryFlagProps | USStateCodeProps>
ticks: Ticks
timeDomain: TimeDomain
}

export function CasesPlotCard({ country, distribution, cluster_names, Icon }: CasesPlotCardProps) {
export function CasesPlotCard({ country, distribution, cluster_names, Icon, ticks, timeDomain }: CasesPlotCardProps) {
return (
<Card className="m-2">
<CardHeader className="d-flex flex-sm-column">
Expand All @@ -37,9 +40,14 @@ export function CasesPlotCard({ country, distribution, cluster_names, Icon }: Ca

<CardBody className="p-0">
<Col className="p-0">
<Row noGutters>
<Row className={'gx-0'}>
<Col className="p-0">
<CasesPlot distribution={distribution} cluster_names={cluster_names} />
<CasesPlot
distribution={distribution}
cluster_names={cluster_names}
ticks={ticks}
timeDomain={timeDomain}
/>
</Col>
</Row>
</Col>
Expand Down
3 changes: 2 additions & 1 deletion web/src/components/Cases/CasesPlotTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Props as DefaultTooltipContentProps } from 'recharts/types/component/De
import { ColoredBox } from '../Common/ColoredBox'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import { formatDateBiweekly, formatInteger, formatProportion } from 'src/helpers/format'
import { getClusterColor } from 'src/io/getClusters'
import { useClusterColors } from 'src/io/getClusters'

const EPSILON = 1e-2

Expand Down Expand Up @@ -46,6 +46,7 @@ export const ClusterNameText = styled.span`

export function CasesPlotTooltip(props: DefaultTooltipContentProps<number, string>) {
const { t } = useTranslationSafe()
const getClusterColor = useClusterColors()

const { payload } = props
if (!payload || payload.length === 0) {
Expand Down
76 changes: 76 additions & 0 deletions web/src/components/Cases/__tests__/Cases.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'
import { screen } from '@testing-library/react'
import React from 'react'
import { http, HttpResponse } from 'msw'
import { ErrorBoundary } from 'react-error-boundary'
import ResizeObserver from 'resize-observer-polyfill'
import { renderWithQueryClient } from 'src/helpers/__tests__/providers'
import { server } from 'src/components/SharedMutations/__tests__/mockRequests'
import { FETCHER } from 'src/hooks/useAxiosQuery'
import { CasesComponents } from 'src/components/Cases/CasesComponents'
import { PerCountryCasesDistribution } from 'src/io/getPerCountryCasesData'

globalThis.ResizeObserver = ResizeObserver

describe('Cases', () => {
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))

afterAll(() => server.close())

afterEach(() => {
server.resetHandlers()
FETCHER.getQueryClient().clear()
})
describe('CasesComponents', () => {
const withClustersFiltered: PerCountryCasesDistribution[] = [
{
country: 'USA',
distribution: [
{
// eslint-disable-next-line camelcase
stand_total_cases: 1114,
week: '2020-04-27',
// eslint-disable-next-line camelcase
stand_estimated_cases: {
'20A.EU2': 0,
},
},
],
},
]
const enabledClusters = ['20A.EU2']

test('does not trigger error boundary when backend call succeeds', async () => {
// Act
renderWithQueryClient(
<ErrorBoundary fallback={'Error boundary'}>
<CasesComponents withClustersFiltered={withClustersFiltered} enabledClusters={enabledClusters} />
</ErrorBoundary>,
)

// Assert
expect(await screen.findByText('USA', undefined, { timeout: 3000 })).toBeDefined()
})

test('triggers error boundary when backend call fails', async () => {
// Arrange
server.use(
http.get('/data/params.json', () => {
return new HttpResponse(null, { status: 404 })
}),
)
// Disable console output
vi.spyOn(console, 'error').mockImplementation(() => null)

// Act
renderWithQueryClient(
<ErrorBoundary fallback={'Error boundary'}>
<CasesComponents withClustersFiltered={withClustersFiltered} enabledClusters={enabledClusters} />
</ErrorBoundary>,
)

// Assert
expect(await screen.findByText('Error boundary', undefined, { timeout: 3000 })).toBeDefined()
})
})
})
12 changes: 6 additions & 6 deletions web/src/components/ClusterButtonPanel/ClusterButtonPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import React from 'react'
import React, { useMemo } from 'react'

import { Card, CardBody, CardHeader, Row } from 'reactstrap'
import { styled } from 'styled-components'
import get from 'lodash/get'
import { ClusterButtonGroup } from 'src/components/ClusterButtonPanel/ClusterButtonGroup'
import { ClusterDatum, getClusters, getClustersGrouped } from 'src/io/getClusters'
import { ClusterDatum, getClustersGrouped, useClusters } from 'src/io/getClusters'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'

const clusters = getClusters().filter((cluster) => !cluster.has_no_page)
const clustersGrouped = getClustersGrouped(clusters)

const ClustersRow = styled(Row)`
display: flex;
flex-wrap: wrap;
Expand Down Expand Up @@ -48,9 +45,12 @@ export interface ClusterPanelProps {

export function ClusterButtonPanel({ currentCluster, className }: ClusterPanelProps) {
const { t } = useTranslationSafe()
const allClusters = useClusters()
const clusters = useMemo(() => allClusters.filter((cluster) => !cluster.has_no_page), [allClusters])
const clustersGrouped = useMemo(() => getClustersGrouped(clusters), [clusters])

return (
<ClustersRow noGutters className={className}>
<ClustersRow className={`gx-0 ${className}`}>
{Object.entries(clustersGrouped).map(([clusterType, clusterGroup]) => {
const clusterTypeHeading = get(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { screen, fireEvent } from '@testing-library/react'
import React from 'react'
import { ClusterButtonGroup } from '../ClusterButtonGroup'
import { ClusterDatum } from 'src/io/getClusters'
import { renderWithThemeAndTranslations } from 'src/helpers/__tests__/theme'
import { renderWithThemeAndTranslations } from 'src/helpers/__tests__/providers'

describe('ClusterButtonGroup', () => {
describe('show more / show less button', () => {
Expand Down
Loading

0 comments on commit 9d2631e

Please sign in to comment.