From 6d5b7a34e1fbc7272d477b98c0f0640d4d3b0cd8 Mon Sep 17 00:00:00 2001 From: Boris De Vloed Date: Sun, 9 Aug 2026 16:45:26 +0200 Subject: [PATCH 1/2] fix(map): split the region counter into appellations vs DGCs; re-home Forez + Lyonnais MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The appellation facet counted a region's appellations and their complementary geographic designations as one number, implying a legal precision it doesn't have: Burgundy read 871 where the regulator recognises 84 appellations (7 régionales + 44 villages + 33 grands crus) carrying 785 complementary designations — premier-cru climats, Bourgogne Côte d'Or, Mâcon-Azé and the like. Reported by Reinier Broeks (Winecastr) while researching an article. The count now renders as "84 +785" with a per-locale tooltip naming both tiers, falling back to a bare number where a region has no sub-denominations. Formatting lives in one helper because the count is written twice: once on the initial paint and again by refreshFacetVisibility on every filter change — the refresh path previously overwrote the split with a plain filtered total, so the tooltip and the visible number disagreed. is_sub_denomination joins STARTUP_AOCS_FIELDS: buildAppellationFacet now reads it, and a field read on a startup path but absent from that set silently degrades the sidebar (every record looks like a parent). Separately, two BOURGOGNE-bassin appellations are not in Burgundy. INAO's comité régional is an administrative grouping: 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. Both move, alongside the existing JURA/SAVOIE/BUGEY/BEAUJOLAIS splits. Verified against a local build: 2908 records, 20 countries, Burgundy 84+785, Beaujolais 12+31, Piemonte flat at 59. Co-Authored-By: Claude Opus 5 --- scripts/_lib/assets/app.js | 38 ++++++++++++++++++++++++++++++---- scripts/_lib/fr_wine_region.py | 17 +++++++++++++++ scripts/_lib/map_template.py | 8 +++++++ 3 files changed, 59 insertions(+), 4 deletions(-) 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. */ From c2db4a6b03f0a15b1c2c9b665f2603874d725c2a Mon Sep 17 00:00:00 2001 From: Boris De Vloed Date: Sun, 9 Aug 2026 16:52:52 +0200 Subject: [PATCH 2/2] style: sort the _lib.ch imports (ruff I001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_lib.ch.canton` sat after `_lib.ch.geometry`, which trips the I001 import-sorting rule CI pins at zero. Pre-existing on main — the branch inherited the red build rather than causing it. Pure reordering of two adjacent imports in the same group; no behaviour change. Co-Authored-By: Claude Opus 5 --- scripts/04_build_maps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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