Mark the two dashboard-only facets, so they stop looking like broken buttons

language_family and religion_family are clickable, they highlight, and they
narrow every chart above them -- and they cannot move the grid below. Measured
2026-09-23: dashboard 2107 -> 677/306/222/151 and 2107 -> 693/649/610/83, grid
unchanged every time. To a user that is indistinguishable from a dead control;
the only way to learn it was to click and squint.

Wiring them is not the fix and would be worse. Both values are DERIVED -- a
language family resolved via langFamilyByIso for ~96% of rows, a religion family
computed from the first letter of a religion code -- so they exist on no row
field. Mapping religion_family onto `religion` would compare "Hinduism" against
a CODE and filter the grid to zero.

So the honest fix is a label, not a wire. A facet now declares
`dashboardOnly: true` in dashboard.config.js; DashboardFacetRail reads that from
config rather than hardcoding facet names, and renders a muted half-circle plus
a title and an extended aria-label saying the row updates the charts only.

These rows stay fully clickable, deliberately unlike `zero` and `other` rows,
which are inert because they would do nothing at all. Dimming them the same way
would understate what they really do.

Verified in a browser on the built tree: 41 of 66 rows marked, 41 glyphs
rendered, marked facets are exactly language_family and religion_family and
nothing else, 40 still clickable (the one that is not is a zero row that
correctly keeps its mark). Behaviour unchanged -- a marked row still takes the
dashboard 2,108 -> 677, an unmarked row still filters at 548.

Gates: facet-cascade, facet-one-field, facet-changes-map, one-palette,
synced-css, composite-srcs, template-literal, sfc-parse all pass;
a11y-button-names passes at 769/769 buttons named.
This commit is contained in:
doxa-maps-88655c70 2026-09-23 13:04:52 +00:00
parent 121275e0f2
commit 9076ec1d61
10 changed files with 121 additions and 66 deletions

View file

