Skip to content
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
4 changes: 2 additions & 2 deletions app/src/components/DataVisualizationContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import DateSelector from './DateSelector';
import ViewSwitchboard from './ViewSwitchboard';
import ErrorBoundary from './ErrorBoundary';
import AboutHubOverlay from './AboutHubOverlay';
import PathogenFrontPage from './PathogenFrontPage';
import FrontPage from './FrontPage';
import { IconShare, IconBrandGithub } from '@tabler/icons-react';
import { useClipboard } from '@mantine/hooks';

Expand Down Expand Up @@ -284,7 +284,7 @@ const DataVisualizationContainer = () => {
</Helmet>
<Container size="xl" py="xl" style={{ maxWidth: '1400px' }}>
<Stack gap="lg">
<PathogenFrontPage />
<FrontPage />
</Stack>
</Container>
</ErrorBoundary>
Expand Down
101 changes: 101 additions & 0 deletions app/src/components/FrontPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { SimpleGrid, Stack, Title, Paper, Anchor } from '@mantine/core';
import PathogenOverviewGraph from './PathogenOverviewGraph';
import NHSNOverviewGraph from './NHSNOverviewGraph'
import Announcement from './Announcement'
import { useView } from '../hooks/useView';

const FluPeakLink = () => {
const { setViewType } = useView();

const handleClick = (e) => {
e.preventDefault();
setViewType('flu_peak');
};

return (
<span>
RespiLens now displays{' '}
<Anchor
component="button"
onClick={handleClick}
fw={700}
c="blue.7"
style={{ fontSize: 'inherit', veriticalAlign: 'baseline' }}
>
flu peak forecasts;
</Anchor>
{' '}forecasts for peak of the current influenza season.
</span>
)
}
const MetroCastLink = () => {
const { setViewType } = useView();

const handleClick = (e) => {
e.preventDefault();
setViewType('metrocast_forecasts');
};

return (
<span>
RespiLens now displays{' '}
<Anchor
component="button"
onClick={handleClick}
fw={700}
c="blue.7"
style={{ fontSize: 'inherit', verticalAlign: 'baseline' }}
>
flu MetroCast forecasts;
</Anchor>
{' '}metro area-level flu forecasts.
</span>
);
};

const FrontPage = () => {
const { selectedLocation } = useView();

return (
<Stack>
<Announcement
id="new-metrocast-2026"
startDate="2026-02-01"
endDate="2026-03-15"
announcementType="update"
text={<MetroCastLink />}
/>
<Announcement
id="new-flu-peaks-2026"
startDate="2026-02-01"
endDate="2026-03-01"
announcementType={"update"}
text={<FluPeakLink />}
/>
<Announcement id={"hub-seasonal-warning"} startDate={'2026-05-31'} endDate={'2026-11-10'} announcementType={'alert'} text={
"Forecast hubs are out of season. Forecasting will begin again in Novembor."
}
/>
<Paper shadow="sm" p="lg" radius="md" withBorder>
<Stack gap="md">
<Title order={3}>Explore forecasts by pathogen</Title>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
<PathogenOverviewGraph viewType="covid_forecasts" title="COVID-19" location={selectedLocation} />
<PathogenOverviewGraph viewType="flu_forecasts" title="Flu" location={selectedLocation} />
<PathogenOverviewGraph viewType="rsv_forecasts" title="RSV" location={selectedLocation} />
</SimpleGrid>
</Stack>
</Paper>
<Paper shadow="sm" p="lg" radius="md" withBorder>
<Stack gap="md">
<Title order={3}>Explore surveillance data</Title>
<SimpleGrid cols={{ base: 1, sm: 2, lg:3 }} spacing="md">
<NHSNOverviewGraph location={selectedLocation}/>
</SimpleGrid>
</Stack>
</Paper>
</Stack>
);
};

export default FrontPage;
51 changes: 43 additions & 8 deletions app/src/components/NHSNOverviewGraph.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,22 @@ const NHSNOverviewGraph = ( {location} ) => {
}, [resolvedLocation]);

