repository is now a presentation-ready development prototype
This commit is contained in:
+28
-79
@@ -6,15 +6,14 @@ import {
|
||||
renderAgeRisk,
|
||||
renderCohortDotPlot,
|
||||
renderCoverageHeatmap,
|
||||
renderNoCalibration,
|
||||
renderOutcomeTrend,
|
||||
renderUtahCoverage,
|
||||
} from "./charts.js";
|
||||
|
||||
const ROUTES = Object.freeze({
|
||||
overview: "Overview",
|
||||
reliability: "Reliability explorer",
|
||||
estimator: "Next-test estimator",
|
||||
cohorts: "Sample cohorts",
|
||||
model: "Model & benchmark",
|
||||
methods: "Data & methods",
|
||||
});
|
||||
|
||||
@@ -66,13 +65,6 @@ function normalizeCounty(value) {
|
||||
return String(value).trim().toLowerCase().replaceAll("_", " ").replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function displayCategory(value) {
|
||||
return String(value)
|
||||
.replaceAll("_", " ")
|
||||
.toLowerCase()
|
||||
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
function routeFromHash() {
|
||||
const candidate = window.location.hash.replace(/^#/, "").toLowerCase();
|
||||
return Object.hasOwn(ROUTES, candidate) ? candidate : "overview";
|
||||
@@ -120,7 +112,7 @@ function setPreviewBanner(manifest) {
|
||||
setText("preview-title", "Development preview");
|
||||
setText(
|
||||
"preview-copy",
|
||||
"These suppressed sample aggregates are not population estimates and must not be used for individual decisions.",
|
||||
"Private 10,000-vehicle development sample · suppressed aggregates only · not population estimates.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,25 +136,6 @@ function setUnavailable(error) {
|
||||
setHeaderStatus("error", "Approved data unavailable");
|
||||
}
|
||||
|
||||
function populateSelect(id, values, formatter = displayCategory) {
|
||||
const select = byId(id);
|
||||
if (!select) return;
|
||||
for (const value of values) {
|
||||
const option = document.createElement("option");
|
||||
option.value = String(value);
|
||||
option.textContent = formatter(value);
|
||||
select.append(option);
|
||||
}
|
||||
}
|
||||
|
||||
function disableUnsupportedFilter(id, explanation) {
|
||||
const select = byId(id);
|
||||
if (!select) return;
|
||||
select.disabled = true;
|
||||
select.title = explanation;
|
||||
select.options[0].textContent = explanation;
|
||||
}
|
||||
|
||||
function renderCountyList(coveredCounties) {
|
||||
const container = byId("county-coverage-list");
|
||||
container.replaceChildren();
|
||||
@@ -195,12 +168,12 @@ function renderOverview(data) {
|
||||
setText("data-cutoff", latest ? `${latest.year} Q${latest.quarter}` : "Unavailable");
|
||||
setText(
|
||||
"model-version",
|
||||
`Release ${data.manifest.release_id.slice(0, 8)} · ${data.manifest.model_versions.join(" / ")}`,
|
||||
`Final: calibrated logistic · release ${data.manifest.release_id.slice(0, 8)}`,
|
||||
);
|
||||
renderOutcomeTrend(byId("outcome-trend-chart"), rows);
|
||||
setText(
|
||||
"outcome-trend-note",
|
||||
"The current approved bundle publishes binary non-pass rates only. Four-class outcome mix and blank rates are not inferred or displayed.",
|
||||
"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);
|
||||
@@ -230,7 +203,7 @@ function renderScorecardCards(rows) {
|
||||
heading.textContent = scorecardLabel(row);
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "cohort-card__meta";
|
||||
meta.textContent = `${compactNumber(row.support_rounded)} rounded support`;
|
||||
meta.textContent = `≈${compactNumber(row.support_rounded)} eligible sample episodes (rounded)`;
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "risk-bar";
|
||||
bar.setAttribute("aria-hidden", "true");
|
||||
@@ -242,7 +215,7 @@ function renderScorecardCards(rows) {
|
||||
const strong = document.createElement("strong");
|
||||
strong.textContent = percent(row.nonpass_rate);
|
||||
const small = document.createElement("small");
|
||||
small.textContent = "Observed non-pass";
|
||||
small.textContent = "Observed sample non-pass";
|
||||
value.append(strong, small);
|
||||
article.append(heading, meta, bar, value);
|
||||
container.append(article);
|
||||
@@ -259,33 +232,14 @@ function updateExplorer() {
|
||||
visibleScorecards = sortedScorecards(matches, sortMode);
|
||||
setText(
|
||||
"result-summary",
|
||||
`${visibleScorecards.length} supported cohort${visibleScorecards.length === 1 ? "" : "s"}; showing up to 18 cards and 12 chart rows.`,
|
||||
`${visibleScorecards.length} supported sample cohort${visibleScorecards.length === 1 ? "" : "s"}; showing up to 18 cards and 12 chart rows.`,
|
||||
);
|
||||
byId("empty-results").hidden = visibleScorecards.length > 0;
|
||||
renderCohortDotPlot(byId("cohort-dot-plot"), visibleScorecards);
|
||||
renderScorecardCards(visibleScorecards);
|
||||
}
|
||||
|
||||
function initializeExplorer(data) {
|
||||
populateSelect("filter-county", data.filters.public_counties);
|
||||
populateSelect("filter-age", data.filters.age_bands, (value) => String(value));
|
||||
populateSelect(
|
||||
"filter-period",
|
||||
data.filters.periods.map((period) => period.year),
|
||||
(value) => String(value),
|
||||
);
|
||||
|
||||
// Current scorecards are make/model aggregates only. These planned controls
|
||||
// remain visible but disabled so the UI never implies unsupported slicing.
|
||||
disableUnsupportedFilter("filter-county", "Unavailable at current scorecard grain");
|
||||
disableUnsupportedFilter("filter-age", "Unavailable at current scorecard grain");
|
||||
disableUnsupportedFilter("filter-fuel", "Fuel not published in this bundle");
|
||||
disableUnsupportedFilter("filter-program", "Program not published in this bundle");
|
||||
disableUnsupportedFilter("filter-period", "Period not published in this scorecard");
|
||||
const adjusted = document.querySelector('input[name="risk-view"][value="adjusted"]');
|
||||
adjusted.disabled = true;
|
||||
adjusted.parentElement.title = "Model-adjusted cohort scorecards are not published.";
|
||||
|
||||
function initializeExplorer() {
|
||||
byId("cohort-search").addEventListener("input", updateExplorer);
|
||||
byId("result-sort").addEventListener("change", updateExplorer);
|
||||
byId("reset-filters").addEventListener("click", () => {
|
||||
@@ -297,42 +251,37 @@ function initializeExplorer(data) {
|
||||
updateExplorer();
|
||||
}
|
||||
|
||||
function chooseDiagnostic(rows) {
|
||||
const partitionRank = { calibrate: 3, tune: 2, train: 1 };
|
||||
return [...rows].sort((left, right) => {
|
||||
const preferredLeft = left.model === "logistic_platt" ? 1 : 0;
|
||||
const preferredRight = right.model === "logistic_platt" ? 1 : 0;
|
||||
return (
|
||||
preferredRight - preferredLeft ||
|
||||
(partitionRank[right.partition] ?? 0) - (partitionRank[left.partition] ?? 0)
|
||||
);
|
||||
})[0];
|
||||
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);
|
||||
const diagnostic = chooseDiagnostic(data.diagnostics.rows);
|
||||
setText(
|
||||
"diagnostic-scope-label",
|
||||
diagnostic
|
||||
? `${diagnostic.partition === "calibrate" ? "Calibration cohort" : "Development diagnostics"} · ${diagnostic.model}`
|
||||
: "Development diagnostics · model unavailable",
|
||||
);
|
||||
setText("diagnostic-ap", diagnostic ? diagnostic.average_precision.toFixed(3) : "—");
|
||||
setText("diagnostic-brier", diagnostic ? diagnostic.brier.toFixed(3) : "—");
|
||||
setText("diagnostic-n", "Not published");
|
||||
renderNoCalibration(byId("calibration-chart"));
|
||||
}
|
||||
|
||||
function renderDashboard(data) {
|
||||
dashboardData = data;
|
||||
setPreviewBanner(data.manifest);
|
||||
renderOverview(data);
|
||||
initializeExplorer(data);
|
||||
initializeExplorer();
|
||||
renderModel(data);
|
||||
renderMethods(data);
|
||||
setHeaderStatus(
|
||||
"ready",
|
||||
data.manifest.development_preview ? "Validated development aggregates" : "Validated public aggregates",
|
||||
data.manifest.development_preview ? "Validated sample aggregates" : "Validated public aggregates",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -349,4 +298,4 @@ async function initialize() {
|
||||
|
||||
initialize();
|
||||
|
||||
export { displayCategory, normalizeCounty, routeFromHash };
|
||||
export { normalizeCounty, routeFromHash };
|
||||
|
||||
+5
-12
@@ -109,7 +109,7 @@ export function renderOutcomeTrend(container, rows) {
|
||||
</svg>`;
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
`Quarterly aggregate non-pass trend. ${description}`,
|
||||
`Development-sample quarterly aggregate non-pass rates, not population estimates. ${description}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ export function renderAgeRisk(container, rows) {
|
||||
</svg>`;
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
`Non-pass risk by vehicle-age band. ${points
|
||||
`Observed development-sample non-pass rates by vehicle-age band, not population estimates. ${points
|
||||
.map((point) => `${point.age_band}: ${percent(point.nonpass_rate)}`)
|
||||
.join("; ")}`,
|
||||
);
|
||||
@@ -199,7 +199,7 @@ export function renderCohortDotPlot(container, rows) {
|
||||
</svg>`;
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
`Ranked supported cohort non-pass risk. ${points
|
||||
`Supported development-sample cohort associations, not reliability rankings or population estimates. ${points
|
||||
.map((point) => `${point.prior_make} ${point.prior_model}: ${percent(point.nonpass_rate)}`)
|
||||
.join("; ")}`,
|
||||
);
|
||||
@@ -234,7 +234,7 @@ export function renderUtahCoverage(container, coveredCounties) {
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
coveredCounties.length
|
||||
? `Utah feed coverage includes ${coveredCounties.join(", ")}. Other counties are unavailable.`
|
||||
? `Development-sample feed availability includes ${coveredCounties.join(", ")}. This is not an outcome ranking; other counties are unavailable.`
|
||||
: "No county feed coverage is available.",
|
||||
);
|
||||
}
|
||||
@@ -267,14 +267,7 @@ export function renderCoverageHeatmap(container, rows) {
|
||||
</div>`;
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
`Source-era coverage from ${years[0]} through ${years[years.length - 1]} for ${eras.join(", ")}.`,
|
||||
);
|
||||
}
|
||||
|
||||
export function renderNoCalibration(container) {
|
||||
emptyChart(
|
||||
container,
|
||||
"Calibration-bin data is not published in this development bundle. No calibration curve is shown.",
|
||||
`Development-sample source-era coverage from ${years[0]} through ${years[years.length - 1]} for ${eras.join(", ")}.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
const SCHEMA_VERSION = "dashboard_data_v1";
|
||||
const APPROVED_PARTITIONS = Object.freeze(["train", "tune", "calibrate"]);
|
||||
const APPROVED_TARGET = "first-attempt next-episode binary non-pass rate";
|
||||
const REQUIRED_CALIBRATION_MODELS = Object.freeze([
|
||||
"hist_gradient_boosting_platt",
|
||||
"logistic_platt",
|
||||
]);
|
||||
const ENVELOPE_KEYS = Object.freeze([
|
||||
"development_preview",
|
||||
"population_estimate_allowed",
|
||||
@@ -20,15 +25,22 @@ export const REQUIRED_ASSETS = Object.freeze({
|
||||
const SENSITIVE_KEY_PARTS = new Set([
|
||||
"address",
|
||||
"certificate",
|
||||
"credential",
|
||||
"email",
|
||||
"inspector",
|
||||
"internal",
|
||||
"ip",
|
||||
"owner",
|
||||
"password",
|
||||
"pid",
|
||||
"plate",
|
||||
"prediction",
|
||||
"probability",
|
||||
"raw",
|
||||
"secret",
|
||||
"session",
|
||||
"station",
|
||||
"technician",
|
||||
"token",
|
||||
"user",
|
||||
"vin",
|
||||
@@ -337,6 +349,11 @@ export function validateAssetSet(rawAssets) {
|
||||
requireExactKeys(rawAssets, Object.keys(REQUIRED_ASSETS), "asset set");
|
||||
const manifest = rawAssets.manifest;
|
||||
validateEnvelope(manifest, "data_manifest");
|
||||
if (!manifest.development_preview || manifest.population_estimate_allowed) {
|
||||
throw new DataContractError(
|
||||
"The dashboard accepts development-sample, non-population assets only.",
|
||||
);
|
||||
}
|
||||
requireExactKeys(
|
||||
manifest,
|
||||
[
|
||||
@@ -427,6 +444,9 @@ export function validateAssetSet(rawAssets) {
|
||||
requireInteger(manifest.definitions[key], `data_manifest.definitions.${key}`, { min: 1 });
|
||||
}
|
||||
requireString(manifest.definitions.target, "data_manifest.definitions.target");
|
||||
if (manifest.definitions.target !== APPROVED_TARGET) {
|
||||
throw new DataContractError("data_manifest.definitions.target is outside the approved scope.");
|
||||
}
|
||||
if (!Array.isArray(manifest.assets)) {
|
||||
throw new DataContractError("data_manifest.assets must be an array.");
|
||||
}
|
||||
@@ -463,6 +483,16 @@ export function validateAssetSet(rawAssets) {
|
||||
throw new DataContractError(`model_diagnostics.rows[${index}].model is not cataloged.`);
|
||||
}
|
||||
}
|
||||
for (const model of REQUIRED_CALIBRATION_MODELS) {
|
||||
const matches = validated.diagnostics.rows.filter(
|
||||
(row) => row.model === model && row.partition === "calibrate",
|
||||
);
|
||||
if (matches.length !== 1) {
|
||||
throw new DataContractError(
|
||||
`model_diagnostics must contain exactly one approved calibration row for ${model}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const minimumSupport = manifest.definitions.suppression_min_support;
|
||||
const supportRounding = manifest.definitions.support_rounding;
|
||||
for (const name of ["overview", "ageRisk", "scorecards", "coverage"]) {
|
||||
|
||||
Reference in New Issue
Block a user