feat(country-map): manifest-driven control choices

Replace hardcoded WORLDVIEW / COUNTRY / REGION_SET / COMPOSITE choice
tables in controlPanel with options derived from the build pipeline's
manifest.json. Adding a new entry to a YAML config + re-running
./build.sh now populates the control automatically; no plugin code
change needed.

Build script: also writes manifest.json to the plugin's
src/data/manifest.json so controlPanel can `import` it synchronously
(no async fetch needed at chart-edit time). Data files (the actual
GeoJSONs) still live only at superset/static/assets/country-maps/ —
we don't want to bundle 17MB of choropleth data into the JS payload.

Lookup helpers preserve human-friendly labels:
- WORLDVIEW_LABELS — maps NE worldview codes (ukr, default, ind, ...)
  to friendly names ("Ukraine (default — Crimea as Ukrainian)" etc.);
  unmapped codes render as the raw code
- COUNTRY_LABELS — ISO_A3 → English country name (~85 entries);
  formatCountry renders as "France (FRA)"; unmapped codes render raw
- REGION_SET_LABELS / COMPOSITE_LABELS — same pattern

Manifest's regional_aggregations array is grouped by country into
the {country: [(set_id, label), ...]} shape the control panel needs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-05-12 17:32:52 -07:00
parent 1817e37b06
commit 5efb93b99b
3 changed files with 455 additions and 73 deletions

View File

