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", `Quarterly aggregate non-pass trend. ${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", `Non-pass risk by vehicle-age band. ${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", `Ranked supported cohort non-pass risk. ${points .map((point) => `${point.prior_make} ${point.prior_model}: ${percent(point.nonpass_rate)}`) .join("; ")}`, ); } const COUNTY_POINTS = { cache: [154, 40], weber: [137, 86], davis: [126, 108], salt_lake: [133, 133], "salt lake": [133, 133], utah: [140, 170], }; export function renderUtahCoverage(container, coveredCounties) { const normalized = new Set(coveredCounties.map((county) => county.toLowerCase())); const points = [...normalized] .map((county) => ({ county, coordinates: COUNTY_POINTS[county] })) .filter((item) => item.coordinates); container.innerHTML = ` `; container.setAttribute( "aria-label", coveredCounties.length ? `Utah feed coverage includes ${coveredCounties.join(", ")}. Other counties are unavailable.` : "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", `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.", ); } export { aggregateOverview, compactNumber, percent };