diff --git a/app/src/App.jsx b/app/src/App.jsx index 026be490..cb0df091 100644 --- a/app/src/App.jsx +++ b/app/src/App.jsx @@ -10,6 +10,7 @@ import { HelmetProvider } from "react-helmet-async"; import { ViewProvider } from "./contexts/ViewContext"; import { useView } from "./hooks/useView"; import DataVisualizationContainer from "./components/DataVisualizationContainer"; +import MyPlots from "./components/myplots/MyPlots"; import NarrativeBrowser from "./components/narratives/NarrativeBrowser"; import SlideNarrativeViewer from "./components/narratives/SlideNarrativeViewer"; import ForecastleGame from "./components/forecastle/ForecastleGame"; @@ -20,7 +21,6 @@ import Documentation from "./components/Documentation"; import ReportingDelayPage from "./components/reporting/ReportingDelayPage"; import ToolsPage from "./components/tools/ToolsPage"; import { Center, Text } from "@mantine/core"; -// import ShutdownBanner from './components/ShutdownBanner';, no longer necessary const ForecastApp = () => { // This component uses the view context, so it must be inside the provider. @@ -59,6 +59,7 @@ const AppLayout = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/app/src/components/DataVisualizationContainer.jsx b/app/src/components/DataVisualizationContainer.jsx index 810831c9..478b8904 100644 --- a/app/src/components/DataVisualizationContainer.jsx +++ b/app/src/components/DataVisualizationContainer.jsx @@ -12,12 +12,17 @@ import { List, } from "@mantine/core"; import { useView } from "../hooks/useView"; +import { extractPlotData } from "../hooks/extractPlotDataFromURL"; import DateSelector from "./DateSelector"; import ViewSwitchboard from "./ViewSwitchboard"; import ErrorBoundary from "./ErrorBoundary"; import AboutHubOverlay from "./AboutHubOverlay"; import FrontPage from "./FrontPage"; -import { IconShare, IconBrandGithub } from "@tabler/icons-react"; +import { + IconShare, + IconBrandGithub, + IconChartScatter, +} from "@tabler/icons-react"; import { useClipboard } from "@mantine/hooks"; const DataVisualizationContainer = () => { @@ -51,6 +56,17 @@ const DataVisualizationContainer = () => { }); const clipboard = useClipboard({ timeout: 2000 }); + const [isAdded, setIsAdded] = useState(false); + const handleSaveToMyPlots = () => { + const plotData = extractPlotData(viewType, window.location.href, data); + // visual cue to signal it has been added + setIsAdded(true); + // Reset text after 2 seconds + setTimeout(() => setIsAdded(false), 2000); + // Temporary console feedback for development + console.log("Saved the plot", plotData); + }; + // Configuration for AboutHubOverlay based on viewType const aboutHubConfig = { covid_forecasts: { @@ -552,22 +568,38 @@ const DataVisualizationContainer = () => { )} {windowSize.width <= 800 && ( - - + )} + - {clipboard.copied ? "URL Copied" : "Share View"} - - + + + )} {currentDataset?.hasDateSelector && windowSize.width > 800 && ( @@ -585,6 +617,19 @@ const DataVisualizationContainer = () => { )} {windowSize.width > 800 && (
+ {viewType !== "flu_peak" && ( + + )} {
diff --git a/app/src/components/ForecastPlotView.jsx b/app/src/components/ForecastPlotView.jsx index 03779c5c..96fc0e23 100644 --- a/app/src/components/ForecastPlotView.jsx +++ b/app/src/components/ForecastPlotView.jsx @@ -1,5 +1,5 @@ import { useState, useEffect, useMemo, useRef, useCallback } from "react"; -import { useMantineColorScheme, Stack, Text } from "@mantine/core"; +import { useMantineColorScheme, Stack, Text, Box, Center } from "@mantine/core"; import Plot from "react-plotly.js"; import Plotly from "plotly.js/dist/plotly"; import ModelSelector from "./ModelSelector"; @@ -359,6 +359,7 @@ const ForecastPlotView = ({ return baseConfig; }, [calculateYRange, configOverrides]); + const hasForecasts = projectionsData.length > 1; if (requireTarget && !selectedTarget) { return ( @@ -374,12 +375,40 @@ const ForecastPlotView = ({ timestamp={metadata?.last_updated} />
+ {!hasForecasts && ( + +
+ + No forecast data available for the current selection + +
+
+ )} + { + return ( + + Check out the new{" "} + + My Plots + {" "} + feature, where you can assemble your own dashboard of saved plots. + + ); +}; + const MetroCastLink = () => { const { setViewType } = useView(); @@ -41,6 +58,13 @@ const FrontPage = () => { announcementType={"update"} text={} /> + } + /> { opened={opened} onClose={close} title={ - + RespiLens logo - + <Title order={2} c="blue" style={{ lineHeight: 1 }}> RespiLens @@ -78,12 +79,14 @@ const InfoOverlay = () => { URL-shareable views for specific forecast settings - Responsive and mobile-friendly site - Frequent and automatic site updates + + Responsive and mobile-friendly site with frequent and automatic + updates + Multi date, target, and model comparison the Forecastle game! - MyRespiLens, a safe visualization tool for your own data + MyRespiLens: a safe visualization tool for your own data @@ -96,8 +99,8 @@ const InfoOverlay = () => { Hubverse {" "} - project which standardizes and consolidates forecast data formats. - For each of the hub displayed on RespiLens, the data, organization + project, which standardizes and consolidates forecast data formats. + For each of the hubs displayed on RespiLens, the data, organization, and forecasts belong to their respective teams.{" "} RespiLens is only a visualization layer, and contains no original @@ -106,7 +109,7 @@ const InfoOverlay = () => {
- You can find information and alternative visualization for each + You can find information (and alternative visualization) for each pathogen at the following locations: @@ -119,7 +122,7 @@ const InfoOverlay = () => { > official CDC page {" "} - –{" "} + |{" "} { > Hubverse dashboard {" "} - –{" "} + |{" "} { > official CDC page {" "} - –{" "} + |{" "} { > Hubverse dashboard {" "} - –  + |  { > official dashboard {" "} - –{" "} + |{" "} { > site {" "} - –  + | { icon: IconDashboard, active: isActive("/myrespilens"), }, + { + href: "/myplots", + label: "My Plots (α)", + icon: IconChartScatter, + active: isActive("/myplots"), + }, // { href: '/documentation', label: 'Documentation', icon: IconClipboard, active: isActive('/documentation')} ]; diff --git a/app/src/components/layout/UnifiedAppShell.jsx b/app/src/components/layout/UnifiedAppShell.jsx index 8f3b67bf..f0c2f24e 100644 --- a/app/src/components/layout/UnifiedAppShell.jsx +++ b/app/src/components/layout/UnifiedAppShell.jsx @@ -14,6 +14,7 @@ import { IconTrophy, IconDashboard, IconClipboard, + IconChartScatter, } from "@tabler/icons-react"; import MainNavigation from "./MainNavigation"; import StateSelector from "../StateSelector"; @@ -95,6 +96,12 @@ const UnifiedAppShell = ({ children, forecastProps = {} }) => { icon: IconClipboard, active: location.pathname.startsWith("/documentation"), }, + { + href: "/myplots", + label: "My Plots", + icon: IconChartScatter, + active: location.pathname.startsWith("/myplots"), + }, ]; const renderNavbar = () => { diff --git a/app/src/components/myplots/MiniPlot.jsx b/app/src/components/myplots/MiniPlot.jsx new file mode 100644 index 00000000..ea23303e --- /dev/null +++ b/app/src/components/myplots/MiniPlot.jsx @@ -0,0 +1,297 @@ +import { useState, useEffect, useMemo } from "react"; +import { + Center, + Loader, + Text, + Box, + Stack, + Group, + Badge, + Tooltip, + useMantineColorScheme, +} from "@mantine/core"; +import Plot from "react-plotly.js"; +import useQuantileForecastTraces from "../../hooks/useQuantileForecastTraces"; +import { MODEL_COLORS } from "../../config/datasets"; +import { nhsnSlugToNameMap, targetDisplayNameMap } from "../../utils/mapUtils"; + +const MiniPlot = ({ plot }) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const { colorScheme } = useMantineColorScheme(); + + const isNHSN = plot.viewType === "nhsnall"; + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + const dataUrl = `/processed_data/${plot.fullDataPath}`; + const response = await fetch(dataUrl); + if (!response.ok) throw new Error(`Data not found`); + const json = await response.json(); + setData(json); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + fetchData(); + }, [plot.fullDataPath]); + + const { traces: forecastTraces } = useQuantileForecastTraces({ + groundTruth: isNHSN ? null : data?.ground_truth, + forecasts: isNHSN ? null : data?.forecasts, + selectedDates: plot.settings.dates || [], + selectedModels: plot.settings.models || [], + target: plot.settings.target, + showMedian: plot.settings.intervals?.includes("median") ?? true, + show50: plot.settings.intervals?.includes("ci50") ?? true, + show95: plot.settings.intervals?.includes("ci95") ?? true, + showLegendForFirstDate: false, + modelLineWidth: 1.5, + modelMarkerSize: 4, + }); + + const nhsnTraces = useMemo(() => { + if (!isNHSN || !data?.series) return []; + + const dateAxis = data.series.dates; + const applySqrt = plot.settings.scale === "sqrt"; + + return (plot.settings.columns || []) + .map((slug, index) => { + const longformName = nhsnSlugToNameMap[slug] || slug; + const rawY = data.series[longformName] || []; + const yValues = applySqrt + ? rawY.map((v) => (v !== null ? Math.sqrt(Math.max(0, v)) : v)) + : rawY; + + return { + x: dateAxis, + y: yValues, + name: longformName, + type: "scatter", + mode: "lines", + line: { + color: MODEL_COLORS[index % MODEL_COLORS.length], + width: 2, + }, + }; + }) + .filter((trace) => trace.y.length > 0); + }, [isNHSN, data, plot.settings]); + + const finalTraces = isNHSN ? nhsnTraces : forecastTraces; + + const layout = useMemo(() => { + let xRange = undefined; + let yRange = undefined; + + if (data) { + if (isNHSN && data.series?.dates?.length > 0) { + const lastDate = new Date( + data.series.dates[data.series.dates.length - 1], + ); + const startDate = new Date(lastDate); + startDate.setMonth(startDate.getMonth() - 3); + xRange = [ + startDate.toISOString().split("T")[0], + data.series.dates[data.series.dates.length - 1], + ]; + } else if (!isNHSN && plot.settings.dates?.length > 0) { + const sortedDates = [...plot.settings.dates].sort(); + const earliestDate = new Date(sortedDates[0]); + const latestDate = new Date(sortedDates[sortedDates.length - 1]); + const startDate = new Date(earliestDate); + startDate.setMonth(startDate.getMonth() - 3); + const endDate = new Date(latestDate); + endDate.setDate(endDate.getDate() + 42); + + xRange = [ + startDate.toISOString().split("T")[0], + endDate.toISOString().split("T")[0], + ]; + } + + if (xRange && finalTraces?.length > 0) { + const [viewStart, viewEnd] = xRange; + let maxY = 0; + finalTraces.forEach((trace) => { + if (!trace.x || !trace.y) return; + trace.x.forEach((xVal, i) => { + if (xVal >= viewStart && xVal <= viewEnd) { + const yVal = trace.y[i]; + if (yVal !== null && !isNaN(yVal)) { + maxY = Math.max(maxY, yVal); + } + } + }); + }); + const padding = maxY === 0 ? 1 : maxY * 0.2; + yRange = [0, maxY + padding]; + } + } + + return { + autosize: true, + height: 230, + margin: { l: 45, r: 10, t: 10, b: 35 }, + showlegend: false, + template: colorScheme === "dark" ? "plotly_dark" : "plotly_white", + paper_bgcolor: "rgba(0,0,0,0)", + plot_bgcolor: "rgba(0,0,0,0)", + dragmode: "pan", + xaxis: { + showgrid: false, + fixedrange: false, + tickfont: { size: 8 }, + range: xRange, + }, + yaxis: { + showgrid: true, + gridcolor: colorScheme === "dark" ? "#333" : "#eee", + fixedrange: true, + tickfont: { size: 8 }, + type: plot.settings.scale === "log" ? "log" : "linear", + range: plot.settings.scale === "log" ? undefined : yRange, + nticks: 5, + ticksuffix: + plot.settings.target?.includes("%") || + plot.settings.target?.includes("pct") || + plot.settings.target?.includes("Percent") || + plot.settings.target?.includes("percent") + ? "%" + : "", + }, + shapes: !isNHSN + ? (plot.settings.dates || []).map((date) => ({ + type: "line", + x0: date, + x1: date, + y0: 0, + y1: 1, + yref: "paper", + line: { color: "red", width: 1, dash: "dash" }, + })) + : [], + }; + }, [colorScheme, plot.settings, isNHSN, data, finalTraces]); + + // Helper for hover label content + const tooltipContent = useMemo(() => { + const resolvedTarget = + targetDisplayNameMap[plot.settings.target] || plot.settings.target; + + return ( + + + PLOT INFO + + + + + TARGET: + + {resolvedTarget} + + + + + SCALE: + + + {plot.settings.scale?.toUpperCase()} + + + + + + {isNHSN ? "COLUMNS:" : "DATES:"} + + + {isNHSN + ? plot.settings.columns?.map((slug) => ( + + {nhsnSlugToNameMap[slug] || slug} + + )) + : plot.settings.dates?.map((date) => ( + + {date} + + ))} + + + + {!isNHSN && ( + + + MODELS: + + + {plot.settings.models?.map((model) => ( + + {model} + + ))} + + + )} + + ); + }, [plot.settings, isNHSN]); + + if (loading) + return ( +
+ +
+ ); + if (error) + return ( +
+ + Error loading chart + +
+ ); + + return ( + + + + + + ); +}; + +export default MiniPlot; diff --git a/app/src/components/myplots/MyPlots.jsx b/app/src/components/myplots/MyPlots.jsx new file mode 100644 index 00000000..075bb9f5 --- /dev/null +++ b/app/src/components/myplots/MyPlots.jsx @@ -0,0 +1,199 @@ +import { useState, useEffect } from "react"; +import { + Title, + Text, + Paper, + Stack, + ThemeIcon, + Center, + SimpleGrid, + Box, + Badge, + Group, + Button, +} from "@mantine/core"; +import { + IconChartScatter, + IconExternalLink, + IconTrash, +} from "@tabler/icons-react"; +import { getSavedPlots, deletePlot } from "../../utils/plotStorage"; +import MiniPlot from "./MiniPlot"; + +const MyPlots = () => { + const [userSavedPlots, setUserSavedPlots] = useState([]); + + useEffect(() => { + const plots = getSavedPlots(); + setUserSavedPlots(plots); + }, []); + + const handleDelete = (id) => { + if (deletePlot(id)) { + setUserSavedPlots(getSavedPlots()); + } + }; + + const hasPlots = userSavedPlots.length > 0; + + const pageContainerStyle = { + width: "100%", + minHeight: "calc(100vh - 80px)", + backgroundColor: "var(--mantine-color-body)", + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "40px", + }; + + return ( + + {!hasPlots ? ( +
+ + + + + + +
+ + No plots saved yet... + + + You haven't added any visualizations to My Plots yet. + Click the "Add to My Plots" button on any plot to see it here + with any editorializations you choose. This feature is in its + alpha release; if you encounter bugs or have suggestions, + please report them{" "} + + here. + + +
+ + + Plots are stored locally in your browser. + +
+
+
+ ) : ( + + + +
+ My Plots + + Your personalized library of saved visualizations. + + + This feature is in its alpha release, and is still under + develoment. If you encounter a bug or have a suggestion, + please{" "} + + let us know. + + +
+ + {userSavedPlots.length} Saved + +
+
+ + + {userSavedPlots.map((plot) => ( + + + + + + + {plot.viewDisplayName.toUpperCase()} + + + {plot.settings.location.toUpperCase()} + + + + + + + + + + + + + + ))} + +
+ )} +
+ ); +}; + +export default MyPlots; diff --git a/app/src/components/views/NHSNView.jsx b/app/src/components/views/NHSNView.jsx index 703f6575..51671bfb 100644 --- a/app/src/components/views/NHSNView.jsx +++ b/app/src/components/views/NHSNView.jsx @@ -27,7 +27,7 @@ import { const nhsnYAxisLabelMap = { "Hospital Admissions (count)": "Patient Count", - "Hospital Admissions (rates)": "Rate per 100k", + "Hospital Admissions (rate)": "Rate per 100k", "Hospital Admissions (%)": "Percent (%)", "Bed Capacity (count)": "Bed Count", "Bed Capacity (%)": "Percent (%)", @@ -41,7 +41,7 @@ const getDefaultColumnsForTarget = (target) => { "Total Influenza Admissions", "Total RSV Admissions", ], - "Hospital Admissions (rates)": [ + "Hospital Admissions (rate)": [ "Total number of COVID-19 Admissions per 100,000 population", "Total number of Influenza Admissions per 100,000 population", "Total number of RSV Admissions per 100,000 population", @@ -74,6 +74,7 @@ const NHSNView = ({ location }) => { const [filteredAvailableColumns, setFilteredAvailableColumns] = useState([]); // Columns for the selected target const [selectedColumns, setSelectedColumns] = useState([]); + const hasInteractedRef = useRef(false); const [availableTargets, setAvailableTargets] = useState([]); const [selectedTarget, setSelectedTarget] = useState(null); // This is the string key, e.g., "Raw Patient Counts" @@ -88,6 +89,20 @@ const NHSNView = ({ location }) => { const plotRef = useRef(null); const isResettingRef = useRef(false); + const getProcessedYValues = useCallback( + (columnName, rawValues) => { + if (!rawValues) return []; + return rawValues.map((val) => { + if (val === null || val === undefined) return val; + const transformed = val; + return chartScale === "sqrt" + ? Math.sqrt(Math.max(0, transformed)) + : transformed; + }); + }, + [chartScale], + ); + useEffect(() => { const fetchData = async () => { if (!location) return; @@ -181,14 +196,24 @@ const NHSNView = ({ location }) => { setFilteredAvailableColumns(filtered); const urlSlugs = searchParams.getAll("nhsn_cols"); + const urlTarget = searchParams.get("nhsn_target"); + + const isExplicitlyEmpty = urlSlugs.includes("none"); + const validUrlCols = urlSlugs .map((slug) => nhsnSlugToNameMap[slug]) .filter((colName) => colName && filtered.includes(colName)); let newSelectedCols; + if (validUrlCols.length > 0) { newSelectedCols = validUrlCols; - } else if (filtered.length > 0) { + } else if (isExplicitlyEmpty) { + newSelectedCols = []; + } else if ( + !hasInteractedRef.current || + (urlTarget !== selectedTarget && urlSlugs.length === 0) + ) { const defaultColumns = getDefaultColumnsForTarget(selectedTarget); const filteredDefaults = defaultColumns.filter((col) => filtered.includes(col), @@ -200,10 +225,11 @@ const NHSNView = ({ location }) => { } setSelectedColumns((currentCols) => { - const newSorted = [...newSelectedCols].sort(); - const currentSorted = [...currentCols].sort(); - if (JSON.stringify(newSorted) !== JSON.stringify(currentSorted)) + const sortedNew = [...newSelectedCols].sort(); + const sortedCurrent = [...currentCols].sort(); + if (JSON.stringify(sortedNew) !== JSON.stringify(sortedCurrent)) { return newSelectedCols; + } return currentCols; }); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -218,18 +244,25 @@ const NHSNView = ({ location }) => { ) { return; } + const currentSearch = window.location.search; const newParams = new URLSearchParams(currentSearch); + + // Target Sync const defaultTarget = availableTargets[0]; - if (selectedTarget && selectedTarget !== defaultTarget) + if (selectedTarget && selectedTarget !== defaultTarget) { newParams.set("nhsn_target", selectedTarget); - else newParams.delete("nhsn_target"); + } else { + newParams.delete("nhsn_target"); + } - const columnsForTarget = nhsnTargetsToColumnsMap[selectedTarget] || []; + // Column Sync + newParams.delete("nhsn_cols"); + + const defaultColumnsArray = getDefaultColumnsForTarget(selectedTarget); const filteredCols = allDataColumns.filter((col) => - columnsForTarget.includes(col), + (nhsnTargetsToColumnsMap[selectedTarget] || []).includes(col), ); - const defaultColumnsArray = getDefaultColumnsForTarget(selectedTarget); const filteredDefaults = defaultColumnsArray.filter((col) => filteredCols.includes(col), ); @@ -239,20 +272,22 @@ const NHSNView = ({ location }) => { : filteredCols.length > 0 ? [filteredCols[0]] : []; - const sortedSelected = [...selectedColumns].sort(); - const sortedDefault = [...defaultColumns].sort(); - const selectedSlugs = sortedSelected - .map((name) => nhsnNameToSlugMap[name]) - .filter(Boolean); - const defaultSlugs = sortedDefault - .map((name) => nhsnNameToSlugMap[name]) - .filter(Boolean); - if (JSON.stringify(selectedSlugs) !== JSON.stringify(defaultSlugs)) { - newParams.delete("nhsn_cols"); - selectedSlugs.forEach((slug) => newParams.append("nhsn_cols", slug)); - } else { - newParams.delete("nhsn_cols"); + + const isDefault = + JSON.stringify([...selectedColumns].sort()) === + JSON.stringify([...defaultColumns].sort()); + + if (!isDefault) { + if (selectedColumns.length > 0) { + selectedColumns.forEach((name) => { + const slug = nhsnNameToSlugMap[name]; + if (slug) newParams.append("nhsn_cols", slug); + }); + } else if (hasInteractedRef.current) { + newParams.set("nhsn_cols", "none"); + } } + if ( newParams.toString() !== new URLSearchParams(currentSearch).toString() ) { @@ -267,6 +302,11 @@ const NHSNView = ({ location }) => { setSearchParams, ]); + const handleSetSelectedColumns = useCallback((newCols) => { + hasInteractedRef.current = true; + setSelectedColumns(newCols); + }, []); + useEffect(() => { if (data) setPlotRevision((p) => p + 1); }, [data, selectedTarget]); @@ -341,19 +381,10 @@ const NHSNView = ({ location }) => { return; } - const isPercentage = selectedTarget && selectedTarget.includes("%"); - const traces = selectedColumns.map((column) => { - const yValues = data.series[column]; - const processedYValues = isPercentage - ? yValues.map((val) => - val !== null && val !== undefined ? val * 100 : val, - ) - : yValues; - return { - x: data.series.dates, - y: processedYValues, - }; - }); + const currentTraces = selectedColumns.map((column) => ({ + x: data.series.dates, + y: getProcessedYValues(column, data.series[column]), + })); const currentXRange = xAxisRange || defaultRange; @@ -362,15 +393,8 @@ const NHSNView = ({ location }) => { return; } - const newYRange = calculateYRange(traces, currentXRange); - if (chartScale === "sqrt" && newYRange) { - const [minY, maxY] = newYRange; - const sqrtMin = Math.sqrt(Math.max(0, minY)); - const sqrtMax = Math.sqrt(Math.max(0, maxY)); - setYAxisRange([sqrtMin, sqrtMax]); - } else { - setYAxisRange(newYRange); - } + const newYRange = calculateYRange(currentTraces, currentXRange); + setYAxisRange(newYRange); }, [ data, selectedColumns, @@ -378,7 +402,7 @@ const NHSNView = ({ location }) => { selectedTarget, defaultRange, calculateYRange, - chartScale, + getProcessedYValues, ]); const handleRelayout = useCallback( @@ -399,21 +423,12 @@ const NHSNView = ({ location }) => { const rawTraces = useMemo(() => { if (!data) return []; - const isPercentage = selectedTarget && selectedTarget.includes("%"); - return selectedColumns.map((column) => { - const yValues = data.series[column]; - const processedYValues = isPercentage - ? yValues.map((val) => - val !== null && val !== undefined ? val * 100 : val, - ) - : yValues; - return { - x: data.series.dates, - y: processedYValues, - name: column, - }; - }); - }, [data, selectedTarget, selectedColumns]); + return selectedColumns.map((column) => ({ + x: data.series.dates, + y: getProcessedYValues(column, data.series[column]), + name: column, + })); + }, [data, getProcessedYValues, selectedColumns]); const rawYRange = useMemo(() => getYRangeFromTraces(rawTraces), [rawTraces]); @@ -424,22 +439,29 @@ const NHSNView = ({ location }) => { const traces = useMemo(() => { if (!data) return []; - const applySqrt = chartScale === "sqrt"; - - return rawTraces.map((trace) => { - const columnIndex = filteredAvailableColumns.indexOf(trace.name); - const transformedY = applySqrt - ? trace.y.map((val) => - val === null || val === undefined - ? val - : Math.sqrt(Math.max(0, val)), - ) - : trace.y; + if (selectedColumns.length === 0) { + return [ + { + x: [data.series.dates[0]], + y: [null], + type: "scatter", + mode: "lines", + showlegend: false, + }, + ]; + } + + return selectedColumns.map((columnName) => { + const columnIndex = filteredAvailableColumns.indexOf(columnName); + const processedY = getProcessedYValues( + columnName, + data.series[columnName], + ); return { - x: trace.x, - y: transformedY, - name: trace.name, + x: data.series.dates, + y: processedY, + name: columnName, type: "scatter", mode: "lines+markers", line: { @@ -449,7 +471,7 @@ const NHSNView = ({ location }) => { marker: { size: 6 }, }; }); - }, [data, rawTraces, filteredAvailableColumns, chartScale]); + }, [data, selectedColumns, filteredAvailableColumns, getProcessedYValues]); const layout = useMemo( () => ({ @@ -481,7 +503,10 @@ const NHSNView = ({ location }) => { yaxis: { title: nhsnYAxisLabelMap[selectedTarget] || "Value", range: chartScale === "log" ? undefined : yAxisRange, - autorange: chartScale === "log" ? true : yAxisRange === null, + autorange: + chartScale === "log" + ? true + : yAxisRange === null || selectedColumns.length === 0, type: chartScale === "log" ? "log" : "linear", tickmode: chartScale === "sqrt" && sqrtTicks ? "array" : undefined, tickvals: @@ -505,6 +530,21 @@ const NHSNView = ({ location }) => { }, margin: { t: 40, r: 10, l: 60, b: 120 }, uirevision: plotRevision, + annotations: + selectedColumns.length === 0 + ? [ + { + text: "No columns selected", + xref: "paper", + yref: "paper", + showarrow: false, + font: { + size: 20, + color: colorScheme === "dark" ? "#5c5f66" : "#adb5bd", + }, + }, + ] + : [], }), [ colorScheme, @@ -543,16 +583,10 @@ const NHSNView = ({ location }) => { const newDefaultRange = getDefaultXRange(); if (!newDefaultRange || newDefaultRange[0] === null) return; - const isPct = selectedTarget && selectedTarget.includes("%"); - const currentTraces = selectedColumns.map((column) => { - const yValues = data.series[column]; - const pYValues = isPct - ? yValues.map((val) => - val !== null && val !== undefined ? val * 100 : val, - ) - : yValues; - return { x: data.series.dates, y: pYValues }; - }); + const currentTraces = selectedColumns.map((column) => ({ + x: data.series.dates, + y: getProcessedYValues(column, data.series[column]), + })); const newYRange = calculateYRange(currentTraces, newDefaultRange); @@ -569,7 +603,13 @@ const NHSNView = ({ location }) => { }, ], }), - [data, selectedTarget, selectedColumns, getDefaultXRange, calculateYRange], + [ + data, + selectedColumns, + getDefaultXRange, + calculateYRange, + getProcessedYValues, + ], ); if (loading) @@ -618,11 +658,14 @@ const NHSNView = ({ location }) => { { + hasInteractedRef.current = false; + setSelectedTarget(val); + }} loading={loading} />
diff --git a/app/src/config/datasets.js b/app/src/config/datasets.js index c9c8fd36..0cff87d9 100644 --- a/app/src/config/datasets.js +++ b/app/src/config/datasets.js @@ -48,8 +48,8 @@ export const DATASETS = { }, nhsn: { shortName: "nhsn", - fullName: "NHSN Respiratory Data", - titleName: "NHSN Respiratory Data", + fullName: "NHSN Surveillance Data", + titleName: "NHSN Surveillance Data", views: [{ key: "all", label: "All Data", value: "nhsnall" }], defaultView: "nhsnall", defaultColumn: "Number of Adult COVID-19 Admissions, 18-49 years", diff --git a/app/src/hooks/extractPlotDataFromURL.js b/app/src/hooks/extractPlotDataFromURL.js new file mode 100644 index 00000000..85725d04 --- /dev/null +++ b/app/src/hooks/extractPlotDataFromURL.js @@ -0,0 +1,237 @@ +import { savePlot } from "../utils/plotStorage"; + +/** + * Parses the current URL and viewType to extract a serialized state + * for the "My Plots" persistence feature. + * * @param {string} viewType - The current active view + * @param {string} href - The full window.location.href string + * @returns {Object} The processed plotData object + */ +export const extractPlotData = (viewType, href, data) => { + const url = new URL(href); + const params = url.searchParams; + const id = crypto.randomUUID(); + const currentDate = new Date().toISOString().split("T")[0]; + let dataSuffix = ""; + let fileName = ""; + let fullDataPath = ""; + + // plot settings + let location = ""; + let target = ""; + let columns = []; + let models = []; + let dates = []; + let scale = ""; + let intervals = []; + let viewDisplayName = ""; + + // forecast views set date dynamically if it is the default date (not present in URL) + switch (viewType) { + case "covid_forecasts": { + dataSuffix = "covid19"; + location = params.has("location") ? params.get("location") : "US"; + fileName = `${location}_${dataSuffix}.json`; + fullDataPath = `covid19forecasthub/${fileName}`; + target = params.has("covid_target") + ? params.get("covid_target") + : "wk inc covid hosp"; + const covidModelsString = params.get("covid_models"); + models = covidModelsString + ? covidModelsString.split(",") + : ["CovidHub-ensemble"]; + const covidDatesString = params.get("covid_dates"); + if (covidDatesString) { + dates = covidDatesString.split(","); + } else { + const availableDates = Object.keys(data?.forecasts || {}); + if (availableDates.length > 0) { + const mostRecent = availableDates.sort().pop(); + dates = [mostRecent]; + } else { + throw new Error( + `Unable to extract plot data: No dates found in URL and no forecast data available for ${viewType}.`, + ); + } + } + scale = params.has("scale") ? params.get("scale") : "linear"; + const covidIntervalsString = params.get("intervals"); + intervals = covidIntervalsString + ? covidIntervalsString.split(",") + : ["median", "ci50", "ci95"]; + viewDisplayName = "COVID-19 Forecasts"; + break; + } + + case "flu_forecasts": + case "fludetailed": { + dataSuffix = "flu"; + location = params.has("location") ? params.get("location") : "US"; + fileName = `${location}_${dataSuffix}.json`; + fullDataPath = `flusight/${fileName}`; + target = params.has("flu_target") + ? params.get("flu_target") + : "wk inc flu hosp"; + const fluModelsString = params.get("flu_models"); + models = fluModelsString + ? fluModelsString.split(",") + : ["FluSight-ensemble"]; + const fluDatesString = params.get("flu_dates"); + if (fluDatesString) { + dates = fluDatesString.split(","); + } else { + const availableDates = Object.keys(data?.forecasts || {}); + if (availableDates.length > 0) { + const mostRecent = availableDates.sort().pop(); + dates = [mostRecent]; + } else { + throw new Error( + `Unable to extract plot data: No dates found in URL and no forecast data available for ${viewType}.`, + ); + } + } + scale = params.has("scale") ? params.get("scale") : "linear"; + const fluIntervalsString = params.get("intervals"); + intervals = fluIntervalsString + ? fluIntervalsString.split(",") + : ["median", "ci50", "ci95"]; + viewDisplayName = "Flu Forecasts"; + break; + } + + case "rsv_forecasts": { + dataSuffix = "rsv"; + location = params.has("location") ? params.get("location") : "US"; + fileName = `${location}_${dataSuffix}.json`; + fullDataPath = `rsvforecasthub/${fileName}`; + target = params.has("rsv_target") + ? params.get("rsv_target") + : "wk inc rsv hosp"; + const rsvModelsString = params.get("rsv_models"); + models = rsvModelsString + ? rsvModelsString.split(",") + : ["RSVHub-ensemble"]; + const rsvDatesString = params.get("rsv_dates"); + if (rsvDatesString) { + dates = rsvDatesString.split(","); + } else { + const availableDates = Object.keys(data?.forecasts || {}); + if (availableDates.length > 0) { + const mostRecent = availableDates.sort().pop(); + dates = [mostRecent]; + } else { + throw new Error( + `Unable to extract plot data: No dates found in URL and no forecast data available for ${viewType}.`, + ); + } + } + scale = params.has("scale") ? params.get("scale") : "linear"; + const rsvIntervalsString = params.get("intervals"); + intervals = rsvIntervalsString + ? rsvIntervalsString.split(",") + : ["median", "ci50", "ci95"]; + viewDisplayName = "RSV Forecasts"; + break; + } + + case "metrocast_forecasts": { + dataSuffix = "flu_metrocast"; + location = params.has("location") ? params.get("location") : "colorado"; + fileName = `${location}_${dataSuffix}.json`; + fullDataPath = `flumetrocast/${fileName}`; + target = "Flu ED visits pct"; + const metrocastModelsString = params.get("metrocast_models"); + models = metrocastModelsString + ? metrocastModelsString.split(",") + : ["epiENGAGE-ensemble_mean"]; + const metrocastDatesString = params.get("metrocast_dates"); + if (metrocastDatesString) { + dates = metrocastDatesString.split(","); + } else { + const availableDates = Object.keys(data?.forecasts || {}); + if (availableDates.length > 0) { + const mostRecent = availableDates.sort().pop(); + dates = [mostRecent]; + } else { + throw new Error( + `Unable to extract plot data: No dates found in URL and no forecast data available for ${viewType}.`, + ); + } + } + scale = params.has("scale") ? params.get("scale") : "linear"; + const metrocastIntervalsString = params.get("intervals"); + intervals = metrocastIntervalsString + ? metrocastIntervalsString.split(",") + : ["median", "ci50", "ci95"]; + viewDisplayName = "Flu Forecasts"; + break; + } + + case "nhsnall": { + dataSuffix = "nhsn"; + location = params.has("location") ? params.get("location") : "US"; + fileName = `${location}_${dataSuffix}.json`; + fullDataPath = `nhsn/${fileName}`; + target = params.has("nhsn_target") + ? params.get("nhsn_target") + : "Hospital Admissions (count)"; + const nhsnColsFromUrl = params.getAll("nhsn_cols"); + const defaultColumnSlugsByTarget = { + "Hospital Admissions (count)": [ + "totalconfc19newadm", + "totalconfflunewadm", + "totalconfrsvnewadm", + ], + "Hospital Admissions (rate)": [ + "totalconfc19newadmper100k", + "totalconfflunewadmper100k", + "totalconfrsvnewadmper100k", + ], + "Hospital Admissions (%)": [ + "pctconfc19newadmadult", + "pctconfflunewadmadult", + "pctconfrsvnewadmadult", + ], + "Bed Capacity (count)": ["numinptbeds", "numinptbedsocc"], + "Bed Capacity (%)": ["pctinptbedsocc"], + }; + if (nhsnColsFromUrl.length > 0) { + columns = nhsnColsFromUrl; + // default columns are dependent on target (aka column unit) + } else { + columns = defaultColumnSlugsByTarget[target] || []; + } + dates = [currentDate]; + scale = params.has("scale") ? params.get("scale") : "linear"; + viewDisplayName = "NHSN Data"; + break; + } + + default: + throw new Error(`Unknown view type: ${viewType}`); + } + + const plotData = { + viewType: viewType, + fullUrl: href, + viewDisplayName, + id, + currentDate, + dataSuffix, + fileName, + fullDataPath, + settings: { + location, + target, + columns, + models, + dates, + scale, + intervals, + }, + }; + + savePlot(plotData); + + return plotData; +}; diff --git a/app/src/utils/mapUtils.js b/app/src/utils/mapUtils.js index af654940..a52bb2de 100644 --- a/app/src/utils/mapUtils.js +++ b/app/src/utils/mapUtils.js @@ -72,7 +72,7 @@ export const nhsnTargetsToColumnsMap = { "Number of Pediatric RSV Admissions, 5-17 years", "Number of RSV Admissions, unknown age", ], - "Hospital Admissions (rates)": [ + "Hospital Admissions (rate)": [ "Total Number of Adult COVID-19 Admissions per 100,000 population", "Total Number of Adult Influenza Admissions per 100,000 population", "Total Number of Adult RSV Admissions per 100,000 population", @@ -135,6 +135,7 @@ export const nhsnTargetsToColumnsMap = { "Percent Adult Inpatient Beds Occupied", "Percent Pediatric Inpatient Beds Occupied", "Percent Adult ICU Beds Occupied", + "Percent Pediatric ICU Beds Occupied", ], }; @@ -805,26 +806,38 @@ export const nhsnNameToPrettyNameMap = { "Total Adult RSV Admissions": "Adult Admissions", "Number of RSV Admissions, unknown age": "Admissions (unknown age)", "Total RSV Admissions": "Total Admissions", - "Percent Inpatient Beds Occupied": "Inpatient Beds Occupied", + "Percent Adult ICU Beds Occupied": "Percent of Adult ICU Beds Occupied", + "Percent Pediatric ICU Beds Occupied": + "Percent of Pediatric ICU Beds Occupied", + "Percent Adult Inpatient Beds Occupied": + "Percent of Adult Inpatient Beds Occupied", + "Percent Pediatric Inpatient Beds Occupied": + "Percent of Pediatric Inpatient Beds Occupied", + "Percent Inpatient Beds Occupied": "Percent of Inpatient Beds Occupied", "Percent Inpatient Beds Occupied by COVID-19 Patients": - "Inpatient Beds Occupied by COVID-19 Patients", + "Percent of Inpatient Beds Occupied by COVID-19 Patients", "Percent Inpatient Beds Occupied by Influenza Patients": - "Inpatient Beds Occupied by Influenza Patients", + "Percent of Inpatient Beds Occupied by Influenza Patients", "Percent Inpatient Beds Occupied by RSV Patients": - "Inpatient Beds Occupied by RSV Patients", - "Percent ICU Beds Occupied": "ICU Beds Occupied", + "Percent of Inpatient Beds Occupied by RSV Patients", + "Percent ICU Beds Occupied": "Percent of ICU Beds Occupied", "Percent ICU Beds Occupied by COVID-19 Patients": - "ICU Beds Occupied by COVID-19 Patients", + "Percent of ICU Beds Occupied by COVID-19 Patients", "Percent ICU Beds Occupied by Influenza Patients": - "ICU Beds Occupied by Influenza Patients", + "Percent of ICU Beds Occupied by Influenza Patients", "Percent ICU Beds Occupied by RSV Patients": - "ICU Beds Occupied by RSV Patients", - "Percent Adult COVID-19 Admissions": "Adult Admissions", - "Percent Pediatric COVID-19 Admissions": "Pediatric Admissions", - "Percent Adult Influenza Admissions": "Adult Admissions", - "Percent Pediatric Influenza Admissions": "Pediatric Admissions", - "Percent Adult RSV Admissions": "Adult Admissions", - "Percent Pediatric RSV Admissions": "Pediatric Admissions", + "Percent of ICU Beds Occupied by RSV Patients", + "Percent Adult COVID-19 Admissions": + "Percent of COVID-19 Admissions that are Adults", + "Percent Pediatric COVID-19 Admissions": + "Percent of COVID-19 Admissions that are Pediatric", + "Percent Adult Influenza Admissions": + "Percent of Influenza Admissions that are Adults", + "Percent Pediatric Influenza Admissions": + "Percent of Influenza Admissions that are Pediatric", + "Percent Adult RSV Admissions": "Percent of RSV Admissions that are Adults", + "Percent Pediatric RSV Admissions": + "Percent of RSV Admissions that are Pediatric", "Number of Pediatric COVID-19 Admissions, 0-4 years, per 100,000 population": "Pediatric Admissions (0-4 years)", "Number of Pediatric COVID-19 Admissions, 5-17 years, per 100,000 population": diff --git a/app/src/utils/plotStorage.js b/app/src/utils/plotStorage.js new file mode 100644 index 00000000..1470817d --- /dev/null +++ b/app/src/utils/plotStorage.js @@ -0,0 +1,113 @@ +/** + * RespiLens My Plots Storage Utility + */ + +const STORAGE_KEY = "respilens_user_saved_plots"; + +function isLocalStorageAvailable() { + try { + const test = "__storage_test__"; + localStorage.setItem(test, test); + localStorage.removeItem(test); + return true; + } catch { + return false; + } +} + +/** + * Validates the plot object against the schema defined in extractPlotData.js + */ +function isValidPlot(plot) { + return ( + plot && + typeof plot === "object" && + typeof plot.id === "string" && + typeof plot.fullUrl === "string" && + typeof plot.viewType === "string" && + typeof plot.viewDisplayName === "string" && + typeof plot.fullDataPath === "string" && + plot.settings && + typeof plot.settings === "object" && + typeof plot.settings.location === "string" && + typeof plot.settings.target === "string" && + Array.isArray(plot.settings.dates) + ); +} + +/** + * Get all stored saved plots + */ +export function getSavedPlots() { + if (!isLocalStorageAvailable()) return []; + + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (!stored) return []; + + const parsed = JSON.parse(stored); + if (!Array.isArray(parsed)) return []; + + // Filters out any old/legacy schemas that don't match the new extractPlotData structure + return parsed.filter((plot) => isValidPlot(plot)); + } catch (error) { + console.error("Error reading plots from localStorage:", error); + return []; + } +} + +/** + * Save a plot to My Plots + */ +export function savePlot(plotData) { + if (!isLocalStorageAvailable()) return false; + + try { + const plots = getSavedPlots(); + + // Check for duplicates based on URL to prevent clutter + const existingIndex = plots.findIndex( + (p) => p.fullUrl === plotData.fullUrl, + ); + + const plotEntry = { + ...plotData, + // Ensure we have a UUID and a timestamp for sorting + id: plotData.id || crypto.randomUUID(), + savedAt: new Date().toISOString(), + }; + + if (existingIndex >= 0) { + // Update existing entry + plots[existingIndex] = plotEntry; + } else { + // Add new entry to the top of the list + plots.unshift(plotEntry); + } + + localStorage.setItem(STORAGE_KEY, JSON.stringify(plots)); + return true; + } catch (error) { + if (error.name === "QuotaExceededError" || error.code === 22) { + console.error("Storage quota exceeded."); + } else { + console.error("Failed to save plot:", error); + } + return false; + } +} + +/** + * Delete a specific saved plot + */ +export function deletePlot(id) { + if (!isLocalStorageAvailable()) return false; + try { + const plots = getSavedPlots(); + const filtered = plots.filter((p) => p.id !== id); + localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered)); + return true; + } catch { + return false; + } +} diff --git a/app/src/utils/respilensStorage.js b/app/src/utils/respilensStorage.js index eefeb8ea..1ed480e0 100644 --- a/app/src/utils/respilensStorage.js +++ b/app/src/utils/respilensStorage.js @@ -1,11 +1,3 @@ -/** - * RespiLens Storage Utility - * - * Manages localStorage for RespiLens games (currently Forecastle). - * Stores raw game data (user predictions, intervals, ground truth). - * Statistics are computed on-the-fly from stored data. - */ - const STORAGE_KEY = "respilens_forecastle_games"; /** diff --git a/scripts/hub_dataset_processor.py b/scripts/hub_dataset_processor.py index 957dc495..79dc7896 100644 --- a/scripts/hub_dataset_processor.py +++ b/scripts/hub_dataset_processor.py @@ -21,7 +21,6 @@ class HubDatasetConfig: file_suffix: str dataset_label: str - ground_truth_date_column: str ground_truth_min_date: Optional[pd.Timestamp] = None series_type: str = "projection" observation_column: str = "observation" @@ -34,7 +33,7 @@ class HubDataProcessorBase: Subclasses supply dataset-specific configuration via HubDatasetConfig. """ - + def __init__( self, data: pd.DataFrame, @@ -175,7 +174,7 @@ def _prepare_ground_truth_df(self, location: str) -> pd.DataFrame: if filtered.empty: return filtered - date_col = self.config.ground_truth_date_column + date_col = "target_end_date" filtered["as_of"] = pd.to_datetime(filtered["as_of"]) filtered[date_col] = pd.to_datetime(filtered[date_col]) @@ -197,7 +196,7 @@ def _format_ground_truth_output(self, ground_truth_df: pd.DataFrame) -> Dict[str if ground_truth_df.empty: return {"dates": []} - date_col = self.config.ground_truth_date_column + date_col = "target_end_date" pivot_truth = ground_truth_df.pivot( index=date_col, columns="target", diff --git a/scripts/locations.csv b/scripts/locations.csv index 43f20155..a9b63f94 100644 --- a/scripts/locations.csv +++ b/scripts/locations.csv @@ -1,54 +1,54 @@ -abbreviation,location,location_name,population,count_rate0p3,count_rate0p5,count_rate0p7,count_rate1,count_rate1p7,count_rate3,count_rate4,count_rate5 -US,US,US,334914895,1005,1675,2344,3349,5694,10047,13397,16746 -AL,01,Alabama,5108468,15,26,36,51,87,153,204,255 -AK,02,Alaska,733406,2,4,5,7,12,22,29,37 -AZ,04,Arizona,7431344,22,37,52,74,126,223,297,372 -AR,05,Arkansas,3067732,9,15,21,31,52,92,123,153 -CA,06,California,38965193,117,195,273,390,662,1169,1559,1948 -CO,08,Colorado,5877610,18,29,41,59,100,176,235,294 -CT,09,Connecticut,3617176,11,18,25,36,61,109,145,181 -DE,10,Delaware,1031890,3,5,7,10,18,31,41,52 -DC,11,District of Columbia,678972,2,3,5,7,12,20,27,34 -FL,12,Florida,22610726,68,113,158,226,384,678,904,1131 -GA,13,Georgia,11029227,33,55,77,110,187,331,441,551 -HI,15,Hawaii,1435138,4,7,10,14,24,43,57,72 -ID,16,Idaho,1964726,6,10,14,20,33,59,79,98 -IL,17,Illinois,12549689,38,63,88,125,213,376,502,627 -IN,18,Indiana,6862199,21,34,48,69,117,206,274,343 -IA,19,Iowa,3207004,10,16,22,32,55,96,128,160 -KS,20,Kansas,2940546,9,15,21,29,50,88,118,147 -KY,21,Kentucky,4526154,14,23,32,45,77,136,181,226 -LA,22,Louisiana,4573749,14,23,32,46,78,137,183,229 -ME,23,Maine,1395722,4,7,10,14,24,42,56,70 -MD,24,Maryland,6180253,19,31,43,62,105,185,247,309 -MA,25,Massachusetts,7001399,21,35,49,70,119,210,280,350 -MI,26,Michigan,10037261,30,50,70,100,171,301,401,502 -MN,27,Minnesota,5737915,17,29,40,57,98,172,230,287 -MS,28,Mississippi,2939690,9,15,21,29,50,88,118,147 -MO,29,Missouri,6196156,19,31,43,62,105,186,248,310 -MT,30,Montana,1132812,3,6,8,11,19,34,45,57 -NE,31,Nebraska,1978379,6,10,14,20,34,59,79,99 -NV,32,Nevada,3194176,10,16,22,32,54,96,128,160 -NH,33,New Hampshire,1402054,4,7,10,14,24,42,56,70 -NJ,34,New Jersey,9290841,28,46,65,93,158,279,372,465 -NM,35,New Mexico,2114371,6,11,15,21,36,63,85,106 -NY,36,New York,19571216,59,98,137,196,333,587,783,979 -NC,37,North Carolina,10835491,33,54,76,108,184,325,433,542 -ND,38,North Dakota,783926,2,4,5,8,13,24,31,39 -OH,39,Ohio,11785935,35,59,83,118,200,354,471,589 -OK,40,Oklahoma,4053824,12,20,28,41,69,122,162,203 -OR,41,Oregon,4233358,13,21,30,42,72,127,169,212 -PA,42,Pennsylvania,12961683,39,65,91,130,220,389,518,648 -RI,44,Rhode Island,1095962,3,5,8,11,19,33,44,55 -SC,45,South Carolina,5373555,16,27,38,54,91,161,215,269 -SD,46,South Dakota,919318,3,5,6,9,16,28,37,46 -TN,47,Tennessee,7126489,21,36,50,71,121,214,285,356 -TX,48,Texas,30503301,92,153,214,305,519,915,1220,1525 -UT,49,Utah,3417734,10,17,24,34,58,103,137,171 -VT,50,Vermont,647464,2,3,5,6,11,19,26,32 -VA,51,Virginia,8715698,26,44,61,87,148,261,349,436 -WA,53,Washington,7812880,23,39,55,78,133,234,313,391 -WV,54,West Virginia,1770071,5,9,12,18,30,53,71,89 -WI,55,Wisconsin,5910955,18,30,41,59,100,177,236,296 -WY,56,Wyoming,584057,2,3,4,6,10,18,23,29 -PR,72,Puerto Rico,3205691,10,16,22,32,54,96,128,160 +abbreviation,location,location_name,population +US,US,US,341784857 +AL,01,Alabama,5193088 +AK,02,Alaska,737270 +AZ,04,Arizona,7623818 +AR,05,Arkansas,3114791 +CA,06,California,39355309 +CO,08,Colorado,6012561 +CT,09,Connecticut,3688496 +DE,10,Delaware,1059952 +DC,11,District of Columbia,693645 +FL,12,Florida,23462518 +GA,13,Georgia,11302748 +HI,15,Hawaii,1432820 +ID,16,Idaho,2029733 +IL,17,Illinois,12719141 +IN,18,Indiana,6973333 +IA,19,Iowa,3238387 +KS,20,Kansas,2977220 +KY,21,Kentucky,4606864 +LA,22,Louisiana,4618189 +ME,23,Maine,1414874 +MD,24,Maryland,6265347 +MA,25,Massachusetts,7154084 +MI,26,Michigan,10127884 +MN,27,Minnesota,5830405 +MS,28,Mississippi,2954160 +MO,29,Missouri,6270541 +MT,30,Montana,1144694 +NE,31,Nebraska,2018006 +NV,32,Nevada,3282188 +NH,33,New Hampshire,1415342 +NJ,34,New Jersey,9548215 +NM,35,New Mexico,2125498 +NY,36,New York,20002427 +NC,37,North Carolina,11197968 +ND,38,North Dakota,799358 +OH,39,Ohio,11900510 +OK,40,Oklahoma,4123288 +OR,41,Oregon,4273586 +PA,42,Pennsylvania,13059432 +RI,44,Rhode Island,1114521 +SC,45,South Carolina,5570274 +SD,46,South Dakota,935094 +TN,47,Tennessee,7315076 +TX,48,Texas,31709821 +UT,49,Utah,3538904 +VT,50,Vermont,644663 +VA,51,Virginia,8880107 +WA,53,Washington,8001020 +WV,54,West Virginia,1766147 +WI,55,Wisconsin,5972787 +WY,56,Wyoming,588753 +PR,72,Puerto Rico,3184835 \ No newline at end of file diff --git a/scripts/process_RespiLens_data.py b/scripts/process_RespiLens_data.py index de064faf..87195048 100644 --- a/scripts/process_RespiLens_data.py +++ b/scripts/process_RespiLens_data.py @@ -17,6 +17,9 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) +SCRIPT_LOCATION = Path(__file__).resolve().parent +LOCATIONS_DATA = pd.read_csv(SCRIPT_LOCATION / "locations.csv") + def main(): """ @@ -63,13 +66,12 @@ def main(): logger.info("Success ✅") logger.info("Collecting data from FluSight repo...") flu_hubverse_df = clean_nan_values(hubverse_df_preprocessor(df=flu_hub_conn.get_dataset().to_table().to_pandas(), filter_nowcasts=True)) - flu_locations_data = clean_nan_values(pd.read_csv(Path(args.flusight_hub_path) / 'auxiliary-data/locations.csv')) flu_target_data = clean_nan_values(connect_target_data(hub_path=args.flusight_hub_path, target_type=TargetType.TIME_SERIES).to_table().to_pandas()) logger.info("Success ✅") # Initialize converter object flu_processor_object = FlusightDataProcessor( data=flu_hubverse_df, - locations_data=flu_locations_data, + locations_data=LOCATIONS_DATA, target_data=flu_target_data, ) # Iteratively save output files @@ -92,13 +94,12 @@ def main(): logger.info("Success ✅") logger.info("Collecting data from RSV repo...") rsv_hubverse_df = clean_nan_values(hubverse_df_preprocessor(df=rsv_hub_conn.get_dataset().to_table().to_pandas(), filter_nowcasts=True)) - rsv_locations_data = clean_nan_values(pd.read_csv(Path(args.rsv_hub_path) / 'auxiliary-data/locations.csv')) rsv_target_data = clean_nan_values(connect_target_data(hub_path=args.rsv_hub_path, target_type=TargetType.TIME_SERIES).to_table().to_pandas()) logger.info("Success ✅") # Initialize converter object rsv_processor_object = RSVDataProcessor( data=rsv_hubverse_df, - locations_data=rsv_locations_data, + locations_data=LOCATIONS_DATA, target_data=rsv_target_data, ) # Iteratively save output files @@ -120,13 +121,12 @@ def main(): logger.info("Success ✅") logger.info("Collecting data from covid19 repo...") covid_hubverse_df = clean_nan_values(hubverse_df_preprocessor(df=covid_hub_conn.get_dataset().to_table().to_pandas(), filter_nowcasts=True)) - covid_locations_data = clean_nan_values(pd.read_csv(Path(args.covid_hub_path) / 'auxiliary-data/locations.csv')) covid_target_data = clean_nan_values(connect_target_data(hub_path=args.covid_hub_path, target_type=TargetType.TIME_SERIES).to_table().to_pandas()) logger.info("Success ✅") # Initialize converter object covid_processor_object = COVIDDataProcessor( data=covid_hubverse_df, - locations_data=covid_locations_data, + locations_data=LOCATIONS_DATA, target_data=covid_target_data, ) # Iteratively save output files @@ -148,7 +148,7 @@ def main(): logger.info("Success ✅") logger.info("Collecting data from flu metrocast repo...") flu_metrocast_hubverse_df = clean_nan_values(hubverse_df_preprocessor(df=flu_metrocast_hub_conn.get_dataset().to_table().to_pandas(), filter_nowcasts=True)) - flu_metrocast_locations_data = clean_nan_values(pd.read_csv(Path(args.flu_metrocast_hub_path) / 'auxiliary-data/locations.csv')) + flu_metrocast_locations_data = clean_nan_values(pd.read_csv(Path(args.flu_metrocast_hub_path) / 'auxiliary-data/locations.csv')) # DEP: metrocast still pulls hub locations.csv flu_metrocast_target_data = clean_nan_values(connect_target_data(hub_path=args.flu_metrocast_hub_path, target_type=TargetType.TIME_SERIES).to_table().to_pandas()) logger.info("Success ✅") # Initialize converter oject diff --git a/scripts/processors/covid19_forecast_hub.py b/scripts/processors/covid19_forecast_hub.py index d5ab5e2c..9fa96c4e 100644 --- a/scripts/processors/covid19_forecast_hub.py +++ b/scripts/processors/covid19_forecast_hub.py @@ -10,7 +10,6 @@ def __init__(self, data: pd.DataFrame, locations_data: pd.DataFrame, target_data config = HubDatasetConfig( file_suffix="covid19", dataset_label="covid19 forecast hub", - ground_truth_date_column="date", ground_truth_min_date=pd.Timestamp("2023-10-01"), ) super().__init__( diff --git a/scripts/processors/flu_metrocast_hub.py b/scripts/processors/flu_metrocast_hub.py index 55f52642..3d8c9d24 100644 --- a/scripts/processors/flu_metrocast_hub.py +++ b/scripts/processors/flu_metrocast_hub.py @@ -10,7 +10,6 @@ def __init__(self, data: pd.DataFrame, locations_data: pd.DataFrame, target_data config = HubDatasetConfig( file_suffix="flu_metrocast", dataset_label="flu metrocast forecasts", - ground_truth_date_column="target_end_date", ground_truth_min_date=pd.Timestamp("2024-08-01"), ) super().__init__( diff --git a/scripts/processors/flusight.py b/scripts/processors/flusight.py index 88b14d08..01455ab3 100644 --- a/scripts/processors/flusight.py +++ b/scripts/processors/flusight.py @@ -10,7 +10,6 @@ def __init__(self, data: pd.DataFrame, locations_data: pd.DataFrame, target_data config = HubDatasetConfig( file_suffix="flu", dataset_label="flusight forecasts", - ground_truth_date_column="target_end_date", ground_truth_min_date=pd.Timestamp("2022-10-01"), ) super().__init__( diff --git a/scripts/processors/rsv_forecast_hub.py b/scripts/processors/rsv_forecast_hub.py index 206f4405..018c4cf7 100644 --- a/scripts/processors/rsv_forecast_hub.py +++ b/scripts/processors/rsv_forecast_hub.py @@ -10,7 +10,6 @@ def __init__(self, data: pd.DataFrame, locations_data: pd.DataFrame, target_data config = HubDatasetConfig( file_suffix="rsv", dataset_label="rsv forecast hub", - ground_truth_date_column="date", ground_truth_min_date=pd.Timestamp("2023-10-01"), ) super().__init__(