diff --git a/superset-frontend/plugins/plugin-chart-country-map/src/plugin/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-country-map/src/plugin/controlPanel.tsx index 9105f3d9101..b33dc5d713d 100644 --- a/superset-frontend/plugins/plugin-chart-country-map/src/plugin/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-country-map/src/plugin/controlPanel.tsx @@ -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; diff --git a/superset-frontend/plugins/plugin-chart-country-map/src/plugin/migrateFromLegacy.ts b/superset-frontend/plugins/plugin-chart-country-map/src/plugin/migrateFromLegacy.ts new file mode 100644 index 00000000000..4088204c0ae --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-country-map/src/plugin/migrateFromLegacy.ts @@ -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 = { + 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 = { + 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 {}; +} diff --git a/superset-frontend/plugins/plugin-chart-country-map/test/plugin/migrateFromLegacy.test.ts b/superset-frontend/plugins/plugin-chart-country-map/test/plugin/migrateFromLegacy.test.ts new file mode 100644 index 00000000000..81c28ba9662 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-country-map/test/plugin/migrateFromLegacy.test.ts @@ -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'); + }); +});