Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
126eb62
add multiplot view for states
emprzy Jan 26, 2026
f0fe492
add back in availableModels
emprzy Jan 26, 2026
1db10f0
make metro area cards clickable
emprzy Jan 26, 2026
6117f92
add gt history for metrocast data
emprzy Jan 27, 2026
9d0daa4
add locations underneath every plot for every view
emprzy Jan 27, 2026
71340bf
add disclaimer to metrocast view
emprzy Jan 27, 2026
4b3fd8d
change metrocast default location to `colorado`
emprzy Jan 27, 2026
a557dd1
transparent plotly tool bar, improved hover text in `MetroCastView.jsx`
emprzy Jan 28, 2026
5f7e7a0
make hover text background color match model selection color
emprzy Jan 28, 2026
2090768
add state label to NHSN
emprzy Jan 28, 2026
2950ddd
better hover label for COVID view
emprzy Jan 28, 2026
38963fb
better hover label for RSV view
emprzy Jan 28, 2026
b289662
better hover label for flu views (detailed and projections)
emprzy Jan 28, 2026
8ac254c
Merge pull request #84 from ACCIDDA/updates
emprzy Jan 28, 2026
32386c6
keydown ability on date (left-right keys)
emprzy Jan 28, 2026
79f086c
enable multi-date keydown date movement
emprzy Jan 29, 2026
2a43cd4
address linting
emprzy Jan 29, 2026
3d4e5cd
fix bug in keydown logic
emprzy Jan 29, 2026
630bb37
Merge pull request #85 from ACCIDDA/arrow-key-dates
emprzy Jan 29, 2026
5753799
change verbiage
emprzy Jan 29, 2026
228f96b
add NHSN overview plot to landing page
emprzy Jan 29, 2026
3ff2f1b
add an extra year to flusight gt data
emprzy Jan 30, 2026
0773de3
make NHSN overview graph responsive to selected location
emprzy Jan 30, 2026
01bdec7
basic dynamic announcement banner
emprzy Feb 2, 2026
d37d775
link patch
emprzy Feb 2, 2026
003fd5e
`endDate` fix
emprzy Feb 2, 2026
4b42a7f
add error safety net for `announcementType` property
emprzy Feb 2, 2026
7a4c255
make announcement dismissible
emprzy Feb 2, 2026
ed9c6f9
Merge pull request #86 from ACCIDDA/updates
emprzy Feb 3, 2026
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
83 changes: 83 additions & 0 deletions app/src/components/Announcement.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useState } from 'react';
import { Paper, Group, Text, ThemeIcon, Stack, CloseButton } from '@mantine/core';
import { IconSpeakerphone, IconAlertSquareRounded } from '@tabler/icons-react';

// Announcement component params:
// `id` | unique ID for the announcement
// `startDate` | date for announcement to start being displayed
// `endDate` | date for announcement to stop being displayed
// `text` | text for the announcement
// `announcementType` | alert or update
const Announcement = ({ id, startDate, endDate, text, announcementType }) => {
const storageKey = `dismissed-announcement-${id}`;

const [dismissed, setDismissed] = useState(() => {
if (typeof window === 'undefined') return false;
return localStorage.getItem(storageKey) === 'true';
});

const currentDate = new Date();
const start = new Date(startDate);
const end = new Date(endDate);

const validTypes = ['update', 'alert'];
if (!validTypes.includes(announcementType)) {
console.error(`[Announcement Error]: Invalid type "${announcementType}".`);
}

const isVisible = currentDate >= start && currentDate <= end;
if (!isVisible || dismissed) return null;

const handleDismiss = () => {
localStorage.setItem(storageKey, 'true');
setDismissed(true);
};

const isAlert = announcementType === 'alert';

return (
<Stack>
<Paper
withBorder
p="xs"
radius="md"
shadow="xs"
style={{
background: isAlert
? 'linear-gradient(45deg, #fef3c7, #fffbeb)'
: 'linear-gradient(45deg, var(--mantine-color-blue-light), var(--mantine-color-cyan-light))',
borderColor: isAlert
? '#f59e0b'
: 'var(--mantine-color-blue-outline)',
}}
>
<Group justify="space-between" wrap="nowrap">
<Group gap="sm">
<ThemeIcon
variant="light"
color={isAlert ? "yellow" : "blue"}
radius="xl"
size="sm"
>
{isAlert ? <IconAlertSquareRounded size={14} /> : <IconSpeakerphone size={14} />}
</ThemeIcon>
<Text size="sm" fw={500} c={isAlert ? "yellow.9" : "blue.9"}>
<strong>{isAlert ? 'Alert' : 'Update'}:</strong> {text}
</Text>
</Group>

<CloseButton
size="sm"
iconSize={14}
onClick={handleDismiss}
variant="transparent"
c={isAlert ? "yellow.9" : "blue.9"}
aria-label="Dismiss announcement"
/>
</Group>
</Paper>
</Stack>
);
};