const { traces, layout } = useMemo(() => {
if (!data || !data.series) return { traces: [], layout: {} };
if (!data || !data.series || !data.series.dates) return { traces: [], layout: {} };

const dates = data.series.dates;
const lastDateStr = dates[dates.length - 1];
const lastDate = new Date(lastDateStr);
const twoMonthsAgo = new Date(lastDate);
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);

const xRange = [twoMonthsAgo.toISOString().split('T')[0], lastDateStr];

const activeTraces = DEFAULT_COLS.map((col) => {
const yData = data.series[col];
if (!yData) return null;

return {
x: data.series.dates,
x: dates,
y: yData,
name: col.replace('Total ', '').replace(' Admissions', ''),
type: 'scatter',
Expand All @@ -68,20 +76,47 @@ const NHSNOverviewGraph = ( {location} ) => {
};
}).filter(Boolean);

const dates = data.series.dates;
const lastDate = new Date(dates[dates.length - 1]);
const twoMonthsAgo = new Date(lastDate);
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);
let minY = Infinity;
let maxY = -Infinity;

activeTraces.forEach((trace) => {
trace.x.forEach((dateVal, i) => {
const currentPointDate = new Date(dateVal);
if (currentPointDate >= twoMonthsAgo && currentPointDate <= lastDate) {
const val = trace.y[i];
if (val !== null && val !== undefined && !Number.isNaN(val)) {
minY = Math.min(minY, val);
maxY = Math.max(maxY, val);
}
}
});
});

if (minY === Infinity || maxY === -Infinity) {
minY = 0;
maxY = 100;
}


const diff = maxY - minY;
const paddingTop = diff * 0.15; // 15% headroom
const paddingBottom = diff * 0.05;

const dynamicYRange = [
Math.max(0, minY - paddingBottom), // Maintain 0 as a hard floor for admissions
maxY + paddingTop
];

const layoutConfig = {
height: 280,
margin: { l: 40, r: 20, t: 10, b: 40 },
margin: { l: 45, r: 20, t: 10, b: 40 },
xaxis: {
range: [twoMonthsAgo.toISOString().split('T')[0], lastDate.toISOString().split('T')[0]],
range: xRange,
showgrid: false,
tickfont: { size: 10 }
},
yaxis: {
range: dynamicYRange,
automargin: true,
tickfont: { size: 10 },
fixedrange: true,
Expand Down
43 changes: 0 additions & 43 deletions app/src/components/PathogenFrontPage.jsx

This file was deleted.

4 changes: 2 additions & 2 deletions app/src/components/StateSelector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const StateSelector = () => {

const fetchStates = async () => { // different fetching/ordering if it is metrocast vs. other views
try {
const isMetro = viewType === 'metrocast_projs';
const isMetro = viewType === 'metrocast_forecasts';
const directory = isMetro ? 'flumetrocast' : 'flusight';

const manifestResponse = await fetch(
Expand Down Expand Up @@ -184,7 +184,7 @@ const StateSelector = () => {
!isSelected;

// Only apply nested styling in Metrocast view
const isCity = viewType === 'metrocast_projs' && state.location_name.includes(',');
const isCity = viewType === 'metrocast_forecasts' && state.location_name.includes(',');

let variant = 'subtle';
let color = 'blue';
Expand Down
2 changes: 1 addition & 1 deletion app/src/contexts/ViewContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export const ViewProvider = ({ children }) => {
const newSearchParams = new URLSearchParams(searchParams);


const isMovingToMetrocast = newView === 'metrocast_projs';
const isMovingToMetrocast = newView === 'metrocast_forecasts';

if (isMovingToMetrocast) {
const needsCityDefault = selectedLocation === APP_CONFIG.defaultLocation || selectedLocation.length === 2;
Expand Down