const SVG_NS = "http://www.w3.org/2000/svg"; function escapeText(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function percent(value, digits = 1) { return new Intl.NumberFormat("en-US", { style: "percent", minimumFractionDigits: digits, maximumFractionDigits: digits, }).format(value); } function compactNumber(value) { return new Intl.NumberFormat("en-US", { notation: value >= 10_000 ? "compact" : "standard", maximumFractionDigits: 1, }).format(value); } function emptyChart(container, message) { container.innerHTML = `

${escapeText(message)}

`; container.setAttribute("aria-label", message); } function aggregateOverview(rows) { const groups = new Map(); for (const row of rows) { const key = `${row.year}-Q${row.quarter}`; const current = groups.get(key) ?? { label: `${row.year} Q${row.quarter}`, year: row.year, quarter: row.quarter, support: 0, weightedRisk: 0, }; current.support += row.support_rounded; current.weightedRisk += row.support_rounded * row.nonpass_rate; groups.set(key, current); } return [...groups.values()] .map((group) => ({ ...group, risk: group.support > 0 ? group.weightedRisk / group.support : 0, })) .sort((left, right) => left.year - right.year || left.quarter - right.quarter); } export function renderOutcomeTrend(container, rows) { const points = aggregateOverview(rows); if (points.length < 2) { emptyChart(container, "Not enough approved periods to draw a trend."); return; } const width = 920; const height = 330; const margin = { top: 24, right: 22, bottom: 54, left: 58 }; const plotWidth = width - margin.left - margin.right; const plotHeight = height - margin.top - margin.bottom; const maxRisk = Math.max(0.05, ...points.map((point) => point.risk)); const yMax = Math.min(1, Math.ceil(maxRisk * 20) / 20); const x = (index) => margin.left + (index / (points.length - 1)) * plotWidth; const y = (value) => margin.top + plotHeight - (value / yMax) * plotHeight; const path = points .map((point, index) => `${index === 0 ? "M" : "L"}${x(index).toFixed(1)},${y(point.risk).toFixed(1)}`) .join(" "); const yTicks = Array.from({ length: 5 }, (_, index) => (index / 4) * yMax); const tickEvery = Math.max(1, Math.ceil(points.length / 8)); const labelled = points.filter( (_point, index) => index % tickEvery === 0 || index === points.length - 1, ); const description = points .map((point) => `${point.label}: ${percent(point.risk)}`) .join("; "); container.innerHTML = ` `; container.setAttribute( "aria-label", `Development-sample quarterly aggregate non-pass rates, not population estimates. ${description}`, ); } export function renderAgeRisk(container, rows) { if (!rows.length) { emptyChart(container, "No supported vehicle-age bands are available."); return; } const points = [...rows]; const width = 600; const height = 300; const margin = { top: 24, right: 20, bottom: 58, left: 52 }; const plotWidth = width - margin.left - margin.right; const plotHeight = height - margin.top - margin.bottom; const maxRisk = Math.max(0.05, ...points.map((point) => point.nonpass_rate)); const yMax = Math.min(1, Math.ceil(maxRisk * 20) / 20); const denominator = Math.max(1, points.length - 1); const x = (index) => margin.left + (index / denominator) * plotWidth; const y = (value) => margin.top + plotHeight - (value / yMax) * plotHeight; const path = points .map((point, index) => `${index === 0 ? "M" : "L"}${x(index)},${y(point.nonpass_rate)}`) .join(" "); container.innerHTML = ` `; container.setAttribute( "aria-label", `Observed development-sample non-pass rates by vehicle-age band, not population estimates. ${points .map((point) => `${point.age_band}: ${percent(point.nonpass_rate)}`) .join("; ")}`, ); } export function renderCohortDotPlot(container, rows) { const points = rows.slice(0, 12); if (!points.length) { emptyChart(container, "No supported cohorts match the current filters."); return; } const width = 820; const rowHeight = 34; const margin = { top: 24, right: 60, bottom: 42, left: 220 }; const height = margin.top + margin.bottom + points.length * rowHeight; const plotWidth = width - margin.left - margin.right; const maxRisk = Math.max(0.05, ...points.map((point) => point.nonpass_rate)); const xMax = Math.min(1, Math.ceil(maxRisk * 20) / 20); const x = (value) => margin.left + (value / xMax) * plotWidth; container.innerHTML = ` `; container.setAttribute( "aria-label", `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("; ")}`, ); } // Simplified from the Census 2025 1:20m cartographic boundary for Utah // (GEOID 49). Utah's defining step is on the northeast edge, not both sides. const UTAH_OUTLINE_PATH = "M44 20H153V67H226V255H44Z"; // Census county centers projected into the same Utah map frame. const COUNTY_POINTS = { cache: [128, 33], weber: [123, 54], davis: [111, 65], "salt lake": [121, 83], utah: [131, 108], }; function normalizeCountyName(county) { return String(county).trim().toLowerCase().replaceAll("_", " ").replace(/\s+/g, " "); } function displayCountyName(county) { return county.replace(/\b\w/g, (letter) => letter.toUpperCase()); } export function renderUtahCoverage(container, coveredCounties) { const normalized = new Set(coveredCounties.map(normalizeCountyName)); const points = [...normalized] .map((county) => ({ county, coordinates: COUNTY_POINTS[county] })) .filter((item) => item.coordinates); const countyNames = [...normalized].map(displayCountyName); container.innerHTML = ` `; container.setAttribute( "aria-label", countyNames.length ? `Outline of Utah showing development-sample feed availability for ${countyNames.join(", ")} County feeds. This is not an outcome ranking; other counties are unavailable.` : "Outline of Utah. No county feed coverage is available.", ); } export function renderCoverageHeatmap(container, rows) { if (!rows.length) { emptyChart(container, "No source-era coverage rows are available."); return; } const years = [...new Set(rows.map((row) => row.year))].sort((a, b) => a - b); const eras = [...new Set(rows.map((row) => row.source_era))].sort(); const lookup = new Map(rows.map((row) => [`${row.source_era}-${row.year}`, row])); const maxSupport = Math.max(1, ...rows.map((row) => row.support_rounded)); const columns = `90px repeat(${years.length}, minmax(34px, 1fr))`; container.innerHTML = `
${years.map((year) => `${year}`).join("")} ${eras .map( (era) => `${escapeText(era)}${years .map((year) => { const row = lookup.get(`${era}-${year}`); if (!row) return ``; const alpha = 0.15 + 0.75 * Math.sqrt(row.support_rounded / maxSupport); return ``; }) .join("")}`, ) .join("")}
`; container.setAttribute( "aria-label", `Development-sample source-era coverage from ${years[0]} through ${years[years.length - 1]} for ${eras.join(", ")}.`, ); } export { aggregateOverview, compactNumber, percent };