feat(plugin-chart-country-map): friendlier labels + auto-migrate from legacy plugin

Two small UX wins on top of the previous control-panel fixes:

1. Rename admin_level options to "World" / "Country" / "Aggregated
   regions" (from the technical "Countries (Admin 0)" / "Subdivisions
   (Admin 1)"). The control's own label becomes "Map view" with an
   explanatory description. Underlying form_data values stay 0 / 1 /
   aggregated so saved charts don't break.

2. Auto-migrate form_data when a user switches a saved legacy
   country_map chart's viz type to country_map_v2:
     - select_country: 'france'         → admin_level=1, country=FRA
     - select_country: 'france_overseas' → composite=france_overseas
     - select_country: 'turkey_regions'  → admin_level=aggregated,
                                            country=TUR, region_set=nuts_1
     - …same pattern for france_regions / italy_regions /
       philippines_regions and ~180 other legacy country files.

   The mapping lives in src/plugin/migrateFromLegacy.ts (covered by
   12 unit tests). Migration only fills fields the user hasn't already
   set on the new chart, so explicit edits win over inferred values.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Superset Dev
2026-05-13 09:44:01 -07:00
parent d1a7c9e82f
commit 6f882f215e
3 changed files with 464 additions and 10 deletions

View File

@@ -25,6 +25,7 @@ import {
getStandardizedControls,
} from '@superset-ui/chart-controls';
import manifest from '../data/manifest.json';
import migrateFromLegacy from './migrateFromLegacy';
// ----------------------------------------------------------------------
// Choice tables — sourced from the build pipeline's manifest.json
@@ -96,9 +97,11 @@ const WORLDVIEW_CHOICES: Array<[string, string]> = M.worldviews.map(wv => [
WORLDVIEW_LABELS[wv] || wv,
]);
// Map scope choices. The underlying values stay 0/1/aggregated so saved
// charts and form_data don't break — only the labels change for clarity.
const ADMIN_LEVEL_CHOICES: Array<[string, string]> = [
[String(0), t('Countries (Admin 0)')],
[String(1), t('Subdivisions (Admin 1)')],
[String(0), t('World')],
[String(1), t('Country')],
['aggregated', t('Aggregated regions')],
];
@@ -316,10 +319,13 @@ const config: ControlPanelConfig = {
name: 'admin_level',
config: {
type: 'SelectControl',
label: t('Admin level'),
label: t('Map view'),
description: t(
'Choose the geographic level: countries (world map), ' +
'subdivisions of one country, or an aggregated regional layer.',
'World shows all countries; Country shows subdivisions of ' +
'one country (states/provinces/departments); Aggregated ' +
"regions dissolves a country's subdivisions into coarser " +
'administrative regions (e.g. French regions, Turkish ' +
'NUTS-1 regions). Stored as admin_level (0 / 1 / aggregated).',
),
choices: ADMIN_LEVEL_CHOICES,
default: String(0),
@@ -528,11 +534,31 @@ const config: ControlPanelConfig = {
renderTrigger: true,
},
},
formDataOverrides: formData => ({
...formData,
entity: getStandardizedControls().shiftColumn(),
metric: getStandardizedControls().shiftMetric(),
}),
// formDataOverrides runs when the user switches a chart's viz_type to
// this plugin. We use it for two jobs:
// 1. Standard control hand-off (entity, metric) via getStandardizedControls
// 2. Migration from the legacy country_map plugin — translate the
// legacy `select_country` value into admin_level / country /
// composite / region_set so the new chart lands pre-populated
// rather than dumping the user back to an empty Country dropdown.
formDataOverrides: formData => {
const fromLegacy =
typeof formData.select_country === 'string' && formData.select_country
? migrateFromLegacy(formData)
: {};
return {
...formData,
entity: getStandardizedControls().shiftColumn(),
metric: getStandardizedControls().shiftMetric(),
// Only fill fields the user has not already set on the new chart;
// explicit user edits on the new viz win over legacy migration.
...Object.fromEntries(
Object.entries(fromLegacy).filter(
([k]) => formData[k as keyof typeof formData] == null,
),
),
};
},
};
export default config;