export default Announcement;
48 changes: 44 additions & 4 deletions app/src/components/COVID19View.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const COVID19View = ({ data, metadata, selectedDates, selectedModels, models, se
const [xAxisRange, setXAxisRange] = useState(null); // Track user's zoom/rangeslider selection
const plotRef = useRef(null);
const isResettingRef = useRef(false); // Flag to prevent capturing programmatic resets
const stateName = data?.metadata?.location_name;

// This allows the "frozen" Plotly button to access fresh data
const getDefaultRangeRef = useRef(getDefaultRange);
Expand Down Expand Up @@ -69,7 +70,8 @@ const COVID19View = ({ data, metadata, selectedDates, selectedModels, models, se
type: 'scatter',
mode: 'lines+markers',
line: { color: 'black', width: 2, dash: 'dash' },
marker: { size: 4, color: 'black' }
marker: { size: 4, color: 'black' },
hovertemplate: '<b>Observed Data</b><br>Date: %{x}<br>Value: <b>%{y}</b><extra></extra>'
};

const modelTraces = selectedModels.flatMap(model =>
Expand All @@ -79,10 +81,13 @@ const COVID19View = ({ data, metadata, selectedDates, selectedModels, models, se
if (!forecast || forecast.type !== 'quantile') return [];

const forecastDates = [], medianValues = [], ci95Upper = [], ci95Lower = [], ci50Upper = [], ci50Lower = [];
const hoverTexts = [];

const sortedPredictions = Object.values(forecast.predictions || {}).sort((a, b) => new Date(a.date) - new Date(b.date));

sortedPredictions.forEach((pred) => {
forecastDates.push(pred.date);
const pointDate = pred.date;
forecastDates.push(pointDate);
const { quantiles = [], values = [] } = pred;
const findValue = (q) => {
const index = quantiles.indexOf(q);
Expand All @@ -101,6 +106,21 @@ const COVID19View = ({ data, metadata, selectedDates, selectedModels, models, se
medianValues.push(val_50);
ci50Upper.push(val_75);
ci95Upper.push(val_975);

// Build dynamic hover string
const formattedMedian = val_50.toLocaleString(undefined, { maximumFractionDigits: 2 });
const formatted50 = `${val_25.toLocaleString(undefined, { maximumFractionDigits: 2 })} - ${val_75.toLocaleString(undefined, { maximumFractionDigits: 2 })}`;
const formatted95 = `${val_025.toLocaleString(undefined, { maximumFractionDigits: 2 })} - ${val_975.toLocaleString(undefined, { maximumFractionDigits: 2 })}`;

hoverTexts.push(
`<b>${model}</b><br>` +
`Date: ${pointDate}<br>` +
`Median: <b>${formattedMedian}</b><br>` +
`50% CI: [${formatted50}]<br>` +
`95% CI: [${formatted95}]<br>` +
`<span style="color: rgba(255,255,255,0.8); font-size: 0.8em">predicted as of ${date}</span>` +
`<extra></extra>`
);
} else {
console.warn(`Missing quantiles for model ${model}, date ${date}, target ${selectedTarget}, prediction date ${pred.date}`);
}
Expand All @@ -114,7 +134,24 @@ const COVID19View = ({ data, metadata, selectedDates, selectedModels, models, se
return [
{ x: [...forecastDates, ...forecastDates.slice().reverse()], y: [...ci95Upper, ...ci95Lower.slice().reverse()], fill: 'toself', fillcolor: `${modelColor}10`, line: { color: 'transparent' }, showlegend: false, type: 'scatter', name: `${model} 95% CI`, hoverinfo: 'none', legendgroup: model },
{ x: [...forecastDates, ...forecastDates.slice().reverse()], y: [...ci50Upper, ...ci50Lower.slice().reverse()], fill: 'toself', fillcolor: `${modelColor}30`, line: { color: 'transparent' }, showlegend: false, type: 'scatter', name: `${model} 50% CI`, hoverinfo: 'none', legendgroup: model },
{ x: forecastDates, y: medianValues, name: model, type: 'scatter', mode: 'lines+markers', line: { color: modelColor, width: 2, dash: 'solid' }, marker: { size: 6, color: modelColor }, showlegend: isFirstDate, legendgroup: model }
{
x: forecastDates,
y: medianValues,
name: model,
type: 'scatter',
mode: 'lines+markers',
line: { color: modelColor, width: 2, dash: 'solid' },
marker: { size: 6, color: modelColor },
showlegend: isFirstDate,
legendgroup: model,
text: hoverTexts,
hovertemplate: '%{text}',
hoverlabel: {
bgcolor: modelColor,
font: { color: '#ffffff' },
bordercolor: '#ffffff'
}
}
];
})
);
Expand Down Expand Up @@ -195,7 +232,7 @@ const COVID19View = ({ data, metadata, selectedDates, selectedModels, models, se
size: 10
}
},
hovermode: 'x unified',
hovermode: 'closest',
dragmode: false,
margin: { l: 60, r: 30, t: 30, b: 30 },
xaxis: {
Expand Down Expand Up @@ -300,6 +337,9 @@ const COVID19View = ({ data, metadata, selectedDates, selectedModels, models, se
onRelayout={handlePlotUpdate}
/>
</div>
<Text fw={700} size="sm" mb={5} ta="center">
{stateName}
</Text>
<div style={{ borderTop: '1px solid #FFF', paddingTop: '1px', marginTop: 'auto' }}>
<p style={{
fontStyle: 'italic',
Expand Down
8 changes: 4 additions & 4 deletions app/src/components/DataVisualizationContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const DataVisualizationContainer = () => {

// Configuration for AboutHubOverlay based on viewType
const aboutHubConfig = {
'covid_projs': {
'covid_forecasts': {
title: (
<Group gap="sm">
<Title order={4}>COVID-19 Forecast Hub</Title>
Expand Down Expand Up @@ -71,7 +71,7 @@ const DataVisualizationContainer = () => {
</>
)
},
'rsv_projs': {
'rsv_forecasts': {
title: (
<Group gap="sm">
<Title order={4}>RSV Forecast Hub</Title>
Expand Down Expand Up @@ -140,7 +140,7 @@ const DataVisualizationContainer = () => {
</>
)
},
'flu_projs': {
'flu_forecasts': {
title: (
<Group gap="sm">
<Title order={4}>FluSight Forecast Hub</Title>
Expand Down Expand Up @@ -206,7 +206,7 @@ const DataVisualizationContainer = () => {
</>
)
},
'metrocast_projs': {
'metrocast_forecasts': {
title: (
<Group gap="sm">
<Title order={4}>Flu MetroCast</Title>
Expand Down
Loading