SummerProject2026/dashboard/js/app.js

309 lines
10 KiB
JavaScript

import { loadDashboardData } from "./data.js";
import {
aggregateOverview,
compactNumber,
percent,
renderAgeRisk,
renderCohortDotPlot,
renderCoverageHeatmap,
renderOutcomeTrend,
renderUtahCoverage,
} from "./charts.js";
const ROUTES = Object.freeze({
overview: "Overview",
cohorts: "Sample cohorts",
model: "Model & benchmark",
methods: "Data & methods",
});
const UTAH_COUNTIES = [
"Beaver",
"Box Elder",
"Cache",
"Carbon",
"Daggett",
"Davis",
"Duchesne",
"Emery",
"Garfield",
"Grand",
"Iron",
"Juab",
"Kane",
"Millard",
"Morgan",
"Piute",
"Rich",
"Salt Lake",
"San Juan",
"Sanpete",
"Sevier",
"Summit",
"Tooele",
"Uintah",
"Utah",
"Wasatch",
"Washington",
"Wayne",
"Weber",
];
let dashboardData = null;
let visibleScorecards = [];
function byId(id) {
return document.getElementById(id);
}
function setText(id, value) {
const element = byId(id);
if (element) element.textContent = value;
}
function normalizeCounty(value) {
return String(value).trim().toLowerCase().replaceAll("_", " ").replace(/\s+/g, " ");
}
function routeFromHash() {
const candidate = window.location.hash.replace(/^#/, "").toLowerCase();
return Object.hasOwn(ROUTES, candidate) ? candidate : "overview";
}
function showRoute({ announce = true, focus = false } = {}) {
const route = routeFromHash();
for (const view of document.querySelectorAll("[data-view]")) {
view.hidden = view.dataset.view !== route;
}
for (const link of document.querySelectorAll("[data-route]")) {
if (link.dataset.route === route) link.setAttribute("aria-current", "page");
else link.removeAttribute("aria-current");
}
document.title = `${ROUTES[route]} · Utah Vehicle Health`;
if (announce) setText("route-announcer", `${ROUTES[route]} view`);
if (focus) {
const heading = document.querySelector(`[data-view="${route}"] h1`);
if (heading) {
heading.setAttribute("tabindex", "-1");
heading.focus({ preventScroll: true });
}
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
}
}
function initializeRouting() {
showRoute({ announce: false, focus: false });
window.addEventListener("hashchange", () => showRoute({ focus: true }));
}
function setHeaderStatus(state, message) {
const status = byId("header-status");
if (!status) return;
status.dataset.state = state;
const text = status.querySelector("span:last-child");
if (text) text.textContent = message;
}
function setPreviewBanner(manifest) {
const preview = manifest.development_preview || !manifest.population_estimate_allowed;
const banner = byId("preview-banner");
banner.hidden = !preview;
if (!preview) return;
setText("preview-title", "Development preview");
setText(
"preview-copy",
"Private 10,000-vehicle development sample · suppressed aggregates only · not population estimates.",
);
}
function setUnavailable(error) {
const safeMessage =
error instanceof Error && error.message
? `${error.message} No estimates are being shown.`
: "The approved aggregate files could not be validated. No estimates are being shown.";
for (const state of document.querySelectorAll("[data-unavailable]")) {
state.hidden = false;
const copy = state.querySelector("[data-unavailable-message]");
if (copy) copy.textContent = safeMessage;
}
for (const region of document.querySelectorAll("[data-requires-data]")) {
region.hidden = true;
}
const banner = byId("preview-banner");
banner.hidden = false;
setText("preview-title", "Data unavailable");
setText("preview-copy", "This static shell could not validate every required public aggregate. No fallback or estimated values are displayed.");
setHeaderStatus("error", "Approved data unavailable");
}
function renderCountyList(coveredCounties) {
const container = byId("county-coverage-list");
container.replaceChildren();
const normalized = new Set(coveredCounties.map(normalizeCounty));
for (const county of UTAH_COUNTIES) {
const chip = document.createElement("span");
chip.className = "county-chip";
chip.dataset.covered = String(normalized.has(normalizeCounty(county)));
chip.textContent = county;
container.append(chip);
}
}
function renderOverview(data) {
const rows = data.overview.rows;
const periods = aggregateOverview(rows);
const totalSupport = rows.reduce((sum, row) => sum + row.support_rounded, 0);
const weightedNonpass = rows.reduce(
(sum, row) => sum + row.support_rounded * row.nonpass_rate,
0,
);
const nonpassRate = totalSupport > 0 ? weightedNonpass / totalSupport : 0;
const counties = [...new Set(rows.map((row) => row.public_county))].sort();
const latest = periods[periods.length - 1];
setText("kpi-eligible", `${compactNumber(totalSupport)}`);
setText("kpi-pass", `${percent(1 - nonpassRate)}`);
setText("kpi-nonpass", `${percent(nonpassRate)}`);
setText("kpi-counties", String(counties.length));
setText("data-cutoff", latest ? `${latest.year} Q${latest.quarter}` : "Unavailable");
setText(
"model-version",
`Final: calibrated logistic · release ${data.manifest.release_id.slice(0, 8)}`,
);
renderOutcomeTrend(byId("outcome-trend-chart"), rows);
setText(
"outcome-trend-note",
"Development-sample aggregates only, not a population trend. The bundle publishes binary non-pass rates; four-class outcome mix and blank rates are not inferred.",
);
renderAgeRisk(byId("age-risk-chart"), data.ageRisk.rows);
renderUtahCoverage(byId("utah-coverage-map"), counties);
renderCountyList(counties);
}
function scorecardLabel(row) {
return `${row.prior_make} ${row.prior_model}`.trim();
}
function sortedScorecards(rows, mode) {
const sorted = [...rows];
if (mode === "risk-desc") sorted.sort((a, b) => b.nonpass_rate - a.nonpass_rate);
else if (mode === "risk-asc") sorted.sort((a, b) => a.nonpass_rate - b.nonpass_rate);
else if (mode === "name") sorted.sort((a, b) => scorecardLabel(a).localeCompare(scorecardLabel(b)));
else sorted.sort((a, b) => b.support_rounded - a.support_rounded);
return sorted;
}
function renderScorecardCards(rows) {
const container = byId("cohort-cards");
container.replaceChildren();
for (const row of rows.slice(0, 18)) {
const article = document.createElement("article");
article.className = "cohort-card";
const heading = document.createElement("h3");
heading.textContent = scorecardLabel(row);
const meta = document.createElement("div");
meta.className = "cohort-card__meta";
meta.textContent = `${compactNumber(row.support_rounded)} eligible sample episodes (rounded)`;
const bar = document.createElement("div");
bar.className = "risk-bar";
bar.setAttribute("aria-hidden", "true");
const fill = document.createElement("span");
fill.style.width = `${Math.min(100, row.nonpass_rate * 100)}%`;
bar.append(fill);
const value = document.createElement("div");
value.className = "cohort-card__value";
const strong = document.createElement("strong");
strong.textContent = percent(row.nonpass_rate);
const small = document.createElement("small");
small.textContent = "Observed sample non-pass";
value.append(strong, small);
article.append(heading, meta, bar, value);
container.append(article);
}
}
function updateExplorer() {
if (!dashboardData) return;
const query = byId("cohort-search").value.trim().toLowerCase();
const sortMode = byId("result-sort").value;
const matches = dashboardData.scorecards.rows.filter((row) =>
scorecardLabel(row).toLowerCase().includes(query),
);
visibleScorecards = sortedScorecards(matches, sortMode);
const cohortCount = visibleScorecards.length;
let summary = "0 supported sample cohorts.";
if (cohortCount > 0 && cohortCount <= 12) {
summary = `${cohortCount} supported sample cohort${cohortCount === 1 ? "" : "s"}; all shown.`;
} else if (cohortCount > 12) {
summary = `${cohortCount} supported sample cohorts; showing ${Math.min(cohortCount, 18)} cards and 12 chart rows.`;
}
setText(
"result-summary",
summary,
);
byId("empty-results").hidden = visibleScorecards.length > 0;
renderCohortDotPlot(byId("cohort-dot-plot"), visibleScorecards);
renderScorecardCards(visibleScorecards);
}
function initializeExplorer() {
byId("cohort-search").addEventListener("input", updateExplorer);
byId("result-sort").addEventListener("change", updateExplorer);
byId("reset-filters").addEventListener("click", () => {
byId("explorer-filters").reset();
byId("cohort-search").value = "";
byId("result-sort").value = "support";
updateExplorer();
});
updateExplorer();
}
function findDiagnostic(rows, model) {
return rows.find((row) => row.model === model && row.partition === "calibrate");
}
function renderModel(data) {
const finalModel = findDiagnostic(data.diagnostics.rows, "logistic_platt");
const benchmark = findDiagnostic(data.diagnostics.rows, "hist_gradient_boosting_platt");
const metric = (row, key) => (row ? row[key].toFixed(3) : "—");
setText("final-ap", metric(finalModel, "average_precision"));
setText("final-roc", metric(finalModel, "roc_auc"));
setText("final-brier", metric(finalModel, "brier"));
setText("benchmark-ap", metric(benchmark, "average_precision"));
setText("benchmark-roc", metric(benchmark, "roc_auc"));
setText("benchmark-brier", metric(benchmark, "brier"));
}
function renderMethods(data) {
renderCoverageHeatmap(byId("coverage-heatmap"), data.coverage.rows);
}
function renderDashboard(data) {
dashboardData = data;
setPreviewBanner(data.manifest);
renderOverview(data);
initializeExplorer();
renderModel(data);
renderMethods(data);
setHeaderStatus(
"ready",
data.manifest.development_preview ? "Validated sample aggregates" : "Validated public aggregates",
);
}
async function initialize() {
initializeRouting();
try {
const data = await loadDashboardData();
renderDashboard(data);
} catch (error) {
console.error("Dashboard data contract rejected the public assets.", error);
setUnavailable(error);
}
}
initialize();
export { normalizeCounty, routeFromHash };