View File

@@ -0,0 +1,308 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Translate a legacy `country_map` chart's form_data into the new
* `country_map_v2` form_data shape.
*
* Triggered from controlPanel.formDataOverrides whenever a user switches
* a saved chart's viz type to country_map_v2. We try to preserve as much
* intent as possible:
*
* - Legacy `select_country: 'france'` → admin_level=1, country='FRA'
* - Legacy `select_country: 'france_overseas'` → composite='france_overseas'
* - Legacy `select_country: 'turkey_regions'` → admin_level='aggregated',
* country='TUR', region_set='nuts_1'
* - Legacy `select_country: 'italy_regions'` → ITA/regions
* - Legacy `select_country: 'philippines_regions'` → PHL/regions
* - Legacy `select_country: 'france_regions'` → FRA/regions
*
* Worldview defaults to 'ukr' (the new plugin's default editorial choice).
* Standard controls (entity, metric, color scheme, number format) flow
* through the standard `formDataOverrides` path; this migration only
* touches the country/admin/composite/region_set quartet.
*/
interface PartialFormData {
select_country?: string;
[k: string]: unknown;
}
interface MigrationOutput {
admin_level?: string;
country?: string;
composite?: string;
region_set?: string;
worldview?: string;
}
// Composite outputs — legacy keys that should map to the new plugin's
// composite_maps.yaml-driven composite control rather than to a country.
const LEGACY_TO_COMPOSITE: Record<string, string> = {
france_overseas: 'france_overseas',
};
// Aggregated region outputs — legacy keys that should map to the new
// plugin's regional_aggregations.yaml-driven country+region_set pair.
const LEGACY_TO_AGGREGATED: Record<
string,
{ country: string; region_set: string }
> = {
france_regions: { country: 'FRA', region_set: 'regions' },
italy_regions: { country: 'ITA', region_set: 'regions' },
philippines_regions: { country: 'PHL', region_set: 'regions' },
turkey_regions: { country: 'TUR', region_set: 'nuts_1' },
};
// Per-country subdivisions — legacy snake_case keys mapped to ISO 3166-1
// alpha-3 codes used by the new plugin's country control. Coverage is
// intentionally broad — every legacy country file maps to a sibling
// entry in the new build's admin 1 outputs.
const LEGACY_TO_ISO_A3: Record<string, string> = {
afghanistan: 'AFG',
aland: 'ALD',
albania: 'ALB',
algeria: 'DZA',
american_samoa: 'ASM',
andorra: 'AND',
angola: 'AGO',
anguilla: 'AIA',
antarctica: 'ATA',
antigua_and_barbuda: 'ATG',
argentina: 'ARG',
armenia: 'ARM',
australia: 'AUS',
austria: 'AUT',
azerbaijan: 'AZE',
bahrain: 'BHR',
bangladesh: 'BGD',
barbados: 'BRB',
belarus: 'BLR',
belgium: 'BEL',
belize: 'BLZ',
benin: 'BEN',
bermuda: 'BMU',
bhutan: 'BTN',
bolivia: 'BOL',
bosnia_and_herzegovina: 'BIH',
botswana: 'BWA',
brazil: 'BRA',
brunei: 'BRN',
bulgaria: 'BGR',
burkina_faso: 'BFA',
burundi: 'BDI',
cambodia: 'KHM',
cameroon: 'CMR',
canada: 'CAN',
cape_verde: 'CPV',
central_african_republic: 'CAF',
chad: 'TCD',
chile: 'CHL',
china: 'CHN',
colombia: 'COL',
comoros: 'COM',
cook_islands: 'COK',
costa_rica: 'CRI',
croatia: 'HRV',
cuba: 'CUB',
cyprus: 'CYP',
czech_republic: 'CZE',
democratic_republic_of_the_congo: 'COD',
denmark: 'DNK',
djibouti: 'DJI',
dominica: 'DMA',
dominican_republic: 'DOM',
ecuador: 'ECU',
egypt: 'EGY',
el_salvador: 'SLV',
equatorial_guinea: 'GNQ',
eritrea: 'ERI',
estonia: 'EST',
ethiopia: 'ETH',
fiji: 'FJI',
finland: 'FIN',
france: 'FRA',
french_polynesia: 'PYF',
gabon: 'GAB',
gambia: 'GMB',
germany: 'DEU',
ghana: 'GHA',
greece: 'GRC',
greenland: 'GRL',
grenada: 'GRD',
guatemala: 'GTM',
guinea: 'GIN',
guyana: 'GUY',
haiti: 'HTI',
honduras: 'HND',
hungary: 'HUN',
iceland: 'ISL',
india: 'IND',
indonesia: 'IDN',
iran: 'IRN',
israel: 'ISR',
italy: 'ITA',
ivory_coast: 'CIV',
japan: 'JPN',
jordan: 'JOR',
kazakhstan: 'KAZ',
kenya: 'KEN',
korea: 'KOR',
kuwait: 'KWT',
kyrgyzstan: 'KGZ',
laos: 'LAO',
latvia: 'LVA',
lebanon: 'LBN',
lesotho: 'LSO',
liberia: 'LBR',
libya: 'LBY',
liechtenstein: 'LIE',
lithuania: 'LTU',
luxembourg: 'LUX',
macedonia: 'MKD',
madagascar: 'MDG',
malawi: 'MWI',
malaysia: 'MYS',
maldives: 'MDV',
mali: 'MLI',
malta: 'MLT',
marshall_islands: 'MHL',
mauritania: 'MRT',
mauritius: 'MUS',
mexico: 'MEX',
moldova: 'MDA',
mongolia: 'MNG',
montenegro: 'MNE',
montserrat: 'MSR',
morocco: 'MAR',
mozambique: 'MOZ',
myanmar: 'MMR',
namibia: 'NAM',
nauru: 'NRU',
nepal: 'NPL',
netherlands: 'NLD',
new_caledonia: 'NCL',
new_zealand: 'NZL',
nicaragua: 'NIC',
niger: 'NER',
nigeria: 'NGA',
northern_mariana_islands: 'MNP',
norway: 'NOR',
oman: 'OMN',
pakistan: 'PAK',
palau: 'PLW',
panama: 'PAN',
papua_new_guinea: 'PNG',
paraguay: 'PRY',
peru: 'PER',
philippines: 'PHL',
poland: 'POL',
portugal: 'PRT',
qatar: 'QAT',
republic_of_serbia: 'SRB',
romania: 'ROU',
russia: 'RUS',
rwanda: 'RWA',
saint_lucia: 'LCA',
saint_pierre_and_miquelon: 'SPM',
saint_vincent_and_the_grenadines: 'VCT',
samoa: 'WSM',
san_marino: 'SMR',
sao_tome_and_principe: 'STP',
saudi_arabia: 'SAU',
senegal: 'SEN',
seychelles: 'SYC',
sierra_leone: 'SLE',
singapore: 'SGP',
slovakia: 'SVK',
slovenia: 'SVN',
solomon_islands: 'SLB',
somalia: 'SOM',
south_africa: 'ZAF',
spain: 'ESP',
sri_lanka: 'LKA',
sudan: 'SDN',
suriname: 'SUR',
sweden: 'SWE',
switzerland: 'CHE',
syria: 'SYR',
taiwan: 'TWN',
tajikistan: 'TJK',
tanzania: 'TZA',
thailand: 'THA',
the_bahamas: 'BHS',
timorleste: 'TLS',
togo: 'TGO',
tonga: 'TON',
trinidad_and_tobago: 'TTO',
tunisia: 'TUN',
turkey: 'TUR',
turkmenistan: 'TKM',
turks_and_caicos_islands: 'TCA',
uganda: 'UGA',
uk: 'GBR',
ukraine: 'UKR',
united_arab_emirates: 'ARE',
united_states_minor_outlying_islands: 'UMI',
united_states_virgin_islands: 'VIR',
uruguay: 'URY',
usa: 'USA',
uzbekistan: 'UZB',
vanuatu: 'VUT',
venezuela: 'VEN',
vietnam: 'VNM',
wallis_and_futuna: 'WLF',
yemen: 'YEM',
zambia: 'ZMB',
zimbabwe: 'ZWE',
};
export default function migrateFromLegacy(
formData: PartialFormData,
): MigrationOutput {
const legacy = String(formData.select_country ?? '').toLowerCase();
if (!legacy) return {};
if (LEGACY_TO_COMPOSITE[legacy]) {
return {
admin_level: '1',
composite: LEGACY_TO_COMPOSITE[legacy],
worldview: 'ukr',
};
}
if (LEGACY_TO_AGGREGATED[legacy]) {
const { country, region_set } = LEGACY_TO_AGGREGATED[legacy];
return {
admin_level: 'aggregated',
country,
region_set,
worldview: 'ukr',
};
}
if (LEGACY_TO_ISO_A3[legacy]) {
return {
admin_level: '1',
country: LEGACY_TO_ISO_A3[legacy],
worldview: 'ukr',
};
}
// Unknown legacy code — leave the country control empty so the user
// can re-pick. Worldview defaults flow from the control's own default.
return {};
}

View File

@@ -0,0 +1,120 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import migrateFromLegacy from '../../src/plugin/migrateFromLegacy';
test('legacy "france" → Country view with FRA pre-selected', () => {
expect(migrateFromLegacy({ select_country: 'france' })).toEqual({
admin_level: '1',
country: 'FRA',
worldview: 'ukr',
});
});
test('legacy "usa" → Country view with USA pre-selected', () => {
expect(migrateFromLegacy({ select_country: 'usa' })).toEqual({
admin_level: '1',
country: 'USA',
worldview: 'ukr',
});
});
test('legacy "uk" maps to GBR (the ISO 3166 code)', () => {
expect(migrateFromLegacy({ select_country: 'uk' })).toEqual({
admin_level: '1',
country: 'GBR',
worldview: 'ukr',
});
});
test('legacy "france_overseas" maps to composite, not country', () => {
expect(migrateFromLegacy({ select_country: 'france_overseas' })).toEqual({
admin_level: '1',
composite: 'france_overseas',
worldview: 'ukr',
});
});
test('legacy "france_regions" maps to aggregated regions for France', () => {
expect(migrateFromLegacy({ select_country: 'france_regions' })).toEqual({
admin_level: 'aggregated',
country: 'FRA',
region_set: 'regions',
worldview: 'ukr',
});
});
test('legacy "turkey_regions" maps to TUR / nuts_1', () => {
expect(migrateFromLegacy({ select_country: 'turkey_regions' })).toEqual({
admin_level: 'aggregated',
country: 'TUR',
region_set: 'nuts_1',
worldview: 'ukr',
});
});
test('legacy "italy_regions" maps to ITA / regions', () => {
expect(migrateFromLegacy({ select_country: 'italy_regions' })).toEqual({
admin_level: 'aggregated',
country: 'ITA',
region_set: 'regions',
worldview: 'ukr',
});
});
test('legacy "philippines_regions" maps to PHL / regions', () => {
expect(migrateFromLegacy({ select_country: 'philippines_regions' })).toEqual({
admin_level: 'aggregated',
country: 'PHL',
region_set: 'regions',
worldview: 'ukr',
});
});
test('uppercase / mixed case legacy values still match', () => {
expect(migrateFromLegacy({ select_country: 'France' })).toEqual({
admin_level: '1',
country: 'FRA',
worldview: 'ukr',
});
});
test('unknown legacy code → empty migration (user re-picks)', () => {
expect(migrateFromLegacy({ select_country: 'atlantis' })).toEqual({});
});
test('missing select_country → empty migration', () => {
expect(migrateFromLegacy({})).toEqual({});
});
test('every legacy "_regions" key resolves to an existing region_set', () => {
// Smoke-check that the four legacy aggregated keys all map to
// (country, region_set) pairs the build pipeline actually emits.
const cases = [
'france_regions',
'italy_regions',
'philippines_regions',
'turkey_regions',
];
cases.forEach(name => {
const m = migrateFromLegacy({ select_country: name });
expect(m.admin_level).toBe('aggregated');
expect(typeof m.country).toBe('string');
expect(typeof m.region_set).toBe('string');
});
});