@@ -871,6 +871,13 @@ def write_manifest(targets: list[tuple[str, int]]) -> Path:
manifest_path = OUTPUT_DIR / "manifest.json"
manifest_path.write_text(json.dumps(manifest, indent=2))
# Also write a copy into the plugin source tree so the control
# panel can `import manifest from '../data/manifest.json'` without
# an async fetch at chart-edit time.
plugin_data_dir = SCRIPT_DIR.parent / "src" / "data"
plugin_data_dir.mkdir(parents=True, exist_ok=True)
(plugin_data_dir / "manifest.json").write_text(json.dumps(manifest, indent=2))
log(
f"\nWrote manifest.json — {len(worldviews)} worldview(s), "
f"{sum(len(v) for v in countries_by_wv.values())} country files, "

View File

@@ -0,0 +1,263 @@
{
"ne_pinned_tag": "v5.1.2",
"ne_pinned_sha": "f1890d9f152c896d250a77557a5751a93d494776",
"build_timestamp_utc": "2026-05-13T00:31:41.547398+00:00",
"worldviews": [
"ukr"
],
"admin_levels": [
0,
1
],
"countries_by_worldview": {
"ukr": [
"AFG",
"AGO",
"AIA",
"ALB",
"ALD",
"AND",
"ARE",
"ARG",
"ARM",
"ASM",
"ATA",
"ATF",
"ATG",
"AUS",
"AUT",
"AZE",
"BDI",
"BEL",
"BEN",
"BFA",
"BGD",
"BGR",
"BHR",
"BHS",
"BIH",
"BLR",
"BLZ",
"BMU",
"BOL",
"BRA",
"BRB",
"BRN",
"BTN",
"BWA",
"CAF",
"CAN",
"CHE",
"CHL",
"CHN",
"CIV",
"CMR",
"COD",
"COG",
"COK",
"COL",
"COM",
"CPV",
"CRI",
"CUB",
"CYP",
"CZE",
"DEU",
"DJI",
"DMA",
"DNK",
"DOM",
"DZA",
"ECU",
"EGY",
"ERI",
"ESP",
"EST",
"ETH",
"FIN",
"FJI",
"FRA",
"FSM",
"GAB",
"GBR",
"GEO",
"GHA",
"GIN",
"GMB",
"GNB",
"GNQ",
"GRC",
"GRD",
"GRL",
"GTM",
"GUY",
"HKG",
"HND",
"HRV",
"HTI",
"HUN",
"IDN",
"IND",
"IOA",
"IRL",
"IRN",
"IRQ",
"ISL",
"ISR",
"ITA",
"JAM",
"JOR",
"JPN",
"KAZ",
"KEN",
"KGZ",
"KHM",
"KIR",
"KNA",
"KOR",
"KOS",
"KWT",
"LAO",
"LBN",
"LBR",
"LBY",
"LCA",
"LIE",
"LKA",
"LSO",
"LTU",
"LUX",
"LVA",
"MAR",
"MDA",
"MDG",
"MDV",
"MEX",
"MHL",
"MKD",
"MLI",
"MLT",
"MMR",
"MNE",
"MNG",
"MNP",
"MOZ",
"MRT",
"MSR",
"MUS",
"MWI",
"MYS",
"NAM",
"NCL",
"NER",
"NGA",
"NIC",
"NLD",
"NOR",
"NPL",
"NRU",
"NZL",
"OMN",
"PAK",
"PAN",
"PER",
"PHL",
"PLW",
"PNG",
"POL",
"PRK",
"PRT",
"PRY",
"PSX",
"PYF",
"QAT",
"ROU",
"RUS",
"RWA",
"SAU",
"SDN",
"SDS",
"SEN",
"SGP",
"SHN",
"SLB",
"SLE",
"SLV",
"SMR",
"SOM",
"SPM",
"SRB",
"STP",
"SUR",
"SVK",
"SVN",
"SWE",
"SWZ",
"SYC",
"SYR",
"TCA",
"TCD",
"TGO",
"THA",
"TJK",
"TKM",
"TLS",
"TON",
"TTO",
"TUN",
"TUR",
"TWN",
"TZA",
"UGA",
"UKR",
"UMI",
"URY",
"USA",
"UZB",
"VCT",
"VEN",
"VIR",
"VNM",
"VUT",
"WLF",
"WSM",
"YEM",
"ZAF",
"ZMB",
"ZWE"
]
},
"regional_aggregations": [
{
"country": "FRA",
"set_id": "regions",
"worldview": "ukr",
"size_bytes": 32077
},
{
"country": "ITA",
"set_id": "regions",
"worldview": "ukr",
"size_bytes": 32116
},
{
"country": "PHL",
"set_id": "regions",
"worldview": "ukr",
"size_bytes": 31805
},
{
"country": "TUR",
"set_id": "nuts_1",
"worldview": "ukr",
"size_bytes": 23036
}
],
"composites": [
{
"id": "france_overseas",
"worldview": "ukr",
"size_bytes": 322058
}
]
}

View File

@@ -24,24 +24,54 @@ import {
D3_FORMAT_OPTIONS,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import manifest from '../data/manifest.json';
// ----------------------------------------------------------------------
// Choice tables
// Choice tables — sourced from the build pipeline's manifest.json
//
// These are hardcoded for the POC. A follow-up commit will replace them
// with options pulled from the build pipeline's manifest.json so adding
// a new worldview or country only requires a build script run, not a
// plugin code change.
// The manifest is regenerated by every `./scripts/build.sh` run; adding
// a new worldview or country to the YAML configs and re-running build
// makes the new option appear in the UI without touching this file.
// ----------------------------------------------------------------------
const WORLDVIEW_CHOICES: Array<[string, string]> = [
['ukr', t('Ukraine (default — Crimea as Ukrainian)')],
['default', t('Natural Earth Default')],
// Future: pull the rest from the build manifest. Available NE
// worldviews include arg, bdg, bra, chn, deu, egy, esp, fra, gbr,
// grc, idn, ind, isr, ita, jpn, kor, mar, nep, nld, pak, pol, prt,
// pse, rus, sau, swe, tur, twn, ukr, usa, vnm.
];
interface CountryMapManifest {
worldviews: string[];
admin_levels: number[];
countries_by_worldview: Record<string, string[]>;
regional_aggregations: Array<{
country: string;
set_id: string;
worldview: string;
size_bytes: number;
}>;
composites: Array<{ id: string; worldview: string; size_bytes: number }>;
}
const M = manifest as CountryMapManifest;
// Display-name labels for worldviews. Anything not listed here renders
// as the worldview code itself in the dropdown.
const WORLDVIEW_LABELS: Record<string, string> = {
ukr: t('Ukraine (default — Crimea as Ukrainian)'),
default: t('Natural Earth Default'),
ind: t('India'),
pak: t('Pakistan'),
chn: t('China'),
rus: t('Russia'),
isr: t('Israel'),
pse: t('Palestine'),
tur: t('Türkiye'),
jpn: t('Japan'),
kor: t('Korea'),
usa: t('United States'),
fra: t('France'),
gbr: t('United Kingdom'),
};
const WORLDVIEW_CHOICES: Array<[string, string]> = M.worldviews.map(wv => [
wv,
WORLDVIEW_LABELS[wv] || wv,
]);
const ADMIN_LEVEL_CHOICES: Array<[string, string]> = [
[String(0), t('Countries (Admin 0)')],
@@ -49,68 +79,150 @@ const ADMIN_LEVEL_CHOICES: Array<[string, string]> = [
['aggregated', t('Aggregated regions')],
];
// ISO_A3 country codes that have at least 2 subdivisions in NE Admin 1.
// Hardcoded snapshot — should come from build manifest in follow-up.
const COUNTRY_CHOICES: Array<[string, string]> = [
['USA', t('United States')],
['CAN', t('Canada')],
['MEX', t('Mexico')],
['BRA', t('Brazil')],
['ARG', t('Argentina')],
['GBR', t('United Kingdom')],
['FRA', t('France')],
['DEU', t('Germany')],
['ITA', t('Italy')],
['ESP', t('Spain')],
['NLD', t('Netherlands')],
['BEL', t('Belgium')],
['CHE', t('Switzerland')],
['AUT', t('Austria')],
['POL', t('Poland')],
['SWE', t('Sweden')],
['NOR', t('Norway')],
['FIN', t('Finland')],
['DNK', t('Denmark')],
['PRT', t('Portugal')],
['GRC', t('Greece')],
['TUR', t('Türkiye')],
['UKR', t('Ukraine')],
['RUS', t('Russia')],
['CHN', t('China (incl. Taiwan / HK / Macau)')],
['JPN', t('Japan')],
['KOR', t('South Korea')],
['IND', t('India')],
['IDN', t('Indonesia')],
['THA', t('Thailand')],
['VNM', t('Vietnam')],
['PHL', t('Philippines')],
['MYS', t('Malaysia')],
['AUS', t('Australia')],
['NZL', t('New Zealand')],
['ZAF', t('South Africa')],
['EGY', t('Egypt')],
['MAR', t('Morocco')],
['NGA', t('Nigeria')],
['KEN', t('Kenya')],
['ETH', t('Ethiopia')],
['LVA', t('Latvia')],
];
// Maps a country to its available regional aggregation sets, sourced
// from regional_aggregations.yaml. Hardcoded snapshot.
const REGION_SET_CHOICES_BY_COUNTRY: Record<string, Array<[string, string]>> = {
TUR: [['nuts_1', t('NUTS-1 statistical regions')]],
FRA: [['regions', t('Administrative regions')]],
ITA: [['regions', t('Administrative regions')]],
PHL: [['regions', t('Administrative regions')]],
// English country names by ISO_A3, for friendly dropdown labels. Codes
// not listed here render as the raw ISO code (rare; missing entries
// here aren't a correctness problem).
const COUNTRY_LABELS: Record<string, string> = {
AFG: 'Afghanistan',
ARG: 'Argentina',
AUS: 'Australia',
AUT: 'Austria',
BEL: 'Belgium',
BGD: 'Bangladesh',
BGR: 'Bulgaria',
BIH: 'Bosnia and Herzegovina',
BLR: 'Belarus',
BOL: 'Bolivia',
BRA: 'Brazil',
CAN: 'Canada',
CHE: 'Switzerland',
CHL: 'Chile',
CHN: 'China',
COL: 'Colombia',
CRI: 'Costa Rica',
CUB: 'Cuba',
CYP: 'Cyprus',
CZE: 'Czechia',
DEU: 'Germany',
DNK: 'Denmark',
DOM: 'Dominican Republic',
ECU: 'Ecuador',
EGY: 'Egypt',
ESP: 'Spain',
EST: 'Estonia',
ETH: 'Ethiopia',
FIN: 'Finland',
FRA: 'France',
GBR: 'United Kingdom',
GHA: 'Ghana',
GRC: 'Greece',
GTM: 'Guatemala',
HND: 'Honduras',
HRV: 'Croatia',
HUN: 'Hungary',
IDN: 'Indonesia',
IND: 'India',
IRL: 'Ireland',
IRN: 'Iran',
IRQ: 'Iraq',
ISL: 'Iceland',
ISR: 'Israel',
ITA: 'Italy',
JPN: 'Japan',
KAZ: 'Kazakhstan',
KEN: 'Kenya',
KGZ: 'Kyrgyzstan',
KHM: 'Cambodia',
KOR: 'South Korea',
LAO: 'Laos',
LBN: 'Lebanon',
LTU: 'Lithuania',
LVA: 'Latvia',
MAR: 'Morocco',
MEX: 'Mexico',
MMR: 'Myanmar',
MNG: 'Mongolia',
MYS: 'Malaysia',
NGA: 'Nigeria',
NLD: 'Netherlands',
NOR: 'Norway',
NPL: 'Nepal',
NZL: 'New Zealand',
PAK: 'Pakistan',
PER: 'Peru',
PHL: 'Philippines',
POL: 'Poland',
PRT: 'Portugal',
PRY: 'Paraguay',
ROU: 'Romania',
RUS: 'Russia',
SAU: 'Saudi Arabia',
SDN: 'Sudan',
SRB: 'Serbia',
SVK: 'Slovakia',
SVN: 'Slovenia',
SWE: 'Sweden',
SYR: 'Syria',
THA: 'Thailand',
TUR: 'Türkiye',
TWN: 'Taiwan',
UKR: 'Ukraine',
URY: 'Uruguay',
USA: 'United States',
UZB: 'Uzbekistan',
VEN: 'Venezuela',
VNM: 'Vietnam',
YEM: 'Yemen',
ZAF: 'South Africa',
ZWE: 'Zimbabwe',
};
// Composite maps from composite_maps.yaml. Each composite is an
// alternative selection for a country (sits alongside the regular
// country picker).
const COMPOSITE_CHOICES: Array<[string, string]> = [
['france_overseas', t('France (with overseas territories)')],
];
const formatCountry = (code: string): string =>
COUNTRY_LABELS[code] ? `${COUNTRY_LABELS[code]} (${code})` : code;
// Build country choices from the union of all worldview manifests
// (typically the same set since Admin 1 is one global file, but
// future per-worldview Admin 1 outputs would naturally differ).
const COUNTRY_CHOICES: Array<[string, string]> = (() => {
const all = new Set<string>();
Object.values(M.countries_by_worldview).forEach(codes =>
codes.forEach(c => all.add(c)),
);
return Array.from(all)
.sort()
.map<[string, string]>(c => [c, formatCountry(c)]);
})();
// Region-set labels keyed by `<set_id>` (TUR's `nuts_1` etc.).
const REGION_SET_LABELS: Record<string, string> = {
nuts_1: t('NUTS-1 statistical regions'),
regions: t('Administrative regions'),
};
// Build {country: [(set_id, label), ...]} from manifest.
const REGION_SET_CHOICES_BY_COUNTRY: Record<string, Array<[string, string]>> = (
() => {
const out: Record<string, Array<[string, string]>> = {};
M.regional_aggregations.forEach(r => {
out[r.country] = out[r.country] || [];
out[r.country].push([
r.set_id,
REGION_SET_LABELS[r.set_id] || r.set_id,
]);
});
return out;
}
)();
// Composite-map labels keyed by `<id>`.
const COMPOSITE_LABELS: Record<string, string> = {
france_overseas: t('France (with overseas territories)'),
};
const COMPOSITE_CHOICES: Array<[string, string]> = M.composites.map(c => [
c.id,
COMPOSITE_LABELS[c.id] || c.id,
]);
// NE NAME_<lang> language codes available across most features.
const NAME_LANGUAGE_CHOICES: Array<[string, string]> = [