diff --git a/scripts/04_build_maps.py b/scripts/04_build_maps.py index 38737ee..71ccacb 100644 --- a/scripts/04_build_maps.py +++ b/scripts/04_build_maps.py @@ -80,9 +80,9 @@ from _lib.be.region import derive_region as derive_be_region from _lib.bg.geometry import BGPolygonIndex from _lib.bg.region import derive_region as derive_bg_region +from _lib.ch.canton import canton_name as ch_canton_name from _lib.ch.geometry import CHCommuneIndex, GESitgIndex from _lib.ch.geometry import resolve as ch_resolve_geometry -from _lib.ch.canton import canton_name as ch_canton_name from _lib.ch.region import derive_region as derive_ch_region from _lib.cy.geometry import CYPolygonIndex from _lib.cy.region import derive_region as derive_cy_region diff --git a/scripts/_lib/assets/app.js b/scripts/_lib/assets/app.js index 4f341b3..6113f44 100644 --- a/scripts/_lib/assets/app.js +++ b/scripts/_lib/assets/app.js @@ -866,6 +866,24 @@ return 'indeterminate'; } + // A region's slug list mixes two legal tiers: appellations proper and their + // complementary geographic designations (FR DGC / premier-cru climat, ES + // subzona, IT sottozona, DE Einzellage). One combined number implies a legal + // precision it doesn't have — Burgundy reads 869 where the regulator + // recognises 84 appellations carrying 785 complementary designations. + // Written by both the initial paint and the filter-driven refresh, so it + // lives here rather than being duplicated at either call site. + function renderRegionCount(el, parents, subs) { + if (!el) return; + el.innerHTML = subs + ? `${parents}+${subs}` + : `${parents}`; + el.setAttribute('title', fmt( + subs ? LABELS.region_count_title : LABELS.region_count_title_flat, + { p: parents, s: subs }, + )); + } + function buildAppellationFacet() { const el = document.getElementById('facet-appellations'); if (!el) return; // defensive null-guard @@ -882,15 +900,21 @@ const openLbl = escapeAttr(fmt(LABELS.open_appellation_aria, { name: rec.name || slug })); return ``; }).join(''); + let parentCount = 0; + for (const s of slugs) if (!AOCS[s].is_sub_denomination) parentCount++; + const subCount = slugs.length - parentCount; const safeRegion = escapeAttr(region); // Checkbox lives outside `` (sibling of `
`, // not a descendant) so the nested-interactive-in-summary // accessibility warning doesn't fire. Visual layout is restored // via `.region-group-wrap`'s flex rule — checkbox + disclosure // sit in the same row. - html.push(`
${escapeHtml(label)}${slugs.length}
${items}
`); + html.push(`
${escapeHtml(label)}
${items}
`); } el.innerHTML = html.join(''); + el.querySelectorAll('.region-group > summary > .count').forEach(c => { + renderRegionCount(c, +c.dataset.parents, +c.dataset.subs); + }); // Reapply current search visibility (so a tree rebuild during a typed // query keeps the filtered view). refreshFacetVisibility('facet-appellations', filters.q); @@ -1349,17 +1373,23 @@ const except = new Set(['appellations']); appEl.querySelectorAll('.region-group').forEach(group => { let visible = 0; + let visibleParents = 0; group.querySelectorAll('label').forEach(lbl => { const inp = lbl.querySelector('input[type=checkbox]'); if (!inp) return; const slug = inp.dataset.key; const rec = AOCS[slug]; const reachable = rec ? matchesExceptFacets(rec, slug, except) : false; const hide = !reachable && !inp.checked; lbl.classList.toggle('facet-unavailable', hide); - if (!hide) visible++; + if (!hide) { + visible++; + if (!(rec && rec.is_sub_denomination)) visibleParents++; + } }); (group.parentElement || group).classList.toggle('facet-unavailable', visible === 0); - const cs = group.querySelector(':scope > summary > .count'); - if (cs) cs.textContent = String(visible); + renderRegionCount( + group.querySelector(':scope > summary > .count'), + visibleParents, visible - visibleParents, + ); }); } } diff --git a/scripts/_lib/fr_wine_region.py b/scripts/_lib/fr_wine_region.py index 96a9ae3..81f2369 100644 --- a/scripts/_lib/fr_wine_region.py +++ b/scripts/_lib/fr_wine_region.py @@ -56,6 +56,19 @@ "roussette-du-bugey", ) +# Two BOURGOGNE-bassin appellations lie outside Burgundy proper. INAO's +# comité régional is an administrative grouping, not a wine region: +# Côtes du Forez sits in the upper Loire beside Côte roannaise and Côtes +# d'Auvergne (which INAO itself files under VAL DE LOIRE), and Coteaux du +# Lyonnais continues the Beaujolais granite south of Lyon. +_LOIRE_SLUGS: frozenset[str] = frozenset({ + "cotes-du-forez", +}) + +_LYONNAIS_SLUGS: frozenset[str] = frozenset({ + "coteaux-du-lyonnais", +}) + def derive_wine_region(record: dict[str, Any]) -> str: """Return the wine-region bucket for an FR record. @@ -69,6 +82,10 @@ def derive_wine_region(record: dict[str, Any]) -> str: return bassin slug = record.get("slug") or "" target = record.get("parent_slug") or slug + if target in _LOIRE_SLUGS: + return "VAL DE LOIRE" + if target in _LYONNAIS_SLUGS: + return "BEAUJOLAIS" if target in _JURA_SLUGS: return "JURA" if target.startswith(_BUGEY_PREFIXES): diff --git a/scripts/_lib/map_template.py b/scripts/_lib/map_template.py index 812acf3..e3447ad 100644 --- a/scripts/_lib/map_template.py +++ b/scripts/_lib/map_template.py @@ -54,6 +54,10 @@ def build_labels(_: Callable[[str], str]) -> dict[str, str]: "options_h": _("Options"), "active_filters_aria": _("Filtres actifs"), "select_all_aria": _("Tout sélectionner"), + "region_count_title": _( + "{p} appellations · {s} dénominations géographiques complémentaires" + ), + "region_count_title_flat": _("{p} appellations"), "open_appellation_aria": _("Ouvrir la fiche de {name}"), "open_appellation_title": _("Ouvrir la fiche"), "show_spirits_label": _("Inclure les spiritueux"), @@ -1310,6 +1314,9 @@ def _country_name(cc: str) -> str: "styles", "styles_simple", "classifications", "grapes_principal", "grapes_accessory", "grapes_all", "bbox", "bbox_villages", "geom_source", + # buildAppellationFacet splits each region's count into appellations + # vs complementary geographic designations. + "is_sub_denomination", }) @@ -1953,6 +1960,7 @@ def _emit(slug: str, meta: dict, ssr: str, has_card: bool) -> None: underline on name-hover signals the "go to this one" link affordance. */ .facet label[data-slug] > .name:hover {{ text-decoration:underline; text-decoration-color:#a8c08a; text-underline-offset:2px }} .facet .count {{ color:#666; font-size:11px; margin-left:4px }} + .facet .count .sub-count {{ opacity:0.65; margin-left:3px }} .facet .syns {{ color:#888; font-size:11px; font-weight:normal }} /* Keyboard/SR path to open an appellation panel (the WebGL polygons aren't DOM-reachable). Subtle until the row is hovered or the button is focused. */