This roadmap plans how to add more interactive data visualization to the scorecard without breaking its constraints. It is triaged into shippable pieces, each of which lands as its own pull request.
The work is aimed at one reader in particular: a state liaison or customer success manager who has the scorecard open on a phone during a check-in call with a small agency, and needs to show "here is how your data is doing and the few things to fix" without leaving the conversation to read a table.
These come from the project's quality bar and do not move:
- Static hosting only. The frontend reads pre-computed JSON, GeoJSON, and PMTiles artifacts. No API server, no database, no runtime queries.
- Vanilla JavaScript, no build step, no framework. Charts today are hand-rolled inline SVG plus CSS bars. Maps use MapLibre GL JS 4.7.1 and PMTiles 3.2.1.
- WCAG 2.2 AAA, visibly. Full keyboard operation, a text alternative for every
chart and map, colour is never the only signal,
prefers-reduced-motionis honoured, and AAA contrast holds across the light, high-contrast, and dark themes. - Mobile-first, under two seconds on 4G, with a small total CSS and JS budget (roughly 130KB today).
Two inputs fed this plan. First, an inventory of what the site renders today and how (MapLibre maps, hand-rolled SVG sparklines and choropleths, CSS meter bars, static HTML tables, and the precomputed JSON flow). Of the comparison and trend pages, two already carry a visual: the equity page a static SVG choropleth (ADR 0022) and the trend page an autoscaled SVG line chart with a by-date table. The leaderboard, NTD, and realtime pages are still plain tables.
Second, a multi-source web research pass across five angles: accessible interactive visualization, tiny dependency-free charting, MapLibre and PMTiles interaction patterns, transit and civic data-quality dashboards, and facilitator-led live sessions. Each claim was checked with a three-vote adversarial pass; 23 of 25 reviewed claims survived, and 2 were rejected.
Two of the five angles produced no claims that survived verification: what comparable transit and civic dashboards (Cal-ITP, MobilityData, the FTA National Transit Database) actually do, and the facilitator and guided-session patterns. Recommendations that rest on those angles are marked below as lower confidence.
Every new visualization ships as a set of three parts, not one:
- the visual, drawn in SVG (or a map canvas), marked
aria-hidden="true"; - a visually hidden data table carrying the same numbers as text, using the
USWDS
.usa-sr-onlypattern; and - a short plain-language summary line stating the takeaway.
The whole thing stays keyboard-operable. This is the single most reusable and
best-supported pattern found, and the site already does part of it (the trend
sparkline carries its full series in an aria-label). Making the three-part
pattern a shared helper is the backbone of the charting work below.
A related finding shapes the approach: the ARIA graphics roles
(graphics-document, and the never-standardized graphics-datachart and
friends) have weak screen-reader support. They can supplement the markup, but
the hidden data table, not ARIA graphics roles, is what actually delivers the
text alternative for AAA.
Sources: USWDS Data visualizations guidance; UK Government Analysis Function, "Data visualisation: testing dashboards for design and accessibility"; W3C WAI-ARIA Authoring Practices; W3C Graphics Module 1.0; Minnesota IT Accessibility Guide for Interactive Web Maps.
- Maps can become interactive with no new dependency. MapLibre 4.7.1 already
supports per-feature hover and selection through
map.setFeatureState, read back in paint properties with acaseplusfeature-stateexpression, and the site already usesmaplibregl.Popup. (W3C and MapLibre docs; confirmed against the version in use.) - For a map to reach AAA, add a keyboard equivalent for click, such as a centre-crosshair "press to inspect the feature at centre" model, pair every map with a synced data table, and move focus to that table after a filter or search runs. (Minnesota IT.)
- Small SVG charts stay tiny and accessible-friendly. Dependency-free sparkline libraries run about 1 to 6KB and need no build step, and SVG keeps the data in the DOM where the site's existing inline-SVG pattern already lives. The site can also keep hand-rolling these, which avoids an unmaintained dependency.
- Richer interactive time series (zoom, live-value legend, cursor sync) would mean uPlot, at about 48KB. It is Canvas-based with no accessibility tree, so it would take a large slice of the byte budget and force a manual table and ARIA fallback. It is not the right default here (see decisions below).
- The official
maplibre-gl-compareplugin gives a swipe slider that syncs two maps, useful for before-and-after or two-agency benchmarking. It instantiates two map instances and needs a non-swipe alternative, so it is a later, gated addition. - On a static site, application state (selected agency, filters, sort, drilled category) can live in the URL and be restored on load, which makes any view shareable with no backend. (Lower confidence: this rests on the facilitator angle, which did not survive verification, but it is a well-established pattern.)
- National trend chart uses hand-rolled accessible SVG, not uPlot. uPlot's 48KB and Canvas rendering conflict with both the byte budget and the AAA text alternative. A hand-rolled SVG line with a hidden table stays in the site's existing idiom and is cheaper to keep accessible.
- The equity page already renders a static SVG choropleth (ADR 0022), so the work there is interactivity, not a new visual: link the choropleth to its state table with the same brushing used on the route map.
- A guided product tour (for example Driver.js) is deferred, not scheduled. It would add a dependency, it overlaps the existing "how to read" page, and the facilitator research angle did not survive verification. Revisit only if a real need shows up in use.
Each lands independently, in this order, so the highest-value and lowest-risk work ships first.
- This roadmap. Captures the plan and the citations.
- Interactive maps. Done. Feature-state hover and selection on the
per-agency route and stop map and the national cluster map (
/map/); linked highlighting between a map feature and its table row (a highlight layer enlarges the hovered row's point, mirroring the agency map'sroutes-hipattern, and hovering a map point scrolls its row into view unlessprefers-reduced-motionis set); a keyboard model for selecting a feature (the agency map's route rows become focusable, and Enter or Space pins the highlight; on the national map the rows' existing agency links stay the tab stops, and Space pins); focus moves to the results region after a user-driven filter runs, never on the initial load. - Interactive and small-multiple sparklines. Done. The agency trend
sparkline (static and app) carries a hover readout at every check (a native
<title>dot with date and score), and the app gained the same "Show the numbers" keyboard/screen-reader table the static page already had. Small autoscaled trend sparklines sit beside rows on the pulse rankings, NTD one-fix, and realtime most-reliable tables (an em dash until a feed has two checks). The three-part accessible pattern is a shared helper (_spark_svg/_spark_miniinrender_site.py), which both trend charts and the row minis now draw through. - Equity choropleth. Done. The static SVG choropleth is linked to its state table with the same brushing used on the route map.
- National trend chart. Done. The trend line carries a hover dot at every date and a visible score-range caption.
- Deep-link URL state. Partially done. The compare page encodes its two agencies in the URL; the general filters/sort/search encoding on the pulse and directory views has not landed. Lower confidence, high call-day value.
- Compare two agencies. Done. A no-dependency side-by-side comparison table
(overall grade, category bars, top fixes), shareable by URL. This replaced the
planned
maplibre-gl-compareswipe: that plugin is built for two overlapping layers of the same area, but the data keeps only current per-agency geometry and agencies sit in different places, and it was the one runtime dependency. - Methodology sandbox. (shipped, EXP-06) A dependency-free what-if on the
/how-to-read/page: four range sliders reweight the rubric categories and the page recomputes, entirely client-side, how many agencies would change letter grade. It fetches the publishedapi/v1/scoring.json(weights + grade bands) andapi/v1/agencies.json(each agency's measured category scores), so the default weights, band thresholds, and the renormalized overall-score formula all come from the published data at runtime — the sandbox and the pipeline agree by construction (the FIX-03 single-source intent, without a schema change). Degrades to the static rubric explanation when scripting or the fetch is unavailable.
- What is the real remaining CSS and JS headroom under the budget after today's code? That decides whether anything heavier than sparkline-class ever fits.
- How should pointer-only interactions (hover popups, the compare swipe) behave on touch and by keyboard so they satisfy both the phone demo and AAA?
- Does the equity page need a map to meet its goal, or is a well-summarized, sortable table the better minimal and AAA-safe choice?
- What do Cal-ITP, MobilityData, and the NTD actually show for coverage, trend, and equity, and which of those patterns are worth adopting? This angle returned no verified evidence and is worth a direct look.