You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/10/11 16:55:03 UTC
(superset) branch master updated: feat(mcp): add typed geographic chart generation (#44148)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new db7557a7a58 feat(mcp): add typed geographic chart generation (#44148)
db7557a7a58 is described below
commit db7557a7a5818859d4a82da1475db4f38fe9d760
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Mon Oct 12 02:54:52 2026 +1000
feat(mcp): add typed geographic chart generation (#44148)
Co-authored-by: Claude Opus 5 <no...@anthropic.com>
---
UPDATING.md | 14 +
docs/admin_docs/configuration/mcp-server.mdx | 137 ++
.../src/shared-controls/sharedControls.tsx | 21 +
.../plugin-chart-country-map/src/CountryMap.ts | 30 +-
.../plugin-chart-country-map/src/controlPanel.ts | 23 +
.../src/countries/uk.geojson | Bin 477503 -> 477503 bytes
.../src/normalizeRegions.ts | 129 ++
.../plugin-chart-country-map/src/regions.ts | 383 ++++
.../plugin-chart-country-map/src/transformProps.ts | 53 +-
.../test/CountryMap.test.tsx | 109 +
.../test/controlPanel.test.ts | 43 +
.../test/geographicRendering.test.tsx | 296 +++
.../test/normalizeRegions.test.ts | 251 +++
.../test/transformProps.test.ts | 125 ++
.../plugins/plugin-chart-world-map/src/WorldMap.ts | 13 +-
.../plugin-chart-world-map/src/controlPanel.ts | 2 +
.../plugin-chart-world-map/src/countries.ts | 36 +-
.../plugin-chart-world-map/src/transformData.ts | 49 +-
.../plugin-chart-world-map/src/transformProps.ts | 20 +-
.../plugin-chart-world-map/src/worldGeometry.ts | 198 ++
.../plugin-chart-world-map/test/WorldMap.test.ts | 78 +
.../test/controlPanel.test.ts | 43 +
.../test/transformData.test.ts | 169 ++
.../test/transformProps.test.ts | 52 +
.../src/layers/Scatter/buildQuery.test.ts | 24 +
.../src/layers/Scatter/buildQuery.ts | 7 +-
.../src/layers/Scatter/controlPanel.test.ts | 43 +
.../src/layers/Scatter/controlPanel.ts | 2 +
.../src/layers/Scatter/transformProps.test.ts | 324 ++-
.../src/layers/Scatter/transformProps.ts | 102 +-
.../preset-chart-deckgl/src/layers/spatialUtils.ts | 7 +-
.../src/layers/utils/metricUtils.ts | 17 +
.../spec/helpers/geographicControlPanel.ts | 75 +
superset/mcp_service/chart/chart_helpers.py | 43 +-
superset/mcp_service/chart/chart_utils.py | 164 +-
superset/mcp_service/chart/compile.py | 8 +-
superset/mcp_service/chart/plugin.py | 15 +-
superset/mcp_service/chart/plugins/__init__.py | 12 +
superset/mcp_service/chart/plugins/geographic.py | 834 ++++++++
superset/mcp_service/chart/plugins/histogram.py | 1 +
superset/mcp_service/chart/preview_utils.py | 19 +
.../mcp_service/chart/resources/chart_configs.py | 20 +
superset/mcp_service/chart/schemas.py | 188 +-
superset/mcp_service/chart/tool/generate_chart.py | 2 +-
superset/mcp_service/chart/tool/get_chart_data.py | 31 +-
.../chart/tool/get_chart_type_schema.py | 38 +-
superset/mcp_service/chart/tool/update_chart.py | 65 +-
.../mcp_service/chart/tool/update_chart_preview.py | 25 +-
.../explore/tool/generate_explore_link.py | 7 +-
superset/translations/ar/LC_MESSAGES/messages.po | 65 +
superset/translations/ca/LC_MESSAGES/messages.po | 65 +
superset/translations/cs/LC_MESSAGES/messages.po | 65 +
superset/translations/de/LC_MESSAGES/messages.po | 65 +
superset/translations/en/LC_MESSAGES/messages.po | 65 +
superset/translations/es/LC_MESSAGES/messages.po | 65 +
superset/translations/fa/LC_MESSAGES/messages.po | 65 +
superset/translations/fi/LC_MESSAGES/messages.po | 65 +
superset/translations/fr/LC_MESSAGES/messages.po | 65 +
superset/translations/it/LC_MESSAGES/messages.po | 65 +
superset/translations/ja/LC_MESSAGES/messages.po | 65 +
superset/translations/ko/LC_MESSAGES/messages.po | 65 +
superset/translations/lv/LC_MESSAGES/messages.po | 65 +
superset/translations/messages.pot | 64 +
superset/translations/mi/LC_MESSAGES/messages.po | 65 +
superset/translations/nl/LC_MESSAGES/messages.po | 65 +
superset/translations/pl/LC_MESSAGES/messages.po | 65 +
superset/translations/pt/LC_MESSAGES/messages.po | 65 +
.../translations/pt_BR/LC_MESSAGES/messages.po | 65 +
superset/translations/ro/LC_MESSAGES/messages.po | 65 +
superset/translations/ru/LC_MESSAGES/messages.po | 65 +
superset/translations/sk/LC_MESSAGES/messages.po | 65 +
superset/translations/sl/LC_MESSAGES/messages.po | 65 +
superset/translations/sr/LC_MESSAGES/messages.po | 65 +
.../translations/sr_Latn/LC_MESSAGES/messages.po | 65 +
superset/translations/ta/LC_MESSAGES/messages.po | 65 +
superset/translations/th/LC_MESSAGES/messages.po | 65 +
superset/translations/tr/LC_MESSAGES/messages.po | 65 +
superset/translations/uk/LC_MESSAGES/messages.po | 65 +
superset/translations/zh/LC_MESSAGES/messages.po | 65 +
.../translations/zh_TW/LC_MESSAGES/messages.po | 65 +
superset/utils/geographic.py | 105 +
superset/utils/geographic_regions.py | 385 ++++
superset/utils/geographic_world.py | 205 ++
.../chart/test_chart_plugin_contract.py | 97 +-
tests/unit_tests/mcp_service/chart/test_compile.py | 26 +
.../mcp_service/chart/test_geographic_chart.py | 2199 ++++++++++++++++++++
.../chart/test_histogram_boxplot_charts.py | 8 +-
.../mcp_service/chart/test_preview_utils.py | 47 +-
.../mcp_service/chart/test_sunburst_chart.py | 32 +
.../mcp_service/chart/test_treemap_completeness.py | 32 +-
.../mcp_service/chart/tool/test_get_chart_data.py | 35 +
.../chart/tool/test_get_chart_preview.py | 55 +
.../chart/tool/test_update_chart_preview.py | 73 +-
tests/unit_tests/utils/geographic_world_test.py | 37 +
94 files changed, 9571 insertions(+), 126 deletions(-)
diff --git a/UPDATING.md b/UPDATING.md
index 3ae2e280ac6..febb5024aea 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -267,6 +267,20 @@ format. Use a consistent timezone and precision for stored strings and bounds.
able to provision a user regardless of `AUTH_USER_REGISTRATION`; it now
requires the same gate as `/register/`.
+### MCP chart tools advertise a compact chart config schema
+
+`generate_chart`, `update_chart`, `update_chart_preview` and
+`generate_explore_link` no longer inline every chart type's JSON Schema in
+their tool input schemas. Their `config` parameter is advertised as an object
+whose `chart_type` is one of the supported chart types. The fields and
+examples for each chart type come from `get_chart_type_schema(chart_type)`,
+which returns the same per-type schema as before.
+
+Server-side validation is unchanged: requests are still validated against the
+complete chart configuration model and invalid fields are rejected with the
+same errors. MCP clients that built chart configs only from the tool input
+schema should call `get_chart_type_schema` first.
+
### SQL execution request limits are safety caps
The MCP `execute_sql` request's `limit` no longer overrides a smaller outer SQL
diff --git a/docs/admin_docs/configuration/mcp-server.mdx b/docs/admin_docs/configuration/mcp-server.mdx
index 3ab2485cb2f..4204187d187 100644
--- a/docs/admin_docs/configuration/mcp-server.mdx
+++ b/docs/admin_docs/configuration/mcp-server.mdx
@@ -1296,6 +1296,10 @@ Every tool with a side effect (create/update/delete/execute) is always excluded
## Chart previews
+Saved and unsaved Vega-Lite previews return `NoDataError` for empty query
+results unless the chart plugin declares `allows_empty_result` (for example,
+Gantt renders an empty task list).
+
For basic Vega-Lite previews, the y-axis uses the first configured metric whose
result label is present and whose first value is numeric or null. Boolean and
string metric values are skipped. If metric labels match but none has a suitable
@@ -1777,6 +1781,139 @@ For saved and unsaved charts, `get_chart_data` column metadata excludes missing
values from `unique_count`. Values serialized as JSON `null`, including NaN,
NaT, and non-finite numbers, contribute to `null_count` instead.
+## Geographic chart generation
+
+`generate_chart`, `update_chart`, and `generate_explore_link` accept the same
+bounded geographic configs. Call `get_chart_type_schema` with `country_map`,
+`world_map`, or `deck_scatter` to discover availability and complete examples.
+The `chart_type` discriminator belongs **inside** `config`. Existing chart
+validation contracts and legacy native map behavior are unchanged, but tool
+**discovery schemas have changed for every chart type**, including existing
+types such as gauge and treemap. The `config` input schema on `generate_chart`,
+`update_chart`, `update_chart_preview`, and `generate_explore_link` advertises
+only the `chart_type` enum and allows additional properties; it does not expose
+each type's required fields or bounds. Schema-driven clients must first call
+`get_chart_type_schema(chart_type)` for the complete per-type schema and
+examples. See the compact chart config schema migration note in `UPDATING.md`.
+
+```json
+{
+ "dataset_id": 3,
+ "config": {
+ "chart_type": "country_map",
+ "country": "usa",
+ "region_format": "abbreviation",
+ "entity": {"name": "state"},
+ "metric": {"name": "sales", "aggregate": "SUM"},
+ "filters": [{"column": "country", "op": "IN", "value": ["USA"]}]
+ }
+}
+```
+
+- **Country map:** supported boundary sets are `usa`, `canada`, `australia`,
+ `japan`, and `uk`. Select the source `region_format`: `name` (e.g.
+ `California`), `abbreviation` (ISO suffix, e.g. `CA`), or `iso_3166_2`
+ (e.g. `US-CA`). Identifiers are limited to Superset's bundled boundaries,
+ not the entire ISO registry. Japanese abbreviation values are numeric
+ **strings**, such as `13`. Names are exact-first, then case/diacritic-insensitive
+ only when the result is unique. No fuzzy matching or geocoding is performed.
+ A name that folds onto more than one boundary is rejected as ambiguous and
+ needs an abbreviation or ISO code instead. After an Explore switch to
+ **Legacy ISO values**, saved MCP reads and exports validate full ISO 3166-2
+ codes (e.g. `US-CA`); this option does not accept abbreviation-only values.
+- **World map:** set `country_format` to `name`, `cca2`, `cca3`, or `cioc`,
+ plus `entity` and `metric`. Choropleth color uses `metric`. For bubbles set
+ `show_bubbles: true` and supply `secondary_metric` for size (it may be the
+ same metric). Same-type saved and cached-preview updates can enable bubbles
+ while omitting `secondary_metric` when the existing chart already has one;
+ the size-metric requirement is checked against the merged configuration.
+ A `secondary_metric` kept with `show_bubbles: false` is not
+ rendered, so its values are not validated. Superset's bundled
+ world topology defines choropleth coverage, not the broader country
+ dictionary. Countries without a polygon (such as Singapore) require bubbles
+ or an explicit dataset filter; otherwise MCP validation returns an error.
+- **Geographic points:** set `chart_type: "deck_scatter"`,
+ `latitude: {"name": "latitude"}`, and `longitude: {"name": "longitude"}`.
+ Numeric coordinates must be within ±90 and ±180 respectively. Optional
+ `dimension` groups/colors points; `radius_metric` aggregates a nonnegative
+ size at each location. Otherwise `radius` is fixed in `point_unit`: `radius_m` (default), `radius_km`,
+ `radius_miles`, `square_m`, `square_km`, or `square_miles`. Native minimum
+ and maximum on-screen radii are 2 and 250 pixels. Spatial null filters
+ exclude rows without coordinates. Dimension columns and radius
+ metric aliases cannot be `position`,
+ `weight`, or `extraProps`, which are reserved native spatial feature fields.
+ A dimension must also differ from both coordinate columns. Updates validate
+ the merged roles, including saved dimensions and radius metrics omitted
+ from the request. Explicitly clear or replace a dimension when changing a
+ coordinate to that column, or a saved radius metric with a reserved label.
+
+Explore saves preserve generated dashboard-time binding provenance. Explicit
+`temporal_column: null` clears that binding, and a replacement column removes
+the old generated predicate without removing user-authored time filters.
+
+Region values that cannot resolve for the selected country/format return an
+`InvalidGeographicResult` error (`INVALID_GEOGRAPHIC_RESULT` during compilation).
+For example, `BC` does not resolve for USA; choose Canada or explicitly filter
+the dataset. No unrecognized returned rows are silently discarded. Multiple
+query groups resolving to one boundary also fail: normalize source values
+before aggregation rather than incorrectly adding averages or distinct counts.
+When rendering in Explore, country maps with an explicit region format omit
+rows with missing, NULL, non-numeric, or non-finite metrics, leaving those
+regions blank. Region identifiers are still validated for every row, and blank
+regions still cross-filter and drill on their original source value.
+Typed geographic point maps omit rows with invalid coordinates or missing,
+non-numeric, non-finite, or negative radius metrics rather than failing the
+chart, and log a browser-console warning with the number of skipped points.
+After switching a point map to Geohash or delimited coordinates in Explore,
+coordinates use the native spatial decoder rather than the typed latitude/longitude
+guard; radius-metric validation remains active. This display behavior does not
+relax MCP generation's result validation. Preserved numeric-string radii are
+interpreted as fixed values only for typed MCP point maps (`mcp_geographic`).
+Native Scatter charts continue to interpret bare strings as saved metric names,
+including numeric names such as `100`.
+Country and world maps edited in Explore can use Custom SQL entities; MCP
+reads and exports resolve the saved entity's output label (or SQL expression).
+Source values remain intact for data/export and country-map cross-filtering.
+Database `NUMERIC`/Decimal query coordinates and metrics are supported when
+finite and representable as JSON numbers. Coordinate bounds are checked before
+JSON conversion; numeric strings and booleans are not accepted as coordinates.
+
+Generation validates the configured result (up to `row_limit`, bounded at
+10,000), not just the first two rows. This is not an audit of rows excluded by
+filters or limits. Saved and cached-preview updates preserve omitted controls;
+explicit null optional roles and empty filters clear them. Native saved maps
+with null `adhoc_filters` are treated as having no filters. Preserved numeric-string
+point radii (for example, `"100"`) render as fixed sizes, not metric names. Dataset rebinding
+requires a complete typed config and discards old dataset roles and filters.
+
+Geographic updates cap inherited native row limits at 10,000 so compilation
+validates the same rows the native query renders. Explicit
+`temporal_column: null` removes generated dashboard time bindings, including
+dataset defaults, while preserving user-authored predicates. Subsequent same-dataset
+updates that omit `temporal_column` preserve that cleared state. Explicit `filters`
+replace inherited predicates, including native `filters`, `where`, and `having`,
+whether the replacement list is empty or nonempty. Disabled types
+cannot be used for fresh previews or type conversions; an existing cached
+preview or saved chart of the same type can still be updated, including charts
+whose visualization type is stored only on the saved chart. Explicit temporal
+changes override inherited native time controls on same-type updates and type
+conversions. Saved point charts switched in Explore to Geohash or comma-delimited
+coordinates remain readable through MCP data and JSON/CSV/Excel exports; exports
+retain the original spatial column values. Delimited coordinates accept the
+same leading numeric prefixes as the native decoder (for example,
+`-122.4 W,37.8 N`). Values the native decoder skips still fail MCP result
+validation, consistent with invalid latitude/longitude and geohash values.
+
+Country/world map interactions use original source identifiers rather than
+normalized rendering codes or names. With an explicit region format, boundaries
+absent from the result do not emit cross-filters or drill filters. Empty country
+identifiers are rejected rather than matched to a country with no code.
+
+ASCII/table previews show source data and do not reproduce geography. Vega
+previews return `UnsupportedGeographicPreview` rather than an unrelated chart.
+Use the returned Explore link for the native map. Point maps use a public
+Carto basemap; browser network access and deployment basemap policy still apply.
+
## Big Number headlines
For Big Number charts (`big_number` and `big_number_total`), `get_chart_data`
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
index 9660dfc059e..537734fe3f0 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
@@ -419,6 +419,25 @@ const temporal_columns_lookup: SharedControlConfig<'HiddenControl'> = {
),
};
+const mcp_geographic: SharedControlConfig<'HiddenControl'> = {
+ type: 'HiddenControl',
+ hidden: true,
+ renderTrigger: false,
+ description: t(
+ 'Marks charts created through the MCP geographic contract so Explore keeps their validation guards.',
+ ),
+};
+
+const _mcp_dashboard_time_filter_subject: SharedControlConfig<'HiddenControl'> =
+ {
+ type: 'HiddenControl',
+ hidden: true,
+ renderTrigger: false,
+ description: t(
+ 'Preserves the generated dashboard-time predicate subject through Explore saves.',
+ ),
+ };
+
const zoomable: SharedControlConfig<'CheckboxControl'> = {
type: 'CheckboxControl',
label: t('Data Zoom'),
@@ -510,6 +529,8 @@ const controlConfigs = {
zoomable,
show_empty_columns,
temporal_columns_lookup,
+ mcp_geographic,
+ _mcp_dashboard_time_filter_subject,
currency_format,
sort_by_metric,
order_by_cols,
diff --git a/superset-frontend/plugins/plugin-chart-country-map/src/CountryMap.ts b/superset-frontend/plugins/plugin-chart-country-map/src/CountryMap.ts
index 7a8768d70aa..3b7618f8861 100644
--- a/superset-frontend/plugins/plugin-chart-country-map/src/CountryMap.ts
+++ b/superset-frontend/plugins/plugin-chart-country-map/src/CountryMap.ts
@@ -42,6 +42,7 @@ function escapeHtml(text: string): string {
interface CountryMapDataItem {
country_id: string;
+ source_value?: string;
metric: number;
}
@@ -60,6 +61,8 @@ interface GeoData {
interface CountryMapProps {
data: CountryMapDataItem[];
+ /** Original source value for each normalized region ISO code. */
+ sourceValues?: Record<string, string>;
width: number;
height: number;
country: string;
@@ -95,6 +98,7 @@ const zoomStates = new WeakMap<
function CountryMap(element: HTMLElement, props: CountryMapProps) {
const {
data,
+ sourceValues,
width,
height,
country,
@@ -178,6 +182,16 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
// Track mouse position to distinguish clicks from drags
let mousedownPos: { x: number; y: number } | null = null;
+ // Without a source map the ISO code is the raw entity value (legacy path).
+ // With one, a code absent from it has no source row, so interactions on it
+ // are suppressed by returning undefined.
+ const sourceValue = (code: string) => {
+ if (!sourceValues) return code;
+ return Object.prototype.hasOwnProperty.call(sourceValues, code)
+ ? sourceValues[code]
+ : undefined;
+ };
+
// Cross-filter support
const getCrossFilterDataMask = (
source: GeoFeature,
@@ -188,18 +202,27 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
const iso = source?.properties?.ISO;
if (!iso) return undefined;
+ // An already-selected boundary must stay clearable even when a later
+ // dashboard filter removed its source row.
const isSelected = selected.includes(iso);
+ if (!isSelected && sourceValue(iso) === undefined) return undefined;
const values = isSelected ? [] : [iso];
return {
dataMask: {
extraFormData: {
filters: values.length
- ? [{ col: entity, op: 'IN', val: values }]
+ ? [
+ {
+ col: entity,
+ op: 'IN',
+ val: values.map(sourceValue),
+ },
+ ]
: [],
},
filterState: {
- value: values.length ? values : null,
+ value: values.length ? values.map(sourceValue) : null,
selectedValues: values.length ? values : null,
},
},
@@ -218,7 +241,8 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
const iso = feature?.properties?.ISO;
if (!iso || typeof onContextMenu !== 'function' || !entity) return;
- const drillVal = iso;
+ const drillVal = sourceValue(iso);
+ if (drillVal === undefined) return;
const drillToDetailFilters = [
{ col: entity, op: '==', val: drillVal, formattedVal: drillVal },
];
diff --git a/superset-frontend/plugins/plugin-chart-country-map/src/controlPanel.ts b/superset-frontend/plugins/plugin-chart-country-map/src/controlPanel.ts
index fb5e2d9eea8..3cf5a190bc7 100644
--- a/superset-frontend/plugins/plugin-chart-country-map/src/controlPanel.ts
+++ b/superset-frontend/plugins/plugin-chart-country-map/src/controlPanel.ts
@@ -48,8 +48,31 @@ const config: ControlPanelConfig = {
},
],
['entity'],
+ [
+ {
+ name: 'region_format',
+ config: {
+ type: 'SelectControl',
+ label: t('Region value format'),
+ default: null,
+ choices: [
+ [null, t('Legacy ISO values')],
+ ['name', t('Region name')],
+ ['abbreviation', t('ISO suffix / abbreviation')],
+ ['iso_3166_2', t('ISO 3166-2')],
+ ],
+ description: t(
+ 'Explicit formats validate all values against bundled boundaries. Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or ambiguous values fail rather than displaying blank regions.',
+ ),
+ renderTrigger: true,
+ },
+ },
+ ],
['metric'],
['adhoc_filters'],
+ ['row_limit'],
+ ['mcp_geographic'],
+ ['_mcp_dashboard_time_filter_subject'],
],
},
{
diff --git a/superset-frontend/plugins/plugin-chart-country-map/src/countries/uk.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/uk.geojson
index f81e5d067d2..afd4a6552db 100644
Binary files a/superset-frontend/plugins/plugin-chart-country-map/src/countries/uk.geojson and b/superset-frontend/plugins/plugin-chart-country-map/src/countries/uk.geojson differ
diff --git a/superset-frontend/plugins/plugin-chart-country-map/src/normalizeRegions.ts b/superset-frontend/plugins/plugin-chart-country-map/src/normalizeRegions.ts
new file mode 100644
index 00000000000..4bdcd5f7402
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-country-map/src/normalizeRegions.ts
@@ -0,0 +1,129 @@
+/**
+ * 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 { t } from '@apache-superset/core/translation';
+import regions from './regions';
+
+export type RegionFormat = 'name' | 'abbreviation' | 'iso_3166_2';
+
+interface Boundary {
+ properties: { ISO?: string; NAME_1?: string; NAME_2?: string };
+}
+
+/** Index immutable aliases once per transform, preserving ambiguity checks. */
+function createRegionResolver(
+ boundaries: Boundary[],
+ format: RegionFormat,
+): (value: unknown) => string {
+ const fold = (text: string) => {
+ const lower = text.toLowerCase();
+ return format === 'name'
+ ? lower.normalize('NFD').replace(/[\u0300-\u036F]/g, '')
+ : lower;
+ };
+ const entries = boundaries.flatMap(({ properties: p }) => {
+ if (!p.ISO) return [];
+ const alias =
+ format === 'name'
+ ? p.NAME_2 || p.NAME_1
+ : format === 'abbreviation'
+ ? p.ISO.split('-').slice(1).join('-')
+ : p.ISO;
+ return alias ? [{ alias, code: p.ISO }] : [];
+ });
+ const exactIndex = new Map<string, Set<string>>();
+ const foldedIndex = new Map<string, Set<string>>();
+ entries.forEach(({ alias, code }) => {
+ for (const [index, key] of [
+ [exactIndex, alias],
+ [foldedIndex, fold(alias)],
+ ] as const) {
+ const codes = index.get(key) ?? new Set<string>();
+ codes.add(code);
+ index.set(key, codes);
+ }
+ });
+ return value => {
+ if (typeof value !== 'string' || !value || value.length > 500) {
+ throw new Error(
+ t(
+ 'Geographic values must be nonempty strings of at most 500 characters',
+ ),
+ );
+ }
+ const matches = exactIndex.get(value) ?? foldedIndex.get(fold(value));
+ if (matches?.size !== 1) {
+ throw new Error(
+ matches?.size
+ ? t(
+ 'Ambiguous region %s; choose the matching country/region format or explicitly filter the dataset.',
+ JSON.stringify(value.slice(0, 100)),
+ )
+ : t(
+ 'Unrecognized region %s; choose the matching country/region format or explicitly filter the dataset.',
+ JSON.stringify(value.slice(0, 100)),
+ ),
+ );
+ }
+ return [...matches][0];
+ };
+}
+
+/** Exact matches win; folded matches must resolve to one rendered boundary. */
+export function resolveRegion(
+ value: unknown,
+ boundaries: Boundary[],
+ format: RegionFormat,
+): string {
+ return createRegionResolver(boundaries, format)(value);
+}
+
+/** Normalize display-only values, leaving raw query/export records intact. */
+export default function normalizeRegions(
+ records: Record<string, unknown>[],
+ entity: string,
+ country: string,
+ format: string,
+): Record<string, unknown>[] {
+ if (
+ !['usa', 'canada', 'australia', 'japan', 'uk'].includes(country) ||
+ !['name', 'abbreviation', 'iso_3166_2'].includes(format)
+ ) {
+ throw new Error(
+ t('Unsupported country or region_format for typed geographic chart'),
+ );
+ }
+ const boundaries = regions[country].map(([ISO, NAME_1]) => ({
+ properties: { ISO, NAME_1 },
+ }));
+ const resolve = createRegionResolver(boundaries, format as RegionFormat);
+ const seen = new Set<string>();
+ return records.map(record => {
+ const iso = resolve(record[entity]);
+ if (seen.has(iso)) {
+ throw new Error(
+ t(
+ 'Multiple result rows resolve to %s; normalize source values before aggregation.',
+ iso,
+ ),
+ );
+ }
+ seen.add(iso);
+ return { ...record, [entity]: iso };
+ });
+}
diff --git a/superset-frontend/plugins/plugin-chart-country-map/src/regions.ts b/superset-frontend/plugins/plugin-chart-country-map/src/regions.ts
new file mode 100644
index 00000000000..0adbef4a595
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-country-map/src/regions.ts
@@ -0,0 +1,383 @@
+/**
+ * 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.
+ */
+/** Boundary identifiers generated from the bundled country-map GeoJSON. */
+const regions: Record<string, [string, string][]> = {
+ australia: [
+ ['AU-ACT', 'Australian Capital Territory'],
+ ['AU-NSW', 'New South Wales'],
+ ['AU-NT', 'Northern Territory'],
+ ['AU-QLD', 'Queensland'],
+ ['AU-SA', 'South Australia'],
+ ['AU-TAS', 'Tasmania'],
+ ['AU-VIC', 'Victoria'],
+ ['AU-WA', 'Western Australia'],
+ ['AU-X02~', 'Jervis Bay Territory'],
+ ],
+ canada: [
+ ['CA-AB', 'Alberta'],
+ ['CA-BC', 'British Columbia'],
+ ['CA-MB', 'Manitoba'],
+ ['CA-NB', 'New Brunswick'],
+ ['CA-NL', 'Newfoundland and Labrador'],
+ ['CA-NS', 'Nova Scotia'],
+ ['CA-NT', 'Northwest Territories'],
+ ['CA-NU', 'Nunavut'],
+ ['CA-ON', 'Ontario'],
+ ['CA-PE', 'Prince Edward Island'],
+ ['CA-QC', 'Québec'],
+ ['CA-SK', 'Saskatchewan'],
+ ['CA-YT', 'Yukon'],
+ ],
+ japan: [
+ ['JP-01', 'Hokkaidō'],
+ ['JP-02', 'Aomori'],
+ ['JP-03', 'Iwate'],
+ ['JP-04', 'Miyagi'],
+ ['JP-05', 'Akita'],
+ ['JP-06', 'Yamagata'],
+ ['JP-07', 'Fukushima'],
+ ['JP-08', 'Ibaraki'],
+ ['JP-09', 'Tochigi'],
+ ['JP-10', 'Gunma'],
+ ['JP-11', 'Saitama'],
+ ['JP-12', 'Chiba'],
+ ['JP-13', 'Tokyo'],
+ ['JP-14', 'Kanagawa'],
+ ['JP-15', 'Niigata'],
+ ['JP-16', 'Toyama'],
+ ['JP-17', 'Ishikawa'],
+ ['JP-18', 'Fukui'],
+ ['JP-19', 'Yamanashi'],
+ ['JP-20', 'Nagano'],
+ ['JP-21', 'Gifu'],
+ ['JP-22', 'Shizuoka'],
+ ['JP-23', 'Aichi'],
+ ['JP-24', 'Mie'],
+ ['JP-25', 'Shiga'],
+ ['JP-26', 'Kyōto'],
+ ['JP-27', 'Ōsaka'],
+ ['JP-28', 'Hyōgo'],
+ ['JP-29', 'Nara'],
+ ['JP-30', 'Wakayama'],
+ ['JP-31', 'Tottori'],
+ ['JP-32', 'Shimane'],
+ ['JP-33', 'Okayama'],
+ ['JP-34', 'Hiroshima'],
+ ['JP-35', 'Yamaguchi'],
+ ['JP-36', 'Tokushima'],
+ ['JP-37', 'Kagawa'],
+ ['JP-38', 'Ehime'],
+ ['JP-39', 'Kōchi'],
+ ['JP-40', 'Fukuoka'],
+ ['JP-41', 'Saga'],
+ ['JP-42', 'Nagasaki'],
+ ['JP-43', 'Kumamoto'],
+ ['JP-44', 'Ōita'],
+ ['JP-45', 'Miyazaki'],
+ ['JP-46', 'Kagoshima'],
+ ['JP-47', 'Okinawa'],
+ ],
+ uk: [
+ ['GB-ABD', 'Aberdeenshire'],
+ ['GB-ABE', 'Aberdeen'],
+ ['GB-AGB', 'Argyll and Bute'],
+ ['GB-AGY', 'Anglesey'],
+ ['GB-ANS', 'Angus'],
+ ['GB-ANT', 'Antrim'],
+ ['GB-ARD', 'Ards'],
+ ['GB-ARM', 'Armagh'],
+ ['GB-BAS', 'Bath and North East Somerset'],
+ ['GB-BBD', 'Blackburn with Darwen'],
+ ['GB-BDF', 'Bedford'],
+ ['GB-BDG', 'Barking and Dagenham'],
+ ['GB-BEN', 'Brent'],
+ ['GB-BEX', 'Bexley'],
+ ['GB-BFS', 'Belfast'],
+ ['GB-BGE', 'Bridgend'],
+ ['GB-BGW', 'Blaenau Gwent'],
+ ['GB-BIR', 'Birmingham'],
+ ['GB-BKM', 'Buckinghamshire'],
+ ['GB-BLA', 'Ballymena'],
+ ['GB-BLY', 'Ballymoney'],
+ ['GB-BMH', 'Bournemouth'],
+ ['GB-BNB', 'Banbridge'],
+ ['GB-BNE', 'Barnet'],
+ ['GB-BNH', 'Brighton and Hove'],
+ ['GB-BNS', 'Barnsley'],
+ ['GB-BOL', 'Bolton'],
+ ['GB-BPL', 'Blackpool'],
+ ['GB-BRC', 'Bracknell Forest'],
+ ['GB-BRD', 'Bradford'],
+ ['GB-BRY', 'Bromley'],
+ ['GB-BST', 'Bristol'],
+ ['GB-BUR', 'Bury'],
+ ['GB-CAM', 'Cambridgeshire'],
+ ['GB-CAY', 'Caerphilly'],
+ ['GB-CBF', 'Central Bedfordshire'],
+ ['GB-CGN', 'Ceredigion'],
+ ['GB-CGV', 'Craigavon'],
+ ['GB-CHE', 'Cheshire East'],
+ ['GB-CHW', 'Cheshire West and Chester'],
+ ['GB-CKF', 'Carrickfergus'],
+ ['GB-CKT', 'Mid Ulster'],
+ ['GB-CLD', 'Calderdale'],
+ ['GB-CLK', 'Clackmannanshire'],
+ ['GB-CLR', 'Coleraine'],
+ ['GB-CMA', 'Cumbria'],
+ ['GB-CMD', 'Camden'],
+ ['GB-CMN', 'Carmarthenshire'],
+ ['GB-CON', 'Cornwall'],
+ ['GB-COV', 'Coventry'],
+ ['GB-CRF', 'Cardiff'],
+ ['GB-CRY', 'Croydon'],
+ ['GB-CSR', 'Castlereagh'],
+ ['GB-CWY', 'Conwy'],
+ ['GB-DAL', 'Darlington'],
+ ['GB-DBY', 'Derbyshire'],
+ ['GB-DEN', 'Denbighshire'],
+ ['GB-DER', 'Derby'],
+ ['GB-DEV', 'Devon'],
+ ['GB-DGN', 'Dungannon'],
+ ['GB-DGY', 'Dumfries and Galloway'],
+ ['GB-DNC', 'Doncaster'],
+ ['GB-DND', 'Dundee'],
+ ['GB-DOR', 'Dorset'],
+ ['GB-DOW', 'Down'],
+ ['GB-DRY', 'Derry'],
+ ['GB-DUD', 'Dudley'],
+ ['GB-DUR', 'Durham'],
+ ['GB-EAL', 'Ealing'],
+ ['GB-EAY', 'East Ayrshire'],
+ ['GB-EDH', 'Edinburgh'],
+ ['GB-EDU', 'East Dunbartonshire'],
+ ['GB-ELN', 'East Lothian'],
+ ['GB-ENF', 'Enfield'],
+ ['GB-ERW', 'East Renfrewshire'],
+ ['GB-ERY', 'East Riding of Yorkshire'],
+ ['GB-ESS', 'Essex'],
+ ['GB-ESX', 'East Sussex'],
+ ['GB-FAL', 'Falkirk'],
+ ['GB-FER', 'Fermanagh'],
+ ['GB-FIF', 'Fife'],
+ ['GB-FLN', 'Flintshire'],
+ ['GB-GAT', 'Gateshead'],
+ ['GB-GLG', 'Glasgow'],
+ ['GB-GLS', 'Gloucestershire'],
+ ['GB-GRE', 'Greenwich'],
+ ['GB-GWN', 'Gwynedd'],
+ ['GB-HAL', 'Halton'],
+ ['GB-HAM', 'Hampshire'],
+ ['GB-HAV', 'Havering'],
+ ['GB-HCK', 'Hackney'],
+ ['GB-HEF', 'Herefordshire'],
+ ['GB-HIL', 'Hillingdon'],
+ ['GB-HLD', 'Highland'],
+ ['GB-HMF', 'Hammersmith and Fulham'],
+ ['GB-HNS', 'Hounslow'],
+ ['GB-HPL', 'Hartlepool'],
+ ['GB-HRT', 'Hertfordshire'],
+ ['GB-HRW', 'Harrow'],
+ ['GB-HRY', 'Haringey'],
+ ['GB-IOS', 'Isles of Scilly'],
+ ['GB-IOW', 'Isle of Wight'],
+ ['GB-ISL', 'Islington'],
+ ['GB-IVC', 'Inverclyde'],
+ ['GB-KEC', 'Kensington and Chelsea'],
+ ['GB-KEN', 'Kent'],
+ ['GB-KHL', 'Kingston upon Hull'],
+ ['GB-KIR', 'Kirklees'],
+ ['GB-KTT', 'Kingston upon Thames'],
+ ['GB-KWL', 'Knowsley'],
+ ['GB-LAN', 'Lancashire'],
+ ['GB-LBH', 'Lambeth'],
+ ['GB-LCE', 'Leicester'],
+ ['GB-LDS', 'Leeds'],
+ ['GB-LEC', 'Leicestershire'],
+ ['GB-LEW', 'Lewisham'],
+ ['GB-LIN', 'Lincolnshire'],
+ ['GB-LIV', 'Liverpool'],
+ ['GB-LMV', 'Limavady'],
+ ['GB-LND', 'City'],
+ ['GB-LRN', 'Larne'],
+ ['GB-LSB', 'Lisburn'],
+ ['GB-LUT', 'Luton'],
+ ['GB-MAN', 'Manchester'],
+ ['GB-MDB', 'Middlesbrough'],
+ ['GB-MDW', 'Medway'],
+ ['GB-MFT', 'Magherafelt'],
+ ['GB-MIK', 'Milton Keynes'],
+ ['GB-MLN', 'Midlothian'],
+ ['GB-MON', 'Monmouthshire'],
+ ['GB-MRT', 'Merton'],
+ ['GB-MRY', 'Moray'],
+ ['GB-MTY', 'Merthyr Tydfil'],
+ ['GB-MYL', 'Moyle'],
+ ['GB-NAY', 'North Ayshire'],
+ ['GB-NBL', 'Northumberland'],
+ ['GB-NDN', 'North Down'],
+ ['GB-NEL', 'North East Lincolnshire'],
+ ['GB-NET', 'Newcastle upon Tyne'],
+ ['GB-NFK', 'Norfolk'],
+ ['GB-NGM', 'Nottingham'],
+ ['GB-NLK', 'North Lanarkshire'],
+ ['GB-NLN', 'North Lincolnshire'],
+ ['GB-NSM', 'North Somerset'],
+ ['GB-NTA', 'Newtownabbey'],
+ ['GB-NTH', 'Northamptonshire'],
+ ['GB-NTL', 'Neath Port Talbot'],
+ ['GB-NTT', 'Nottinghamshire'],
+ ['GB-NTY', 'North Tyneside'],
+ ['GB-NWM', 'Newham'],
+ ['GB-NWP', 'Newport'],
+ ['GB-NYK', 'North Yorkshire'],
+ ['GB-NYM', 'Newry and Mourne'],
+ ['GB-OLD', 'Oldham'],
+ ['GB-OMH', 'Omagh'],
+ ['GB-ORK', 'Orkney'],
+ ['GB-OXF', 'Oxfordshire'],
+ ['GB-PEM', 'Pembrokeshire'],
+ ['GB-PKN', 'Perthshire and Kinross'],
+ ['GB-PLY', 'Plymouth'],
+ ['GB-POL', 'Poole'],
+ ['GB-POR', 'Portsmouth'],
+ ['GB-POW', 'Powys'],
+ ['GB-PTE', 'Peterborough'],
+ ['GB-RCC', 'Redcar and Cleveland'],
+ ['GB-RCH', 'Rochdale'],
+ ['GB-RCT', 'Rhondda, Cynon, Taff'],
+ ['GB-RDB', 'Redbridge'],
+ ['GB-RDG', 'Reading'],
+ ['GB-RFW', 'Renfrewshire'],
+ ['GB-RIC', 'Richmond upon Thames'],
+ ['GB-ROT', 'Rotherham'],
+ ['GB-RUT', 'Rutland'],
+ ['GB-SAW', 'Sandwell'],
+ ['GB-SAY', 'South Ayrshire'],
+ ['GB-SCB', 'Scottish Borders'],
+ ['GB-SFK', 'Suffolk'],
+ ['GB-SFT', 'Sefton'],
+ ['GB-SGC', 'South Gloucestershire'],
+ ['GB-SHF', 'Sheffield'],
+ ['GB-SHN', 'Merseyside'],
+ ['GB-SHR', 'Shropshire'],
+ ['GB-SKP', 'Stockport'],
+ ['GB-SLF', 'Salford'],
+ ['GB-SLG', 'Slough'],
+ ['GB-SLK', 'South Lanarkshire'],
+ ['GB-SND', 'Sunderland'],
+ ['GB-SOL', 'Solihull'],
+ ['GB-SOM', 'Somerset'],
+ ['GB-SOS', 'Southend-on-Sea'],
+ ['GB-SRY', 'Surrey'],
+ ['GB-STB', 'Strabane'],
+ ['GB-STE', 'Stoke-on-Trent'],
+ ['GB-STG', 'Stirling'],
+ ['GB-STH', 'Southampton'],
+ ['GB-STN', 'Sutton'],
+ ['GB-STS', 'Staffordshire'],
+ ['GB-STT', 'Stockton-on-Tees'],
+ ['GB-STY', 'South Tyneside'],
+ ['GB-SWA', 'Swansea'],
+ ['GB-SWD', 'Swindon'],
+ ['GB-SWK', 'Southwark'],
+ ['GB-TAM', 'Tameside'],
+ ['GB-TFW', 'Telford and Wrekin'],
+ ['GB-THR', 'Thurrock'],
+ ['GB-TOB', 'Torbay'],
+ ['GB-TOF', 'Torfaen'],
+ ['GB-TRF', 'Trafford'],
+ ['GB-TWH', 'Tower Hamlets'],
+ ['GB-VGL', 'Vale of Glamorgan'],
+ ['GB-WAR', 'Warwickshire'],
+ ['GB-WBK', 'West Berkshire'],
+ ['GB-WDU', 'West Dunbartonshire'],
+ ['GB-WFT', 'Waltham Forest'],
+ ['GB-WGN', 'Wigan'],
+ ['GB-WIL', 'Wiltshire'],
+ ['GB-WKF', 'Wakefield'],
+ ['GB-WLL', 'Walsall'],
+ ['GB-WLN', 'West Lothian'],
+ ['GB-WLV', 'Wolverhampton'],
+ ['GB-WND', 'Wandsworth'],
+ ['GB-WNM', 'Royal Borough of Windsor and Maidenhead'],
+ ['GB-WOK', 'Wokingham'],
+ ['GB-WOR', 'Worcestershire'],
+ ['GB-WRL', 'Wirral'],
+ ['GB-WRT', 'Warrington'],
+ ['GB-WRX', 'Wrexham'],
+ ['GB-WSM', 'Westminster'],
+ ['GB-WSX', 'West Sussex'],
+ ['GB-YOR', 'York'],
+ ],
+ usa: [
+ ['US-AK', 'Alaska'],
+ ['US-AL', 'Alabama'],
+ ['US-AR', 'Arkansas'],
+ ['US-AZ', 'Arizona'],
+ ['US-CA', 'California'],
+ ['US-CO', 'Colorado'],
+ ['US-CT', 'Connecticut'],
+ ['US-DC', 'District of Columbia'],
+ ['US-DE', 'Delaware'],
+ ['US-FL', 'Florida'],
+ ['US-GA', 'Georgia'],
+ ['US-HI', 'Hawaii'],
+ ['US-IA', 'Iowa'],
+ ['US-ID', 'Idaho'],
+ ['US-IL', 'Illinois'],
+ ['US-IN', 'Indiana'],
+ ['US-KS', 'Kansas'],
+ ['US-KY', 'Kentucky'],
+ ['US-LA', 'Louisiana'],
+ ['US-MA', 'Massachusetts'],
+ ['US-MD', 'Maryland'],
+ ['US-ME', 'Maine'],
+ ['US-MI', 'Michigan'],
+ ['US-MN', 'Minnesota'],
+ ['US-MO', 'Missouri'],
+ ['US-MS', 'Mississippi'],
+ ['US-MT', 'Montana'],
+ ['US-NC', 'North Carolina'],
+ ['US-ND', 'North Dakota'],
+ ['US-NE', 'Nebraska'],
+ ['US-NH', 'New Hampshire'],
+ ['US-NJ', 'New Jersey'],
+ ['US-NM', 'New Mexico'],
+ ['US-NV', 'Nevada'],
+ ['US-NY', 'New York'],
+ ['US-OH', 'Ohio'],
+ ['US-OK', 'Oklahoma'],
+ ['US-OR', 'Oregon'],
+ ['US-PA', 'Pennsylvania'],
+ ['US-RI', 'Rhode Island'],
+ ['US-SC', 'South Carolina'],
+ ['US-SD', 'South Dakota'],
+ ['US-TN', 'Tennessee'],
+ ['US-TX', 'Texas'],
+ ['US-UT', 'Utah'],
+ ['US-VA', 'Virginia'],
+ ['US-VT', 'Vermont'],
+ ['US-WA', 'Washington'],
+ ['US-WI', 'Wisconsin'],
+ ['US-WV', 'West Virginia'],
+ ['US-WY', 'Wyoming'],
+ ],
+};
+
+export default regions;
diff --git a/superset-frontend/plugins/plugin-chart-country-map/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-country-map/src/transformProps.ts
index fc05a829a7b..ed66b7303d2 100644
--- a/superset-frontend/plugins/plugin-chart-country-map/src/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-country-map/src/transformProps.ts
@@ -24,6 +24,8 @@ import {
} from '@superset-ui/core';
import { getColorFormatters } from '@superset-ui/chart-controls';
+import normalizeRegions from './normalizeRegions';
+
export default function transformProps(chartProps: ChartProps) {
const {
width,
@@ -60,12 +62,48 @@ export default function transformProps(chartProps: ChartProps) {
// labels, so the rename happens here.
const entityLabel = getColumnLabel(entity);
const metricLabel = getMetricLabel(metric);
- // rename only rows carrying both source labels, so pre-shaped legacy
- // payloads pass through even when the entity column is named country_id
- const data = (rawData ?? []).map((row: Record<string, unknown>) =>
- entityLabel in row && metricLabel in row
- ? { country_id: row[entityLabel], metric: row[metricLabel] }
- : row,
+ const country = String(selectCountry).toLowerCase();
+ const displayData = formData.regionFormat
+ ? normalizeRegions(
+ rawData ?? [],
+ entityLabel,
+ country,
+ formData.regionFormat,
+ )
+ : (rawData ?? []);
+ // Every normalized region maps back to its source value, including regions
+ // whose metric is blank, so cross-filters and drills on returned regions
+ // filter on the original identifier rather than the boundary ISO.
+ const sourceValues: Record<string, string> = {};
+ if (formData.regionFormat) {
+ displayData.forEach((row: Record<string, unknown>, index: number) => {
+ sourceValues[String(row[entityLabel])] = rawData[index][entityLabel];
+ });
+ }
+ const data = displayData.flatMap(
+ (row: Record<string, unknown>, index: number) => {
+ // Validate regions even for sparse rows. A row without a finite metric
+ // is omitted from the rendered data, so its region stays unshaded.
+ if (
+ formData.regionFormat &&
+ (typeof row[metricLabel] !== 'number' ||
+ !Number.isFinite(row[metricLabel]))
+ ) {
+ return [];
+ }
+ // Pre-shaped legacy payloads already carry country_id and metric.
+ return [
+ entityLabel in row && metricLabel in row
+ ? {
+ country_id: row[entityLabel],
+ metric: row[metricLabel],
+ ...(formData.regionFormat
+ ? { source_value: rawData[index][entityLabel] }
+ : {}),
+ }
+ : row,
+ ];
+ },
);
const formatters = getColorFormatters(conditionalFormatting, data, theme);
@@ -87,7 +125,8 @@ export default function transformProps(chartProps: ChartProps) {
width,
height,
data,
- country: selectCountry ? String(selectCountry).toLowerCase() : null,
+ sourceValues: formData.regionFormat ? sourceValues : undefined,
+ country: selectCountry ? country : null,
linearColorScheme,
numberFormat, // left for backward compatibility
colorScheme,
diff --git a/superset-frontend/plugins/plugin-chart-country-map/test/CountryMap.test.tsx b/superset-frontend/plugins/plugin-chart-country-map/test/CountryMap.test.tsx
index fcc345f0cef..b51c65e7307 100644
--- a/superset-frontend/plugins/plugin-chart-country-map/test/CountryMap.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-country-map/test/CountryMap.test.tsx
@@ -414,3 +414,112 @@ describe('CountryMap conditional formatting', () => {
);
});
});
+
+test.each([
+ { sourceValues: { CAN: ' Canada ' }, expectedValue: ' Canada ' },
+ { sourceValues: {}, expectedValue: undefined },
+])(
+ 'click uses only mapped source values: $sourceValues',
+ ({ sourceValues, expectedValue }) => {
+ d3Any.json.mockImplementation((_url: string, cb: D3JsonCallback) =>
+ cb(null, mockMapData),
+ );
+ const setDataMask = jest.fn();
+
+ const { container } = render(
+ <ReactCountryMap
+ width={500}
+ height={300}
+ data={[{ country_id: 'CAN', metric: 100 }]}
+ sourceValues={sourceValues}
+ country="canada"
+ linearColorScheme="bnbColors"
+ colorScheme=""
+ formatter={jest.fn().mockReturnValue('100')}
+ entity="country_name"
+ emitCrossFilters
+ setDataMask={setDataMask}
+ filterState={{ selectedValues: [] }}
+ />,
+ );
+ const region = container.querySelector('path.region');
+ expect(region).not.toBeNull();
+ fireEvent.mouseDown(region!);
+ fireEvent.click(region!);
+
+ if (expectedValue === undefined) {
+ expect(setDataMask).not.toHaveBeenCalled();
+ } else {
+ expect(setDataMask).toHaveBeenCalledTimes(1);
+ expect(setDataMask).toHaveBeenCalledWith({
+ extraFormData: {
+ filters: [{ col: 'country_name', op: 'IN', val: [expectedValue] }],
+ },
+ filterState: { value: [expectedValue], selectedValues: ['CAN'] },
+ });
+ }
+ },
+);
+
+test.each([
+ { sourceValues: { CAN: ' Canada ' }, expectedValue: ' Canada ' },
+ { sourceValues: {}, expectedValue: undefined },
+])(
+ 'context menu uses only mapped source values: $sourceValues',
+ ({ sourceValues, expectedValue }) => {
+ d3Any.json.mockImplementation((_url: string, cb: D3JsonCallback) =>
+ cb(null, mockMapData),
+ );
+ const onContextMenu = jest.fn();
+
+ const { container } = render(
+ <ReactCountryMap
+ width={500}
+ height={300}
+ data={[{ country_id: 'CAN', metric: 100 }]}
+ sourceValues={sourceValues}
+ country="canada"
+ linearColorScheme="bnbColors"
+ colorScheme=""
+ formatter={jest.fn().mockReturnValue('100')}
+ entity="country_name"
+ onContextMenu={onContextMenu}
+ filterState={{ selectedValues: [] }}
+ />,
+ );
+ const region = container.querySelector('path.region');
+ expect(region).not.toBeNull();
+ fireEvent.contextMenu(region!, { clientX: 123, clientY: 45 });
+
+ if (expectedValue === undefined) {
+ expect(onContextMenu).not.toHaveBeenCalled();
+ } else {
+ expect(onContextMenu).toHaveBeenCalledTimes(1);
+ expect(onContextMenu).toHaveBeenCalledWith(123, 45, {
+ drillToDetail: [
+ {
+ col: 'country_name',
+ op: '==',
+ val: expectedValue,
+ formattedVal: expectedValue,
+ },
+ ],
+ crossFilter: {
+ dataMask: {
+ extraFormData: {
+ filters: [
+ { col: 'country_name', op: 'IN', val: [expectedValue] },
+ ],
+ },
+ filterState: { value: [expectedValue], selectedValues: ['CAN'] },
+ },
+ isCurrentValueSelected: false,
+ },
+ drillBy: {
+ filters: [{ col: 'country_name', op: '==', val: expectedValue }],
+ groupbyFieldName: 'entity',
+ },
+ });
+ }
+ },
+);
diff --git a/superset-frontend/plugins/plugin-chart-country-map/test/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-country-map/test/controlPanel.test.ts
new file mode 100644
index 00000000000..c279e7a3740
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-country-map/test/controlPanel.test.ts
@@ -0,0 +1,43 @@
+/**
+ * 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 { testGeographicTimeProvenance } from 'spec/helpers/geographicControlPanel';
+import controlPanel from '../src/controlPanel';
+
+const controlNames = (): string[] =>
+ (controlPanel.controlPanelSections ?? [])
+ .flatMap(section => section?.controlSetRows ?? [])
+ .flat()
+ .map(item =>
+ typeof item === 'string'
+ ? item
+ : (item as { name?: string } | null)?.name,
+ )
+ .filter((name): name is string => Boolean(name));
+
+test('keeps the MCP geographic marker and row limit through Explore hydration', () => {
+ expect(controlNames()).toEqual(
+ expect.arrayContaining([
+ 'mcp_geographic',
+ 'row_limit',
+ '_mcp_dashboard_time_filter_subject',
+ ]),
+ );
+});
+
+testGeographicTimeProvenance(controlPanel);
diff --git a/superset-frontend/plugins/plugin-chart-country-map/test/geographicRendering.test.tsx b/superset-frontend/plugins/plugin-chart-country-map/test/geographicRendering.test.tsx
new file mode 100644
index 00000000000..66c5335b335
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-country-map/test/geographicRendering.test.tsx
@@ -0,0 +1,296 @@
+/**
+ * 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 { render, fireEvent } from '@testing-library/react';
+import d3 from 'd3';
+import { ChartProps } from '@superset-ui/core';
+import { supersetTheme } from '@apache-superset/core/theme';
+import ReactCountryMap from '../src/ReactCountryMap';
+import transformProps from '../src/transformProps';
+import fs from 'fs';
+import path from 'path';
+
+interface RegionFeature {
+ properties: { ISO: string };
+}
+
+const usa: { features: RegionFeature[] } = JSON.parse(
+ fs.readFileSync(path.join(__dirname, '../src/countries/usa.geojson'), 'utf8'),
+);
+
+test('typed state codes render real boundary paths and cross-filter original values', () => {
+ const loader = d3 as unknown as {
+ json: (
+ url: string,
+ callback: (error: Error | null, data: unknown) => void,
+ ) => void;
+ };
+ const json = jest
+ .spyOn(loader, 'json')
+ .mockImplementation((_url, callback) => {
+ callback(null, usa);
+ });
+ const setDataMask = jest.fn();
+ try {
+ const props = transformProps(
+ new ChartProps({
+ theme: supersetTheme,
+ width: 800,
+ height: 600,
+ formData: {
+ entity: 'state',
+ metric: 'sales',
+ select_country: 'usa',
+ region_format: 'abbreviation',
+ linear_color_scheme: 'schemeBlues',
+ },
+ queriesData: [{ data: [{ state: 'CA', sales: 10 }] }],
+ datasource: { currencyFormats: {}, columnFormats: {} },
+ hooks: { setDataMask },
+ emitCrossFilters: true,
+ }),
+ );
+ const { container } = render(<ReactCountryMap {...props} />);
+ const regions = container.querySelectorAll<SVGPathElement>('path.region');
+ expect(regions).toHaveLength(usa.features.length);
+ const california = [...regions].find(
+ path =>
+ (d3.select(path).datum() as RegionFeature).properties.ISO === 'US-CA',
+ );
+ expect(california).toBeDefined();
+ expect(california?.getAttribute('d')?.length).toBeGreaterThan(100);
+ expect(california?.style.fill).toMatch(/^(rgb|#)/);
+ expect(california?.style.fill).not.toBe('rgb(217, 217, 217)');
+ if (california) {
+ fireEvent.mouseDown(california);
+ fireEvent.click(california);
+ }
+ expect(setDataMask).toHaveBeenCalledWith(
+ expect.objectContaining({
+ extraFormData: { filters: [{ col: 'state', op: 'IN', val: ['CA'] }] },
+ }),
+ );
+ } finally {
+ json.mockRestore();
+ }
+});
+
+test('an ISO selection is recognised as selected and toggles the filter off', () => {
+ const loader = d3 as unknown as {
+ json: (
+ url: string,
+ callback: (error: Error | null, data: unknown) => void,
+ ) => void;
+ };
+ const json = jest
+ .spyOn(loader, 'json')
+ .mockImplementation((_url, callback) => {
+ callback(null, usa);
+ });
+ const setDataMask = jest.fn();
+ try {
+ const props = transformProps(
+ new ChartProps({
+ theme: supersetTheme,
+ width: 800,
+ height: 600,
+ formData: {
+ entity: 'state',
+ metric: 'sales',
+ select_country: 'usa',
+ region_format: 'abbreviation',
+ linear_color_scheme: 'schemeBlues',
+ },
+ queriesData: [{ data: [{ state: 'CA', sales: 10 }] }],
+ datasource: { currencyFormats: {}, columnFormats: {} },
+ hooks: { setDataMask },
+ // the chart keys its own cross-filter state by rendered ISO code
+ filterState: { selectedValues: ['US-CA'] },
+ emitCrossFilters: true,
+ }),
+ );
+ const { container } = render(<ReactCountryMap {...props} />);
+ const regions = container.querySelectorAll<SVGPathElement>('path.region');
+ const california = [...regions].find(
+ path =>
+ (d3.select(path).datum() as RegionFeature).properties.ISO === 'US-CA',
+ );
+ expect(california).toBeDefined();
+ // recognised as selected, so it is the only fully opaque region
+ expect(california?.style.fillOpacity).toBe('1');
+ const other = [...regions].find(
+ path =>
+ (d3.select(path).datum() as RegionFeature).properties.ISO !== 'US-CA',
+ );
+ expect(other?.style.fillOpacity).toBe('0.3');
+ if (california) {
+ fireEvent.mouseDown(california);
+ fireEvent.click(california);
+ }
+ // clicking an already-selected region clears the filter
+ expect(setDataMask).toHaveBeenCalledWith(
+ expect.objectContaining({
+ extraFormData: { filters: [] },
+ filterState: expect.objectContaining({ selectedValues: null }),
+ }),
+ );
+ } finally {
+ json.mockRestore();
+ }
+});
+
+type Loader = {
+ json: (
+ url: string,
+ callback: (error: Error | null, data: unknown) => void,
+ ) => void;
+};
+
+function renderUsaMap(
+ rows: Record<string, unknown>[],
+ options: { selectedValues?: string[]; regionFormat?: string } = {},
+) {
+ const json = jest
+ .spyOn(d3 as unknown as Loader, 'json')
+ .mockImplementation((_url, callback) => {
+ callback(null, usa);
+ });
+ const setDataMask = jest.fn();
+ const onContextMenu = jest.fn();
+ const props = transformProps(
+ new ChartProps({
+ theme: supersetTheme,
+ width: 800,
+ height: 600,
+ formData: {
+ entity: 'state',
+ metric: 'sales',
+ select_country: 'usa',
+ region_format: options.regionFormat ?? 'abbreviation',
+ linear_color_scheme: 'schemeBlues',
+ },
+ queriesData: [{ data: rows }],
+ datasource: { currencyFormats: {}, columnFormats: {} },
+ hooks: { setDataMask, onContextMenu },
+ emitCrossFilters: true,
+ filterState: options.selectedValues
+ ? { selectedValues: options.selectedValues }
+ : {},
+ }),
+ );
+ const { container } = render(<ReactCountryMap {...props} />);
+ const region = (iso: string) =>
+ [...container.querySelectorAll<SVGPathElement>('path.region')].find(
+ path => (d3.select(path).datum() as RegionFeature).properties.ISO === iso,
+ );
+ const interact = (iso: string) => {
+ const target = region(iso);
+ expect(target).toBeDefined();
+ if (target) {
+ fireEvent.mouseDown(target);
+ fireEvent.click(target);
+ fireEvent.contextMenu(target);
+ }
+ };
+ return { container, interact, json, onContextMenu, setDataMask };
+}
+
+test('a region with a blank metric still cross-filters on its source value', () => {
+ const { interact, json, onContextMenu, setDataMask } = renderUsaMap([
+ { state: 'CA', sales: null },
+ { state: 'TX', sales: 10 },
+ ]);
+ try {
+ interact('US-CA');
+ expect(setDataMask).toHaveBeenCalledWith(
+ expect.objectContaining({
+ extraFormData: { filters: [{ col: 'state', op: 'IN', val: ['CA'] }] },
+ }),
+ );
+ expect(onContextMenu).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ expect.objectContaining({
+ drillToDetail: [
+ { col: 'state', op: '==', val: 'CA', formattedVal: 'CA' },
+ ],
+ }),
+ );
+ } finally {
+ json.mockRestore();
+ }
+});
+
+test('unmapped boundaries do not guess identifiers with an explicit region format', () => {
+ const { container, interact, json, onContextMenu, setDataMask } =
+ renderUsaMap([{ state: 'CA', sales: 10 }]);
+ try {
+ expect(container.querySelectorAll('path.region')).toHaveLength(
+ usa.features.length,
+ );
+ interact('US-TX');
+ expect(setDataMask).not.toHaveBeenCalled();
+ expect(onContextMenu).not.toHaveBeenCalled();
+ } finally {
+ json.mockRestore();
+ }
+});
+
+test('a selected boundary stays clearable after another filter drops its row', () => {
+ const { interact, json, setDataMask } = renderUsaMap(
+ [{ state: 'TX', sales: 10 }],
+ { selectedValues: ['US-CA'] },
+ );
+ try {
+ interact('US-CA');
+ expect(setDataMask).toHaveBeenCalledWith(
+ expect.objectContaining({
+ extraFormData: { filters: [] },
+ filterState: { value: null, selectedValues: null },
+ }),
+ );
+ setDataMask.mockClear();
+ interact('US-NY');
+ expect(setDataMask).not.toHaveBeenCalled();
+ } finally {
+ json.mockRestore();
+ }
+});
+
+test.each([
+ ['name', 'California'],
+ ['abbreviation', 'CA'],
+ ['iso_3166_2', 'US-CA'],
+])(
+ 'region format %s uses source values for cross-filter labels',
+ (regionFormat, value) => {
+ const { interact, json, setDataMask } = renderUsaMap(
+ [{ state: value, sales: 10 }],
+ { regionFormat },
+ );
+ try {
+ interact('US-CA');
+ expect(setDataMask).toHaveBeenCalledWith({
+ extraFormData: { filters: [{ col: 'state', op: 'IN', val: [value] }] },
+ filterState: { value: [value], selectedValues: ['US-CA'] },
+ });
+ } finally {
+ json.mockRestore();
+ }
+ },
+);
diff --git a/superset-frontend/plugins/plugin-chart-country-map/test/normalizeRegions.test.ts b/superset-frontend/plugins/plugin-chart-country-map/test/normalizeRegions.test.ts
new file mode 100644
index 00000000000..75ab91d842f
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-country-map/test/normalizeRegions.test.ts
@@ -0,0 +1,251 @@
+/**
+ * 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 { configure } from '@apache-superset/core/translation';
+import normalizeRegions, {
+ resolveRegion,
+ RegionFormat,
+} from '../src/normalizeRegions';
+
+test.each([
+ ['usa', 'CA', 'abbreviation', 'US-CA'],
+ ['usa', 'ca', 'abbreviation', 'US-CA'],
+ ['usa', 'California', 'name', 'US-CA'],
+ ['usa', 'us-ca', 'iso_3166_2', 'US-CA'],
+ ['canada', 'BC', 'abbreviation', 'CA-BC'],
+ ['australia', 'Victoria', 'name', 'AU-VIC'],
+ ['australia', 'NSW', 'abbreviation', 'AU-NSW'],
+ ['australia', 'Queensland', 'name', 'AU-QLD'],
+ ['japan', 'Tokyo', 'name', 'JP-13'],
+ ['japan', 'Osaka', 'name', 'JP-27'],
+ ['uk', 'Isle of Wight', 'name', 'GB-IOW'],
+])('normalizes %s %s in %s format', (country, value, format, expected) => {
+ const data = [{ state: value, sales: 10 }];
+ expect(normalizeRegions(data, 'state', country, format)).toEqual([
+ { state: expected, sales: 10 },
+ ]);
+ expect(data[0].state).toBe(value);
+});
+
+test.each([
+ 'BC',
+ 'Victoria',
+ 'NSW',
+ 'Queensland',
+ 'Tokyo',
+ 'Osaka',
+ 'Isle of Wight',
+ null,
+ '',
+ 1,
+])('rejects non-US value %s without blanking the map', value => {
+ expect(() =>
+ normalizeRegions([{ state: value }], 'state', 'usa', 'abbreviation'),
+ ).toThrow();
+});
+
+test('exact matches win and ambiguous case folding fails', () => {
+ const boundaries = [
+ { properties: { ISO: 'A', NAME_1: 'Region' } },
+ { properties: { ISO: 'B', NAME_1: 'REGION' } },
+ ];
+ expect(resolveRegion('Region', boundaries, 'name' as RegionFormat)).toBe('A');
+ expect(() => resolveRegion('region', boundaries, 'name')).toThrow(
+ 'Ambiguous',
+ );
+});
+
+test('duplicate normalized boundaries cannot lose or incorrectly sum aggregates', () => {
+ expect(() =>
+ normalizeRegions(
+ [{ state: 'CA' }, { state: 'ca' }],
+ 'state',
+ 'usa',
+ 'abbreviation',
+ ),
+ ).toThrow('normalize source values before aggregation');
+});
+
+test('compact boundary identifiers match the actual GeoJSON assets', () => {
+ const fs = jest.requireActual<typeof import('fs')>('fs');
+ const path = jest.requireActual<typeof import('path')>('path');
+ const regions =
+ jest.requireActual<typeof import('../src/regions')>(
+ '../src/regions',
+ ).default;
+ for (const [country, pairs] of Object.entries(regions)) {
+ const geometry: {
+ features: {
+ properties: { ISO: string; NAME_1: string; NAME_2?: string };
+ }[];
+ } = JSON.parse(
+ fs.readFileSync(
+ path.join(__dirname, `../src/countries/${country}.geojson`),
+ 'utf8',
+ ),
+ );
+ const expected = new Set(
+ geometry.features.map(({ properties: p }) =>
+ JSON.stringify([p.ISO, p.NAME_2 || p.NAME_1]),
+ ),
+ );
+ expect(new Set(pairs.map(pair => JSON.stringify(pair)))).toEqual(expected);
+ }
+});
+
+test('bundled UK names resolve without cross-transform leakage', () => {
+ expect(
+ normalizeRegions([{ region: 'Halton' }], 'region', 'uk', 'name'),
+ ).toEqual([{ region: 'GB-HAL' }]);
+ expect(
+ normalizeRegions([{ region: 'Wirral' }], 'region', 'uk', 'name'),
+ ).toEqual([{ region: 'GB-WRL' }]);
+ expect(
+ normalizeRegions([{ region: 'GB-HAL' }], 'region', 'uk', 'iso_3166_2'),
+ ).toEqual([{ region: 'GB-HAL' }]);
+ expect(
+ normalizeRegions([{ region: 'WRL' }], 'region', 'uk', 'abbreviation'),
+ ).toEqual([{ region: 'GB-WRL' }]);
+ expect(
+ normalizeRegions([{ region: 'ca' }], 'region', 'usa', 'abbreviation'),
+ ).toEqual([{ region: 'US-CA' }]);
+ expect(
+ normalizeRegions([{ region: 'bc' }], 'region', 'canada', 'abbreviation'),
+ ).toEqual([{ region: 'CA-BC' }]);
+});
+
+test.each([
+ ['Cá', 'abbreviation'],
+ ['US-CÁ', 'iso_3166_2'],
+])('does not fold diacritics in region code %s', (value, format) => {
+ expect(() =>
+ normalizeRegions([{ state: value }], 'state', 'usa', format),
+ ).toThrow('Unrecognized');
+});
+
+test.each(['Kōchi', 'Kochi', 'kōchi'])(
+ 'folds diacritics in region name %s',
+ value => {
+ expect(
+ normalizeRegions([{ state: value }], 'state', 'japan', 'name'),
+ ).toEqual([{ state: 'JP-39' }]);
+ },
+);
+
+const frenchErrors = {
+ 'Geographic values must be nonempty strings of at most 500 characters': [
+ 'Les valeurs géographiques doivent être des chaînes non vides de 500 caractères au maximum',
+ ],
+ 'Unrecognized region %s; choose the matching country/region format or explicitly filter the dataset.':
+ [
+ 'Région inconnue %s ; choisissez le pays/format correspondant ou filtrez les données.',
+ ],
+ 'Ambiguous region %s; choose the matching country/region format or explicitly filter the dataset.':
+ [
+ 'Région ambiguë %s ; choisissez le pays/format correspondant ou filtrez les données.',
+ ],
+ 'Unsupported country or region_format for typed geographic chart': [
+ 'Pays ou format de région non pris en charge pour ce graphique géographique',
+ ],
+ 'Multiple result rows resolve to %s; normalize source values before aggregation.':
+ [
+ 'Plusieurs lignes correspondent à %s ; normalisez les valeurs avant agrégation.',
+ ],
+};
+
+test.each([
+ {
+ name: 'unrecognized region',
+ run: () =>
+ normalizeRegions([{ state: 'BC' }], 'state', 'usa', 'abbreviation'),
+ message:
+ 'Région inconnue "BC" ; choisissez le pays/format correspondant ou filtrez les données.',
+ },
+ {
+ name: 'duplicate normalized region',
+ run: () =>
+ normalizeRegions(
+ [{ state: 'CA' }, { state: 'ca' }],
+ 'state',
+ 'usa',
+ 'abbreviation',
+ ),
+ message:
+ 'Plusieurs lignes correspondent à US-CA ; normalisez les valeurs avant agrégation.',
+ },
+ {
+ name: 'unsupported format',
+ run: () => normalizeRegions([], 'state', 'usa', 'unknown'),
+ message:
+ frenchErrors[
+ 'Unsupported country or region_format for typed geographic chart'
+ ][0],
+ },
+ {
+ name: 'unsupported country',
+ run: () => normalizeRegions([], 'state', 'unknown', 'abbreviation'),
+ message:
+ frenchErrors[
+ 'Unsupported country or region_format for typed geographic chart'
+ ][0],
+ },
+ {
+ name: 'invalid value',
+ run: () =>
+ normalizeRegions([{ state: null }], 'state', 'usa', 'abbreviation'),
+ message:
+ frenchErrors[
+ 'Geographic values must be nonempty strings of at most 500 characters'
+ ][0],
+ },
+ {
+ name: 'ambiguous region',
+ run: () =>
+ resolveRegion(
+ 'region',
+ [
+ { properties: { ISO: 'A', NAME_1: 'Region' } },
+ { properties: { ISO: 'B', NAME_1: 'REGION' } },
+ ],
+ 'name',
+ ),
+ message:
+ 'Région ambiguë "region" ; choisissez le pays/format correspondant ou filtrez les données.',
+ },
+])('localizes $name in a French Explore session', ({ run, message }) => {
+ configure({
+ languagePack: {
+ domain: 'superset',
+ locale_data: {
+ superset: {
+ '': {
+ domain: 'superset',
+ lang: 'fr',
+ plural_forms: 'nplurals=2; plural=(n != 1)',
+ },
+ ...frenchErrors,
+ },
+ },
+ },
+ });
+ try {
+ expect(run).toThrow(message);
+ } finally {
+ configure();
+ }
+});
diff --git a/superset-frontend/plugins/plugin-chart-country-map/test/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-country-map/test/transformProps.test.ts
index 6b25e9eef51..7854bb312ab 100644
--- a/superset-frontend/plugins/plugin-chart-country-map/test/transformProps.test.ts
+++ b/superset-frontend/plugins/plugin-chart-country-map/test/transformProps.test.ts
@@ -129,6 +129,31 @@ test('renames v1 API records using adhoc metric labels', () => {
expect(transformed.data).toEqual([{ country_id: 'FRA', metric: 3.14 }]);
});
+test('normalizes typed region display while preserving source values for cross-filters', () => {
+ const data = [{ state: 'CA', count: 10 }];
+ const transformed = transformProps(
+ createProps(
+ { entity: 'state', selectCountry: 'usa', regionFormat: 'abbreviation' },
+ { queriesData: [{ data }] },
+ ),
+ );
+ expect(transformed.data).toEqual([
+ { country_id: 'US-CA', source_value: 'CA', metric: 10 },
+ ]);
+ expect(data).toEqual([{ state: 'CA', count: 10 }]);
+});
+
+test('typed country maps reject unrecognized values', () => {
+ expect(() =>
+ transformProps(
+ createProps(
+ { entity: 'state', selectCountry: 'usa', regionFormat: 'abbreviation' },
+ { queriesData: [{ data: [{ state: 'BC', count: 10 }] }] },
+ ),
+ ),
+ ).toThrow('Unrecognized');
+});
+
test('builds color formatters from conditional formatting config', () => {
const transformed = transformProps(
createProps({
@@ -154,3 +179,103 @@ test('returns an empty formatters array when conditional formatting is unset', (
const transformed = transformProps(createProps());
expect(transformed.formatters).toEqual([]);
});
+
+test('conditional formatting uses the normalized region metric and preserves the source identifier', () => {
+ const transformed = transformProps(
+ createProps(
+ {
+ entity: 'state',
+ selectCountry: 'usa',
+ regionFormat: 'abbreviation',
+ conditionalFormatting: [
+ {
+ column: 'metric',
+ colorScheme: '#FF0000',
+ operator: Comparator.GreaterThan,
+ targetValue: 5,
+ useGradient: false,
+ },
+ ],
+ },
+ { queriesData: [{ data: [{ state: 'CA', count: 10 }] }] },
+ ),
+ );
+ expect(transformed.data).toEqual([
+ { country_id: 'US-CA', source_value: 'CA', metric: 10 },
+ ]);
+ expect(transformed.formatters[0].getColorFromValue(10)).toBe('#FF0000');
+});
+
+test.each([undefined, null, 'missing', '10', Number.NaN, Infinity])(
+ 'typed country maps leave sparse/non-numeric metric %s blank',
+ count => {
+ const sparse =
+ count === undefined ? { state: 'CA' } : { state: 'CA', count };
+ const data = [
+ sparse,
+ { state: 'NY', count: 0 },
+ { state: 'TX', count: 10 },
+ ];
+ const original = data.map(row => ({ ...row }));
+ const transformed = transformProps(
+ createProps(
+ { entity: 'state', selectCountry: 'usa', regionFormat: 'abbreviation' },
+ { queriesData: [{ data }] },
+ ),
+ );
+ expect(transformed.data).toEqual([
+ { country_id: 'US-NY', source_value: 'NY', metric: 0 },
+ { country_id: 'US-TX', source_value: 'TX', metric: 10 },
+ ]);
+ expect(data).toEqual(original);
+ },
+);
+
+test('typed country maps still reject unresolved regions with NULL metrics', () => {
+ expect(() =>
+ transformProps(
+ createProps(
+ { entity: 'state', selectCountry: 'usa', regionFormat: 'abbreviation' },
+ { queriesData: [{ data: [{ state: 'BC', count: null }] }] },
+ ),
+ ),
+ ).toThrow('Unrecognized');
+});
+
+test('source values cover regions whose metric is blank', () => {
+ const transformed = transformProps(
+ createProps(
+ { entity: 'state', selectCountry: 'usa', regionFormat: 'abbreviation' },
+ {
+ queriesData: [
+ {
+ data: [
+ { state: 'CA', count: null },
+ { state: 'TX', count: 10 },
+ ],
+ },
+ ],
+ },
+ ),
+ );
+ expect(transformed.data).toEqual([
+ { country_id: 'US-TX', source_value: 'TX', metric: 10 },
+ ]);
+ expect(transformed.sourceValues).toEqual({ 'US-CA': 'CA', 'US-TX': 'TX' });
+});
+
+test.each(['USA', 'Usa', 'usa'])(
+ 'typed country maps accept select_country %s in any case',
+ selectCountry => {
+ const transformed = transformProps(
+ createProps(
+ { entity: 'state', selectCountry, regionFormat: 'abbreviation' },
+ { queriesData: [{ data: [{ state: 'CA', count: 10 }] }] },
+ ),
+ );
+ expect(transformed.country).toBe('usa');
+ expect(transformed.data).toEqual([
+ { country_id: 'US-CA', source_value: 'CA', metric: 10 },
+ ]);
+ },
+);
diff --git a/superset-frontend/plugins/plugin-chart-world-map/src/WorldMap.ts b/superset-frontend/plugins/plugin-chart-world-map/src/WorldMap.ts
index afaaaac5719..332ef324ae1 100644
--- a/superset-frontend/plugins/plugin-chart-world-map/src/WorldMap.ts
+++ b/superset-frontend/plugins/plugin-chart-world-map/src/WorldMap.ts
@@ -31,6 +31,7 @@ import { ColorBy } from './utils';
interface WorldMapDataEntry {
country: string;
+ sourceValue?: string;
code: string;
latitude: number;
longitude: number;
@@ -97,6 +98,7 @@ const propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
country: PropTypes.string,
+ sourceValue: PropTypes.string,
code: PropTypes.string,
latitude: PropTypes.number,
longitude: PropTypes.number,
@@ -187,11 +189,15 @@ function WorldMap(element: HTMLElement, props: WorldMapProps): void {
mapData[d.country] = d;
});
+ /** Resolve the dataset value shared by cross-filter and drill actions. */
+ const getCountryValue = (key: string) =>
+ mapData[key]?.sourceValue ??
+ (countryFieldtype === 'name' ? mapData[key]?.name : mapData[key]?.code);
+
const getCrossFilterDataMask = (source: DatamapSource) => {
const selected = Object.values(filterState.selectedValues || {});
const key = source.id || source.country;
- const country =
- countryFieldtype === 'name' ? mapData[key]?.name : mapData[key]?.code;
+ const country = getCountryValue(key);
if (!country) {
return undefined;
@@ -244,8 +250,7 @@ function WorldMap(element: HTMLElement, props: WorldMapProps): void {
const pointerEvent = d3.event;
pointerEvent.preventDefault();
const key = source.id || source.country;
- const val =
- countryFieldtype === 'name' ? mapData[key]?.name : mapData[key]?.code;
+ const val = getCountryValue(key);
let drillToDetailFilters;
let drillByFilters;
if (val) {
diff --git a/superset-frontend/plugins/plugin-chart-world-map/src/controlPanel.ts b/superset-frontend/plugins/plugin-chart-world-map/src/controlPanel.ts
index 974264e80fb..b26848b473f 100644
--- a/superset-frontend/plugins/plugin-chart-world-map/src/controlPanel.ts
+++ b/superset-frontend/plugins/plugin-chart-world-map/src/controlPanel.ts
@@ -55,6 +55,8 @@ const config: ControlPanelConfig = {
['adhoc_filters'],
['row_limit'],
['sort_by_metric'],
+ ['mcp_geographic'],
+ ['_mcp_dashboard_time_filter_subject'],
],
},
{
diff --git a/superset-frontend/plugins/plugin-chart-world-map/src/countries.ts b/superset-frontend/plugins/plugin-chart-world-map/src/countries.ts
index 2db1539bb49..9d548fae6a6 100644
--- a/superset-frontend/plugins/plugin-chart-world-map/src/countries.ts
+++ b/superset-frontend/plugins/plugin-chart-world-map/src/countries.ts
@@ -2010,21 +2010,47 @@ export const countries: CountryInfo[] = [
},
];
-const lookups: Record<CountryFieldType, Map<string, CountryInfo>> = {
- name: new Map(),
+/** Match country names without guessing or folding short country codes. */
+function foldName(value: string): string {
+ return value
+ .toLowerCase()
+ .normalize('NFD')
+ .replace(/[\u0300-\u036F]/g, '');
+}
+
+const exactNames = new Map(countries.map(country => [country.name, country]));
+const foldedNames = new Map<string, CountryInfo[]>();
+// Names resolve through exactNames/foldedNames; codes match case-insensitively.
+type CountryCodeField = Exclude<CountryFieldType, 'name'>;
+const lookups: Record<CountryCodeField, Map<string, CountryInfo>> = {
cca2: new Map(),
cca3: new Map(),
cioc: new Map(),
};
-(Object.keys(lookups) as CountryFieldType[]).forEach(field => {
+(Object.keys(lookups) as CountryCodeField[]).forEach(field => {
countries.forEach(country => {
- lookups[field].set(country[field].toLowerCase(), country);
+ if (country[field]) {
+ lookups[field].set(country[field].toLowerCase(), country);
+ }
});
});
+countries.forEach(country => {
+ const key = foldName(country.name);
+ const matches = foldedNames.get(key) ?? [];
+ matches.push(country);
+ foldedNames.set(key, matches);
+});
export function getCountry(
field: string,
symbol: string,
): CountryInfo | undefined {
- return lookups[field as CountryFieldType]?.get(symbol.toLowerCase());
+ if (!symbol.trim()) return undefined;
+ if (field === 'name') {
+ const exact = exactNames.get(symbol);
+ if (exact) return exact;
+ const matches = foldedNames.get(foldName(symbol));
+ return matches?.length === 1 ? matches[0] : undefined;
+ }
+ return lookups[field as CountryCodeField]?.get(symbol.toLowerCase());
}
diff --git a/superset-frontend/plugins/plugin-chart-world-map/src/transformData.ts b/superset-frontend/plugins/plugin-chart-world-map/src/transformData.ts
index f9d584f9f96..a9abf629c13 100644
--- a/superset-frontend/plugins/plugin-chart-world-map/src/transformData.ts
+++ b/superset-frontend/plugins/plugin-chart-world-map/src/transformData.ts
@@ -16,11 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { getColumnLabel, getMetricLabel } from '@superset-ui/core';
+import {
+ getColumnLabel,
+ getMetricLabel,
+ QueryFormColumn,
+} from '@superset-ui/core';
+import { t } from '@apache-superset/core/translation';
import { getCountry } from './countries';
+import { WORLD_BOUNDARY_IDS } from './worldGeometry';
export interface WorldMapDataRow {
country: string;
+ sourceValue?: string;
m1: unknown;
m2?: unknown;
code?: string;
@@ -38,10 +45,12 @@ export interface WorldMapDataRow {
export default function transformData(
records: Record<string, unknown>[],
options: {
- entity?: string;
+ entity?: QueryFormColumn;
metric?: unknown;
secondaryMetric?: unknown;
countryFieldtype?: string;
+ strict?: boolean;
+ showBubbles?: boolean;
},
): WorldMapDataRow[] {
const entityLabel = getColumnLabel(options.entity ?? '');
@@ -51,6 +60,11 @@ export default function transformData(
: undefined;
const fieldtype = options.countryFieldtype;
+ // The secondary metric only sizes bubbles, so an unused secondary metric
+ // kept with bubbles off does not gate the choropleth.
+ const sizeLabel = options.showBubbles ? secondaryLabel : undefined;
+ const metricLabels = [metricLabel, ...(sizeLabel ? [sizeLabel] : [])];
+ const seen = new Set<string>();
return records.map(record => {
const row: WorldMapDataRow = {
country: record[entityLabel] as string,
@@ -66,7 +80,38 @@ export default function transformData(
typeof row.country === 'string' && fieldtype
? getCountry(fieldtype, row.country)
: undefined;
+ if (options.strict) {
+ if (!countryInfo || seen.has(countryInfo.cca3)) {
+ throw new Error(
+ t(
+ 'Unrecognized or duplicate country value; choose the matching country format or normalize source values before aggregation.',
+ ),
+ );
+ }
+ if (!options.showBubbles && !WORLD_BOUNDARY_IDS.has(countryInfo.cca3)) {
+ throw new Error(
+ t(
+ 'Country %s has no world-map boundary; enable bubbles or filter the dataset.',
+ countryInfo.cca3,
+ ),
+ );
+ }
+ seen.add(countryInfo.cca3);
+ for (const label of metricLabels) {
+ const value = record[label];
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
+ throw new Error(
+ t('Geographic metric %s must be a finite number', label),
+ );
+ }
+ }
+ const size = sizeLabel ? record[sizeLabel] : undefined;
+ if (typeof size === 'number' && size < 0) {
+ throw new Error(t('Bubble-size metric must be nonnegative'));
+ }
+ }
if (countryInfo) {
+ row.sourceValue = row.country;
row.code = countryInfo[fieldtype as keyof typeof countryInfo] as string;
row.country = countryInfo.cca3;
row.latitude = countryInfo.lat;
diff --git a/superset-frontend/plugins/plugin-chart-world-map/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-world-map/src/transformProps.ts
index 65b255a2098..9956ed36b81 100644
--- a/superset-frontend/plugins/plugin-chart-world-map/src/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-world-map/src/transformProps.ts
@@ -23,6 +23,7 @@ import {
getMetricLabel,
getValueFormatter,
} from '@superset-ui/core';
+import { t } from '@apache-superset/core/translation';
import transformData from './transformData';
export default function transformProps(chartProps: ChartProps) {
@@ -61,21 +62,30 @@ export default function transformProps(chartProps: ChartProps) {
} = datasource;
const { data: rawData, detected_currency: detectedCurrency } = queriesData[0];
+ if (formData.mcpGeographic && !Array.isArray(rawData)) {
+ throw new Error(
+ t('Expected geographic query data to be a list of records'),
+ );
+ }
+
// The legacy explore_json endpoint joined country metadata server-side;
// rows carrying both the entity and metric labels are v1 records that
// still need that join, anything else is a pre-shaped legacy payload.
const entityLabel = getColumnLabel(entity);
const metricLabel = getMetricLabel(metric);
const data =
- Array.isArray(rawData) &&
- rawData.length > 0 &&
- entityLabel in rawData[0] &&
- metricLabel in rawData[0]
- ? transformData(rawData, {
+ formData.mcpGeographic ||
+ (Array.isArray(rawData) &&
+ rawData.length > 0 &&
+ entityLabel in rawData[0] &&
+ metricLabel in rawData[0])
+ ? transformData(rawData ?? [], {
entity,
metric,
secondaryMetric,
countryFieldtype,
+ strict: formData.mcpGeographic,
+ showBubbles: Boolean(showBubbles),
})
: rawData;
diff --git a/superset-frontend/plugins/plugin-chart-world-map/src/worldGeometry.ts b/superset-frontend/plugins/plugin-chart-world-map/src/worldGeometry.ts
new file mode 100644
index 00000000000..bc614f307aa
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-world-map/src/worldGeometry.ts
@@ -0,0 +1,198 @@
+/**
+ * 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.
+ */
+// Snapshot of datamaps/dist/datamaps.all.min world topology, verified by tests.
+// Countries outside this set have no polygon but can still render as bubbles.
+export const WORLD_BOUNDARY_IDS = new Set([
+ '-99',
+ 'AFG',
+ 'AGO',
+ 'ALB',
+ 'ARE',
+ 'ARG',
+ 'ARM',
+ 'ATA',
+ 'ATF',
+ 'AUS',
+ 'AUT',
+ 'AZE',
+ 'BDI',
+ 'BEL',
+ 'BEN',
+ 'BFA',
+ 'BGD',
+ 'BGR',
+ 'BHS',
+ 'BIH',
+ 'BLR',
+ 'BLZ',
+ 'BOL',
+ 'BRA',
+ 'BRN',
+ 'BTN',
+ 'BWA',
+ 'CAF',
+ 'CAN',
+ 'CHE',
+ 'CHL',
+ 'CHN',
+ 'CIV',
+ 'CMR',
+ 'COD',
+ 'COG',
+ 'COL',
+ 'CRI',
+ 'CUB',
+ 'CYP',
+ 'CZE',
+ 'DEU',
+ 'DJI',
+ 'DNK',
+ 'DOM',
+ 'DZA',
+ 'ECU',
+ 'EGY',
+ 'ERI',
+ 'ESH',
+ 'ESP',
+ 'EST',
+ 'ETH',
+ 'FIN',
+ 'FJI',
+ 'FLK',
+ 'FRA',
+ 'GAB',
+ 'GBR',
+ 'GEO',
+ 'GHA',
+ 'GIN',
+ 'GMB',
+ 'GNB',
+ 'GNQ',
+ 'GRC',
+ 'GRL',
+ 'GTM',
+ 'GUF',
+ 'GUY',
+ 'HND',
+ 'HRV',
+ 'HTI',
+ 'HUN',
+ 'IDN',
+ 'IND',
+ 'IRL',
+ 'IRN',
+ 'IRQ',
+ 'ISL',
+ 'ISR',
+ 'ITA',
+ 'JAM',
+ 'JOR',
+ 'JPN',
+ 'KAZ',
+ 'KEN',
+ 'KGZ',
+ 'KHM',
+ 'KOR',
+ 'KWT',
+ 'LAO',
+ 'LBN',
+ 'LBR',
+ 'LBY',
+ 'LKA',
+ 'LSO',
+ 'LTU',
+ 'LUX',
+ 'LVA',
+ 'MAR',
+ 'MDA',
+ 'MDG',
+ 'MEX',
+ 'MKD',
+ 'MLI',
+ 'MMR',
+ 'MNE',
+ 'MNG',
+ 'MOZ',
+ 'MRT',
+ 'MWI',
+ 'MYS',
+ 'NAM',
+ 'NCL',
+ 'NER',
+ 'NGA',
+ 'NIC',
+ 'NLD',
+ 'NOR',
+ 'NPL',
+ 'NZL',
+ 'OMN',
+ 'PAK',
+ 'PAN',
+ 'PER',
+ 'PHL',
+ 'PNG',
+ 'POL',
+ 'PRI',
+ 'PRK',
+ 'PRT',
+ 'PRY',
+ 'PSE',
+ 'QAT',
+ 'ROU',
+ 'RUS',
+ 'RWA',
+ 'SAU',
+ 'SDN',
+ 'SEN',
+ 'SLB',
+ 'SLE',
+ 'SLV',
+ 'SOM',
+ 'SRB',
+ 'SSD',
+ 'SUR',
+ 'SVK',
+ 'SVN',
+ 'SWE',
+ 'SWZ',
+ 'SYR',
+ 'TCD',
+ 'TGO',
+ 'THA',
+ 'TJK',
+ 'TKM',
+ 'TLS',
+ 'TTO',
+ 'TUN',
+ 'TUR',
+ 'TWN',
+ 'TZA',
+ 'UGA',
+ 'UKR',
+ 'URY',
+ 'USA',
+ 'UZB',
+ 'VEN',
+ 'VNM',
+ 'VUT',
+ 'YEM',
+ 'ZAF',
+ 'ZMB',
+ 'ZWE',
+]);
diff --git a/superset-frontend/plugins/plugin-chart-world-map/test/WorldMap.test.ts b/superset-frontend/plugins/plugin-chart-world-map/test/WorldMap.test.ts
index d53740827f8..5a1d4f267ae 100644
--- a/superset-frontend/plugins/plugin-chart-world-map/test/WorldMap.test.ts
+++ b/superset-frontend/plugins/plugin-chart-world-map/test/WorldMap.test.ts
@@ -19,6 +19,7 @@
import d3 from 'd3';
import { getNumberFormatter, ValueFormatter } from '@superset-ui/core';
+import transformData from '../src/transformData';
import WorldMap from '../src/WorldMap';
import { ColorBy } from '../src/utils';
@@ -421,6 +422,83 @@ test('popupTemplate handles null/undefined country data gracefully', () => {
expect(tooltipHtml).toBeFalsy();
});
+test.each([
+ ['name', 'México', 'MEX'],
+ ['name', 'MÉXICO', 'MEX'],
+ ['cca2', 'fr', 'FRA'],
+])(
+ 'cross-filters and drills preserve source %s value %s',
+ (countryFieldtype, sourceValue, country) => {
+ const handlers = new Map<string, (source: { id: string }) => void>();
+ mockSvg.on.mockImplementation(
+ (event: string, handler: (source: { id: string }) => void) => {
+ handlers.set(event, handler);
+ return mockSvg;
+ },
+ );
+ const originalEvent = d3.event;
+ Object.assign(d3, {
+ event: { preventDefault: jest.fn(), clientX: 100, clientY: 200 },
+ });
+ try {
+ const data = transformData([{ country: sourceValue, sales: 10 }], {
+ entity: 'country',
+ metric: 'sales',
+ countryFieldtype,
+ strict: true,
+ }).map(row => ({
+ ...row,
+ code: row.code!,
+ latitude: row.latitude!,
+ longitude: row.longitude!,
+ name: row.name!,
+ m1: row.m1 as number,
+ m2: 0,
+ }));
+ const setDataMask = jest.fn();
+ const onContextMenu = jest.fn();
+ WorldMap(container, {
+ ...baseProps,
+ data,
+ countryFieldtype,
+ setDataMask,
+ onContextMenu,
+ emitCrossFilters: true,
+ });
+ expect(handlers.get('click')).toBeDefined();
+ handlers.get('click')!({ id: country });
+ expect(setDataMask).toHaveBeenCalledWith(
+ expect.objectContaining({
+ extraFormData: {
+ filters: [{ col: 'country', op: 'IN', val: [sourceValue] }],
+ },
+ }),
+ );
+ handlers.get('contextmenu')!({ id: country });
+ expect(onContextMenu).toHaveBeenCalledWith(
+ 100,
+ 200,
+ expect.objectContaining({
+ drillToDetail: [
+ {
+ col: 'country',
+ op: '==',
+ val: sourceValue,
+ formattedVal: sourceValue,
+ },
+ ],
+ drillBy: {
+ filters: [{ col: 'country', op: '==', val: sourceValue }],
+ groupbyFieldName: 'entity',
+ },
+ }),
+ );
+ } finally {
+ Object.assign(d3, { event: originalEvent });
+ }
+ },
+);
+
test('draws bubbles for the processed data when showBubbles is true', () => {
WorldMap(container, { ...baseProps, showBubbles: true });
diff --git a/superset-frontend/plugins/plugin-chart-world-map/test/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-world-map/test/controlPanel.test.ts
new file mode 100644
index 00000000000..c279e7a3740
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-world-map/test/controlPanel.test.ts
@@ -0,0 +1,43 @@
+/**
+ * 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 { testGeographicTimeProvenance } from 'spec/helpers/geographicControlPanel';
+import controlPanel from '../src/controlPanel';
+
+const controlNames = (): string[] =>
+ (controlPanel.controlPanelSections ?? [])
+ .flatMap(section => section?.controlSetRows ?? [])
+ .flat()
+ .map(item =>
+ typeof item === 'string'
+ ? item
+ : (item as { name?: string } | null)?.name,
+ )
+ .filter((name): name is string => Boolean(name));
+
+test('keeps the MCP geographic marker and row limit through Explore hydration', () => {
+ expect(controlNames()).toEqual(
+ expect.arrayContaining([
+ 'mcp_geographic',
+ 'row_limit',
+ '_mcp_dashboard_time_filter_subject',
+ ]),
+ );
+});
+
+testGeographicTimeProvenance(controlPanel);
diff --git a/superset-frontend/plugins/plugin-chart-world-map/test/transformData.test.ts b/superset-frontend/plugins/plugin-chart-world-map/test/transformData.test.ts
index 099c32c6b80..9ad26da3415 100644
--- a/superset-frontend/plugins/plugin-chart-world-map/test/transformData.test.ts
+++ b/superset-frontend/plugins/plugin-chart-world-map/test/transformData.test.ts
@@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { WORLD_BOUNDARY_IDS } from '../src/worldGeometry';
+import { getCountry } from '../src/countries';
import transformData from '../src/transformData';
const formData = {
@@ -81,3 +83,170 @@ test('joins by full country name', () => {
expect(rows[0].country).toEqual('FRA');
expect(rows[0].code).toEqual('France');
});
+
+test('typed world maps retain country and bubble metric semantics', () => {
+ expect(
+ transformData([{ country: 'us', sales: 10, population: 20 }], {
+ entity: 'country',
+ metric: 'sales',
+ secondaryMetric: 'population',
+ countryFieldtype: 'cca2',
+ strict: true,
+ })[0],
+ ).toMatchObject({ country: 'USA', m1: 10, m2: 20 });
+});
+
+test.each([
+ { country: 'not-a-country', sales: 10, population: 20 },
+ { country: 'US', sales: null, population: 20 },
+ { country: 'US', sales: 10, population: -1 },
+])(
+ 'typed world maps reject invalid values instead of XXX placeholders',
+ row => {
+ expect(() =>
+ transformData([row], {
+ entity: 'country',
+ metric: 'sales',
+ secondaryMetric: 'population',
+ countryFieldtype: 'cca2',
+ strict: true,
+ showBubbles: true,
+ }),
+ ).toThrow();
+ },
+);
+
+test.each([-1, null, Number.NaN, 'n/a'])(
+ 'typed world maps without bubbles ignore an unused secondary metric %s',
+ population => {
+ const options = {
+ entity: 'country',
+ metric: 'sales',
+ secondaryMetric: 'population',
+ countryFieldtype: 'cca2',
+ strict: true,
+ };
+ const row = { country: 'US', sales: 10, population };
+ expect(
+ transformData([row], { ...options, showBubbles: false })[0],
+ ).toMatchObject({ country: 'USA', m1: 10 });
+ expect(() =>
+ transformData([row], { ...options, showBubbles: true }),
+ ).toThrow();
+ },
+);
+
+test.each(['Curaçao', 'Curacao', 'CURAÇAO'])(
+ 'typed world maps render diacritic-folded country name %s',
+ country => {
+ const data = [{ country_code: country, sum__num: 1 }];
+ expect(
+ transformData(data, {
+ ...formData,
+ countryFieldtype: 'name',
+ strict: true,
+ showBubbles: true,
+ secondaryMetric: formData.metric,
+ }),
+ ).toMatchObject([{ country: 'CUW', name: 'Curacao', m1: 1 }]);
+ expect(data[0].country_code).toBe(country);
+ },
+);
+
+test.each([
+ ['cca2', 'Áo'],
+ ['cca3', 'ÁGO'],
+ ['cioc', 'ÁNG'],
+])(
+ 'typed world maps do not fold diacritics in %s codes',
+ (countryFieldtype, country) => {
+ expect(() =>
+ transformData([{ country_code: country, sum__num: 1 }], {
+ ...formData,
+ countryFieldtype,
+ strict: true,
+ }),
+ ).toThrow('Unrecognized');
+ },
+);
+
+test.each(['cca2', 'cca3', 'cioc', 'name'])(
+ 'empty country values never resolve through %s lookup',
+ countryFieldtype => {
+ expect(getCountry(countryFieldtype, '')).toBeUndefined();
+ const options = { ...formData, countryFieldtype };
+ const records = [{ country_code: '', sum__num: 1 }];
+ expect(() => transformData(records, { ...options, strict: true })).toThrow(
+ 'Unrecognized',
+ );
+ expect(transformData(records, options)[0].country).toBe('XXX');
+ },
+);
+
+test('world boundary snapshots match the real imported Datamaps topology', () => {
+ const Datamap = jest.requireActual<{
+ prototype: {
+ worldTopo: { objects: { world: { geometries: { id: string }[] } } };
+ };
+ }>('datamaps/dist/datamaps.all.min');
+ const geometryIds = new Set(
+ Datamap.prototype.worldTopo.objects.world.geometries.map(
+ geometry => geometry.id,
+ ),
+ );
+ expect(geometryIds.has('USA')).toBe(true);
+ expect(geometryIds.has('SGP')).toBe(false);
+ expect(WORLD_BOUNDARY_IDS).toEqual(geometryIds);
+});
+
+test('strict choropleths reject Singapore while bubbles can draw it', () => {
+ const records = [{ country_code: 'SG', sum__num: 42 }];
+ const options = { ...formData, countryFieldtype: 'cca2', strict: true };
+ expect(() => transformData(records, options)).toThrow(
+ 'SGP has no world-map boundary',
+ );
+ expect(
+ transformData(records, {
+ ...options,
+ showBubbles: true,
+ secondaryMetric: formData.metric,
+ })[0],
+ ).toMatchObject({ country: 'SGP', m1: 42, m2: 42 });
+ expect(transformData(records, { ...options, strict: false })[0].country).toBe(
+ 'SGP',
+ );
+});
+
+test('saved Custom SQL entities resolve their result label', () => {
+ expect(
+ transformData([{ region: 'USA', sum__num: 42 }], {
+ ...formData,
+ entity: {
+ sqlExpression: 'UPPER(country_code)',
+ label: 'region',
+ expressionType: 'SQL',
+ },
+ strict: true,
+ })[0],
+ ).toMatchObject({ country: 'USA', sourceValue: 'USA', m1: 42 });
+});
+
+test.each(['US', 'us'])(
+ 'typed world maps reject two country aggregates resolving to USA (%s)',
+ country => {
+ expect(() =>
+ transformData(
+ [
+ { country: 'US', sales: 10 },
+ { country, sales: 20 },
+ ],
+ {
+ entity: 'country',
+ metric: 'sales',
+ countryFieldtype: 'cca2',
+ strict: true,
+ },
+ ),
+ ).toThrow('Unrecognized or duplicate country value');
+ },
+);
diff --git a/superset-frontend/plugins/plugin-chart-world-map/test/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-world-map/test/transformProps.test.ts
index 91af7950ff1..2bd467cb43f 100644
--- a/superset-frontend/plugins/plugin-chart-world-map/test/transformProps.test.ts
+++ b/superset-frontend/plugins/plugin-chart-world-map/test/transformProps.test.ts
@@ -78,3 +78,55 @@ test('passes through pre-shaped legacy payloads', () => {
};
expect(data).toEqual([legacyRow]);
});
+
+test.each([null, undefined, {}, 'invalid'])(
+ 'rejects malformed typed query data %p with an actionable error',
+ data => {
+ const props = new ChartProps({
+ width: 800,
+ height: 600,
+ theme: supersetTheme,
+ datasource: { columnFormats: {}, currencyFormats: {}, verboseMap: {} },
+ formData: {
+ mcp_geographic: true,
+ entity: 'country',
+ metric: 'sales',
+ color_picker: { r: 0, g: 0, b: 0, a: 1 },
+ },
+ queriesData: [{ data: data as never }],
+ });
+ expect(() => transformProps(props)).toThrow(
+ 'Expected geographic query data to be a list of records',
+ );
+ },
+);
+
+test.each(['US', 'us'])(
+ 'typed chart props reject duplicate country aggregates (%s)',
+ country => {
+ const props = new ChartProps({
+ width: 800,
+ height: 600,
+ theme: supersetTheme,
+ datasource: { columnFormats: {}, currencyFormats: {}, verboseMap: {} },
+ formData: {
+ mcp_geographic: true,
+ entity: 'country',
+ country_fieldtype: 'cca2',
+ metric: 'sales',
+ color_picker: { r: 0, g: 0, b: 0, a: 1 },
+ },
+ queriesData: [
+ {
+ data: [
+ { country: 'US', sales: 10 },
+ { country, sales: 20 },
+ ],
+ },
+ ],
+ });
+ expect(() => transformProps(props)).toThrow(
+ 'Unrecognized or duplicate country value',
+ );
+ },
+);
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.test.ts
index 99d4e78056e..bc3151c6fb8 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.test.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.test.ts
@@ -530,3 +530,27 @@ test('Scatter buildQuery should deduplicate legacy string metric with existing m
expect(query.metrics).toEqual(['COUNT(*)', 'SUM(value)']);
expect(query.metrics).toHaveLength(2);
});
+
+test.each(['100', '2.5'])(
+ 'typed geographic buildQuery treats numeric-string radius %s as fixed',
+ radius => {
+ const [query] = buildQuery({
+ ...baseFormData,
+ mcp_geographic: true,
+ point_radius_fixed: radius,
+ }).queries;
+
+ expect(query.metrics).toEqual([]);
+ expect(query.orderby).toEqual([]);
+ },
+);
+
+test('native buildQuery keeps a numeric-string radius as a saved metric', () => {
+ const [query] = buildQuery({
+ ...baseFormData,
+ point_radius_fixed: '100',
+ }).queries;
+
+ expect(query.metrics).toEqual(['100']);
+ expect(query.orderby).toEqual([['100', false]]);
+});
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.ts
index 5519b7cd4e6..f5bfb519f95 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.ts
@@ -31,7 +31,7 @@ import {
SpatialFormData,
} from '../spatialUtils';
import { addTooltipColumnsToQuery } from '../buildQueryUtils';
-import { isMetricValue } from '../utils/metricUtils';
+import { getTypedFixedRadius, isMetricValue } from '../utils/metricUtils';
export interface DeckScatterFormData
extends Omit<SpatialFormData, 'color_picker'>, SqlaFormData {
@@ -48,6 +48,7 @@ export interface DeckScatterFormData
max_radius?: number;
color_picker?: { r: number; g: number; b: number; a: number };
dimension?: string;
+ mcp_geographic?: boolean;
}
export default function buildQuery(formData: DeckScatterFormData) {
@@ -69,7 +70,9 @@ export default function buildQuery(formData: DeckScatterFormData) {
columns = addTooltipColumnsToQuery(columns, tooltip_contents);
// Only add metric if point_radius_fixed is a metric type
- const isMetric = isMetricValue(point_radius_fixed);
+ const isMetric =
+ getTypedFixedRadius(formData) === null &&
+ isMetricValue(point_radius_fixed);
// Extract metric value: legacy string format or object with metric value
const rawValue =
typeof point_radius_fixed === 'string'
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/controlPanel.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/controlPanel.test.ts
new file mode 100644
index 00000000000..fa5eae3a8c9
--- /dev/null
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/controlPanel.test.ts
@@ -0,0 +1,43 @@
+/**
+ * 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 { testGeographicTimeProvenance } from 'spec/helpers/geographicControlPanel';
+import controlPanel from './controlPanel';
+
+const controlNames = (): string[] =>
+ (controlPanel.controlPanelSections ?? [])
+ .flatMap(section => section?.controlSetRows ?? [])
+ .flat()
+ .map(item =>
+ typeof item === 'string'
+ ? item
+ : (item as { name?: string } | null)?.name,
+ )
+ .filter((name): name is string => Boolean(name));
+
+test('keeps the MCP geographic marker and row limit through Explore hydration', () => {
+ expect(controlNames()).toEqual(
+ expect.arrayContaining([
+ 'mcp_geographic',
+ 'row_limit',
+ '_mcp_dashboard_time_filter_subject',
+ ]),
+ );
+});
+
+testGeographicTimeProvenance(controlPanel);
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/controlPanel.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/controlPanel.ts
index 0fe63dbb53c..0a40d26cfe3 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/controlPanel.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/controlPanel.ts
@@ -59,6 +59,8 @@ const config: ControlPanelConfig = {
['adhoc_filters'],
[tooltipContents],
[tooltipTemplate],
+ ['mcp_geographic'],
+ ['_mcp_dashboard_time_filter_subject'],
],
},
{
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.test.ts
index bbdf7ced7bf..9728425c937 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.test.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.test.ts
@@ -18,7 +18,10 @@
*/
import { ChartProps, DatasourceType } from '@superset-ui/core';
-import transformProps from './transformProps';
+import { logging } from '@apache-superset/core/utils';
+import transformProps, {
+ filterDrawableGeographicPoints,
+} from './transformProps';
interface ScatterFeature {
position: [number, number];
@@ -301,3 +304,322 @@ test('Scatter transformProps should preserve extra properties from records', ()
another_field: 123,
});
});
+
+test.each([91, -91, Number.NaN, Number.POSITIVE_INFINITY, '37.8', null])(
+ 'typed geographic points skip invalid latitude %s',
+ latitude => {
+ const props = {
+ ...mockChartProps,
+ rawFormData: {
+ ...mockChartProps.rawFormData,
+ mcp_geographic: true,
+ point_radius_fixed: { type: 'fix', value: 1000 },
+ },
+ queriesData: [{ data: [{ LATITUDE: latitude, LONGITUDE: -122.4 }] }],
+ } as ChartProps;
+ expect(transformProps(props).payload.data.features).toEqual([]);
+ },
+);
+
+test('typed geographic points preserve longitude-latitude ordering and metric radius', () => {
+ const props = {
+ ...mockChartProps,
+ rawFormData: {
+ ...mockChartProps.rawFormData,
+ mcp_geographic: true,
+ point_radius_fixed: { type: 'metric', value: 'population' },
+ },
+ } as ChartProps;
+ expect(transformProps(props).payload.data.features[0]).toMatchObject({
+ position: [-122.4, 37.8],
+ radius: 50000,
+ });
+});
+
+test.each([undefined, null, 'missing', '10', Number.NaN, Infinity, -1])(
+ 'typed geographic points skip sparse/non-numeric radius %s',
+ population => {
+ const sparse = { LATITUDE: 37.8, LONGITUDE: -122.4 };
+ const data = [
+ population === undefined ? sparse : { ...sparse, population },
+ { LATITUDE: 37.9, LONGITUDE: -122.3, population: 0 },
+ { LATITUDE: 38, LONGITUDE: -122, population: 10 },
+ ];
+ const original = data.map(row => ({ ...row }));
+ const props = {
+ ...mockChartProps,
+ rawFormData: {
+ ...mockChartProps.rawFormData,
+ mcp_geographic: true,
+ point_radius_fixed: { type: 'metric', value: 'population' },
+ },
+ queriesData: [{ data }],
+ } as ChartProps;
+ expect(transformProps(props).payload.data.features).toMatchObject([
+ { position: [-122.3, 37.9], radius: 0 },
+ { position: [-122, 38], radius: 10 },
+ ]);
+ expect(data).toEqual(original);
+ },
+);
+
+test('typed geographic points log how many points were skipped', () => {
+ const warn = jest.spyOn(logging, 'warn').mockImplementation(() => {});
+ try {
+ const props = {
+ ...mockChartProps,
+ rawFormData: {
+ ...mockChartProps.rawFormData,
+ mcp_geographic: true,
+ point_radius_fixed: { type: 'metric', value: 'population' },
+ },
+ queriesData: [
+ {
+ data: [
+ { LATITUDE: 91, LONGITUDE: -122.4, population: 1 },
+ { LATITUDE: 37.8, LONGITUDE: -122.4, population: null },
+ { LATITUDE: 38, LONGITUDE: -122, population: 10 },
+ ],
+ },
+ ],
+ } as ChartProps;
+ expect(transformProps(props).payload.data.features).toHaveLength(1);
+ expect(warn).toHaveBeenCalledWith(
+ expect.stringContaining('Skipped 2 of 3 geographic points'),
+ );
+ } finally {
+ warn.mockRestore();
+ }
+});
+
+test('filterDrawableGeographicPoints keeps valid points without logging', () => {
+ const warn = jest.spyOn(logging, 'warn').mockImplementation(() => {});
+ try {
+ const records = [
+ { LATITUDE: 90, LONGITUDE: -180 },
+ { LATITUDE: -90, LONGITUDE: 180 },
+ ];
+ expect(
+ filterDrawableGeographicPoints(records, {
+ type: 'latlong',
+ latCol: 'LATITUDE',
+ lonCol: 'LONGITUDE',
+ }),
+ ).toEqual(records);
+ expect(warn).not.toHaveBeenCalled();
+ } finally {
+ warn.mockRestore();
+ }
+});
+
+test.each([
+ { type: 'geohash', geohashCol: 'location' },
+ { type: 'delimited', lonlatCol: 'location' },
+])(
+ 'MCP points remain drawable after switching spatial format to $type',
+ spatial => {
+ const props = {
+ ...mockChartProps,
+ rawFormData: {
+ ...mockChartProps.rawFormData,
+ mcp_geographic: true,
+ spatial,
+ point_radius_fixed: { type: 'metric', value: 'population' },
+ },
+ queriesData: [
+ {
+ data: [
+ {
+ location: spatial.type === 'geohash' ? '9q8yy' : '-122.4,37.8',
+ population: 10,
+ },
+ {
+ location: spatial.type === 'geohash' ? '9q8yy' : '-122.4,37.8',
+ population: -1,
+ },
+ ],
+ },
+ ],
+ } as ChartProps;
+ const features = transformProps(props).payload.data
+ .features as ScatterFeature[];
+ expect(features).toHaveLength(1);
+ expect(features[0].position[0]).toBeCloseTo(-122.4, 1);
+ expect(features[0].position[1]).toBeCloseTo(37.8, 1);
+ expect(features[0].radius).toBe(10);
+ },
+);
+
+test.each(['position', 'weight', 'extraProps'])(
+ 'typed scatter skips radius alias %s rather than corrupting native spatial fields',
+ label => {
+ const props = {
+ ...mockChartProps,
+ rawFormData: {
+ ...mockChartProps.rawFormData,
+ mcp_geographic: true,
+ point_radius_fixed: { type: 'metric', value: label },
+ },
+ queriesData: [
+ {
+ data: [{ LATITUDE: 37.8, LONGITUDE: -122.4, [label]: 10 }],
+ },
+ ],
+ } as ChartProps;
+ expect(transformProps(props).payload.data.features).toEqual([]);
+ },
+);
+
+test.each(['position', 'weight', 'extraProps'])(
+ 'fixed-radius scatter keeps coordinates when dimension is %s',
+ dimension => {
+ const props = {
+ ...mockChartProps,
+ rawFormData: {
+ ...mockChartProps.rawFormData,
+ mcp_geographic: true,
+ dimension,
+ point_radius_fixed: { type: 'fix', value: 100 },
+ },
+ queriesData: [
+ { data: [{ LATITUDE: 37.8, LONGITUDE: -122.4, [dimension]: 'A' }] },
+ ],
+ };
+ const result = transformProps(props as ChartProps);
+ const features = result.payload.data.features as ScatterFeature[];
+ expect(features).toHaveLength(1);
+ expect(features[0].position).toEqual([-122.4, 37.8]);
+ expect(features[0].cat_color).toBe('A');
+ expect(features[0].radius).toBe(100);
+ },
+);
+
+test('legacy scatter reads a weight radius from the source without replacing geometry', () => {
+ const props = {
+ ...mockChartProps,
+ rawFormData: {
+ ...mockChartProps.rawFormData,
+ point_radius_fixed: { type: 'metric', value: 'weight' },
+ },
+ queriesData: [
+ { data: [{ LATITUDE: 37.8, LONGITUDE: -122.4, weight: 42 }] },
+ ],
+ };
+ const features = transformProps(props as ChartProps).payload.data
+ .features as ScatterFeature[];
+ expect(features[0].position).toEqual([-122.4, 37.8]);
+ expect(features[0].radius).toBe(42);
+});
+
+test.each(['constructor', 'toString', 'hasOwnProperty', 'valueOf'])(
+ 'scatter reads prototype-named radius metric %s from the source',
+ label => {
+ const props = {
+ ...mockChartProps,
+ rawFormData: {
+ ...mockChartProps.rawFormData,
+ mcp_geographic: true,
+ point_radius_fixed: { type: 'metric', value: label },
+ },
+ queriesData: [
+ { data: [{ LATITUDE: 37.8, LONGITUDE: -122.4, [label]: 42 }] },
+ ],
+ };
+ const features = transformProps(props as ChartProps).payload.data
+ .features as ScatterFeature[];
+ expect(features).toHaveLength(1);
+ expect(features[0]).toMatchObject({
+ position: [-122.4, 37.8],
+ radius: 42,
+ metric: 42,
+ });
+ },
+);
+
+test.each(['constructor', 'toString', 'hasOwnProperty', 'valueOf'])(
+ 'scatter reads prototype-named dimension %s from the source',
+ dimension => {
+ const props = {
+ ...mockChartProps,
+ rawFormData: {
+ ...mockChartProps.rawFormData,
+ mcp_geographic: true,
+ dimension,
+ point_radius_fixed: { type: 'fix', value: 100 },
+ },
+ queriesData: [
+ { data: [{ LATITUDE: 37.8, LONGITUDE: -122.4, [dimension]: 'A' }] },
+ ],
+ };
+ const features = transformProps(props as ChartProps).payload.data
+ .features as ScatterFeature[];
+ expect(features).toHaveLength(1);
+ expect(features[0]).toMatchObject({
+ position: [-122.4, 37.8],
+ radius: 100,
+ cat_color: 'A',
+ });
+ },
+);
+
+test.each(['100', '2.5', '0'])(
+ 'typed geographic points preserve legacy fixed radius %s',
+ radius => {
+ const result = transformProps({
+ ...mockChartProps,
+ rawFormData: {
+ ...mockChartProps.rawFormData,
+ mcp_geographic: true,
+ point_radius_fixed: radius,
+ },
+ } as ChartProps);
+ const features = result.payload.data.features as ScatterFeature[];
+ expect(features).toHaveLength(2);
+ features.forEach(feature => {
+ expect(feature.radius).toBe(Number(radius));
+ expect(feature.metric).toBeUndefined();
+ });
+ expect(result.payload.data.metricLabels).toEqual([]);
+ },
+);
+
+test('typed geographic points preserve legacy saved-metric radius', () => {
+ const result = transformProps({
+ ...mockChartProps,
+ rawFormData: {
+ ...mockChartProps.rawFormData,
+ mcp_geographic: true,
+ point_radius_fixed: 'population',
+ },
+ } as ChartProps);
+ const features = result.payload.data.features as ScatterFeature[];
+ expect(features).toHaveLength(2);
+ expect(features.map(feature => feature.radius)).toEqual([50000, 75000]);
+ expect(result.payload.data.metricLabels).toEqual(['population']);
+});
+
+test.each([undefined, false])(
+ 'native scatter preserves numeric-string metric names with mcp_geographic=%s',
+ mcpGeographic => {
+ const result = transformProps({
+ ...mockChartProps,
+ rawFormData: {
+ ...mockChartProps.rawFormData,
+ mcp_geographic: mcpGeographic,
+ point_radius_fixed: '100',
+ },
+ queriesData: [
+ {
+ data: [
+ { LATITUDE: 37.8, LONGITUDE: -122.4, '100': 10 },
+ { LATITUDE: 37.9, LONGITUDE: -122.3, '100': 20 },
+ ],
+ },
+ ],
+ } as ChartProps);
+ const features = result.payload.data.features as ScatterFeature[];
+ expect(features.map(feature => feature.radius)).toEqual([10, 20]);
+ expect(features.map(feature => feature.metric)).toEqual([10, 20]);
+ expect(result.payload.data.metricLabels).toEqual(['100']);
+ },
+);
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts
index ad8f7e3eedf..6f2f2612d0a 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts
@@ -17,6 +17,7 @@
* under the License.
*/
import { ChartProps } from '@superset-ui/core';
+import { logging } from '@apache-superset/core/utils';
import { processSpatialData, DataRecord } from '../spatialUtils';
import {
createBaseTransformResult,
@@ -26,7 +27,11 @@ import {
addPropertiesToFeature,
} from '../transformUtils';
import { DeckScatterFormData } from './buildQuery';
-import { isFixedValue, getFixedValue } from '../utils/metricUtils';
+import {
+ isFixedValue,
+ getFixedValue,
+ getTypedFixedRadius,
+} from '../utils/metricUtils';
interface ScatterPoint {
position: [number, number];
@@ -38,6 +43,58 @@ interface ScatterPoint {
[key: string]: unknown;
}
+function isFiniteNumber(value: unknown): value is number {
+ return typeof value === 'number' && Number.isFinite(value);
+}
+
+/**
+ * Keep only typed geographic points that can be drawn: numeric, in-range
+ * latitude/longitude and, for a metric radius, a finite nonnegative radius.
+ * Other spatial formats use the native decoder after Explore format changes.
+ *
+ * The MCP data and export contract rejects such rows outright; the render
+ * layer skips them so one sparse row does not blank the whole map, and logs
+ * how many points were skipped so the loss is diagnosable.
+ */
+export function filterDrawableGeographicPoints(
+ records: DataRecord[],
+ spatial: DeckScatterFormData['spatial'],
+ radiusMetricLabel?: string,
+): DataRecord[] {
+ const coordinateColumns = [
+ [spatial?.latCol, 90],
+ [spatial?.lonCol, 180],
+ ] as const;
+ const reservedMetricLabel =
+ radiusMetricLabel !== undefined &&
+ ['position', 'weight', 'extraProps'].includes(radiusMetricLabel);
+ const drawable = records.filter(record => {
+ // Explore can change a metric label after MCP schema validation.
+ if (reservedMetricLabel) {
+ return false;
+ }
+ const validCoordinates =
+ spatial?.type !== 'latlong' ||
+ coordinateColumns.every(([column, bound]) => {
+ const value = column ? record[column] : undefined;
+ return isFiniteNumber(value) && Math.abs(value) <= bound;
+ });
+ if (!validCoordinates || !radiusMetricLabel) {
+ return validCoordinates;
+ }
+ const radius = record[radiusMetricLabel];
+ return isFiniteNumber(radius) && radius >= 0;
+ });
+ const skipped = records.length - drawable.length;
+ if (skipped > 0) {
+ logging.warn(
+ `Skipped ${skipped} of ${records.length} geographic points with ` +
+ 'missing or out-of-range coordinates or radius',
+ );
+ }
+ return drawable;
+}
+
function processScatterData(
records: DataRecord[],
spatial: DeckScatterFormData['spatial'],
@@ -66,30 +123,41 @@ function processScatterData(
categoryColumn,
]);
+ const parsedFixedRadius = parseMetricValue(fixedRadiusValue);
+
return spatialFeatures.map(feature => {
let scatterPoint: ScatterPoint = {
position: feature.position,
extraProps: feature.extraProps || {},
};
+ // Reserved source columns live in extraProps to protect computed geometry.
+ const getSourceValue = (column?: string): unknown =>
+ column
+ ? feature.extraProps &&
+ Object.prototype.hasOwnProperty.call(feature.extraProps, column)
+ ? feature.extraProps[column]
+ : feature[column]
+ : undefined;
+
// Handle radius: either from metric or fixed value
if (fixedRadiusValue != null) {
// Use fixed radius value for all points
- const parsedFixedRadius = parseMetricValue(fixedRadiusValue);
if (parsedFixedRadius !== undefined) {
scatterPoint.radius = parsedFixedRadius;
}
- } else if (radiusMetricLabel && feature[radiusMetricLabel] != null) {
+ } else if (radiusMetricLabel) {
// Use metric value for radius
- const radiusValue = parseMetricValue(feature[radiusMetricLabel]);
+ const radiusValue = parseMetricValue(getSourceValue(radiusMetricLabel));
if (radiusValue !== undefined) {
scatterPoint.radius = radiusValue;
scatterPoint.metric = radiusValue;
}
}
- if (categoryColumn && feature[categoryColumn] != null) {
- scatterPoint.cat_color = String(feature[categoryColumn]);
+ const categoryValue = getSourceValue(categoryColumn);
+ if (categoryValue != null) {
+ scatterPoint.cat_color = String(categoryValue);
}
scatterPoint = addPropertiesToFeature(
@@ -106,16 +174,26 @@ export default function transformProps(chartProps: ChartProps) {
const { spatial, point_radius_fixed, dimension } =
formData as DeckScatterFormData;
- // Check if this is a fixed value or metric
- const fixedRadiusValue = isFixedValue(point_radius_fixed)
- ? getFixedValue(point_radius_fixed)
- : null;
+ const legacyFixedRadius = getTypedFixedRadius(
+ formData as DeckScatterFormData,
+ );
+ const fixedRadiusValue =
+ legacyFixedRadius ??
+ (isFixedValue(point_radius_fixed)
+ ? getFixedValue(point_radius_fixed)
+ : null);
- const radiusMetricLabel = getMetricLabelFromFormData(point_radius_fixed);
+ const radiusMetricLabel =
+ legacyFixedRadius !== null
+ ? undefined
+ : getMetricLabelFromFormData(point_radius_fixed);
const records = getRecordsFromQuery(chartProps.queriesData);
+ const displayRecords = formData.mcp_geographic
+ ? filterDrawableGeographicPoints(records, spatial, radiusMetricLabel)
+ : records;
const features = processScatterData(
- records,
+ displayRecords,
spatial,
radiusMetricLabel,
dimension,
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/spatialUtils.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/spatialUtils.ts
index ce2209f4dc6..fe497a827d3 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/spatialUtils.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/spatialUtils.ts
@@ -256,10 +256,11 @@ export function processSpatialData(
}
}
+ const extraProps: Record<string, unknown> = {};
const spatialPoint: SpatialPoint = {
position,
weight,
- extraProps: {},
+ extraProps,
};
Object.keys(record).forEach(key => {
@@ -271,6 +272,10 @@ export function processSpatialData(
return;
}
+ if (['position', 'weight', 'extraProps'].includes(key)) {
+ extraProps[key] = record[key];
+ return;
+ }
spatialPoint[key] = record[key];
});
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/utils/metricUtils.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/utils/metricUtils.ts
index 688ef78e083..4c77787b8ca 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/utils/metricUtils.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/utils/metricUtils.ts
@@ -118,3 +118,20 @@ export function getFixedValue(
return undefined;
}
+
+/**
+ * Typed MCP compatibility reads a bare numeric-string radius as a fixed size.
+ * Native charts interpret every bare string as a saved metric name.
+ */
+export function getTypedFixedRadius(formData: {
+ mcp_geographic?: boolean;
+ point_radius_fixed?: unknown;
+}): number | null {
+ const { mcp_geographic, point_radius_fixed } = formData;
+ return mcp_geographic &&
+ typeof point_radius_fixed === 'string' &&
+ point_radius_fixed.trim() !== '' &&
+ Number.isFinite(Number(point_radius_fixed))
+ ? Number(point_radius_fixed)
+ : null;
+}
diff --git a/superset-frontend/spec/helpers/geographicControlPanel.ts b/superset-frontend/spec/helpers/geographicControlPanel.ts
new file mode 100644
index 00000000000..a9eb6e5960e
--- /dev/null
+++ b/superset-frontend/spec/helpers/geographicControlPanel.ts
@@ -0,0 +1,75 @@
+/**
+ * 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 {
+ DatasourceType,
+ QueryFormData,
+ getChartControlPanelRegistry,
+} from '@superset-ui/core';
+import {
+ ControlPanelConfig,
+ ControlStateMapping,
+} from '@superset-ui/chart-controls';
+import {
+ getAllControlsState,
+ getFormDataFromControls,
+} from 'src/explore/controlUtils';
+
+/** Registers the Explore save round-trip tests for geographic time provenance. */
+export function testGeographicTimeProvenance(
+ controlPanel: ControlPanelConfig,
+): void {
+ test.each(['event_time', null])(
+ 'Explore saves preserve dashboard-time provenance %s',
+ subject => {
+ const vizType = 'test-geographic-time-binding';
+ const registry = getChartControlPanelRegistry();
+ registry.registerValue(vizType, controlPanel);
+ try {
+ const input: QueryFormData = {
+ viz_type: vizType,
+ datasource: '3__table',
+ mcp_geographic: true,
+ _mcp_dashboard_time_filter_subject: subject,
+ adhoc_filters: subject
+ ? [
+ {
+ subject,
+ operator: 'TEMPORAL_RANGE',
+ comparator: 'No filter',
+ clause: 'WHERE',
+ expressionType: 'SIMPLE',
+ },
+ ]
+ : [],
+ };
+ const controls = getAllControlsState(
+ vizType,
+ DatasourceType.Table,
+ null,
+ input,
+ );
+ const saved = getFormDataFromControls(controls as ControlStateMapping);
+ expect(saved._mcp_dashboard_time_filter_subject).toBe(subject);
+ expect(saved.adhoc_filters).toEqual(input.adhoc_filters);
+ } finally {
+ registry.remove(vizType);
+ }
+ },
+ );
+}
diff --git a/superset/mcp_service/chart/chart_helpers.py b/superset/mcp_service/chart/chart_helpers.py
index 094d554a854..c3707175839 100644
--- a/superset/mcp_service/chart/chart_helpers.py
+++ b/superset/mcp_service/chart/chart_helpers.py
@@ -1197,17 +1197,11 @@ def build_query_dicts_from_form_data(
# Deck.gl charts use spatial column configs rather than the standard
# metrics / groupby fields. Extract columns from the spatial controls.
if viz_type.startswith("deck_"):
- from superset.common.form_data_query_context import normalize_time_column
-
- qd = build_query_objects_from_form_data(
- form_data,
- viz_type=viz_type,
- row_limit=row_limit,
- order_desc=order_desc,
- filters_prepared=True,
- )[0]
- qd = _deck_query_adapter(form_data, qd, viz_type)
- return [normalize_time_column(form_data, qd)]
+ return [
+ build_deck_gl_query_dict(
+ form_data, viz_type, row_limit=row_limit, order_desc=order_desc
+ )
+ ]
return build_query_objects_from_form_data(
form_data,
viz_type=viz_type,
@@ -1218,6 +1212,33 @@ def build_query_dicts_from_form_data(
)
+def build_deck_gl_query_dict(
+ form_data: dict[str, Any],
+ viz_type: str,
+ *,
+ row_limit: int | None = None,
+ order_desc: bool | None = None,
+) -> dict[str, Any]:
+ """Build the single query a Deck.gl layer issues from its spatial controls.
+
+ ``form_data`` must already be prepared for querying (filters normalized).
+ """
+ from superset.common.form_data_query_context import (
+ build_query_objects_from_form_data,
+ normalize_time_column,
+ )
+
+ qd = build_query_objects_from_form_data(
+ form_data,
+ viz_type=viz_type,
+ row_limit=row_limit,
+ order_desc=order_desc,
+ filters_prepared=True,
+ )[0]
+ qd = _deck_query_adapter(form_data, qd, viz_type)
+ return normalize_time_column(form_data, qd)
+
+
def resolve_form_data_datasource(
form_data: dict[str, Any],
chart: Any | None = None,
diff --git a/superset/mcp_service/chart/chart_utils.py b/superset/mcp_service/chart/chart_utils.py
index da9b7f1bf4f..cf546dc27d3 100644
--- a/superset/mcp_service/chart/chart_utils.py
+++ b/superset/mcp_service/chart/chart_utils.py
@@ -24,7 +24,7 @@ generation that can be used by both generate_chart and generate_explore_link too
import hashlib
import logging
-from collections.abc import Mapping
+from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Any, cast, Dict, TYPE_CHECKING
@@ -472,7 +472,7 @@ def map_config_to_form_data(
def _add_adhoc_filters(
- form_data: Dict[str, Any], filters: list[FilterConfig] | None
+ form_data: Dict[str, Any], filters: Sequence[FilterConfig] | None
) -> None:
"""Add adhoc filters to form_data if any are specified."""
if filters:
@@ -910,14 +910,14 @@ def merge_gantt_ui_config(
return validate_gantt_form_data(new_form_data)
-def _without_generated_gauge_time_filter(
+def _without_generated_dashboard_time_filter(
form_data: dict[str, Any],
) -> list[Any]:
"""Return cached filters without the mapper-owned temporal binding."""
generated_subject = form_data.get(MCP_DASHBOARD_TIME_FILTER_SUBJECT)
return [
filter_
- for filter_ in form_data.get("adhoc_filters", [])
+ for filter_ in form_data.get("adhoc_filters") or []
if not (
generated_subject
and isinstance(filter_, dict)
@@ -974,13 +974,13 @@ def _merge_treemap_filters(
"""Separate explicit filter/temporal changes from mapper-generated defaults."""
fields = config.model_fields_set
if "temporal_column" in fields and config.temporal_column is None:
- patch["adhoc_filters"] = _without_generated_gauge_time_filter(patch)
+ patch["adhoc_filters"] = _without_generated_dashboard_time_filter(patch)
patch.pop(MCP_DASHBOARD_TIME_FILTER_SUBJECT, None)
if dataset_rebind:
return
if "temporal_column" not in fields:
# Discard the mapper's default binding before removing its provenance.
- patch["adhoc_filters"] = _without_generated_gauge_time_filter(patch)
+ patch["adhoc_filters"] = _without_generated_dashboard_time_filter(patch)
patch.pop(MCP_DASHBOARD_TIME_FILTER_SUBJECT, None)
if "filters" in fields and config.filters:
if subject := existing.get(MCP_DASHBOARD_TIME_FILTER_SUBJECT):
@@ -989,7 +989,9 @@ def _merge_treemap_filters(
if "filters" not in fields:
if "temporal_column" in fields:
inherited = (
- [] if dataset_rebind else _without_generated_gauge_time_filter(existing)
+ []
+ if dataset_rebind
+ else _without_generated_dashboard_time_filter(existing)
)
patch["adhoc_filters"] = [*inherited, *patch.get("adhoc_filters", [])]
else:
@@ -1047,7 +1049,11 @@ def merge_chart_form_data(
preserve because they do not reference the old dataset.
"""
if existing_form_data.get("viz_type") != new_form_data.get("viz_type"):
- return dict(new_form_data)
+ converted = dict(new_form_data)
+ if converted.get("mcp_geographic"):
+ _apply_geographic_temporal_update(converted, config)
+ return converted
+ # Loading the registry at module scope cycles through plugin imports.
from superset.mcp_service.chart.registry import plugin_for_viz_type
if (plugin := plugin_for_viz_type(new_form_data.get("viz_type"))) is not None:
@@ -1147,7 +1153,7 @@ def merge_gauge_update_form_data( # noqa: C901
preserved_filters = (
[]
if dataset_rebind
- else _without_generated_gauge_time_filter(existing_form_data)
+ else _without_generated_dashboard_time_filter(existing_form_data)
)
generated_filters = patch.get("adhoc_filters", [])
patch["adhoc_filters"] = [*preserved_filters, *generated_filters]
@@ -3568,9 +3574,17 @@ def analyze_chart_capabilities(viz_type: str | None, config: Any) -> ChartCapabi
from superset.mcp_service.chart.preview_utils import (
plugin_unsupported_preview,
)
+ from superset.mcp_service.chart.registry import plugin_for_viz_type
optimal_formats.append("interactive")
- if plugin_unsupported_preview(viz_type, "vega_lite") is None:
+ # Only advertise Vega-Lite where a spec can actually be produced. A
+ # plugin whose output cannot be expressed as a Vega-Lite spec (e.g. map
+ # geometry) opts out, so the capability list never promises a failing
+ # preview.
+ owner = plugin_for_viz_type(viz_type)
+ if (
+ owner is None or owner.supports_vega_lite_preview
+ ) and plugin_unsupported_preview(viz_type, "vega_lite") is None:
optimal_formats.append("vega_lite")
optimal_formats.extend(["ascii", "table"])
@@ -3609,6 +3623,11 @@ def analyze_chart_semantics(viz_type: str | None, config: Any) -> ChartSemantics
"Interactive table with advanced features like column resizing, "
"sorting, filtering, and server-side pagination"
),
+ "country_map": "Colors regional boundaries by an aggregated metric",
+ "world_map": "Colors countries by a metric with optional metric-sized bubbles",
+ "deck_scatter": (
+ "Plots numeric latitude/longitude locations with optional sized points"
+ ),
"pie": "Shows proportional relationships within a dataset",
"echarts_area": "Emphasizes cumulative totals and part-to-whole relationships",
"pivot_table_v2": (
@@ -3746,3 +3765,128 @@ def preserve_previous_adhoc_filters(
merged_filters.append(generated_filter)
new_form_data["adhoc_filters"] = merged_filters
+
+
+def _apply_geographic_temporal_update(
+ form_data: dict[str, Any], config: ChartConfig
+) -> None:
+ """Apply explicit time changes after native controls have been merged."""
+ if "temporal_column" not in config.model_fields_set:
+ return
+ # Native granularity takes precedence over the requested adhoc binding.
+ form_data.pop("granularity_sqla", None)
+ if config.temporal_column is None:
+ form_data["adhoc_filters"] = _without_generated_dashboard_time_filter(form_data)
+ form_data.pop(MCP_DASHBOARD_TIME_FILTER_SUBJECT, None)
+
+
+def merge_geographic_update_form_data( # noqa: C901
+ existing: dict[str, Any],
+ mapped: dict[str, Any],
+ config: ChartConfig,
+ *,
+ dataset_rebind: bool,
+) -> dict[str, Any]:
+ """Preserve omitted native controls; clear explicit nullable roles and filters.
+
+ Rebinding drops old dataset references and Jinja/query state. All optional
+ roles are remapped from the complete target-dataset config.
+ """
+ if dataset_rebind:
+ existing = {
+ key: value
+ for key, value in existing.items()
+ if key
+ in {
+ "linear_color_scheme",
+ "number_format",
+ "max_bubble_size",
+ "point_unit",
+ "color_by",
+ "color_picker",
+ "color_scheme",
+ "y_axis_format",
+ "map_renderer",
+ "maplibre_style",
+ "mapbox_style",
+ "min_radius",
+ "max_radius",
+ "multiplier",
+ }
+ }
+ field_map = {
+ "country": "select_country",
+ "region_format": "region_format",
+ "country_format": "country_fieldtype",
+ "entity": "entity",
+ "metric": "metric",
+ "secondary_metric": "secondary_metric",
+ "dimension": "dimension",
+ "show_bubbles": "show_bubbles",
+ "max_bubble_size": "max_bubble_size",
+ "sort_by_metric": "sort_by_metric",
+ "linear_color_scheme": "linear_color_scheme",
+ "number_format": "number_format",
+ "row_limit": "row_limit",
+ "time_range": "time_range",
+ "point_unit": "point_unit",
+ }
+ fields = config.model_fields_set
+ patch = dict(mapped)
+ if "temporal_column" in fields and config.temporal_column is None:
+ patch["adhoc_filters"] = _without_generated_dashboard_time_filter(patch)
+ elif "temporal_column" not in fields and not dataset_rebind:
+ # Omission keeps the saved dashboard-time binding instead of remapping
+ # it to the dataset default the mapper generated for this update.
+ # An absent marker also preserves an explicitly cleared binding.
+ saved_binding = [
+ filter_
+ for filter_ in existing.get("adhoc_filters") or []
+ if filter_ not in _without_generated_dashboard_time_filter(existing)
+ ]
+ patch["adhoc_filters"] = [
+ *saved_binding,
+ *_without_generated_dashboard_time_filter(patch),
+ ]
+ if binding := existing.get(MCP_DASHBOARD_TIME_FILTER_SUBJECT):
+ patch[MCP_DASHBOARD_TIME_FILTER_SUBJECT] = binding
+ else:
+ patch.pop(MCP_DASHBOARD_TIME_FILTER_SUBJECT, None)
+ # Native UI-only presentation controls are not part of this public config.
+ for key in (
+ "color_by",
+ "color_picker",
+ "color_scheme",
+ "y_axis_format",
+ "map_renderer",
+ "maplibre_style",
+ "mapbox_style",
+ "viewport",
+ "autozoom",
+ "min_radius",
+ "max_radius",
+ "multiplier",
+ ):
+ if key in existing:
+ patch.pop(key, None)
+ for source, target in field_map.items():
+ if source not in fields and target in existing:
+ patch.pop(target, None)
+ if not {"radius", "radius_metric"} & fields and "point_radius_fixed" in existing:
+ patch.pop("point_radius_fixed", None)
+ if "filters" not in fields:
+ source_form = dict(existing)
+ if "temporal_column" in fields:
+ source_form["adhoc_filters"] = _without_generated_dashboard_time_filter(
+ existing
+ )
+ preserve_previous_adhoc_filters(patch, source_form)
+ merged = {**existing, **patch}
+ _apply_geographic_temporal_update(merged, config)
+ if "time_range" in fields and getattr(config, "time_range", None) is None:
+ merged.pop("time_range", None)
+ if "filters" in fields:
+ merged["adhoc_filters"] = patch.get("adhoc_filters", [])
+ for key in ("filters", "where", "having"):
+ merged.pop(key, None)
+ return merged
diff --git a/superset/mcp_service/chart/compile.py b/superset/mcp_service/chart/compile.py
index bc107f7148d..6fb87a6dc5d 100644
--- a/superset/mcp_service/chart/compile.py
+++ b/superset/mcp_service/chart/compile.py
@@ -40,6 +40,7 @@ from typing import Any, Dict, List, Literal
from sqlalchemy.exc import SQLAlchemyError
from superset.commands.exceptions import CommandException
+from superset.exceptions import QueryObjectValidationError
from superset.mcp_service.chart.chart_helpers import canonicalize_operation_form_data
from superset.mcp_service.chart.query_result import (
first_query_data,
@@ -228,7 +229,12 @@ def _compile_chart( # noqa: C901
tier="compile",
error_obj=_build_compile_error(str(exc)),
)
- except (CommandException, ValueError, KeyError) as exc:
+ except (
+ CommandException,
+ QueryObjectValidationError,
+ ValueError,
+ KeyError,
+ ) as exc:
return CompileResult(
success=False,
error=str(exc),
diff --git a/superset/mcp_service/chart/plugin.py b/superset/mcp_service/chart/plugin.py
index 50b356ca00e..da3bb75ae52 100755
--- a/superset/mcp_service/chart/plugin.py
+++ b/superset/mcp_service/chart/plugin.py
@@ -84,8 +84,12 @@ REGISTERED_PLUGIN_QUERY_ROLE_KEYS = (
)
QUERY_ROLE_KEYS = SHARED_FORM_DATA_QUERY_ROLE_KEYS | REGISTERED_PLUGIN_QUERY_ROLE_KEYS
+DEFAULT_COMPILE_ROW_LIMIT = 10
-def capped_compile_row_limit(form_data: Mapping[str, Any], cap: int = 10) -> int:
+
+def capped_compile_row_limit(
+ form_data: Mapping[str, Any], cap: int = DEFAULT_COMPILE_ROW_LIMIT
+) -> int:
"""Cap compile samples, falling back to the cap for invalid saved limits."""
value = form_data.get("row_limit")
if isinstance(value, bool):
@@ -272,6 +276,9 @@ class ChartTypePlugin(Protocol):
#: saved query roles cannot be trusted on another dataset.
requires_config_for_dataset_rebind: ClassVar[bool]
+ #: Roles named in the guidance returned when such a rebind is rejected.
+ dataset_rebind_roles: ClassVar[str]
+
#: Whether a replacement config on another dataset is merged by the
#: plugin's own rebind contract (``merge_update_form_data`` with
#: ``dataset_rebind=True``) instead of the shared inherited-state pruning.
@@ -301,6 +308,10 @@ class ChartTypePlugin(Protocol):
#: (``add_columns``) instead of requiring a complete replacement config.
supports_column_append: ClassVar[bool]
+ #: Whether a Vega-Lite preview can represent this chart, so capability
+ #: metadata may advertise the ``vega_lite`` format.
+ supports_vega_lite_preview: ClassVar[bool]
+
#: Caveat appended to saved-chart preview descriptions, if any.
preview_note: ClassVar[str | None]
@@ -487,6 +498,7 @@ class BaseChartPlugin:
additional_viz_types: ClassVar[frozenset[str]] = frozenset()
requires_compile_check: ClassVar[bool] = False
requires_config_for_dataset_rebind: ClassVar[bool] = False
+ dataset_rebind_roles: ClassVar[str] = "roles"
strict_dataset_rebind: ClassVar[bool] = False
owns_update_merge: ClassVar[bool] = False
unbound_form_data_is_rebind: ClassVar[bool] = False
@@ -494,6 +506,7 @@ class BaseChartPlugin:
allows_empty_result: ClassVar[bool] = False
resizes_saved_preview: ClassVar[bool] = False
supports_column_append: ClassVar[bool] = False
+ supports_vega_lite_preview: ClassVar[bool] = True
preview_note: ClassVar[str | None] = None
null_data_is_empty: ClassVar[bool] = True
invalid_result_error_code: ClassVar[str] = "INVALID_CHART_RESULT"
diff --git a/superset/mcp_service/chart/plugins/__init__.py b/superset/mcp_service/chart/plugins/__init__.py
index 2b5008f76cf..efc5423eeac 100644
--- a/superset/mcp_service/chart/plugins/__init__.py
+++ b/superset/mcp_service/chart/plugins/__init__.py
@@ -32,6 +32,11 @@ from superset.mcp_service.chart.plugins.box_plot import BoxPlotChartPlugin
from superset.mcp_service.chart.plugins.bubble import BubbleChartPlugin
from superset.mcp_service.chart.plugins.gantt import GanttChartPlugin
from superset.mcp_service.chart.plugins.gauge import GaugeChartPlugin
+from superset.mcp_service.chart.plugins.geographic import (
+ CountryMapChartPlugin,
+ DeckScatterChartPlugin,
+ WorldMapChartPlugin,
+)
from superset.mcp_service.chart.plugins.handlebars import HandlebarsChartPlugin
from superset.mcp_service.chart.plugins.histogram import HistogramChartPlugin
from superset.mcp_service.chart.plugins.interactive_pivot import (
@@ -73,6 +78,9 @@ __all__ = [
"BubbleChartPlugin",
"GanttChartPlugin",
"GaugeChartPlugin",
+ "CountryMapChartPlugin",
+ "WorldMapChartPlugin",
+ "DeckScatterChartPlugin",
"HandlebarsChartPlugin",
"HistogramChartPlugin",
"InteractivePivotChartPlugin",
@@ -85,3 +93,7 @@ __all__ = [
"WaterfallChartPlugin",
"XYChartPlugin",
]
+
+register(CountryMapChartPlugin())
+register(WorldMapChartPlugin())
+register(DeckScatterChartPlugin())
diff --git a/superset/mcp_service/chart/plugins/geographic.py b/superset/mcp_service/chart/plugins/geographic.py
new file mode 100644
index 00000000000..217b40a26f9
--- /dev/null
+++ b/superset/mcp_service/chart/plugins/geographic.py
@@ -0,0 +1,834 @@
+# 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.
+
+"""Native geographic chart mapping shared by the three public map types."""
+
+from __future__ import annotations
+
+import logging
+import math
+import re
+from collections.abc import Mapping
+from decimal import Decimal
+from functools import lru_cache
+from numbers import Real
+from typing import Any, ClassVar, TypeGuard
+
+from superset.mcp_service.chart.chart_utils import (
+ _add_adhoc_filters,
+ _apply_geographic_temporal_update,
+ _DECK_DATASET_ROLE_KEYS,
+ create_metric_object,
+ merge_geographic_update_form_data,
+)
+from superset.mcp_service.chart.plugin import BaseChartPlugin
+from superset.mcp_service.chart.query_result import (
+ column_result_label,
+ metric_result_label,
+ query_result_failure,
+)
+from superset.mcp_service.chart.schemas import (
+ ChartError,
+ ColumnRef,
+ CountryMapChartConfig,
+ DeckScatterChartConfig,
+ VegaLitePreview,
+ WorldMapChartConfig,
+)
+from superset.mcp_service.chart.validation.dataset_validator import DatasetValidator
+
+logger = logging.getLogger(__name__)
+
+GeographicConfig = CountryMapChartConfig | WorldMapChartConfig | DeckScatterChartConfig
+ROLE_FIELDS = (
+ "entity",
+ "metric",
+ "secondary_metric",
+ "latitude",
+ "longitude",
+ "dimension",
+ "radius_metric",
+)
+MAX_GEOGRAPHIC_ROWS = 10000
+ASCII_BANNER = "Geographic source data (geometry not reproduced)"
+
+
+def _is_finite_geographic_number(value: object) -> TypeGuard[Real | Decimal]:
+ """Accept database NUMERIC/real scalars that remain finite in JSON.
+
+ Validation precedes JSON conversion; retain the original Decimal values for
+ data/export while rejecting booleans, complex numbers, and numeric strings.
+ """
+ if isinstance(value, bool) or not isinstance(value, (Real, Decimal)):
+ return False
+ if isinstance(value, Decimal) and not value.is_finite():
+ return False
+ try:
+ return math.isfinite(value)
+ except (OverflowError, ValueError):
+ return False
+
+
+def _decode_geographic_coordinates(value: object, spatial_type: str) -> list[float]:
+ """Decode native encoded coordinates for validation, not result replacement."""
+ if not isinstance(value, str) or not value:
+ raise ValueError("Spatial coordinates require a nonempty string")
+ if spatial_type == "geohash":
+ import pygeohash
+
+ try:
+ latitude, longitude = pygeohash.decode(value.lower())
+ except (ValueError, KeyError, TypeError) as ex:
+ raise ValueError("Invalid geographic geohash") from ex
+ return [longitude, latitude]
+ # Match the numeric prefix consumed by JavaScript parseFloat in the native
+ # Deck.gl spatial transform, including incomplete exponents and suffixes.
+ coordinates: list[float] = []
+ for part in value.split(","):
+ prefix = re.match(
+ r"[+-]?(?:Infinity|(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)"
+ r"(?:[eE][+-]?[0-9]+)?)",
+ part.lstrip(
+ "\t\n\v\f\r \u00a0\u1680\u2000\u2001\u2002\u2003"
+ "\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028"
+ "\u2029\u202f\u205f\u3000\ufeff"
+ ),
+ )
+ if prefix is None:
+ raise ValueError("Invalid delimited geographic coordinates")
+ coordinates.append(float(prefix[0]))
+ if len(coordinates) != 2:
+ raise ValueError("Delimited coordinates require two values")
+ return coordinates
+
+
+@lru_cache(maxsize=4)
+def _world_country_entries(field: str) -> tuple[tuple[str, str], ...]:
+ """Reuse immutable country aliases for the four supported world formats."""
+ from superset.examples.countries import countries
+
+ return tuple(
+ (country[field], country["cca3"]) for country in countries if country[field]
+ )
+
+
+def _metric_expression(metric: object) -> tuple[object, ...] | None:
+ """Identify native metric expressions without UI labels or column metadata."""
+ if isinstance(metric, str):
+ return ("saved", metric)
+ if not isinstance(metric, Mapping):
+ return None
+ expression_type = metric.get("expressionType")
+ if expression_type == "SQL" and isinstance(metric.get("sqlExpression"), str):
+ return (expression_type, metric["sqlExpression"])
+ column = metric.get("column")
+ if expression_type == "SIMPLE" and isinstance(column, Mapping):
+ name = column.get("column_name") or column.get("columnName")
+ if isinstance(name, str) and isinstance(metric.get("aggregate"), str):
+ return (expression_type, metric["aggregate"], name)
+ return None
+
+
+def _bind_time_column(qd: dict[str, Any], form_data: Mapping[str, Any]) -> None:
+ """Bind the legacy SQL time column so a saved ``time_range`` stays applied.
+
+ Native map viz classes filter ``time_range`` on ``granularity_sqla``; a
+ normalized dashboard ``granularity`` override takes precedence.
+ """
+ granularity = form_data.get("granularity", form_data.get("granularity_sqla"))
+ if granularity:
+ qd["granularity"] = granularity
+
+
+def _typed_row_limit(form_data: Mapping[str, Any]) -> int:
+ """Bound the typed map row limit, matching the frontend's full-map query."""
+ try:
+ limit = int(form_data.get("row_limit") or MAX_GEOGRAPHIC_ROWS)
+ except (TypeError, ValueError, OverflowError):
+ return MAX_GEOGRAPHIC_ROWS
+ return min(MAX_GEOGRAPHIC_ROWS, max(1, limit))
+
+
+def _normalize_fixed_radius(form_data: dict[str, Any]) -> None:
+ """Save a preserved numeric-string radius as an explicit fixed radius.
+
+ The typed contract reads a bare numeric string as a fixed size, while the
+ native Scatter query reads any bare string as a saved metric; the explicit
+ ``fix`` object keeps both in agreement.
+ """
+ from superset.mcp_service.chart.chart_helpers import _is_metric_ref
+
+ radius = form_data.get("point_radius_fixed")
+ if not isinstance(radius, str) or not radius or _is_metric_ref(radius):
+ return
+ value = float(radius)
+ if math.isfinite(value):
+ form_data["point_radius_fixed"] = {
+ "type": "fix",
+ "value": int(value) if value.is_integer() else value,
+ }
+
+
+class GeographicChartPlugin(BaseChartPlugin):
+ """Translate explicit geographic roles into native plugin controls.
+
+ Typed MCP charts carry ``mcp_geographic`` in their saved form_data. Only
+ those charts get the full-map row limits and strict result contract;
+ charts built in Explore keep their native behavior.
+ """
+
+ native_viz_types: ClassVar[Mapping[str, str]] = {}
+ owned_query_roles: ClassVar[frozenset[str]] = frozenset()
+ requires_compile_check = True
+ requires_config_for_dataset_rebind = True
+ dataset_rebind_roles = "geographic/metric roles"
+ strict_dataset_rebind = True
+ owns_update_merge = True
+ normalize_data_results = True
+ supports_vega_lite_preview = False
+ invalid_result_error_code = "INVALID_GEOGRAPHIC_RESULT"
+ invalid_result_message = "Geographic query returned invalid values"
+ invalid_result_suggestions: ClassVar[tuple[str, ...]] = (
+ "Match country and value format to the source identifiers",
+ "Correct source values or filter other geographies",
+ "Use finite numeric metrics and valid latitude/longitude",
+ )
+
+ # ------------------------------------------------------------------
+ # Result contract
+ # ------------------------------------------------------------------
+
+ def result_metrics(self, form_data: Mapping[str, Any]) -> list[Any]:
+ """Return the metrics whose values every result row must carry."""
+ raise NotImplementedError
+
+ def size_metric_labels(
+ self, form_data: Mapping[str, Any], labels: list[str]
+ ) -> set[str]:
+ """Return the metric labels that size marks and must be nonnegative."""
+ return set()
+
+ def row_identifier(
+ self, row: Mapping[str, Any], form_data: Mapping[str, Any]
+ ) -> str | None:
+ """Resolve the row's geography, or validate it and return None."""
+ raise NotImplementedError
+
+ def build_query_dicts(
+ self,
+ form_data: dict[str, Any],
+ *,
+ viz_type: str,
+ engine: str,
+ row_limit: int | None,
+ order_desc: bool | None,
+ ) -> list[dict[str, Any]] | None:
+ """Build the shared single query, keeping the saved time column bound."""
+ from superset.mcp_service.chart.chart_helpers import build_single_query_dict
+
+ fields = self.resolve_query_fields(form_data, viz_type)
+ if fields is None:
+ return None
+ metrics, groupby = fields
+ qd = build_single_query_dict(
+ form_data, groupby, metrics, row_limit=row_limit, order_desc=order_desc
+ )
+ # Match the native buildQuery's ``[metric, ascending]`` pair shape.
+ qd["orderby"] = [list(item) for item in qd.get("orderby") or []]
+ _bind_time_column(qd, form_data)
+ return [qd]
+
+ def _metric_labels(self, form_data: Mapping[str, Any]) -> list[str]:
+ labels = [metric_result_label(m) for m in self.result_metrics(form_data)]
+ if any(label is None for label in labels):
+ raise ValueError("Geographic metric has no resolvable result label")
+ return [label for label in labels if label is not None]
+
+ def _validate_rows(self, result: Any, form_data: Mapping[str, Any]) -> None:
+ if (
+ not isinstance(result, Mapping)
+ or not isinstance(result.get("queries"), list)
+ or len(result["queries"]) != 1
+ ):
+ raise ValueError("Expected exactly one geographic query result")
+ query = result["queries"][0]
+ if not isinstance(query, Mapping) or not isinstance(query.get("data"), list):
+ raise ValueError("Expected geographic query data to be a list of records")
+ labels = self._metric_labels(form_data)
+ size_labels = self.size_metric_labels(form_data, labels)
+ seen: set[str] = set()
+ for row in query["data"]:
+ if not isinstance(row, Mapping):
+ raise ValueError("Expected geographic rows to be records")
+ for label in labels:
+ value = row.get(label)
+ if not _is_finite_geographic_number(value):
+ raise ValueError(
+ f"Geographic metric {label!r} must be a finite number"
+ )
+ if value < 0 and label in size_labels:
+ raise ValueError("Geographic size metrics must be nonnegative")
+ if identifier := self.row_identifier(row, form_data):
+ if identifier in seen:
+ raise ValueError(
+ f"Multiple result rows resolve to {identifier}; "
+ "normalize source values before aggregation"
+ )
+ seen.add(identifier)
+
+ def normalize_query_result(self, result: Any, form_data: Mapping[str, Any]) -> Any:
+ """Reject unresolved regions and malformed or nonfinite results.
+
+ Source values are preserved for exports and filtering; the native
+ transform owns display-only ISO mapping using the same bundled
+ boundary identifiers. Charts without the typed MCP marker keep their
+ native behavior.
+ """
+ if failure := query_result_failure(result):
+ return failure
+ if not form_data.get("mcp_geographic"):
+ return result
+ try:
+ self._validate_rows(result, form_data)
+ except (ValueError, TypeError, KeyError) as exc:
+ return ChartError(error=str(exc), error_type="InvalidGeographicResult")
+ return result
+
+ # ------------------------------------------------------------------
+ # Query limits and previews
+ # ------------------------------------------------------------------
+
+ def compile_row_limit(self, form_data: Mapping[str, Any]) -> int:
+ """Validate the full bounded map, not only its first rows."""
+ if form_data.get("mcp_geographic"):
+ return _typed_row_limit(form_data)
+ return super().compile_row_limit(form_data)
+
+ def preview_row_limit(self, form_data: Mapping[str, Any], fallback: int) -> int:
+ """Preview the same bounded rows the native map renders."""
+ if form_data.get("mcp_geographic"):
+ return _typed_row_limit(form_data)
+ return fallback
+
+ def ascii_preview(
+ self,
+ data: list[Any],
+ form_data: dict[str, Any],
+ width: int,
+ height: int = 20,
+ ) -> str | ChartError | None:
+ """Render the source rows; map geometry has no ASCII form."""
+ from superset.mcp_service.chart.ascii_charts import generate_ascii_table
+
+ try:
+ return f"{ASCII_BANNER}\n" + generate_ascii_table(data, max(width, 21))
+ except (TypeError, ValueError, KeyError, IndexError) as exc:
+ logger.error("ASCII chart generation failed: %s", exc, exc_info=True)
+ return "ASCII chart generation failed"
+
+ def vega_lite_preview(
+ self, data: list[Any], form_data: dict[str, Any]
+ ) -> VegaLitePreview | ChartError | None:
+ """Never fabricate a non-geographic Vega-Lite chart for a map."""
+ return ChartError(
+ error=(
+ "Geographic Vega previews are not supported. Use table/ascii for "
+ "source data, or open Explore for native geography."
+ ),
+ error_type="UnsupportedGeographicPreview",
+ )
+
+ # ------------------------------------------------------------------
+ # Updates
+ # ------------------------------------------------------------------
+
+ def merge_update_form_data(
+ self,
+ existing_form_data: dict[str, Any],
+ new_form_data: dict[str, Any],
+ config: Any,
+ *,
+ dataset_rebind: bool,
+ ) -> dict[str, Any] | None:
+ """Preserve omitted native controls; a rebind keeps presentation only."""
+ merged = merge_geographic_update_form_data(
+ existing_form_data, new_form_data, config, dataset_rebind=dataset_rebind
+ )
+ # Saved query aliases this map never reads are replaced, not inherited.
+ for key in self.query_role_keys - self.owned_query_roles:
+ if key not in new_form_data:
+ merged.pop(key, None)
+ # Native query and compile validation must cover the same bounded rows.
+ if merged.get("mcp_geographic"):
+ merged["row_limit"] = _typed_row_limit(merged)
+ _normalize_fixed_radius(merged)
+ return merged
+
+ def finalize_update_form_data(
+ self,
+ existing_form_data: dict[str, Any],
+ new_form_data: dict[str, Any],
+ merged: dict[str, Any],
+ config: Any,
+ ) -> dict[str, Any]:
+ """Let explicit time changes win over controls kept by a conversion."""
+ if merged.get("mcp_geographic"):
+ _apply_geographic_temporal_update(merged, config)
+ return merged
+
+ def extract_column_refs(self, config: GeographicConfig) -> list[ColumnRef]:
+ """Include all spatial, metric, and filter references."""
+ refs = [
+ ref
+ for field in ROLE_FIELDS
+ if (ref := getattr(config, field, None)) is not None
+ ]
+ refs.extend(ColumnRef(name=f.column) for f in config.filters or [])
+ return refs
+
+ def normalize_column_refs(
+ self, config: GeographicConfig, dataset_context: Any
+ ) -> GeographicConfig:
+ """Canonicalize dataset identifiers without losing explicit field sets."""
+ patch = config.model_dump(exclude_unset=True)
+ for field in ROLE_FIELDS:
+ ref = patch.get(field)
+ if ref and ref.get("name"):
+ canonical = (
+ DatasetValidator.get_canonical_metric_name
+ if ref.get("saved_metric")
+ else DatasetValidator.get_canonical_column_name
+ )
+ ref["name"] = canonical(ref["name"], dataset_context)
+ DatasetValidator.normalize_filters(patch, dataset_context)
+ return type(config).model_validate(patch)
+
+ def resolve_viz_type(self, config: GeographicConfig) -> str:
+ """Public names match frontend registration keys."""
+ return config.chart_type
+
+ def generate_name(
+ self, config: GeographicConfig, dataset_name: str | None = None
+ ) -> str:
+ """Name geographic charts without guessing the source geography."""
+ return self._with_context(self.display_name, dataset_name)
+
+ def to_form_data(
+ self, config: GeographicConfig, dataset_id: int | str | None = None
+ ) -> dict[str, Any]:
+ """Mirror native country/world/deck Scatter controls and defaults."""
+ result: dict[str, Any] = {
+ "viz_type": config.chart_type,
+ "row_limit": config.row_limit,
+ "mcp_geographic": True,
+ }
+ if config.time_range is not None:
+ result["time_range"] = config.time_range
+ _add_adhoc_filters(result, config.filters)
+ if isinstance(config, CountryMapChartConfig):
+ result.update(
+ entity=config.entity.name,
+ metric=create_metric_object(config.metric),
+ select_country=config.country,
+ region_format=config.region_format,
+ linear_color_scheme=config.linear_color_scheme,
+ number_format=config.number_format,
+ )
+ elif isinstance(config, WorldMapChartConfig):
+ result.update(
+ entity=config.entity.name,
+ metric=create_metric_object(config.metric),
+ country_fieldtype=config.country_format,
+ secondary_metric=create_metric_object(config.secondary_metric)
+ if config.secondary_metric
+ else None,
+ show_bubbles=config.show_bubbles,
+ max_bubble_size=config.max_bubble_size,
+ sort_by_metric=config.sort_by_metric,
+ linear_color_scheme=config.linear_color_scheme,
+ color_by="metric",
+ color_picker={"r": 0, "g": 122, "b": 135, "a": 1},
+ color_scheme="supersetColors",
+ y_axis_format="SMART_NUMBER",
+ )
+ else:
+ radius: dict[str, Any] = {"type": "fix", "value": config.radius}
+ if config.radius_metric:
+ radius = {
+ "type": "metric",
+ "value": create_metric_object(config.radius_metric),
+ }
+ result.update(
+ spatial={
+ "type": "latlong",
+ "latCol": config.latitude.name,
+ "lonCol": config.longitude.name,
+ },
+ dimension=config.dimension.name if config.dimension else None,
+ point_radius_fixed=radius,
+ point_unit=config.point_unit,
+ multiplier=1,
+ min_radius=2,
+ max_radius=250,
+ color_picker={"r": 0, "g": 122, "b": 135, "a": 1},
+ color_scheme="supersetColors",
+ map_renderer="maplibre",
+ maplibre_style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
+ viewport={
+ "longitude": 0,
+ "latitude": 0,
+ "zoom": 1,
+ "bearing": 0,
+ "pitch": 0,
+ },
+ autozoom=True,
+ filter_nulls=True,
+ )
+ return result
+
+
+class CountryMapChartPlugin(GeographicChartPlugin):
+ """Register the Country Map visualization."""
+
+ chart_type = "country_map"
+ display_name = "Country Map"
+ native_viz_types: ClassVar[Mapping[str, str]] = {"country_map": "Country Map"}
+ owned_query_roles: ClassVar[frozenset[str]] = frozenset({"entity", "metric"})
+ query_role_keys = BaseChartPlugin.query_role_keys | {
+ "entity",
+ "metric",
+ "series_columns",
+ }
+
+ def resolve_query_fields(
+ self, form_data: Mapping[str, Any], viz_type: str
+ ) -> tuple[list[Any], list[Any]] | None:
+ """Query the region entity and its single metric."""
+ metric = form_data.get("metric")
+ entity = form_data.get("entity")
+ return [metric] if metric else [], [entity] if entity else []
+
+ def result_metrics(self, form_data: Mapping[str, Any]) -> list[Any]:
+ return [form_data.get("metric")]
+
+ def row_identifier(
+ self, row: Mapping[str, Any], form_data: Mapping[str, Any]
+ ) -> str | None:
+ """Resolve the row to one bundled region boundary."""
+ from superset.utils.geographic import resolve_region
+
+ entity = column_result_label(form_data.get("entity"))
+ if entity is None:
+ raise ValueError("Geographic maps require an entity column")
+ # Explore's legacy format joins on exact boundary ISO codes without
+ # normalization, so only an exact identifier renders.
+ region_format = form_data.get("region_format")
+ return resolve_region(
+ row.get(entity),
+ form_data.get("select_country", ""),
+ region_format or "iso_3166_2",
+ exact_only=not region_format,
+ )
+
+
+def _bubbles_shown(form_data: Mapping[str, Any]) -> bool:
+ """Whether the world map renders its secondary metric as bubble sizes."""
+ return bool(form_data.get("show_bubbles"))
+
+
+class WorldMapChartPlugin(GeographicChartPlugin):
+ """Register the World Map visualization."""
+
+ chart_type = "world_map"
+ display_name = "World Map"
+ native_viz_types: ClassVar[Mapping[str, str]] = {"world_map": "World Map"}
+ owned_query_roles: ClassVar[frozenset[str]] = frozenset(
+ {"entity", "metric", "secondary_metric"}
+ )
+ query_role_keys = BaseChartPlugin.query_role_keys | {
+ "entity",
+ "metric",
+ "secondary_metric",
+ "series_columns",
+ }
+
+ def resolve_query_fields(
+ self, form_data: Mapping[str, Any], viz_type: str
+ ) -> tuple[list[Any], list[Any]] | None:
+ """Query the country entity, its metric and the bubble-size metric.
+
+ The secondary metric is queried unless its result label is shared
+ with the primary metric.
+ """
+ metrics: list[Any] = []
+ labels: set[str | None] = set()
+ for field in ("metric", "secondary_metric"):
+ if metric := form_data.get(field):
+ label = metric_result_label(metric)
+ if label not in labels:
+ metrics.append(metric)
+ labels.add(label)
+ entity = form_data.get("entity")
+ return metrics, [entity] if entity else []
+
+ def validate_merged_form_data(
+ self,
+ form_data: Mapping[str, Any],
+ dataset_id: int | str | None,
+ dataset_context: Any = None,
+ ) -> Any | None:
+ """Reject merged color and bubble metrics that share a label.
+
+ An update can keep a saved ``secondary_metric`` while replacing
+ ``metric`` with a different expression under the same label. Query
+ deduplication would then size bubbles from the color metric.
+ """
+ metric = form_data.get("metric")
+ secondary = form_data.get("secondary_metric")
+ if (
+ _bubbles_shown(form_data)
+ and metric is not None
+ and secondary is not None
+ and metric != secondary
+ and (
+ _metric_expression(metric) is None
+ or _metric_expression(metric) != _metric_expression(secondary)
+ )
+ and metric_result_label(metric) == metric_result_label(secondary)
+ ):
+ raise ValueError(
+ "metric and secondary_metric must have distinct result labels; "
+ "rename one or send both metrics in the update"
+ )
+ return None
+
+ def result_metrics(self, form_data: Mapping[str, Any]) -> list[Any]:
+ """Require the color metric, plus the bubble metric when bubbles show.
+
+ A secondary metric kept alongside ``show_bubbles=False`` is not
+ rendered, so its values do not gate the choropleth.
+ """
+ metrics = [form_data.get("metric")]
+ if not _bubbles_shown(form_data):
+ return metrics
+ secondary = form_data.get("secondary_metric")
+ if secondary is None:
+ raise ValueError("show_bubbles requires secondary_metric")
+ metrics.append(secondary)
+ return metrics
+
+ def size_metric_labels(
+ self, form_data: Mapping[str, Any], labels: list[str]
+ ) -> set[str]:
+ """Bubble sizes come from the secondary metric when bubbles show."""
+ if not _bubbles_shown(form_data):
+ return set()
+ secondary = metric_result_label(form_data.get("secondary_metric"))
+ return {secondary} if secondary is not None else set()
+
+ def row_identifier(
+ self, row: Mapping[str, Any], form_data: Mapping[str, Any]
+ ) -> str | None:
+ """Resolve the row to one ISO 3166-1 alpha-3 country."""
+ from superset.utils.geographic import resolve_geographic_value
+ from superset.utils.geographic_world import WORLD_BOUNDARY_IDS
+
+ entity = column_result_label(form_data.get("entity"))
+ if entity is None:
+ raise ValueError("Geographic maps require an entity column")
+ field = form_data.get("country_fieldtype")
+ if field not in {"name", "cca2", "cca3", "cioc"}:
+ raise ValueError("Choose country_format name, cca2, cca3, or cioc")
+ # Fold diacritics for country names only, so localized spellings such
+ # as "Curaçao" reach their country. The ISO code fields are too short
+ # to fold safely: an accented label like "Áo" would fold onto an
+ # unrelated country's code and resolve silently to the wrong country.
+ identifier = resolve_geographic_value(
+ row.get(entity),
+ _world_country_entries(field),
+ fold_diacritics=field == "name",
+ )
+
+ if not _bubbles_shown(form_data) and identifier not in WORLD_BOUNDARY_IDS:
+ raise ValueError(
+ f"Country {identifier} has no bundled world-map boundary; "
+ "enable bubbles with a secondary metric or filter the dataset"
+ )
+ return identifier
+
+
+class DeckScatterChartPlugin(GeographicChartPlugin):
+ """Register the Geographic Points visualization."""
+
+ chart_type = "deck_scatter"
+ display_name = "Geographic Points"
+ native_viz_types: ClassVar[Mapping[str, str]] = {
+ "deck_scatter": "Geographic Points"
+ }
+ owned_query_roles: ClassVar[frozenset[str]] = frozenset(
+ {"spatial", "dimension", "point_radius_fixed"}
+ )
+ query_role_keys = BaseChartPlugin.query_role_keys | _DECK_DATASET_ROLE_KEYS
+
+ def validate_merged_form_data(
+ self,
+ form_data: Mapping[str, Any],
+ dataset_id: int | str | None,
+ dataset_context: Any = None,
+ ) -> Any | None:
+ """Reject effective roles that conflict with native spatial features.
+
+ Omitted dimensions and radius metrics survive updates. The frontend
+ strips coordinate properties and reserves computed spatial field names.
+ """
+ dimension = column_result_label(form_data.get("dimension"))
+ spatial = form_data.get("spatial")
+ if (
+ dimension is not None
+ and isinstance(spatial, Mapping)
+ and spatial.get("type") == "latlong"
+ and dimension in {spatial.get("latCol"), spatial.get("lonCol")}
+ ):
+ raise ValueError(
+ f"Dimension column {dimension!r} reuses a coordinate column; "
+ "choose a separate category column"
+ )
+ column_labels = {dimension} if dimension is not None else set()
+ if isinstance(spatial, Mapping):
+ spatial_keys = {
+ "latlong": ("latCol", "lonCol"),
+ "geohash": ("geohashCol",),
+ "delimited": ("lonlatCol",),
+ }.get(spatial.get("type", ""), ())
+ column_labels.update(
+ spatial[key] for key in spatial_keys if spatial.get(key) is not None
+ )
+ for metric in self.result_metrics(form_data):
+ label = metric_result_label(metric)
+ if label in column_labels:
+ raise ValueError(
+ f"Radius metric label {label!r} conflicts with an effective "
+ "dimension or coordinate column; clear or replace the radius metric"
+ )
+ if label in {"weight", "position", "extraProps"}:
+ raise ValueError(
+ f"Radius metric label {label!r} conflicts with a native spatial "
+ "field; clear or replace the radius metric"
+ )
+ return None
+
+ def build_query_dicts(
+ self,
+ form_data: dict[str, Any],
+ *,
+ viz_type: str,
+ engine: str,
+ row_limit: int | None,
+ order_desc: bool | None,
+ ) -> list[dict[str, Any]] | None:
+ """Query raw points, ordered by radius metric, without time bucketing.
+
+ Charts without the typed MCP marker use the shared Deck.gl query. Both
+ keep the saved time column bound so ``time_range`` still filters.
+ """
+ from superset.mcp_service.chart.chart_helpers import (
+ build_deck_gl_query_dict,
+ )
+
+ typed = bool(form_data.get("mcp_geographic"))
+ qd = build_deck_gl_query_dict(
+ form_data,
+ viz_type,
+ row_limit=row_limit,
+ order_desc=order_desc,
+ )
+ if not qd.get("is_timeseries"):
+ _bind_time_column(qd, form_data)
+ if not typed:
+ return [qd]
+ metrics = qd.get("metrics") or []
+ qd["is_timeseries"] = False
+ if isinstance(extras := qd.get("extras"), dict):
+ extras.pop("time_grain_sqla", None)
+ qd["orderby"] = [[metric_result_label(metrics[0]), False]] if metrics else []
+ return [qd]
+
+ def result_metrics(self, form_data: Mapping[str, Any]) -> list[Any]:
+ """Require the radius metric when the radius is metric-backed.
+
+ Legacy native charts store a bare saved-metric key (or a numeric
+ string for a fixed radius) instead of the typed ``fix``/``metric``
+ object; both stay valid when an unrelated update preserves them.
+ """
+ from superset.mcp_service.chart.chart_helpers import _is_metric_ref
+
+ radius = form_data.get("point_radius_fixed")
+ if isinstance(radius, str) and radius:
+ return [radius] if _is_metric_ref(radius) else []
+ if not isinstance(radius, Mapping) or radius.get("type") not in {
+ "fix",
+ "metric",
+ }:
+ raise ValueError("Invalid geographic point radius configuration")
+ return [radius.get("value")] if radius["type"] == "metric" else []
+
+ def size_metric_labels(
+ self, form_data: Mapping[str, Any], labels: list[str]
+ ) -> set[str]:
+ """Every point metric sizes the point radius."""
+ return set(labels)
+
+ def row_identifier(
+ self, row: Mapping[str, Any], form_data: Mapping[str, Any]
+ ) -> str | None:
+ """Validate native spatial formats without changing exported source values."""
+ spatial = form_data.get("spatial")
+ if not isinstance(spatial, Mapping):
+ raise ValueError("Geographic points require spatial columns")
+ spatial_type = spatial.get("type")
+ coordinates: list[object] = []
+ if spatial_type == "latlong":
+ for role in ("lonCol", "latCol"):
+ column = spatial.get(role)
+ if not isinstance(column, str):
+ raise ValueError(f"{role} requires a named coordinate column")
+ coordinates.append(row.get(column))
+ elif spatial_type in {"geohash", "delimited"}:
+ column = spatial.get(
+ "geohashCol" if spatial_type == "geohash" else "lonlatCol"
+ )
+ if not isinstance(column, str):
+ raise ValueError("Spatial coordinates require a named column")
+ coordinates.extend(
+ _decode_geographic_coordinates(row.get(column), spatial_type)
+ )
+ else:
+ raise ValueError("Unsupported geographic spatial format")
+ if spatial.get("reverseCheckbox"):
+ coordinates.reverse()
+ for value, role, bound in zip(
+ coordinates, ("lonCol", "latCol"), (180, 90), strict=True
+ ):
+ if (
+ not _is_finite_geographic_number(value)
+ or value < -bound
+ or not value <= bound
+ ):
+ raise ValueError(
+ f"{role} must be a finite number between {-bound} and {bound}"
+ )
+ return None
diff --git a/superset/mcp_service/chart/plugins/histogram.py b/superset/mcp_service/chart/plugins/histogram.py
index 264e487f79f..b25fd28ce29 100644
--- a/superset/mcp_service/chart/plugins/histogram.py
+++ b/superset/mcp_service/chart/plugins/histogram.py
@@ -268,6 +268,7 @@ class HistogramChartPlugin(BaseChartPlugin):
def vega_lite_preview(
self, data: list[Any], form_data: dict[str, Any]
) -> VegaLitePreview | ChartError | None:
+ """Return the histogram-specific Vega-Lite preview."""
from superset.mcp_service.chart.preview_utils import (
generate_histogram_vega_lite_preview,
)
diff --git a/superset/mcp_service/chart/preview_utils.py b/superset/mcp_service/chart/preview_utils.py
index 645888d76d1..5d3db543bc8 100644
--- a/superset/mcp_service/chart/preview_utils.py
+++ b/superset/mcp_service/chart/preview_utils.py
@@ -238,6 +238,23 @@ def plugin_unsupported_preview(
return plugin.unsupported_preview(preview_format)
+def empty_vega_lite_preview_error(
+ data: object, form_data: Dict[str, Any]
+) -> ChartError | None:
+ """Reject empty Vega-Lite results unless the owning plugin allows them."""
+ from superset.mcp_service.chart.registry import plugin_for_viz_type
+
+ if not isinstance(data, list) or data:
+ return None
+ plugin = plugin_for_viz_type(form_data.get("viz_type"))
+ if plugin is not None and plugin.allows_empty_result:
+ return None
+ return ChartError(
+ error="No data available for Vega-Lite visualization",
+ error_type="NoDataError",
+ )
+
+
def plugin_vega_lite_preview(
data: List[Any], form_data: Dict[str, Any]
) -> VegaLitePreview | ChartError | None:
@@ -1647,6 +1664,8 @@ def _generate_vega_lite_preview_from_data( # noqa: C901
data: List[Dict[str, Any]], form_data: Dict[str, Any], *, use_plugin: bool = True
) -> VegaLitePreview | ChartError:
"""Generate Vega-Lite preview from raw data and form_data."""
+ if empty_error := empty_vega_lite_preview_error(data, form_data):
+ return empty_error
viz_type = form_data.get("viz_type", "table")
if use_plugin:
if (plugin_preview := plugin_vega_lite_preview(data, form_data)) is not None:
diff --git a/superset/mcp_service/chart/resources/chart_configs.py b/superset/mcp_service/chart/resources/chart_configs.py
index 3743c7e028e..603fe47d922 100644
--- a/superset/mcp_service/chart/resources/chart_configs.py
+++ b/superset/mcp_service/chart/resources/chart_configs.py
@@ -399,6 +399,26 @@ def get_chart_configs_resource() -> str:
"interactive_pivot_configs": interactive_pivot_configs,
"gantt_chart_configs": gantt_chart_configs,
"gauge_configs": gauge_configs,
+ "geographic_configs": {
+ "regional_choropleth": {
+ "chart_type": "country_map",
+ "country": "usa",
+ "region_format": "abbreviation",
+ "entity": {"name": "state"},
+ "metric": {"name": "sales", "aggregate": "SUM"},
+ },
+ "country_choropleth": {
+ "chart_type": "world_map",
+ "country_format": "cca2",
+ "entity": {"name": "country"},
+ "metric": {"name": "sales", "aggregate": "SUM"},
+ },
+ "geographic_points": {
+ "chart_type": "deck_scatter",
+ "latitude": {"name": "latitude"},
+ "longitude": {"name": "longitude"},
+ },
+ },
"best_practices": best_practices,
"usage_notes": [
"All examples are valid ChartConfig objects that pass validation",
diff --git a/superset/mcp_service/chart/schemas.py b/superset/mcp_service/chart/schemas.py
index 8b34ef08d95..de8509235eb 100644
--- a/superset/mcp_service/chart/schemas.py
+++ b/superset/mcp_service/chart/schemas.py
@@ -5010,6 +5010,189 @@ class GanttChartConfig(BaseChartConfig):
# Discriminated union for runtime validation (not exposed in JSON Schema)
+def _omit_inherited_descriptions(schema: dict[str, Any]) -> None:
+ """Drop field descriptions the published parent schema already carries.
+
+ The geographic references and filters only tighten bounds on the shared
+ ``ColumnRef``/``FilterConfig`` fields, whose descriptions appear in the
+ same tool schema.
+ """
+ for prop in schema.get("properties", {}).values():
+ prop.pop("description", None)
+
+
+class GeographicColumnRef(ColumnRef):
+ """Closed ColumnRef for geographic roles."""
+
+ model_config = ConfigDict(
+ extra="forbid",
+ populate_by_name=True,
+ strict=True,
+ json_schema_extra=_omit_inherited_descriptions,
+ )
+ dtype: str | None = Field(None, max_length=128)
+
+
+GeographicFilterValue = Annotated[str, Field(max_length=1000)] | int | float | bool
+
+
+class GeographicFilterConfig(FilterConfig):
+ """Closed FilterConfig with bounded values."""
+
+ model_config = ConfigDict(
+ extra="forbid",
+ populate_by_name=True,
+ strict=True,
+ allow_inf_nan=False,
+ json_schema_extra=_omit_inherited_descriptions,
+ )
+ value: (
+ GeographicFilterValue
+ | Annotated[list[GeographicFilterValue], Field(max_length=1000)]
+ | None
+ ) = Field(None, validation_alias=AliasChoices("value", "val"))
+
+
+class GeographicChartConfig(BaseChartConfig):
+ """Bounded controls shared by geographic visualizations."""
+
+ model_config = ConfigDict(extra="forbid", strict=True)
+ filters: list[GeographicFilterConfig] | None = Field(None, max_length=100)
+ row_limit: int = Field(10000, ge=1, le=10000)
+ time_range: str | None = Field(None, max_length=1000)
+
+ @field_validator("time_range")
+ @classmethod
+ def validate_geographic_time_range(cls, value: str | None) -> str | None:
+ """Reject time expressions the query parser would silently ignore."""
+ return validate_time_range(value)
+
+ @model_validator(mode="after")
+ def validate_geographic_roles(self) -> "GeographicChartConfig":
+ """Reject metric dimensions and unaggregated metric roles."""
+ for field in ("entity", "latitude", "longitude", "dimension"):
+ ref = getattr(self, field, None)
+ if ref is not None and (not ref.name or ref.is_metric):
+ raise ValueError(
+ f"{field} requires a named dataset column, not a metric"
+ )
+ for field in ("metric", "secondary_metric", "radius_metric"):
+ ref = getattr(self, field, None)
+ if ref is not None and not ref.is_metric:
+ raise ValueError(
+ f"{field} requires aggregate, saved_metric, or sql_expression"
+ )
+ # These helpers import chart schemas, so defer to avoid a cycle.
+ from superset.mcp_service.chart.chart_utils import create_metric_object
+ from superset.mcp_service.chart.query_result import metric_result_label
+
+ dimensions = {
+ ref.name
+ for field in ("entity", "latitude", "longitude", "dimension")
+ if (ref := getattr(self, field, None)) is not None
+ }
+ seen_metrics: dict[str | None, ColumnRef] = {}
+ for field in ("metric", "secondary_metric", "radius_metric"):
+ ref = getattr(self, field, None)
+ if ref is None:
+ continue
+ label = metric_result_label(create_metric_object(ref))
+ if self.chart_type == "deck_scatter" and label in {
+ "position",
+ "weight",
+ "extraProps",
+ }:
+ raise ValueError(
+ f"Metric alias {label!r} conflicts with a native spatial field"
+ )
+ if label in dimensions:
+ raise ValueError(
+ f"Metric alias {label!r} conflicts with a geographic column"
+ )
+ if label in seen_metrics and seen_metrics[label] != ref:
+ raise ValueError(f"Distinct geographic metrics share alias {label!r}")
+ seen_metrics[label] = ref
+ return self
+
+
+class CountryMapChartConfig(GeographicChartConfig):
+ """Regional choropleth joined against the bundled country boundaries."""
+
+ chart_type: Literal["country_map"]
+ country: Literal["usa", "canada", "australia", "japan", "uk"] = Field(
+ ..., description="Bundled boundary set. Only these countries are supported."
+ )
+ region_format: Literal["name", "abbreviation", "iso_3166_2"] = Field(
+ ...,
+ description="Explicit source format; abbreviation means the ISO suffix. "
+ "Names match bundled boundary names, not geocoding or fuzzy matching.",
+ )
+ entity: GeographicColumnRef
+ metric: GeographicColumnRef
+ linear_color_scheme: str = Field("schemeBlues", min_length=1, max_length=100)
+ number_format: str = Field("SMART_NUMBER", min_length=1, max_length=100)
+
+
+class WorldMapChartConfig(GeographicChartConfig):
+ """Country choropleth with optional metric-sized bubbles.
+
+ The plugin checks the bubble metric after omitted update controls merge.
+ """
+
+ chart_type: Literal["world_map"]
+ entity: GeographicColumnRef
+ country_format: Literal["name", "cca2", "cca3", "cioc"]
+ metric: GeographicColumnRef
+ secondary_metric: GeographicColumnRef | None = None
+ show_bubbles: bool = False
+ max_bubble_size: int = Field(25, ge=1, le=100)
+ sort_by_metric: bool = True
+ linear_color_scheme: str = Field("schemeBlues", min_length=1, max_length=100)
+
+
+class DeckScatterChartConfig(GeographicChartConfig):
+ """Geographic points from numeric longitude and latitude columns."""
+
+ chart_type: Literal["deck_scatter"]
+ latitude: GeographicColumnRef
+ longitude: GeographicColumnRef
+ dimension: GeographicColumnRef | None = None
+ radius_metric: GeographicColumnRef | None = None
+ radius: int = Field(1000, ge=1, le=1000000)
+ point_unit: Literal[
+ "square_m",
+ "square_km",
+ "square_miles",
+ "radius_m",
+ "radius_km",
+ "radius_miles",
+ ] = "radius_m"
+
+ @model_validator(mode="after")
+ def validate_coordinates(self) -> "DeckScatterChartConfig":
+ """Require distinct coordinates and protect computed spatial fields."""
+ if self.latitude.name == self.longitude.name:
+ raise ValueError("latitude and longitude must reference different columns")
+ if self.dimension is not None and self.dimension.name in {
+ "position",
+ "weight",
+ "extraProps",
+ }:
+ raise ValueError(
+ f"Dimension column {self.dimension.name!r} "
+ "conflicts with a native spatial field"
+ )
+ if self.dimension is not None and self.dimension.name in {
+ self.latitude.name,
+ self.longitude.name,
+ }:
+ raise ValueError(
+ f"Dimension column {self.dimension.name!r} reuses a coordinate "
+ "column; choose a separate category column"
+ )
+ return self
+
+
CHART_TYPE_DISCRIMINATOR: str = "chart_type"
@@ -5029,7 +5212,10 @@ ChartConfig = Annotated[
| HistogramChartConfig
| BoxPlotChartConfig
| WaterfallChartConfig
- | GanttChartConfig,
+ | GanttChartConfig
+ | CountryMapChartConfig
+ | WorldMapChartConfig
+ | DeckScatterChartConfig,
Field(
discriminator=CHART_TYPE_DISCRIMINATOR,
description=(
diff --git a/superset/mcp_service/chart/tool/generate_chart.py b/superset/mcp_service/chart/tool/generate_chart.py
index d4f2df2e518..f5c5c874a90 100644
--- a/superset/mcp_service/chart/tool/generate_chart.py
+++ b/superset/mcp_service/chart/tool/generate_chart.py
@@ -119,7 +119,7 @@ async def generate_chart( # noqa: C901
- MUST include chart_type in config (one of: 'xy', 'table', 'pie',
'sunburst', 'gauge', 'treemap_v2', 'bubble_v2', 'pivot_table', 'mixed_timeseries',
'handlebars', 'big_number', 'histogram', 'box_plot', 'waterfall',
- 'gantt', plus host-gated
+ 'gantt', 'country_map', 'world_map', 'deck_scatter', plus host-gated
types returned by get_chart_type_schema such as 'interactive_pivot')
IMPORTANT: The 'chart_type' field in the config is a DISCRIMINATOR that determines
diff --git a/superset/mcp_service/chart/tool/get_chart_data.py b/superset/mcp_service/chart/tool/get_chart_data.py
index 38ae16bdf96..a2067b1dec0 100644
--- a/superset/mcp_service/chart/tool/get_chart_data.py
+++ b/superset/mcp_service/chart/tool/get_chart_data.py
@@ -166,7 +166,9 @@ _VIZ_CATEGORY: dict[str, str] = {
"histogram": "histogram",
"histogram_v2": "histogram",
"box_plot": "box_plot",
- "world_map": "map",
+ "world_map": "world_map",
+ "country_map": "country_map",
+ "deck_scatter": "deck_scatter",
"pivot_table_v2": "table",
"ag-grid-pivot-table": "table",
# Own category: cumulative-flow semantics differ from a plain bar, like
@@ -245,8 +247,30 @@ def _build_candidates(
numeric = [c for c in columns if c.data_type == "numeric"]
categorical = [c for c in columns if c.data_type in ("string", "boolean")]
+ numeric_names = {c.name.lower() for c in numeric}
+ categorical_names = {
+ c.name.lower()
+ for c in categorical
+ if c.data_type == "string" and 1 < c.unique_count <= 250
+ }
+ has_coordinates = {"latitude", "longitude"} <= numeric_names
if temporal and numeric:
- return _candidates_temporal_numeric(numeric, row_count)
+ candidates = _candidates_temporal_numeric(numeric, row_count)
+ # Time-spatial data plots on a map as readily as on a time series,
+ # so the coordinates stay on offer instead of being shadowed. When the
+ # row count is high enough to offer lines and the chart is not already
+ # a line chart, the recommendation cap pays for the map by dropping the
+ # trailing "multi-line chart", the closest variant of the "line chart"
+ # that survives, so the suggestions still span every distinct shape the
+ # dataset supports. Below that row threshold, and on a chart whose line
+ # category is already filtered out, nothing is displaced.
+ return ["geographic points", *candidates] if has_coordinates else candidates
+ if has_coordinates:
+ return ["geographic points", "table"]
+ if numeric and categorical_names & {"country", "country_code"}:
+ return ["world map", "bar chart", "table"]
+ if numeric and categorical_names & {"state", "province"}:
+ return ["country map", "bar chart", "table"]
if categorical and numeric:
return _candidates_categorical_numeric(numeric, categorical)
if len(numeric) >= 2:
@@ -322,6 +346,9 @@ _CANDIDATE_CATEGORY: dict[str, str] = {
"gauge chart": "gauge",
"histogram": "histogram",
"table": "table",
+ "geographic points": "deck_scatter",
+ "country map": "country_map",
+ "world map": "world_map",
}
diff --git a/superset/mcp_service/chart/tool/get_chart_type_schema.py b/superset/mcp_service/chart/tool/get_chart_type_schema.py
index f85186cc1e5..17fd98ef1c0 100644
--- a/superset/mcp_service/chart/tool/get_chart_type_schema.py
+++ b/superset/mcp_service/chart/tool/get_chart_type_schema.py
@@ -35,6 +35,8 @@ from superset.mcp_service.chart.schemas import (
BigNumberChartConfig,
BoxPlotChartConfig,
BubbleChartConfig,
+ CountryMapChartConfig,
+ DeckScatterChartConfig,
GanttChartConfig,
GaugeChartConfig,
HandlebarsChartConfig,
@@ -47,6 +49,7 @@ from superset.mcp_service.chart.schemas import (
TableChartConfig,
TreemapChartConfig,
WaterfallChartConfig,
+ WorldMapChartConfig,
XYChartConfig,
)
@@ -65,6 +68,9 @@ class ChartTypeSchemaResponse(TypedDict, total=False):
# Module-level TypeAdapters — one per chart type, compiled once.
_CHART_TYPE_ADAPTERS: Dict[str, TypeAdapter[Any]] = {
+ "country_map": TypeAdapter(CountryMapChartConfig),
+ "world_map": TypeAdapter(WorldMapChartConfig),
+ "deck_scatter": TypeAdapter(DeckScatterChartConfig),
"xy": TypeAdapter(XYChartConfig),
"table": TypeAdapter(TableChartConfig),
"pie": TypeAdapter(PieChartConfig),
@@ -87,6 +93,30 @@ VALID_CHART_TYPES = sorted(_CHART_TYPE_ADAPTERS.keys())
# Per-type examples — lightweight inline examples for each chart type.
_CHART_EXAMPLES: Dict[str, list[Dict[str, Any]]] = {
+ "country_map": [
+ {
+ "chart_type": "country_map",
+ "country": "usa",
+ "region_format": "abbreviation",
+ "entity": {"name": "state"},
+ "metric": {"name": "sales", "aggregate": "SUM"},
+ }
+ ],
+ "world_map": [
+ {
+ "chart_type": "world_map",
+ "country_format": "cca2",
+ "entity": {"name": "country"},
+ "metric": {"name": "sales", "aggregate": "SUM"},
+ }
+ ],
+ "deck_scatter": [
+ {
+ "chart_type": "deck_scatter",
+ "latitude": {"name": "latitude"},
+ "longitude": {"name": "longitude"},
+ }
+ ],
"xy": [
{
"chart_type": "xy",
@@ -386,10 +416,10 @@ def get_chart_type_schema(
for a chart configuration before calling generate_chart or update_chart.
Valid chart_type values depend on the host deployment. Core types are xy,
- table, pie, sunburst, gauge, treemap_v2, bubble_v2, pivot_table, mixed_timeseries,
- handlebars, big_number, histogram, box_plot, waterfall, and gantt.
- Deployments that enable an AG Grid pivot extension also expose
- interactive_pivot.
+ country_map, world_map, deck_scatter, table, pie, sunburst, gauge, treemap_v2,
+ bubble_v2, pivot_table, mixed_timeseries, handlebars, big_number, histogram,
+ box_plot, waterfall, and gantt. Deployments that enable an AG Grid pivot
+ extension also expose interactive_pivot.
Returns the JSON Schema for the requested chart type, optionally
with working examples.
diff --git a/superset/mcp_service/chart/tool/update_chart.py b/superset/mcp_service/chart/tool/update_chart.py
index eb94e20dd54..0ed6012ee58 100644
--- a/superset/mcp_service/chart/tool/update_chart.py
+++ b/superset/mcp_service/chart/tool/update_chart.py
@@ -83,16 +83,23 @@ def _finalize_response(payload: object) -> GenerateChartResponse:
def _get_existing_form_data(chart: Any) -> dict[str, Any]:
- """Return a chart's saved form data, treating malformed params as empty."""
- if not getattr(chart, "params", None):
- return {}
- try:
- parsed = json.loads(chart.params)
- except (ValueError, TypeError):
- parsed = None
- if not isinstance(parsed, dict):
- logger.warning("Failed to parse existing chart.params for chart %s", chart.id)
- return {}
+ """Return saved form data with a chart-level visualization fallback."""
+ parsed: dict[str, Any] = {}
+ if getattr(chart, "params", None):
+ try:
+ value = json.loads(chart.params)
+ except (ValueError, TypeError):
+ value = None
+ if isinstance(value, dict):
+ parsed = value
+ else:
+ logger.warning(
+ "Failed to parse existing chart.params for chart %s", chart.id
+ )
+ if not parsed.get("viz_type") and isinstance(
+ viz_type := getattr(chart, "viz_type", None), str
+ ):
+ parsed["viz_type"] = viz_type
return parsed
@@ -236,6 +243,29 @@ def _merge_replacement_config(
)
+def _map_replacement_config(
+ existing_form_data: dict[str, Any],
+ parsed_config: Any,
+ effective_dataset_id: int | None,
+) -> dict[str, Any]:
+ """Map a replacement config, allowing a disabled type only in place.
+
+ Disabled chart types remain editable when the saved chart already uses
+ them, but a different chart may not be converted into one.
+ """
+ saved_plugin = plugin_for_viz_type(existing_form_data.get("viz_type"))
+ new_form_data = map_config_to_form_data(
+ parsed_config,
+ dataset_id=effective_dataset_id,
+ include_disabled=(
+ saved_plugin is not None
+ and saved_plugin.chart_type == parsed_config.chart_type
+ ),
+ )
+ new_form_data.pop("_mcp_warnings", None)
+ return new_form_data
+
+
def _is_dataset_rebind(request: UpdateChartRequest, chart: Any) -> bool:
"""Return whether a request changes the chart's datasource identity."""
return request.dataset_id is not None and str(request.dataset_id) != str(
@@ -259,12 +289,13 @@ def _dataset_rebind_config_error(chart: Any) -> GenerateChartResponse:
"""Require complete target roles rather than returning an empty query."""
plugin = plugin_for_viz_type(getattr(chart, "viz_type", None))
display_name = plugin.display_name if plugin is not None else "Chart"
+ roles = plugin.dataset_rebind_roles if plugin is not None else "roles"
return _validation_error_response(
message=(
f"{display_name} dataset rebind requires a complete {display_name} config."
),
details=(
- "Provide the chart type and complete roles valid on the target "
+ f"Provide the chart type and complete {roles} valid on the target "
"dataset. This prevents stale metric, groupby, and filter roles "
"from the previous dataset from being retained."
),
@@ -289,11 +320,10 @@ def _build_update_payload( # noqa: C901
)
if parsed_config is not None:
- new_form_data = map_config_to_form_data(
- parsed_config, dataset_id=effective_dataset_id, include_disabled=True
- )
- new_form_data.pop("_mcp_warnings", None)
existing_form_data = _get_existing_form_data(chart)
+ new_form_data = _map_replacement_config(
+ existing_form_data, parsed_config, effective_dataset_id
+ )
dataset_rebind = _is_dataset_rebind(request, chart)
if not dataset_rebind:
merge_table_column_config(existing_form_data, new_form_data)
@@ -410,10 +440,9 @@ def _build_preview_form_data( # noqa: C901
)
if parsed_config is not None:
- new_form_data = map_config_to_form_data(
- parsed_config, dataset_id=effective_dataset_id, include_disabled=True
+ new_form_data = _map_replacement_config(
+ existing_form_data, parsed_config, effective_dataset_id
)
- new_form_data.pop("_mcp_warnings", None)
dataset_rebind = _is_dataset_rebind(request, chart)
if not dataset_rebind:
merge_table_column_config(existing_form_data, new_form_data)
diff --git a/superset/mcp_service/chart/tool/update_chart_preview.py b/superset/mcp_service/chart/tool/update_chart_preview.py
index 3c9b65671c2..850e2c4282d 100644
--- a/superset/mcp_service/chart/tool/update_chart_preview.py
+++ b/superset/mcp_service/chart/tool/update_chart_preview.py
@@ -266,18 +266,29 @@ def update_chart_preview( # noqa: C901
or (previous_form_data or {}).get("datasource_id")
or ""
).split("__", 1)[0]
- plugin = get_registry().get(config.chart_type, include_disabled=True)
+ previous_plugin = plugin_for_viz_type(
+ (previous_form_data or {}).get("viz_type")
+ )
+ include_disabled = bool(
+ previous_plugin and previous_plugin.chart_type == config.chart_type
+ )
+ plugin = get_registry().get(
+ config.chart_type, include_disabled=include_disabled
+ )
dataset_rebind = previous_datasource != str(dataset.id) and (
bool(previous_datasource)
or bool(plugin and plugin.unbound_form_data_is_rebind)
)
try:
- if plugin is not None:
- config = plugin.resolve_update_config(
- config,
- previous_form_data or {},
- dataset_rebind=dataset_rebind,
+ if plugin is None:
+ raise ValueError(
+ f"Chart type '{config.chart_type}' is disabled or unavailable"
)
+ config = plugin.resolve_update_config(
+ config,
+ previous_form_data or {},
+ dataset_rebind=dataset_rebind,
+ )
except ValueError as ex:
return {
"chart": None,
@@ -303,7 +314,7 @@ def update_chart_preview( # noqa: C901
# Map the new config to form_data format
# Pass dataset_id to enable column type checking
new_form_data = map_config_to_form_data(
- config, dataset_id=request.dataset_id, include_disabled=True
+ config, dataset_id=request.dataset_id, include_disabled=include_disabled
)
new_form_data.pop("_mcp_warnings", None)
new_form_data = canonicalize_operation_form_data(
diff --git a/superset/mcp_service/explore/tool/generate_explore_link.py b/superset/mcp_service/explore/tool/generate_explore_link.py
index ddff7617cf2..c51af3d633d 100644
--- a/superset/mcp_service/explore/tool/generate_explore_link.py
+++ b/superset/mcp_service/explore/tool/generate_explore_link.py
@@ -37,6 +37,7 @@ from superset.mcp_service.chart.chart_utils import (
map_config_to_form_data,
)
from superset.mcp_service.chart.compile import validate_and_compile
+from superset.mcp_service.chart.registry import get_registry
from superset.mcp_service.chart.schemas import (
GenerateExploreLinkRequest,
)
@@ -280,8 +281,10 @@ async def generate_explore_link(
normalized_config,
form_data,
dataset,
- run_compile_check=normalized_config.chart_type
- in ("gauge", "treemap_v2"),
+ run_compile_check=bool(
+ (plugin := get_registry().get(normalized_config.chart_type))
+ and plugin.requires_compile_check
+ ),
)
if not compile_result.success:
await ctx.warning(
diff --git a/superset/translations/ar/LC_MESSAGES/messages.po b/superset/translations/ar/LC_MESSAGES/messages.po
index 8ca4aa1e69d..a9c5adac7c0 100644
--- a/superset/translations/ar/LC_MESSAGES/messages.po
+++ b/superset/translations/ar/LC_MESSAGES/messages.po
@@ -2187,6 +2187,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "تصفية عمود التاريخ والوقت الرئيسي دائما"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "حدث خطأ"
@@ -3373,6 +3379,9 @@ msgstr "حجم الفقاعة"
msgid "Bubble size number format"
msgstr "تنسيق رقم حجم الفقاعة"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "نقاط كسر الجرافة"
@@ -7994,6 +8003,9 @@ msgstr "قم بتوسيع الصف"
msgid "Expand settings"
msgstr "توسيع الإعدادات"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -8011,6 +8023,12 @@ msgstr "تجريبي"
msgid "Expired"
msgstr "اكتشف"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "اكتشف"
@@ -8997,6 +9015,13 @@ msgstr "عمود جيوجسون"
msgid "GeoJson Settings"
msgstr "إعدادات جيوجسون"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "جيوهاش"
@@ -9276,12 +9301,18 @@ msgstr ""
msgid "Huge"
msgstr "ضخم"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "رموز أيزو 3166-2"
msgid "ISO 8601"
msgstr "أيزو 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
#, fuzzy
msgid "Icon URL"
msgstr "تحكم"
@@ -10198,6 +10229,9 @@ msgstr "القيمة اليسرى"
msgid "Legacy"
msgstr "تراث"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "أسطورة"
@@ -10702,6 +10736,11 @@ msgstr "حجم العلامة"
msgid "Markers"
msgstr "علامات"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "نوع الترميز"
@@ -11261,6 +11300,12 @@ msgstr ""
"تم قبول تنسيقات متعددة، ابحث في مكتبة geopy.points Python لمزيد من "
"التفاصيل"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "مضاعف"
@@ -13347,6 +13392,12 @@ msgstr "رسوم بيانية مُنعشة"
msgid "Refreshing columns"
msgstr "أعمدة منعشة"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
#, fuzzy
msgid "Register"
msgstr "التصفية المسبقة"
@@ -19891,6 +19942,17 @@ msgstr "إلغاء التثبيت من لوحة النتائج"
msgid "Unpin from top"
msgstr "إلغاء التثبيت من الأعلى"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -19911,6 +19973,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "نوع جملة غير معتمد: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/ca/LC_MESSAGES/messages.po b/superset/translations/ca/LC_MESSAGES/messages.po
index 777aad43426..f54cc486b66 100644
--- a/superset/translations/ca/LC_MESSAGES/messages.po
+++ b/superset/translations/ca/LC_MESSAGES/messages.po
@@ -2019,6 +2019,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Sempre filtrar columna principal de data/hora"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "S'ha Produït un Error"
@@ -3212,6 +3218,9 @@ msgstr "Mida de la bombolla"
msgid "Bubble size number format"
msgstr "Format numèric de la mida de bombolla"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Punts de trencament dels grups"
@@ -7658,6 +7667,9 @@ msgstr "Expandir fila"
msgid "Expand settings"
msgstr "Expandeix la configuració"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7676,6 +7688,12 @@ msgstr "Experimental"
msgid "Expired"
msgstr "Explorar"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Explorar"
@@ -8646,6 +8664,13 @@ msgstr "Columna GeoJson"
msgid "GeoJson Settings"
msgstr "Configuració GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -8918,12 +8943,18 @@ msgstr ""
msgid "Huge"
msgstr "Enorme"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "Codis ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
#, fuzzy
msgid "Icon URL"
msgstr "Copiar URL"
@@ -9820,6 +9851,9 @@ msgstr "Valor esquerre"
msgid "Legacy"
msgstr "Llegat"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Llegenda"
@@ -10312,6 +10346,11 @@ msgstr "Mida del marcador"
msgid "Markers"
msgstr "Marcadors"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Tipus de marcatge"
@@ -10837,6 +10876,12 @@ msgstr ""
"S'accepten múltiples formats, consulta la llibreria Python geopy.points "
"per més detalls"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Multiplicador"
@@ -12875,6 +12920,12 @@ msgstr "Actualitzant gràfics"
msgid "Refreshing columns"
msgstr "Actualitzant columnes"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
#, fuzzy
msgid "Register"
msgstr "Pre-filtre"
@@ -19315,6 +19366,17 @@ msgstr "Desancora del panell de resultats"
msgid "Unpin from top"
msgstr "Desancora de la part superior"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -19335,6 +19397,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Tipus de clàusula no suportada: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/cs/LC_MESSAGES/messages.po b/superset/translations/cs/LC_MESSAGES/messages.po
index ab8603acdb2..dbd7c7fc24f 100644
--- a/superset/translations/cs/LC_MESSAGES/messages.po
+++ b/superset/translations/cs/LC_MESSAGES/messages.po
@@ -2007,6 +2007,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Vždy filtrovat hlavní sloupec data a času"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Došlo k chybě"
@@ -3131,6 +3137,9 @@ msgstr "Velikost bublin"
msgid "Bubble size number format"
msgstr "Formát čísla pro velikost bublin"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Body zlomu intervalů"
@@ -7417,6 +7426,9 @@ msgstr "Rozbalit řádek"
msgid "Expand settings"
msgstr "Rozbalit nastavení"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7435,6 +7447,12 @@ msgstr "Experimentální"
msgid "Expired"
msgstr "Prozkoumat"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Prozkoumat"
@@ -8347,6 +8365,13 @@ msgstr "Sloupec GeoJson"
msgid "GeoJson Settings"
msgstr "Nastavení GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -8604,12 +8629,18 @@ msgstr ""
msgid "Huge"
msgstr "Obrovský"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "Kódy ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr "URL ikony"
@@ -9473,6 +9504,9 @@ msgstr "Levá hodnota"
msgid "Legacy"
msgstr "Starší verze"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Legenda"
@@ -9957,6 +9991,11 @@ msgstr "Velikost značky"
msgid "Markers"
msgstr "Značky"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Typ značkování"
@@ -10464,6 +10503,12 @@ msgstr ""
"Je přijímáno více formátů, podívejte se na knihovnu geopy.points v "
"Pythonu pro více podrobností"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Násobitel"
@@ -12461,6 +12506,12 @@ msgstr "Obnovování grafů"
msgid "Refreshing columns"
msgstr "Obnovování sloupců"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
#, fuzzy
msgid "Register"
msgstr "Registrovat"
@@ -18638,6 +18689,17 @@ msgstr "Připnout k panelu výsledků"
msgid "Unpin from top"
msgstr "Odepnout z horní části"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -18658,6 +18720,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Nepodporovaný typ klauzule: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
msgstr ""
"Nepodporovaný typ souboru. Prosím, použijte soubory CSV, Excel nebo "
diff --git a/superset/translations/de/LC_MESSAGES/messages.po b/superset/translations/de/LC_MESSAGES/messages.po
index ed6c857792b..6091fa00f0a 100644
--- a/superset/translations/de/LC_MESSAGES/messages.po
+++ b/superset/translations/de/LC_MESSAGES/messages.po
@@ -1951,6 +1951,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Hauptdatums-Spalte immer filtern"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Ein Fehler ist aufgetreten"
@@ -3078,6 +3084,9 @@ msgstr "Blasengröße"
msgid "Bubble size number format"
msgstr "Zahlenformat der Blasengröße"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Klassen-Schwellwerte"
@@ -7259,6 +7268,9 @@ msgstr "Zeile zum Bearbeiten erweitern"
msgid "Expand settings"
msgstr "Einstellungen aufklappen"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7276,6 +7288,12 @@ msgstr "Experimentell"
msgid "Expired"
msgstr "Abgelaufen"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Erkunden"
@@ -8160,6 +8178,13 @@ msgstr "GeoJson-Spalte"
msgid "GeoJson Settings"
msgstr "GeoJson-Einstellungen"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geo Hashing"
@@ -8416,12 +8441,18 @@ msgstr ""
msgid "Huge"
msgstr "Riesig"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "ISO-3166-2-Codes"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr "Symbol-URL"
@@ -9259,6 +9290,9 @@ msgstr "Linker Wert"
msgid "Legacy"
msgstr "Veraltet"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Legende"
@@ -9715,6 +9749,11 @@ msgstr "Markergröße"
msgid "Markers"
msgstr "Marker"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Markup-Typ"
@@ -10205,6 +10244,12 @@ msgstr ""
"Mehrere Formate akzeptiert, recherchieren Sie in der geopy.points Python-"
"Bibliothek nach weiteren Details"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Multiplikator"
@@ -12170,6 +12215,12 @@ msgstr "Aktualisieren von Diagrammen"
msgid "Refreshing columns"
msgstr "Aktualisieren von Spalten"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
msgid "Register"
msgstr "Registrieren"
@@ -18252,6 +18303,17 @@ msgstr "Vom Ergebnisbereich lösen"
msgid "Unpin from top"
msgstr "Von oben lösen"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -18272,6 +18334,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Nicht unterstützter Ausdruck-Typ: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
msgstr ""
"Nicht unterstützter Dateityp. Bitte verwenden Sie CSV-, Excel- oder "
diff --git a/superset/translations/en/LC_MESSAGES/messages.po b/superset/translations/en/LC_MESSAGES/messages.po
index eb3c3b35264..f3d37feb0e0 100644
--- a/superset/translations/en/LC_MESSAGES/messages.po
+++ b/superset/translations/en/LC_MESSAGES/messages.po
@@ -1795,6 +1795,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr ""
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr ""
@@ -2807,6 +2813,9 @@ msgstr ""
msgid "Bubble size number format"
msgstr ""
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr ""
@@ -6703,6 +6712,9 @@ msgstr ""
msgid "Expand settings"
msgstr ""
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -6716,6 +6728,12 @@ msgstr ""
msgid "Expired"
msgstr ""
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr ""
@@ -7548,6 +7566,13 @@ msgstr ""
msgid "GeoJson Settings"
msgstr ""
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr ""
@@ -7791,12 +7816,18 @@ msgstr ""
msgid "Huge"
msgstr ""
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr ""
msgid "ISO 8601"
msgstr ""
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr ""
@@ -8603,6 +8634,9 @@ msgstr ""
msgid "Legacy"
msgstr ""
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr ""
@@ -9038,6 +9072,11 @@ msgstr ""
msgid "Markers"
msgstr ""
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr ""
@@ -9507,6 +9546,12 @@ msgid ""
"details"
msgstr ""
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr ""
@@ -11345,6 +11390,12 @@ msgstr ""
msgid "Refreshing columns"
msgstr ""
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
msgid "Register"
msgstr ""
@@ -16760,6 +16811,17 @@ msgstr ""
msgid "Unpin from top"
msgstr ""
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "Unsafe link blocked"
msgstr ""
@@ -16778,6 +16840,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr ""
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
msgstr ""
diff --git a/superset/translations/es/LC_MESSAGES/messages.po b/superset/translations/es/LC_MESSAGES/messages.po
index 0c4a982902d..e669c5f3e07 100644
--- a/superset/translations/es/LC_MESSAGES/messages.po
+++ b/superset/translations/es/LC_MESSAGES/messages.po
@@ -1997,6 +1997,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Filtrar siempre la columna principal de fecha y hora"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Se ha producido un error"
@@ -3172,6 +3178,9 @@ msgstr "Tamaño de las burbujas"
msgid "Bubble size number format"
msgstr "Formato numérico del tamaño de las burbujas"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Puntos de ruptura del «bucket»"
@@ -7575,6 +7584,9 @@ msgstr "Expandir fila"
msgid "Expand settings"
msgstr "Expandir ajustes"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7592,6 +7604,12 @@ msgstr "Experimental"
msgid "Expired"
msgstr "Caducado"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Explorar"
@@ -8545,6 +8563,13 @@ msgstr "Columna GeoJson"
msgid "GeoJson Settings"
msgstr "Ajustes GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -8813,12 +8838,18 @@ msgstr ""
msgid "Huge"
msgstr "Enorme"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "Códigos ISO-3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
#, fuzzy
msgid "Icon URL"
msgstr "Copiar URL"
@@ -9714,6 +9745,9 @@ msgstr "Valor izquierdo"
msgid "Legacy"
msgstr "Heredado"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Leyenda"
@@ -10205,6 +10239,11 @@ msgstr "Tamaño del marcador"
msgid "Markers"
msgstr "Marcadores"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Tipo de marcado"
@@ -10729,6 +10768,12 @@ msgstr ""
"Se aceptan varios formatos; consulta la biblioteca geopy.points de Python"
" para obtener más detalles"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Multiplicador"
@@ -12765,6 +12810,12 @@ msgstr "Actualizando gráficos"
msgid "Refreshing columns"
msgstr "Actualizando columnas"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
#, fuzzy
msgid "Register"
msgstr "Filtro previo"
@@ -19196,6 +19247,17 @@ msgstr "Desanclar en el panel de resultados"
msgid "Unpin from top"
msgstr "Desanclar de la parte superior"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -19216,6 +19278,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Tipo de cláusula no admitido: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
diff --git a/superset/translations/fa/LC_MESSAGES/messages.po b/superset/translations/fa/LC_MESSAGES/messages.po
index 98e4d45cd0e..903219337ab 100644
--- a/superset/translations/fa/LC_MESSAGES/messages.po
+++ b/superset/translations/fa/LC_MESSAGES/messages.po
@@ -2038,6 +2038,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "همیشه ستون اصلی تاریخ و زمان را فیلتر کنید"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "یک خطا رخ داد"
@@ -3205,6 +3211,9 @@ msgstr "اندازه حباب"
msgid "Bubble size number format"
msgstr "فرمت عدد اندازه حباب"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "نقاط شکسته سطل"
@@ -7690,6 +7699,9 @@ msgstr "گسترش ردیف"
msgid "Expand settings"
msgstr "گسترش تنظیمات"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7708,6 +7720,12 @@ msgstr "آزمایشی"
msgid "Expired"
msgstr "کاوش کنید"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "کاوش کنید"
@@ -8681,6 +8699,13 @@ msgstr "ستون GeoJson"
msgid "GeoJson Settings"
msgstr "تنظیمات GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "ژئوهش"
@@ -8954,12 +8979,18 @@ msgstr ""
msgid "Huge"
msgstr "بسیار بزرگ"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "کدهای ISO ۳۱۶۶-۲"
msgid "ISO 8601"
msgstr "استاندارد ISO ۸۶۰۱"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
#, fuzzy
msgid "Icon URL"
msgstr "کنترل"
@@ -9876,6 +9907,9 @@ msgstr "مقدار سمت چپ"
msgid "Legacy"
msgstr "میراث"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "افسانه"
@@ -10376,6 +10410,11 @@ msgstr "اندازه نشانگر"
msgid "Markers"
msgstr "نشانگرها"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "نوع نشانهگذاری"
@@ -10918,6 +10957,12 @@ msgstr ""
"فرمتهای متنوعی پذیرفته میشوند، به کتابخانه پایتون geopy.points برای "
"جزئیات بیشتر مراجعه کنید."
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "ضریب"
@@ -12967,6 +13012,12 @@ msgstr "بهروزرسانی نمودارها"
msgid "Refreshing columns"
msgstr "بارگذاری مجدد ستونها"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
#, fuzzy
msgid "Register"
msgstr "پیشفیلتر"
@@ -19418,6 +19469,17 @@ msgstr "جداکردن از پنل نتایج"
msgid "Unpin from top"
msgstr "جداکردن از بالا"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -19438,6 +19500,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "نوع بند پشتیبانی نشده: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/fi/LC_MESSAGES/messages.po b/superset/translations/fi/LC_MESSAGES/messages.po
index 5f778ea17f3..17457fb8e4e 100644
--- a/superset/translations/fi/LC_MESSAGES/messages.po
+++ b/superset/translations/fi/LC_MESSAGES/messages.po
@@ -3022,6 +3022,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Suodata aina ensisijainen päivämäärä- ja aikasarake"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, tr, uk]
#, fuzzy
@@ -4877,6 +4883,9 @@ msgstr "Kuplan koko"
msgid "Bubble size number format"
msgstr "Kuplan koon lukumuoto"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
@@ -11998,6 +12007,9 @@ msgstr "Laajenna rivi"
msgid "Expand settings"
msgstr "Laajenna asetukset"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk]
#, fuzzy
@@ -12025,6 +12037,12 @@ msgstr "Kokeellinen"
msgid "Expired"
msgstr "Tutki"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, it, ja, lv, mi, nl, pl, pt, pt_BR, ru, sk, sl, uk, zh,
# zh_TW]
@@ -13508,6 +13526,13 @@ msgstr "GeoJson-sarake"
msgid "GeoJson Settings"
msgstr "GeoJson-asetukset"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
@@ -13962,6 +13987,9 @@ msgstr ""
msgid "Huge"
msgstr "Valtava"
+msgid "ISO 3166-2"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
@@ -13974,6 +14002,9 @@ msgstr "ISO 3166-2 -koodit"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, fr, ja,
# lv, ru, sk, uk]
#, fuzzy
@@ -15425,6 +15456,9 @@ msgstr "Vasen arvo"
msgid "Legacy"
msgstr "Vanhentunut"
+msgid "Legacy ISO values"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
@@ -16220,6 +16254,11 @@ msgstr "Merkit"
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Merkintätyyppi"
@@ -17063,6 +17102,12 @@ msgstr ""
"Useita formaatteja hyväksytään, katso geopy.points Python-kirjastosta "
"lisätietoja"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, uk]
#, fuzzy
@@ -20431,6 +20476,12 @@ msgstr "Päivitetään kaavioita"
msgid "Refreshing columns"
msgstr "Päivitetään sarakkeita"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ja, lv, ru,
# uk]
#, fuzzy
@@ -30062,6 +30113,17 @@ msgstr "Kiinnitä tulosruutuun"
msgid "Unpin from top"
msgstr "Irrota yläreunasta"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -30088,6 +30150,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Ei-tuettu lausetyyppi: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, ja, lv,
# ru, sk, uk]
#, fuzzy
diff --git a/superset/translations/fr/LC_MESSAGES/messages.po b/superset/translations/fr/LC_MESSAGES/messages.po
index 0964ff6e37c..f02e3d7083d 100644
--- a/superset/translations/fr/LC_MESSAGES/messages.po
+++ b/superset/translations/fr/LC_MESSAGES/messages.po
@@ -1968,6 +1968,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Toujours filtrer sur la colonne temporelle principale"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Une erreur s'est produite"
@@ -3128,6 +3134,9 @@ msgstr "Taille de la bulle"
msgid "Bubble size number format"
msgstr "Format numérique de la taille des bulles"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Points de rupture du seau"
@@ -7369,6 +7378,9 @@ msgstr "Développer la ligne à modifier"
msgid "Expand settings"
msgstr "Développer les paramètres"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7385,6 +7397,12 @@ msgstr "Expérimental"
msgid "Expired"
msgstr "Expiré"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Explorer"
@@ -8295,6 +8313,13 @@ msgstr "Colonne GeoJson"
msgid "GeoJson Settings"
msgstr "Paramètres GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -8554,12 +8579,18 @@ msgstr ""
msgid "Huge"
msgstr "Énorme"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "Codes ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr "URL de l'icône"
@@ -9407,6 +9438,9 @@ msgstr "Valeur gauche"
msgid "Legacy"
msgstr "Hérité"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Légende"
@@ -9881,6 +9915,11 @@ msgstr "Taille du marqueur"
msgid "Markers"
msgstr "Marqueurs"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Type de marqueur"
@@ -10380,6 +10419,12 @@ msgstr ""
"Plusieurs formats sont acceptés, consultez la bibliothèque Python "
"geopy.points pour plus de détails"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Multiplicateur"
@@ -12360,6 +12405,12 @@ msgstr "Actualisation des graphiques"
msgid "Refreshing columns"
msgstr "Actualisation des colonnes"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
msgid "Register"
msgstr "S'inscrire"
@@ -18601,6 +18652,17 @@ msgstr "Détacher du panneau de résultat"
msgid "Unpin from top"
msgstr "Détacher du haut"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
msgid "Unsafe link blocked"
msgstr "Lien non sécurisé bloqué"
@@ -18620,6 +18682,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Type de clause non pris en charge : %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ru, sk, sr, sr_Latn, tr, uk]
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
diff --git a/superset/translations/it/LC_MESSAGES/messages.po b/superset/translations/it/LC_MESSAGES/messages.po
index 2ccbaf01187..6930f9f468e 100644
--- a/superset/translations/it/LC_MESSAGES/messages.po
+++ b/superset/translations/it/LC_MESSAGES/messages.po
@@ -2381,6 +2381,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Filtra sempre sulla colonna temporale principale"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Errore"
@@ -3762,6 +3768,9 @@ msgstr "Grandezza della bolla"
msgid "Bubble size number format"
msgstr "Formato numero dimensione bolle"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -8953,6 +8962,9 @@ msgstr "Espandi la riga da modificare"
msgid "Expand settings"
msgstr "Espandi impostazioni"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk]
@@ -8974,6 +8986,12 @@ msgstr "Sperimentale"
msgid "Expired"
msgstr "Scaduto"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Esplora"
@@ -10110,6 +10128,13 @@ msgstr "Colonna GeoJson"
msgid "GeoJson Settings"
msgstr "Impostazioni GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -10462,6 +10487,9 @@ msgstr ""
msgid "Huge"
msgstr "Enorme"
+msgid "ISO 3166-2"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -10474,6 +10502,9 @@ msgstr "Codici ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr "URL icona"
@@ -11608,6 +11639,9 @@ msgstr "Valore sinistro"
msgid "Legacy"
msgstr "Eredità"
+msgid "Legacy ISO values"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -12217,6 +12251,11 @@ msgstr "Marcatori"
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Tipo di markup"
@@ -12875,6 +12914,12 @@ msgstr ""
"Sono accettati più formati, guarda la libreria Python geopy.points per "
"maggiori dettagli"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Moltiplicatore"
@@ -15361,6 +15406,12 @@ msgstr "Aggiornamento grafii"
msgid "Refreshing columns"
msgstr "Aggiornamento colonne"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
msgid "Register"
msgstr "Registrazione"
@@ -22938,6 +22989,17 @@ msgstr "Rimuovi dal pannello dei risultati"
msgid "Unpin from top"
msgstr "Rimuovi dalla parte superiore"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
msgid "Unsafe link blocked"
msgstr "Collegamento non sicuro bloccato"
@@ -22966,6 +23028,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Tipo di clausola non supportato: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ru, sk, sr, sr_Latn, tr, uk]
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
diff --git a/superset/translations/ja/LC_MESSAGES/messages.po b/superset/translations/ja/LC_MESSAGES/messages.po
index 1a70f15e1c4..140ee82daea 100644
--- a/superset/translations/ja/LC_MESSAGES/messages.po
+++ b/superset/translations/ja/LC_MESSAGES/messages.po
@@ -1817,6 +1817,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "常にメインの日時列をフィルタリングする"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "エラーが発生しました"
@@ -2829,6 +2835,9 @@ msgstr "バブルのサイズ"
msgid "Bubble size number format"
msgstr "バブルサイズの数値形式"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "バケットの境界点"
@@ -6716,6 +6725,9 @@ msgstr "行を展開して編集"
msgid "Expand settings"
msgstr "設定を展開"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -6731,6 +6743,12 @@ msgstr "試験的"
msgid "Expired"
msgstr "期限切れ"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "探索"
@@ -7566,6 +7584,13 @@ msgstr "GeoJSON列"
msgid "GeoJson Settings"
msgstr "GeoJSON設定"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "ジオハッシュ (Geohash)"
@@ -7809,12 +7834,18 @@ msgstr "タイムシフト(時間差比較)の表示方法: 個別の線とし
msgid "Huge"
msgstr "巨大"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "ISO 3166-2コード"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr "アイコンURL"
@@ -8627,6 +8658,9 @@ msgstr "左の値"
msgid "Legacy"
msgstr "レガシー"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "凡例"
@@ -9063,6 +9097,11 @@ msgstr "マーカーのサイズ"
msgid "Markers"
msgstr "マーカー"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "マークアップの種類"
@@ -9530,6 +9569,12 @@ msgid ""
"details"
msgstr "複数の形式に対応しています。詳細はPythonのgeopy.pointsライブラリを確認してください"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "乗数"
@@ -11369,6 +11414,12 @@ msgstr "チャートを更新中"
msgid "Refreshing columns"
msgstr "列を更新中"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
msgid "Register"
msgstr "登録"
@@ -16850,6 +16901,17 @@ msgstr "結果パネルのピン留めを解除"
msgid "Unpin from top"
msgstr "上部のピン留めを解除"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "Unsafe link blocked"
msgstr "安全でないリンクがブロックされました"
@@ -16868,6 +16930,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "サポートされていない句タイプです: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
msgstr "サポートされていないファイル形式です。CSV, Excel, Columnar ファイルを使用してください。"
diff --git a/superset/translations/ko/LC_MESSAGES/messages.po b/superset/translations/ko/LC_MESSAGES/messages.po
index 1d6bdf184eb..ecc89c0ab0a 100644
--- a/superset/translations/ko/LC_MESSAGES/messages.po
+++ b/superset/translations/ko/LC_MESSAGES/messages.po
@@ -2708,6 +2708,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "항상 기본 날짜/시간 열 필터링"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr, uk]
#, fuzzy
@@ -4336,6 +4342,9 @@ msgstr "버블 크기"
msgid "Bubble size number format"
msgstr "버블 크기 숫자 형식"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -10623,6 +10632,9 @@ msgstr "컬럼 보기"
msgid "Expand settings"
msgstr "설정 펼치기"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk]
@@ -10648,6 +10660,12 @@ msgstr "실험적"
msgid "Expired"
msgstr "테이블 명"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, it, ja, lv, mi, nl, pl, pt, pt_BR, ro, ru, sk, sl, sr,
# sr_Latn, tr, uk, zh, zh_TW]
@@ -12016,6 +12034,13 @@ msgstr "컬럼 추가"
msgid "GeoJson Settings"
msgstr "Query 공유"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -12452,6 +12477,9 @@ msgstr ""
msgid "Huge"
msgstr "매우 큰"
+msgid "ISO 3166-2"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -12466,6 +12494,9 @@ msgstr "ISO 3166-2 코드"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
#, fuzzy
msgid "Icon URL"
msgstr "컬럼 추가"
@@ -13786,6 +13817,9 @@ msgstr "테이블 명"
msgid "Legacy"
msgstr "레거시"
+msgid "Legacy ISO values"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -14540,6 +14574,11 @@ msgstr "마커"
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
#, fuzzy
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "마크업 유형"
@@ -15333,6 +15372,12 @@ msgid ""
"details"
msgstr "여러 형식이 허용됩니다. 자세한 내용은 Python의 geopy.points 라이브러리를 참조하세요"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr, uk]
#, fuzzy
@@ -18458,6 +18503,12 @@ msgstr "데이터 베이스 목록을 가져오는 도중 에러가 발생하였
msgid "Refreshing columns"
msgstr "컬럼 수정"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
#, fuzzy
msgid "Register"
msgstr "필터"
@@ -27262,6 +27313,17 @@ msgstr "결과 패널에서 고정 해제"
msgid "Unpin from top"
msgstr "상단에서 고정 해제"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -27291,6 +27353,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "지원되지 않는 절 유형: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/lv/LC_MESSAGES/messages.po b/superset/translations/lv/LC_MESSAGES/messages.po
index aa5e39e00f4..39b65e244b8 100644
--- a/superset/translations/lv/LC_MESSAGES/messages.po
+++ b/superset/translations/lv/LC_MESSAGES/messages.po
@@ -1994,6 +1994,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Vienmēr filtrēt galveno datuma/laika kolonnu"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Radās kļūda"
@@ -3099,6 +3105,9 @@ msgstr "Burbuļa izmērs"
msgid "Bubble size number format"
msgstr "Burbuļa lieluma skaitļa formāts"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Intervālu pārrāvuma punkti"
@@ -7286,6 +7295,9 @@ msgstr "Izvērst rindu"
msgid "Expand settings"
msgstr "Izvērst iestatījumus"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7304,6 +7316,12 @@ msgstr "Eksperimentāls"
msgid "Expired"
msgstr "Pārlūkot"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Pārlūkot"
@@ -8197,6 +8215,13 @@ msgstr "GeoJson kolonna"
msgid "GeoJson Settings"
msgstr "GeoJson iestatījumi"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Ģeoheša"
@@ -8450,12 +8475,18 @@ msgstr ""
msgid "Huge"
msgstr "Milzīgs"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "ISO 3166-2 kodi"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr "Ikonas URL"
@@ -9292,6 +9323,9 @@ msgstr "Kreisā vērtība"
msgid "Legacy"
msgstr "Mantojums"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Leģenda"
@@ -9768,6 +9802,11 @@ msgstr "Marķiera izmērs"
msgid "Markers"
msgstr "Marķieri"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Iezīmēšanas tips"
@@ -10268,6 +10307,12 @@ msgstr ""
"Tiek pieņemti vairāki formāti, skatiet geopy.points Python bibliotēku "
"plašākai informācijai"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Reizinātājs"
@@ -12228,6 +12273,12 @@ msgstr "Atsvaidzina diagrammas"
msgid "Refreshing columns"
msgstr "Atsvaidzina kolonnas"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
msgid "Register"
msgstr "Reģistrēties"
@@ -18292,6 +18343,17 @@ msgstr "Piespraust rezultātu panelī"
msgid "Unpin from top"
msgstr "Atspraust no augšas"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -18312,6 +18374,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Neatbalstīts teikuma veids: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
msgstr ""
"Neatbalstīts faila veids. Lūdzu, izmantojiet CSV, Excel vai Columnar "
diff --git a/superset/translations/messages.pot b/superset/translations/messages.pot
index 305a84a52b9..77408c0502a 100644
--- a/superset/translations/messages.pot
+++ b/superset/translations/messages.pot
@@ -1685,6 +1685,10 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr ""
+#, python-format
+msgid "Ambiguous region %s; choose the matching country/region format or explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr ""
@@ -2632,6 +2636,9 @@ msgstr ""
msgid "Bubble size number format"
msgstr ""
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr ""
@@ -4141,6 +4148,10 @@ msgstr ""
msgid "Country"
msgstr ""
+#, python-format
+msgid "Country %s has no world-map boundary; enable bubbles or filter the dataset."
+msgstr ""
+
msgid "Country Color Scheme"
msgstr ""
@@ -6308,6 +6319,9 @@ msgstr ""
msgid "Expand settings"
msgstr ""
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the formulas.\n"
@@ -6320,6 +6334,12 @@ msgstr ""
msgid "Expired"
msgstr ""
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr ""
@@ -7107,6 +7127,13 @@ msgstr ""
msgid "GeoJson Settings"
msgstr ""
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr ""
@@ -7345,12 +7372,18 @@ msgstr ""
msgid "Huge"
msgstr ""
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr ""
msgid "ISO 8601"
msgstr ""
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr ""
@@ -8123,6 +8156,9 @@ msgstr ""
msgid "Legacy"
msgstr ""
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr ""
@@ -8527,6 +8563,9 @@ msgstr ""
msgid "Markers"
msgstr ""
+msgid "Marks charts created through the MCP geographic contract so Explore keeps their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr ""
@@ -8975,6 +9014,10 @@ msgstr ""
msgid "Multiple formats accepted, look the geopy.points Python library for more details"
msgstr ""
+#, python-format
+msgid "Multiple result rows resolve to %s; normalize source values before aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr ""
@@ -10381,6 +10424,9 @@ msgstr ""
msgid "Preparing export…"
msgstr ""
+msgid "Preserves the generated dashboard-time predicate subject through Explore saves."
+msgstr ""
+
msgid "Preview"
msgstr ""
@@ -10715,6 +10761,12 @@ msgstr ""
msgid "Refreshing columns"
msgstr ""
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
msgid "Register"
msgstr ""
@@ -15590,6 +15642,15 @@ msgstr ""
msgid "Unpin from top"
msgstr ""
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid "Unrecognized region %s; choose the matching country/region format or explicitly filter the dataset."
+msgstr ""
+
msgid "Unsafe link blocked"
msgstr ""
@@ -15608,6 +15669,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr ""
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
msgstr ""
diff --git a/superset/translations/mi/LC_MESSAGES/messages.po b/superset/translations/mi/LC_MESSAGES/messages.po
index 2f191d53912..63a2a562b9c 100644
--- a/superset/translations/mi/LC_MESSAGES/messages.po
+++ b/superset/translations/mi/LC_MESSAGES/messages.po
@@ -2019,6 +2019,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Tātari i te tīwae wā matua i ngā wā katoa"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "I Puta He Hapa"
@@ -3183,6 +3189,9 @@ msgstr "Rahi pōro"
msgid "Bubble size number format"
msgstr "Hōputu tau rahi pōro"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Tohu wehe peeke"
@@ -7594,6 +7603,9 @@ msgstr "Whakawhānui rārangi"
msgid "Expand settings"
msgstr "Whakawhānui tautuhinga"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7608,6 +7620,12 @@ msgstr "Whakamātautau"
msgid "Expired"
msgstr "Tūhura"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Tūhura"
@@ -8579,6 +8597,13 @@ msgstr "Tīwae GeoJson"
msgid "GeoJson Settings"
msgstr "Tautuhinga GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -8854,12 +8879,18 @@ msgstr ""
msgid "Huge"
msgstr "Nui rawa"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "Waehere ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
#, fuzzy
msgid "Icon URL"
msgstr "Tārua URL"
@@ -9752,6 +9783,9 @@ msgstr "Uara mauī"
msgid "Legacy"
msgstr "Tawhito"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Kōrero"
@@ -10242,6 +10276,11 @@ msgstr "Rahi tohu"
msgid "Markers"
msgstr "Tohu"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Momo tohu"
@@ -10765,6 +10804,12 @@ msgstr ""
"Ka whakaae i ngā hōputu maha, tirohia te whare pukapuka Python "
"geopy.points mō ētahi taipitopito atu"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Kaiwhakarea"
@@ -12796,6 +12841,12 @@ msgstr "E whakahou ana i ngā kauwhata"
msgid "Refreshing columns"
msgstr "E whakahou ana i ngā tīwae"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
#, fuzzy
msgid "Register"
msgstr "Tātari-i-mua"
@@ -19207,6 +19258,17 @@ msgstr "Wetekina mai i te papa hua"
msgid "Unpin from top"
msgstr "Wetekina mai i runga"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -19227,6 +19289,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Momo rerenga kāore e tautokona: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/nl/LC_MESSAGES/messages.po b/superset/translations/nl/LC_MESSAGES/messages.po
index b26cc629983..fc5d4657247 100644
--- a/superset/translations/nl/LC_MESSAGES/messages.po
+++ b/superset/translations/nl/LC_MESSAGES/messages.po
@@ -2060,6 +2060,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Filter altijd de hoofdkolom datumtijd"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Er is een fout opgetreden"
@@ -3290,6 +3296,9 @@ msgstr "Bubbelgrootte"
msgid "Bubble size number format"
msgstr "Bubbel grootte nummer opmaak"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Bucket breakpoints"
@@ -7863,6 +7872,9 @@ msgstr "Rij uitklappen"
msgid "Expand settings"
msgstr "Instellingen uitklappen"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7881,6 +7893,12 @@ msgstr "Experimenteel"
msgid "Expired"
msgstr "Verken"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Verken"
@@ -8875,6 +8893,13 @@ msgstr "GeoJson Kolom"
msgid "GeoJson Settings"
msgstr "GeoJson Instellingen"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -9157,12 +9182,18 @@ msgstr ""
msgid "Huge"
msgstr "Enorm"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "ISO 3166-2 Codes"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
#, fuzzy
msgid "Icon URL"
msgstr "Controle"
@@ -10092,6 +10123,9 @@ msgstr "Linker waarde"
msgid "Legacy"
msgstr "Verouderd"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Legenda"
@@ -10596,6 +10630,11 @@ msgstr "Marker grootte"
msgid "Markers"
msgstr "Markeringen"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Opmaaktype(Markup type)"
@@ -11156,6 +11195,12 @@ msgstr ""
"Meerdere formaten geaccepteerd, zie de geopy.points Python bibliotheek "
"voor meer details"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Vermenigvuldiger"
@@ -13245,6 +13290,12 @@ msgstr "Verversen grafieken"
msgid "Refreshing columns"
msgstr "Verversen kolommen"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
#, fuzzy
msgid "Register"
msgstr "Vooraf-filter"
@@ -19879,6 +19930,17 @@ msgstr "Losmaken van het resultaatpaneel"
msgid "Unpin from top"
msgstr "Losmaken van bovenaan"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -19899,6 +19961,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Niet-ondersteunde clausule type: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/pl/LC_MESSAGES/messages.po b/superset/translations/pl/LC_MESSAGES/messages.po
index 63751454ca5..ee9ca549231 100644
--- a/superset/translations/pl/LC_MESSAGES/messages.po
+++ b/superset/translations/pl/LC_MESSAGES/messages.po
@@ -2112,6 +2112,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Zawsze filtruj główną kolumnę daty i czasu"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
#, fuzzy
msgid "An Error Occurred"
msgstr "Wystąpił błąd"
@@ -3301,6 +3307,9 @@ msgstr "Rozmiar bąbelków"
msgid "Bubble size number format"
msgstr "Format liczbowy rozmiaru bąbelków"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Punkty przerwania segmentów"
@@ -7985,6 +7994,9 @@ msgstr "Rozwiń wiersz"
msgid "Expand settings"
msgstr "Rozwiń ustawienia"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -8002,6 +8014,12 @@ msgstr "Eksperymentalne"
msgid "Expired"
msgstr "Eksploruj"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Eksploruj"
@@ -9030,6 +9048,13 @@ msgstr "Kolumna GeoJson"
msgid "GeoJson Settings"
msgstr "Ustawienia GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -9320,12 +9345,18 @@ msgstr ""
msgid "Huge"
msgstr "Ogromny"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "Kody ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
#, fuzzy
msgid "Icon URL"
msgstr "Sterowanie"
@@ -10248,6 +10279,9 @@ msgstr "Wartość lewa"
msgid "Legacy"
msgstr "Dziedziczenie"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Legenda"
@@ -10748,6 +10782,11 @@ msgstr "Rozmiar znacznika"
msgid "Markers"
msgstr "Znaczniki"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Typ znacznika"
@@ -11305,6 +11344,12 @@ msgstr ""
"Obsługiwane są różne formaty, więcej szczegółów znajdziesz w bibliotece "
"Python geopy.points."
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
#, fuzzy
msgid "Multiplier"
msgstr "Mnożnik"
@@ -13449,6 +13494,12 @@ msgstr "Odświeżanie wykresów"
msgid "Refreshing columns"
msgstr "Odświeżanie kolumn"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
#, fuzzy
msgid "Register"
msgstr "Filtr wstępny"
@@ -20217,6 +20268,17 @@ msgstr "Odepnij od panelu wyników"
msgid "Unpin from top"
msgstr "Odepnij od góry"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -20237,6 +20299,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Nieobsługiwany typ klauzuli: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/pt/LC_MESSAGES/messages.po b/superset/translations/pt/LC_MESSAGES/messages.po
index 7aa6abb5079..18848e971e9 100644
--- a/superset/translations/pt/LC_MESSAGES/messages.po
+++ b/superset/translations/pt/LC_MESSAGES/messages.po
@@ -2490,6 +2490,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Sempre filtrar a coluna principal de data e hora"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Ocorreu um erro"
@@ -3905,6 +3911,9 @@ msgstr "Tamanho da bolha"
msgid "Bubble size number format"
msgstr "Formato do número do tamanho da bolha"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -9309,6 +9318,9 @@ msgstr "Expandir linha para editar"
msgid "Expand settings"
msgstr "Expandir definições"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk]
@@ -9332,6 +9344,12 @@ msgstr "Experimental"
msgid "Expired"
msgstr "Expirado"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Explorar"
@@ -10484,6 +10502,13 @@ msgstr "Coluna GeoJson"
msgid "GeoJson Settings"
msgstr "Configurações de GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -10826,6 +10851,9 @@ msgstr ""
msgid "Huge"
msgstr "Enorme"
+msgid "ISO 3166-2"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -10838,6 +10866,9 @@ msgstr "Códigos ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr "URL do ícone"
@@ -11968,6 +11999,9 @@ msgstr "Valor esquerdo"
msgid "Legacy"
msgstr "Legado"
+msgid "Legacy ISO values"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr,
# uk, zh, zh_TW]
@@ -12608,6 +12642,11 @@ msgstr "Tamanho do marcador"
msgid "Markers"
msgstr "Marcadores"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Tipo de marcação"
@@ -13280,6 +13319,12 @@ msgstr ""
"São aceitos vários formatos, consulte a biblioteca Python geopy.points "
"para mais detalhes"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ro, ru, sk, sl, sr, sr_Latn, tr, uk]
msgid "Multiplier"
@@ -15934,6 +15979,12 @@ msgstr "Atualização de gráficos"
msgid "Refreshing columns"
msgstr "Atualização de colunas"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
msgid "Register"
msgstr "Cadastre-se"
@@ -23666,6 +23717,17 @@ msgstr "Soltar do painel de resultados"
msgid "Unpin from top"
msgstr "Soltar do topo"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
msgid "Unsafe link blocked"
msgstr "Link inseguro bloqueado"
@@ -23694,6 +23756,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Tipo de cláusula sem suporte: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ru, sk, sr, sr_Latn, tr, uk]
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
diff --git a/superset/translations/pt_BR/LC_MESSAGES/messages.po b/superset/translations/pt_BR/LC_MESSAGES/messages.po
index 8e3765f8674..984a37d9dda 100644
--- a/superset/translations/pt_BR/LC_MESSAGES/messages.po
+++ b/superset/translations/pt_BR/LC_MESSAGES/messages.po
@@ -2046,6 +2046,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Sempre filtrar a coluna principal de data e hora"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Ocorreu um erro"
@@ -3263,6 +3269,9 @@ msgstr "Tamanho da bolha"
msgid "Bubble size number format"
msgstr "Formato do número do tamanho da bolha"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Pontos de quebra de balde"
@@ -7864,6 +7873,9 @@ msgstr "Expandir linha para editar"
msgid "Expand settings"
msgstr "Expandir configurações"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7882,6 +7894,12 @@ msgstr "Experimental"
msgid "Expired"
msgstr "Expirado"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Explorar"
@@ -8902,6 +8920,13 @@ msgstr "Coluna GeoJson"
msgid "GeoJson Settings"
msgstr "Configurações de GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -9191,12 +9216,18 @@ msgstr ""
msgid "Huge"
msgstr "Enorme"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "Códigos ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
#, fuzzy
msgid "Icon URL"
msgstr "URL do ícone"
@@ -10155,6 +10186,9 @@ msgstr "Valor esquerdo"
msgid "Legacy"
msgstr "Legado"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Legenda"
@@ -10670,6 +10704,11 @@ msgstr "Tamanho do marcador"
msgid "Markers"
msgstr "Marcadores"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Tipo de marcação"
@@ -11200,6 +11239,12 @@ msgstr ""
"São aceitos vários formatos, consulte a biblioteca Python geopy.points "
"para mais detalhes"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Multiplicador"
@@ -13351,6 +13396,12 @@ msgstr "Atualização de gráficos"
msgid "Refreshing columns"
msgstr "Atualização de colunas"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
#, fuzzy
msgid "Register"
msgstr "Cadastre-se"
@@ -19993,6 +20044,17 @@ msgstr "Soltar do painel de resultados"
msgid "Unpin from top"
msgstr "Soltar do topo"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -20013,6 +20075,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Tipo de cláusula sem suporte: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/ro/LC_MESSAGES/messages.po b/superset/translations/ro/LC_MESSAGES/messages.po
index c5c5eb41a8a..30e01c9f6ee 100644
--- a/superset/translations/ro/LC_MESSAGES/messages.po
+++ b/superset/translations/ro/LC_MESSAGES/messages.po
@@ -2109,6 +2109,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Filtrează întotdeauna coloana principală de dată/timp"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "A Apărut o Eroare"
@@ -3288,6 +3294,9 @@ msgstr "Dimensiune bulă"
msgid "Bubble size number format"
msgstr "Format număr dimensiune bulă"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Puncte de întrerupere compartiment"
@@ -7772,6 +7781,9 @@ msgstr "Extindeți rândul pentru editare"
msgid "Expand settings"
msgstr "Extinde setările"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7792,6 +7804,12 @@ msgstr "Experimental"
msgid "Expired"
msgstr "Expirat"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Explorează"
@@ -8750,6 +8768,13 @@ msgstr "Coloană GeoJson"
msgid "GeoJson Settings"
msgstr "Setări GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -9024,12 +9049,18 @@ msgstr ""
msgid "Huge"
msgstr "Uriaș"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "Coduri ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, fr,
# it, ja, lv, ru, sk, sr, sr_Latn, uk]
#, fuzzy
@@ -9949,6 +9980,9 @@ msgstr "Valoare stângă"
msgid "Legacy"
msgstr "Moștenit"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Legendă"
@@ -10445,6 +10479,11 @@ msgstr "Dimensiune marker"
msgid "Markers"
msgstr "Markeri"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Tip markup"
@@ -10979,6 +11018,12 @@ msgstr ""
"Mai multe formate acceptate, consultă biblioteca Python geopy.points "
"pentru mai multe detalii"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Multiplicator"
@@ -13024,6 +13069,12 @@ msgstr "Actualizare grafice"
msgid "Refreshing columns"
msgstr "Actualizare coloane"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
msgid "Register"
msgstr "Înregistrare"
@@ -19515,6 +19566,17 @@ msgstr "Desprinde din panoul de rezultate"
msgid "Unpin from top"
msgstr "Desprinde din vârf"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ja]
#, fuzzy
msgid "Unsafe link blocked"
@@ -19535,6 +19597,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Tip de clauză neacceptat: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ru, sk, sr, sr_Latn, tr, uk]
#, fuzzy
diff --git a/superset/translations/ru/LC_MESSAGES/messages.po b/superset/translations/ru/LC_MESSAGES/messages.po
index dd8a6632f76..6ecf44bc1d8 100644
--- a/superset/translations/ru/LC_MESSAGES/messages.po
+++ b/superset/translations/ru/LC_MESSAGES/messages.po
@@ -1956,6 +1956,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Всегда фильтровать основной столбец с временем"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Произошла ошибка"
@@ -3055,6 +3061,9 @@ msgstr "Размер маркера"
msgid "Bubble size number format"
msgstr "Числовой формат размера пузырька"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Точки разбиения на сегменты"
@@ -7176,6 +7185,9 @@ msgstr "Развернуть строку"
msgid "Expand settings"
msgstr "Развернуть настройки"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7192,6 +7204,12 @@ msgstr "Экспериментальный"
msgid "Expired"
msgstr "Истек"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Исследовать"
@@ -8069,6 +8087,13 @@ msgstr "Столбец GeoJson"
msgid "GeoJson Settings"
msgstr "Настройки GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -8321,12 +8346,18 @@ msgstr ""
msgid "Huge"
msgstr "Огромный"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "Коды ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr "URL значка"
@@ -9159,6 +9190,9 @@ msgstr "Левое значение"
msgid "Legacy"
msgstr "Устарел"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Легенда"
@@ -9618,6 +9652,11 @@ msgstr "Размер маркера"
msgid "Markers"
msgstr "Маркеры"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Тип разметки"
@@ -10115,6 +10154,12 @@ msgstr ""
"Для уточнения форматов и получения более подробной информации, посмотрите"
" Python-библиотеку geopy.points"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Мультипликатор"
@@ -12045,6 +12090,12 @@ msgstr "Обновление диаграмм"
msgid "Refreshing columns"
msgstr "Обновление столбцов"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
msgid "Register"
msgstr "Регистрация"
@@ -17970,6 +18021,17 @@ msgstr "Открепить от панели результатов"
msgid "Unpin from top"
msgstr "Открепить"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "Unsafe link blocked"
msgstr "Небезопасная ссылка заблокирована"
@@ -17988,6 +18050,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Неподдерживаемый оператор: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
msgstr ""
"Неподдерживаемый тип файла. Пожалуйста, используйте файлы CSV, Excel или "
diff --git a/superset/translations/sk/LC_MESSAGES/messages.po b/superset/translations/sk/LC_MESSAGES/messages.po
index f3e203473b5..333be1191c3 100644
--- a/superset/translations/sk/LC_MESSAGES/messages.po
+++ b/superset/translations/sk/LC_MESSAGES/messages.po
@@ -2001,6 +2001,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Vždy filtrovať hlavní stĺpec data a času"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Došlo k chybe"
@@ -3128,6 +3134,9 @@ msgstr "Veľkosť bublin"
msgid "Bubble size number format"
msgstr "Formát čísla pre veľkosť bublin"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Body zlomu intervalov"
@@ -7419,6 +7428,9 @@ msgstr "Rozbalit riadok"
msgid "Expand settings"
msgstr "Rozbaliť nastavenie"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7437,6 +7449,12 @@ msgstr "Experimentální"
msgid "Expired"
msgstr "Prozkoumat"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Prozkoumat"
@@ -8350,6 +8368,13 @@ msgstr "Stĺpec GeoJson"
msgid "GeoJson Settings"
msgstr "Nastavenie GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -8607,12 +8632,18 @@ msgstr ""
msgid "Huge"
msgstr "Obrovský"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "Kódy ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr "URL ikony"
@@ -9478,6 +9509,9 @@ msgstr "Levá hodnota"
msgid "Legacy"
msgstr "Starší verziia"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Legenda"
@@ -9964,6 +9998,11 @@ msgstr "Veľkosť značky"
msgid "Markers"
msgstr "Značky"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Typ značkovanie"
@@ -10471,6 +10510,12 @@ msgstr ""
"Je prijímano viac formátů, podívejte sa na knihovnu geopy.points v "
"Pythonu pre viac podrobností"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Násobitel"
@@ -12470,6 +12515,12 @@ msgstr "Obnovovanie grafů"
msgid "Refreshing columns"
msgstr "Obnovovanie stĺpcov"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
#, fuzzy
msgid "Register"
msgstr "Registrovať"
@@ -18651,6 +18702,17 @@ msgstr "Připnúť k panelu výsledkov"
msgid "Unpin from top"
msgstr "Odopnúť zhora"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -18671,6 +18733,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Nepodporovaný typ klauzule: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
msgstr ""
"Nepodporovaný typ súboru. Prosím, použijte súbory CSV, Excel alebo "
diff --git a/superset/translations/sl/LC_MESSAGES/messages.po b/superset/translations/sl/LC_MESSAGES/messages.po
index d8f0a157e14..763efcd8097 100644
--- a/superset/translations/sl/LC_MESSAGES/messages.po
+++ b/superset/translations/sl/LC_MESSAGES/messages.po
@@ -2014,6 +2014,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Vedno filtriraj glavni časovni stolpec"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Prišlo je do napake"
@@ -3142,6 +3148,9 @@ msgstr "Velikost mehurčka"
msgid "Bubble size number format"
msgstr "Oblika zapisa velikosti mehurčka"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Točke za razčlenitev razdelkov"
@@ -7390,6 +7399,9 @@ msgstr "Razširite vrstico za urejanje"
msgid "Expand settings"
msgstr "Razširi nastavitve"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7407,6 +7419,12 @@ msgstr "Eksperimentalno"
msgid "Expired"
msgstr "Poteklo"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Raziskovanje"
@@ -8325,6 +8343,13 @@ msgstr "GeoJson stolpec"
msgid "GeoJson Settings"
msgstr "GeoJson nastavitve"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -8583,12 +8608,18 @@ msgstr ""
msgid "Huge"
msgstr "Ogromno"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "Oznake po ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr "URL ikone"
@@ -9436,6 +9467,9 @@ msgstr "Leva vrednost"
msgid "Legacy"
msgstr "Zastarelo"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Legenda"
@@ -9904,6 +9938,11 @@ msgstr "Velikost markerja"
msgid "Markers"
msgstr "Markerji"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Tip označevanja"
@@ -10426,6 +10465,12 @@ msgstr ""
"Sprejema različne zapise - podrobnosti najdete v Pythonovi knjižnici "
"geopy.points"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Množitelj"
@@ -12382,6 +12427,12 @@ msgstr "Osveževanje grafikonov"
msgid "Refreshing columns"
msgstr "Osveževanje stolpcev"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
msgid "Register"
msgstr "Registriraj se"
@@ -18505,6 +18556,17 @@ msgstr "Odpnite s plošče z rezultati"
msgid "Unpin from top"
msgstr "Odpni z vrha"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
msgid "Unsafe link blocked"
msgstr "Nevarna povezava je blokirana"
@@ -18524,6 +18586,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Nepodprt tip izraza: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ru, sk, sr, sr_Latn, tr, uk]
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
diff --git a/superset/translations/sr/LC_MESSAGES/messages.po b/superset/translations/sr/LC_MESSAGES/messages.po
index e2782c670fb..c64e69c7a80 100644
--- a/superset/translations/sr/LC_MESSAGES/messages.po
+++ b/superset/translations/sr/LC_MESSAGES/messages.po
@@ -2337,6 +2337,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Увек филтрирај главну колону датума и времена"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, tr, uk]
msgid "An Error Occurred"
msgstr "Дошло је до грешке"
@@ -3693,6 +3699,9 @@ msgstr "Величина мехурића"
msgid "Bubble size number format"
msgstr "Формат броја величине мехурића"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
msgid "Bucket break points"
msgstr "Тачке прекида интервала"
@@ -8853,6 +8862,9 @@ msgstr "Прошири ред за измену"
msgid "Expand settings"
msgstr "Прошири подешавања"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk]
msgid ""
"Expects a formula with depending time parameter 'x'\n"
@@ -8872,6 +8884,12 @@ msgstr "Експериментално"
msgid "Expired"
msgstr "Истекло"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
# de, es, fa, fr, it, ja, lv, mi, nl, pl, pt, pt_BR, ru, sk, sl, uk, zh,
# zh_TW]
msgid "Explore"
@@ -9944,6 +9962,13 @@ msgstr "GeoJson колона"
msgid "GeoJson Settings"
msgstr "GeoJson подешавања"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -10258,6 +10283,9 @@ msgstr ""
msgid "Huge"
msgstr "Огроман"
+msgid "ISO 3166-2"
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
msgid "ISO 3166-2 Codes"
msgstr "ISO 3166-2 кодови"
@@ -10265,6 +10293,9 @@ msgstr "ISO 3166-2 кодови"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
# lv, ru, sk, uk]
msgid "Icon URL"
msgstr "URL иконе"
@@ -11301,6 +11332,9 @@ msgstr "Лева вредност"
msgid "Legacy"
msgstr "Застарело"
+msgid "Legacy ISO values"
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
msgid "Legend"
msgstr "Легенда"
@@ -11855,6 +11889,11 @@ msgid "Markers"
msgstr "Маркери"
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Тип маркуп-а"
@@ -12459,6 +12498,12 @@ msgstr ""
"Више формата је прихваћено, погледајте Python библиотеку geopy.points за "
"више детаља"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, uk]
msgid "Multiplier"
msgstr "Множитељ"
@@ -14866,6 +14911,12 @@ msgstr "Освежавање графикона"
msgid "Refreshing columns"
msgstr "Освежавање колона"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
# uk]
msgid "Register"
msgstr "Региструј се"
@@ -21990,6 +22041,17 @@ msgstr "Откачи са панела резултата"
msgid "Unpin from top"
msgstr "Откачи са врха"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ja]
#, fuzzy
msgid "Unsafe link blocked"
@@ -22013,6 +22075,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Неподржани тип клаузе: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# ru, sk, uk]
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
msgstr "Неподржани тип датотеке. Користите CSV, Excel или колонске датотеке."
diff --git a/superset/translations/sr_Latn/LC_MESSAGES/messages.po b/superset/translations/sr_Latn/LC_MESSAGES/messages.po
index 0c5792f9b5f..b3153c3c17d 100644
--- a/superset/translations/sr_Latn/LC_MESSAGES/messages.po
+++ b/superset/translations/sr_Latn/LC_MESSAGES/messages.po
@@ -2338,6 +2338,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Uvek filtriraj glavnu kolonu datuma i vremena"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, tr, uk]
msgid "An Error Occurred"
msgstr "Došlo je do greške"
@@ -3696,6 +3702,9 @@ msgstr "Veličina mehurića"
msgid "Bubble size number format"
msgstr "Format broja veličine mehurića"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
msgid "Bucket break points"
msgstr "Tačke prekida intervala"
@@ -8857,6 +8866,9 @@ msgstr "Proširi red za izmenu"
msgid "Expand settings"
msgstr "Proširi podešavanja"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk]
msgid ""
"Expects a formula with depending time parameter 'x'\n"
@@ -8876,6 +8888,12 @@ msgstr "Eksperimentalno"
msgid "Expired"
msgstr "Isteklo"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
# de, es, fa, fr, it, ja, lv, mi, nl, pl, pt, pt_BR, ru, sk, sl, uk, zh,
# zh_TW]
msgid "Explore"
@@ -9951,6 +9969,13 @@ msgstr "GeoJson kolona"
msgid "GeoJson Settings"
msgstr "GeoJson podešavanja"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -10265,6 +10290,9 @@ msgstr ""
msgid "Huge"
msgstr "Ogroman"
+msgid "ISO 3166-2"
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
msgid "ISO 3166-2 Codes"
msgstr "ISO 3166-2 kodovi"
@@ -10272,6 +10300,9 @@ msgstr "ISO 3166-2 kodovi"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
# lv, ru, sk, uk]
msgid "Icon URL"
msgstr "URL ikone"
@@ -11308,6 +11339,9 @@ msgstr "Leva vrednost"
msgid "Legacy"
msgstr "Zastarelo"
+msgid "Legacy ISO values"
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
msgid "Legend"
msgstr "Legenda"
@@ -11862,6 +11896,11 @@ msgid "Markers"
msgstr "Markeri"
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Tip markup-a"
@@ -12466,6 +12505,12 @@ msgstr ""
"Više formata je prihvaćeno, pogledajte Python biblioteku geopy.points za "
"više detalja"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, uk]
msgid "Multiplier"
msgstr "Množitelj"
@@ -14873,6 +14918,12 @@ msgstr "Osvežavanje grafikona"
msgid "Refreshing columns"
msgstr "Osvežavanje kolona"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
# uk]
msgid "Register"
msgstr "Registruj se"
@@ -22007,6 +22058,17 @@ msgstr "Otkači sa panela rezultata"
msgid "Unpin from top"
msgstr "Otkači sa vrha"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ja]
#, fuzzy
msgid "Unsafe link blocked"
@@ -22030,6 +22092,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Nepodržani tip klauze: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# ru, sk, uk]
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
msgstr "Nepodržani tip datoteke. Koristite CSV, Excel ili kolonske datoteke."
diff --git a/superset/translations/ta/LC_MESSAGES/messages.po b/superset/translations/ta/LC_MESSAGES/messages.po
index 74d0c716284..4547238bf6c 100644
--- a/superset/translations/ta/LC_MESSAGES/messages.po
+++ b/superset/translations/ta/LC_MESSAGES/messages.po
@@ -1951,6 +1951,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "எப்போதும் முக்கிய தேதியிடப்பட்ட (datetime) நெடுவரிசையை வடிகட்டவும்"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "ஒரு பிழை ஏற்பட்டது"
@@ -3056,6 +3062,9 @@ msgstr "குமிழி அளவு"
msgid "Bubble size number format"
msgstr "குமிழி அளவு எண் வடிவம்"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "பக்கெட் முறிவுப் புள்ளிகள் (Bucket break points)"
@@ -7218,6 +7227,9 @@ msgstr "வரிசையை விரிவாக்கு"
msgid "Expand settings"
msgstr "அமைப்புகளை விரிவாக்கு"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7233,6 +7245,12 @@ msgstr "சோதனைநிலை (Experimental)"
msgid "Expired"
msgstr ""
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "ஆராய் (Explore)"
@@ -8117,6 +8135,13 @@ msgstr "GeoJson நெடுவரிசை"
msgid "GeoJson Settings"
msgstr "GeoJson அமைப்புகள்"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -8370,12 +8395,18 @@ msgstr ""
msgid "Huge"
msgstr "மிகப்பெரியது"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "ISO 3166-2 குறியீடுகள்"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr "ஐகான் URL"
@@ -9201,6 +9232,9 @@ msgstr "இடது மதிப்பு"
msgid "Legacy"
msgstr "பாரம்பரிய (Legacy)"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "விளக்கக் குறிப்பு (Legend)"
@@ -9650,6 +9684,11 @@ msgstr "மார்க்கர் அளவு"
msgid "Markers"
msgstr "மார்க்கர்கள்"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "மார்க்கப் வகை"
@@ -10128,6 +10167,12 @@ msgid ""
"details"
msgstr "பல வடிவங்கள் ஏற்கப்படுகின்றன."
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "பெருக்கி (Multiplier)"
@@ -12014,6 +12059,12 @@ msgstr "சார்ட்டுகளைப் புதுப்பிக்
msgid "Refreshing columns"
msgstr "நெடுவரிசைகளைப் புதுப்பிக்கிறது"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
msgid "Register"
msgstr "பதிவு செய்"
@@ -17751,6 +17802,17 @@ msgstr "முடிவு பலகத்தில் (result panel) பின
msgid "Unpin from top"
msgstr ""
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "Unsafe link blocked"
msgstr ""
@@ -17769,6 +17831,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "ஆதரிக்கப்படாத நிபந்தனை வகை: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
msgstr ""
"ஆதரிக்கப்படாத கோப்பு வகை. CSV, Excel அல்லது Columnar கோப்புகளைப் "
diff --git a/superset/translations/th/LC_MESSAGES/messages.po b/superset/translations/th/LC_MESSAGES/messages.po
index e8a185a8541..f9294715940 100644
--- a/superset/translations/th/LC_MESSAGES/messages.po
+++ b/superset/translations/th/LC_MESSAGES/messages.po
@@ -2943,6 +2943,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "กรองคอลัมน์วันที่และเวลาหลักเสมอ"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, tr, uk]
#, fuzzy
@@ -4770,6 +4776,9 @@ msgstr "ขนาดฟอง"
msgid "Bubble size number format"
msgstr "รูปแบบตัวเลขของขนาดฟอง"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
@@ -11811,6 +11820,9 @@ msgstr "ขยายแถว"
msgid "Expand settings"
msgstr "ขยายการตั้งค่า"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk]
#, fuzzy
@@ -11837,6 +11849,12 @@ msgstr "ทดลอง"
msgid "Expired"
msgstr "สำรวจ"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, it, ja, lv, mi, nl, pl, pt, pt_BR, ru, sk, sl, uk, zh,
# zh_TW]
@@ -13313,6 +13331,13 @@ msgstr "คอลัมน์ GeoJson"
msgid "GeoJson Settings"
msgstr "การตั้งค่า GeoJson"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
@@ -13768,6 +13793,9 @@ msgstr ""
msgid "Huge"
msgstr "ใหญ่มาก"
+msgid "ISO 3166-2"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
@@ -13780,6 +13808,9 @@ msgstr "รหัส ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, fr, ja,
# lv, ru, sk, uk]
#, fuzzy
@@ -15222,6 +15253,9 @@ msgstr "ค่าซ้าย"
msgid "Legacy"
msgstr "รุ่นเก่า"
+msgid "Legacy ISO values"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
@@ -16011,6 +16045,11 @@ msgstr "มาร์กเกอร์"
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, uk, zh, zh_TW]
#, fuzzy
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "ประเภทมาร์กอัป"
@@ -16850,6 +16889,12 @@ msgid ""
"details"
msgstr "รองรับหลายรูปแบบ ดูรายละเอียดเพิ่มเติมได้ที่ไลบรารี Python geopy.points"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, ca, cs,
# de, es, fa, fr, ja, lv, mi, nl, pt_BR, ru, sk, sl, uk]
#, fuzzy
@@ -20196,6 +20241,12 @@ msgstr "กำลังรีเฟรชแผนภูมิ"
msgid "Refreshing columns"
msgstr "กำลังรีเฟรชคอลัมน์"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ja, lv, ru,
# uk]
#, fuzzy
@@ -29713,6 +29764,17 @@ msgstr "ปักหมุดที่แผงผลลัพธ์"
msgid "Unpin from top"
msgstr "เลิกปักหมุดจากด้านบน"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -29739,6 +29801,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "ประเภทส่วนคำสั่งที่ไม่รองรับ: %(clause)s"
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, ja, lv,
# ru, sk, uk]
#, fuzzy
diff --git a/superset/translations/tr/LC_MESSAGES/messages.po b/superset/translations/tr/LC_MESSAGES/messages.po
index db5f9c78078..795ecc059ed 100644
--- a/superset/translations/tr/LC_MESSAGES/messages.po
+++ b/superset/translations/tr/LC_MESSAGES/messages.po
@@ -2003,6 +2003,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Her zaman filtre ana datetime column"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Bir Hata Oluştu"
@@ -3195,6 +3201,9 @@ msgstr "Bubble büyüklüğü"
msgid "Bubble size number format"
msgstr "Bubble boyut numarası formatı"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Shell"
@@ -7589,6 +7598,9 @@ msgstr "Düzenlemek için satırı genişlet"
msgid "Expand settings"
msgstr "Ayarları genişlet"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7609,6 +7621,12 @@ msgstr "Deneysel"
msgid "Expired"
msgstr "Süresi doldu"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Discover"
@@ -8556,6 +8574,13 @@ msgstr "GeoJson Köşesi"
msgid "GeoJson Settings"
msgstr "GeoJson Ayarları"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Geohash"
@@ -8818,12 +8843,18 @@ msgstr ""
msgid "Huge"
msgstr "Büyük"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "ISO 3166-2 Kod"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
#, fuzzy
msgid "Icon URL"
msgstr "Kontrol"
@@ -9710,6 +9741,9 @@ msgstr "Sol değer"
msgid "Legacy"
msgstr "Mirası"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Legend"
@@ -10209,6 +10243,11 @@ msgstr "Marker boyutu"
msgid "Markers"
msgstr "Markers"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Markup type"
@@ -10729,6 +10768,12 @@ msgstr ""
"Çok sayıda format kabul edildi, geopy'ye bakın. Daha fazla ayrıntı için "
"Python kütüphanesi"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
msgstr "Multiplier"
@@ -12764,6 +12809,12 @@ msgstr "Grafikler yenileniyor"
msgid "Refreshing columns"
msgstr "Kolonlar yenileniyor"
+msgid "Region name"
+msgstr ""
+
+msgid "Region value format"
+msgstr ""
+
msgid "Register"
msgstr "Register"
@@ -19098,6 +19149,17 @@ msgstr "Sonuç panelinden sabitlemeyi kaldır"
msgid "Unpin from top"
msgstr "Üstten sabitlemeyi kaldır"
+msgid ""
+"Unrecognized or duplicate country value; choose the matching country "
+"format or normalize source values before aggregation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Unrecognized region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [no refs]
#, fuzzy
msgid "Unsafe link blocked"
@@ -19118,6 +19180,9 @@ msgstr ""
msgid "Unsupported clause type: %(clause)s"
msgstr "Desteklenen madde türü: %(clause)s "
+msgid "Unsupported country or region_format for typed geographic chart"
+msgstr ""
+
msgid "Unsupported file type. Please use CSV, Excel, or Columnar files."
msgstr "Desteklenen dosya türü. Lütfen CSV, Excel veya köşe dosyaları kullanın."
diff --git a/superset/translations/uk/LC_MESSAGES/messages.po b/superset/translations/uk/LC_MESSAGES/messages.po
index e1e6daf8c5d..db7d90034fa 100644
--- a/superset/translations/uk/LC_MESSAGES/messages.po
+++ b/superset/translations/uk/LC_MESSAGES/messages.po
@@ -2012,6 +2012,12 @@ msgstr ""
msgid "Always filter main datetime column"
msgstr "Завжди фільтрувати основну колонку дати/часу"
+#, python-format
+msgid ""
+"Ambiguous region %s; choose the matching country/region format or "
+"explicitly filter the dataset."
+msgstr ""
+
msgid "An Error Occurred"
msgstr "Трапилась помилка"
@@ -3106,6 +3112,9 @@ msgstr "Розмір бульбашок"
msgid "Bubble size number format"
msgstr "Числовий формат розміру бульбашки"
+msgid "Bubble-size metric must be nonnegative"
+msgstr ""
+
msgid "Bucket break points"
msgstr "Точки розбиття на сегменти"
@@ -7300,6 +7309,9 @@ msgstr "Розгорнути рядок"
msgid "Expand settings"
msgstr "Розгорнути параметри"
+msgid "Expected geographic query data to be a list of records"
+msgstr ""
+
msgid ""
"Expects a formula with depending time parameter 'x'\n"
" in milliseconds since epoch. mathjs is used to evaluate the "
@@ -7318,6 +7330,12 @@ msgstr "Експериментальне"
msgid "Expired"
msgstr "Дослідити"
+msgid ""
+"Explicit formats validate all values against bundled boundaries. "
+"Supported countries: USA, Canada, Australia, Japan, UK. Unrecognized or "
+"ambiguous values fail rather than displaying blank regions."
+msgstr ""
+
msgid "Explore"
msgstr "Дослідити"
@@ -8211,6 +8229,13 @@ msgstr "Стовпець GeoJSON"
msgid "GeoJson Settings"
msgstr "Параметри GeoJSON"
+#, python-format
+msgid "Geographic metric %s must be a finite number"
+msgstr ""
+
+msgid "Geographic values must be nonempty strings of at most 500 characters"
+msgstr ""
+
msgid "Geohash"
msgstr "Геокод"
@@ -8460,12 +8485,18 @@ msgstr ""
msgid "Huge"
msgstr "Величезний"
+msgid "ISO 3166-2"
+msgstr ""
+
msgid "ISO 3166-2 Codes"
msgstr "Коди ISO 3166-2"
msgid "ISO 8601"
msgstr "ISO 8601"
+msgid "ISO suffix / abbreviation"
+msgstr ""
+
msgid "Icon URL"
msgstr "Посилання на піктограму"
@@ -9300,6 +9331,9 @@ msgstr "Ліве значення"
msgid "Legacy"
msgstr "Застаріле"
+msgid "Legacy ISO values"
+msgstr ""
+
msgid "Legend"
msgstr "Легенда"
@@ -9774,6 +9808,11 @@ msgstr "Розмір маркера"
msgid "Markers"
msgstr "Маркери"
+msgid ""
+"Marks charts created through the MCP geographic contract so Explore keeps"
+" their validation guards."
+msgstr ""
+
msgid "Markup type"
msgstr "Тип розмітки"
@@ -10274,6 +10313,12 @@ msgstr ""
"Підтримується декілька форматів. Детальніше перегляньте бібліотеку Python"
" geopy.points"
+#, python-format
+msgid ""
+"Multiple result rows resolve to %s; normalize source values before "
+"aggregation."
+msgstr ""
+
msgid "Multiplier"
... 3975 lines suppressed ...