Background
The Census Demographics PR (#338) added several new pieces of URL-backed state and display preferences to app/pages/tne.vue (choroplethElement, shadeByDensity, onlyWithStops, unitSystem, etc.). Each one is plumbed as an individual v-model or prop:
- ~10 v-models on
cal-filter
- ~15 props on
cal-map
- Further plumbing inside
cal-map → cal-legend and cal-map-viewer-ts
Adding one new knob (like onlyWithStops) currently means editing 4–5 files. The problem is structural: state that belongs together (choropleth display, display preferences) is being plumbed piece-by-piece instead of shared.
Plan: two composables
Vue's idiomatic answer here is composables that own state, not a giant state object passed as a prop. The URL is already the source of truth — composables that read/write route.query give every consumer the same view without prop drilling.
1. useScenarioUrlState()
Owns the URL-backed display state currently scattered across tne.vue:
showAggAreas, aggregateLayer, choroplethElement, shadeByDensity, onlyWithStops
- Possibly:
dataDisplayMode, hideUnmarked, flexServicesEnabled, flexColorBy
tne.vue, cal-filter, and cal-map each call the composable directly. Removes ~10 v-models on cal-filter and several props on cal-map.
2. useDisplayPreferences()
Cross-cutting bits that lots of components want:
unitSystem (currently threaded tne → map → legend / map-viewer-ts / census-panel)
isAllDayMode
- Any other display-only flags that descendants of
cal-map need
Removes the chain plumbing through cal-map.
Out of scope
- No Pinia / app-wide store. URL is already canonical; composables are sufficient.
- No mega
ScenarioState object passed as a prop. Breaks fine-grained reactivity, conflicts with v-model, hides what each component depends on.
- Scenario inputs (bbox, dates, geographyIds) and scenario filters (agencies, route types, weekdays) are already mostly bundled into
scenarioConfig / scenarioFilter computeds. Leave them.
- Selection state (
selectedAggregationGeoid, selectedPanelData) is localized to tne.vue ↔ cal-census-panel. Already small after recent collapse. Leave.
- Leaf-component props. For low-fan-out per-component data, explicit props are still nicer to read. Composables are for the high-fan-out shared state.
Acceptance criteria
Suggested order
useScenarioUrlState first — biggest immediate impact.
useDisplayPreferences second — smaller win, more cross-cutting.
- Stop. Re-evaluate after these land before reaching for more.
Why now
Punt until #338 merges. That PR introduced the bulk of the new plumbing and is already complex enough; doing both at once risks blurring the diff and making review harder.
Background
The Census Demographics PR (#338) added several new pieces of URL-backed state and display preferences to
app/pages/tne.vue(choroplethElement,shadeByDensity,onlyWithStops,unitSystem, etc.). Each one is plumbed as an individual v-model or prop:cal-filtercal-mapcal-map→cal-legendandcal-map-viewer-tsAdding one new knob (like
onlyWithStops) currently means editing 4–5 files. The problem is structural: state that belongs together (choropleth display, display preferences) is being plumbed piece-by-piece instead of shared.Plan: two composables
Vue's idiomatic answer here is composables that own state, not a giant state object passed as a prop. The URL is already the source of truth — composables that read/write
route.querygive every consumer the same view without prop drilling.1.
useScenarioUrlState()Owns the URL-backed display state currently scattered across
tne.vue:showAggAreas,aggregateLayer,choroplethElement,shadeByDensity,onlyWithStopsdataDisplayMode,hideUnmarked,flexServicesEnabled,flexColorBytne.vue,cal-filter, andcal-mapeach call the composable directly. Removes ~10 v-models oncal-filterand several props oncal-map.2.
useDisplayPreferences()Cross-cutting bits that lots of components want:
unitSystem(currently threadedtne→map→legend/map-viewer-ts/census-panel)isAllDayModecal-mapneedRemoves the chain plumbing through
cal-map.Out of scope
ScenarioStateobject passed as a prop. Breaks fine-grained reactivity, conflicts with v-model, hides what each component depends on.scenarioConfig/scenarioFiltercomputeds. Leave them.selectedAggregationGeoid,selectedPanelData) is localized totne.vue↔cal-census-panel. Already small after recent collapse. Leave.Acceptance criteria
cal-filterno longer receives the choropleth/aggregation v-models fromtne.vue; calls the composable directly.cal-legend,cal-map-viewer-ts, andcal-census-panelno longer receiveunitSystemas a prop.Suggested order
useScenarioUrlStatefirst — biggest immediate impact.useDisplayPreferencessecond — smaller win, more cross-cutting.Why now
Punt until #338 merges. That PR introduced the bulk of the new plumbing and is already complex enough; doing both at once risks blurring the diff and making review harder.