@ -90,6 +90,18 @@ export default {
// family totals sum exactly to the leaf-code totals.
religion_family: {
label: 'Religion Family',
// ★ DASHBOARD-ONLY, AND SAYING SO IS THE FIX. This facet narrows the dashboard correctly but
// can never move the grid beneath it, because the value it emits ("Hinduism") is a DERIVED
// family name that exists on no row field — the rows carry religion CODES. Mapping it in
// dashboard-plus-grid's FACET_TO_PROP would compare "Hinduism" against a code and filter the
// grid to ZERO, which is worse than leaving it unmapped.
//
// Measured 2026-09-23 (interop sweep, 4 rows tested): dashboard 2107 -> 693/649/610/83, grid
// unchanged every time. That combination is indistinguishable from a broken button: the row
// highlights, the numbers above move, and the list below ignores you. The honest fix is not to
// wire it — it is to TELL the user this row drives the dashboard only, which is what this flag
// makes the rail render.
dashboardOnly: true,
get: (r) => {
const code = r.religion && typeof r.religion === 'object' ? r.religion.value : r.religion
const c = String(code || '').charAt(0).toUpperCase()
@ -128,6 +140,11 @@ export default {
// byte-match contract intact.
language_family: {
label: 'Language Family',
// ★ DASHBOARD-ONLY — same class as religion_family, different cause. This value is DERIVED
// via langFamilyByIso for ~96% of rows, so it exists on no row field the grid can filter on.
// Measured 2026-09-23 (4 rows tested): dashboard 2107 -> 677/306/222/151, grid unchanged.
// See the religion_family note above for why declaring it beats wiring it.
dashboardOnly: true,
get: (r) => {
const api = lbl(r.imb_language_family)
if (api) return canonicalFamilyName(api)

View file

@ -31,6 +31,26 @@ const filterSet = bus?.filterSet || {}
// facet-key -> row accessor (sourced from config) — drives both the count tally and setFilter.
const FACET_ACC = Object.fromEntries(Object.entries(cfg.facets || {}).map(([k, f]) => [k, f.get]))
// ── FACETS THAT DRIVE THE DASHBOARD BUT CANNOT MOVE A GRID BELOW IT ──────────────────────────
// Read from config (`facets.<key>.dashboardOnly`), never a hardcoded list here: the rail is generic
// and the dashboard that hosts it owns which of its facets are derived.
//
// THE DEFECT THIS MARKS (measured 2026-09-23, interop sweep). language_family and religion_family
// are DERIVED — a language family resolved via langFamilyByIso for ~96% of rows, a religion family
// computed from the first letter of a religion code. Both narrow this dashboard correctly (2107 ->
// 677/306/222/151 and 2107 -> 693/649/610/83) and neither can filter a row grid, because the value
// they emit exists on no row field. Wiring them would be WORSE than leaving them: mapping
// religion_family onto `religion` compares "Hinduism" against a CODE and empties the grid.
//
// ★ SO THE HONEST FIX IS A LABEL, NOT A WIRE. Unmarked, these rows are indistinguishable from
// broken buttons — the row highlights, the numbers above move, the list below ignores you, and the
// only way to learn that is to click and squint. They stay fully CLICKABLE (unlike `zero`/`other`
// rows, which are inert because they would do nothing at all); what changes is that the row now
// says what it does.
const DASHBOARD_ONLY = new Set(
Object.entries(cfg.facets || {}).filter(([, f]) => f && f.dashboardOnly).map(([k]) => k)
)
// rows under the current filterSet, MINUS the facet(s) being aggregated (so each list stays navigable).
const matchSearch = makeSearchMatcher(bus, buildSearchAccessors(cfg))
function rowsExcluding(exclude) {
@ -74,6 +94,9 @@ function toRows(agg, facet, level) {
return agg.map((d) => ({
facet, value: d.key, label: d.key, count: d.value, frac: d.value / max, level,
other: !!d.other, active: !d.other && filterSet[facet] === d.key,
// Carried per-ROW rather than looked up in the template, so the zero-row baseline snapshot
// (which spreads a row object) keeps the mark when a row is re-admitted.
dashboardOnly: DASHBOARD_ONLY.has(facet),
}))
}
@ -599,6 +622,12 @@ useShadowStyles(`
.fr-bar i{ display:block; height:100%; background:var(--dx-accent, #92b195); border-radius:2px; transition:width .3s; }
.fr-row.on .fr-bar i{ background:var(--rd-accent,#e67e22); }
.fr-cnt{ flex:0 0 auto; font-variant-numeric:tabular-nums; color:var(--dx-muted, #6d766e); min-width:30px; text-align:right; font-size:11px; margin-left:8px; }
/* DASHBOARD-ONLY MARK. A half-filled circle reads as "half of this works" without needing a
legend, and it is muted so it never competes with the count beside it. Deliberately NOT the
.zero treatment (opacity:.42 on the whole row): a zero row does nothing at all, while these
rows genuinely drive every chart above — dimming them would understate what they do. */
.fr-row.dash-only .fr-donly{ flex:0 0 auto; margin-left:6px; font-size:10px; line-height:1;
color:var(--dx-muted, #6d766e); opacity:.75; }
`, 'rd-facetrail')
</script>
@ -621,12 +650,13 @@ useShadowStyles(`
a row the current filter drove to zero. Those two render as plain, muted,
non-focusable text so the rail never offers a target that does nothing. -->
<div v-for="r in sec.rows" :key="r.facet + ':' + r.value" class="fr-row"
:class="['l' + r.level, { on: r.active, other: r.other, zero: r.zero, inert: r.other || r.zero }]"
:class="['l' + r.level, { on: r.active, other: r.other, zero: r.zero, inert: r.other || r.zero, 'dash-only': r.dashboardOnly }]"
:data-facet="(r.other || r.zero) ? null : r.facet" :data-value="(r.other || r.zero) ? null : r.value"
:tabindex="(r.other || r.zero) ? null : 0"
:role="(r.other || r.zero) ? null : 'button'"
:aria-pressed="(r.other || r.zero) ? null : (r.active ? 'true' : 'false')"
:aria-label="(r.other || r.zero) ? null : ((r.active ? 'Remove filter ' : 'Filter by ') + r.label + ', ' + r.count.toLocaleString('en-US') + ' groups')">
:aria-label="(r.other || r.zero) ? null : ((r.active ? 'Remove filter ' : 'Filter by ') + r.label + ', ' + r.count.toLocaleString('en-US') + ' groups' + (r.dashboardOnly ? '. Updates the charts on this dashboard only; it does not filter the list below.' : ''))"
:title="r.dashboardOnly ? 'Updates this dashboard only — the list below does not filter on ' + r.label : null">
<!-- tree trunks: one span per ancestor level, same glyph vocabulary as the
research-map's SemanticTreeLegend so the hierarchy is DRAWN, not just padded. -->
<span v-for="(seg, i) in r.trunks" :key="'tr' + i" class="fr-trunk" :class="'fr-trunk-' + seg"></span>
@ -645,6 +675,10 @@ useShadowStyles(`
<span class="fr-lbl">{{ r.label }}</span>
<span class="fr-bar"><i :style="{ width: Math.max(4, r.frac * 100) + '%' }"></i></span>
<span class="fr-cnt">{{ r.count.toLocaleString('en-US') }}</span>
<!-- A VISIBLE mark, not only a title/aria one. A tooltip answers a question the user
does not know to ask: the whole defect is that this row LOOKS identical to a row
that filters the list. The glyph is what prompts the hover. -->
<span v-if="r.dashboardOnly" class="fr-donly" aria-hidden="true">◐</span>
</div>
</div>
</div>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
<script src="../../dashboard-plus-grid.js" integrity="sha384-/qhSKrIAbbUgEn7TNhtn6VuqpE3ApxLg8otkL8MD4KexFyTVrnmvztqD81Ourj1W" crossorigin="anonymous"></script>
<script src="../../dashboard-plus-grid.js" integrity="sha384-ou8O6omzXBxwc6kTFOpeL5BDJT3ZHUNWLuSVl7MoHpQDBjniuzGLf48sOVwC0L8+" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
<script src="../dashboard-plus-grid.js" integrity="sha384-/qhSKrIAbbUgEn7TNhtn6VuqpE3ApxLg8otkL8MD4KexFyTVrnmvztqD81Ourj1W" crossorigin="anonymous"></script>
<script src="../dashboard-plus-grid.js" integrity="sha384-ou8O6omzXBxwc6kTFOpeL5BDJT3ZHUNWLuSVl7MoHpQDBjniuzGLf48sOVwC0L8+" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -189,7 +189,7 @@
"bundle": "dashboard-plus-grid",
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"output": "doxa-maps/dashboard-plus-grid/dashboard-plus-grid.js",
"integrity": "sha384-/qhSKrIAbbUgEn7TNhtn6VuqpE3ApxLg8otkL8MD4KexFyTVrnmvztqD81Ourj1W",
"integrity": "sha384-ou8O6omzXBxwc6kTFOpeL5BDJT3ZHUNWLuSVl7MoHpQDBjniuzGLf48sOVwC0L8+",
"elements": [
"dashboard-plus-grid"
],
@ -205,7 +205,7 @@
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"embed": {
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-/qhSKrIAbbUgEn7TNhtn6VuqpE3ApxLg8otkL8MD4KexFyTVrnmvztqD81Ourj1W\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-ou8O6omzXBxwc6kTFOpeL5BDJT3ZHUNWLuSVl7MoHpQDBjniuzGLf48sOVwC0L8+\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
},
"instances": [
{
@ -764,7 +764,7 @@
"bundle": "research-dashboard",
"scriptUrl": "./research-dashboard/research-dashboard.js",
"output": "doxa-maps/research-dashboard/research-dashboard.js",
"integrity": "sha384-7deBWTaAE1237o9NDmPofHmH+1/3OXG+9lOkAlrPqTbCgSHe6TqRFvBlZX7JZ7iv",
"integrity": "sha384-4HNE72UOF2zrBYURaocKyph8t3C+COB4DAROu4CfbSE/GqvwsEe4pKRGbN1rNlk/",
"elements": [
"research-dashboard-map"
],
@ -780,7 +780,7 @@
"scriptUrl": "./research-dashboard/research-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-7deBWTaAE1237o9NDmPofHmH+1/3OXG+9lOkAlrPqTbCgSHe6TqRFvBlZX7JZ7iv\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-4HNE72UOF2zrBYURaocKyph8t3C+COB4DAROu4CfbSE/GqvwsEe4pKRGbN1rNlk/\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
},
"bundle": "research-dashboard"
}

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <research-dashboard-map> custom element. -->
<script src="../research-dashboard.js" integrity="sha384-7deBWTaAE1237o9NDmPofHmH+1/3OXG+9lOkAlrPqTbCgSHe6TqRFvBlZX7JZ7iv" crossorigin="anonymous"></script>
<script src="../research-dashboard.js" integrity="sha384-4HNE72UOF2zrBYURaocKyph8t3C+COB4DAROu4CfbSE/GqvwsEe4pKRGbN1rNlk/" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: