This is an automated email from the ASF dual-hosted git repository.
asoare 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 7743183401b fix(bugs): fixing bugs for world map chart (#38030)
7743183401b is described below
commit 7743183401b8f7e366d94d8a70fc8acfd724c3cf
Author: Alexandru Soare <[email protected]>
AuthorDate: Fri Feb 27 11:33:35 2026 +0200
fix(bugs): fixing bugs for world map chart (#38030)
---
.../legacy-plugin-chart-world-map/src/WorldMap.ts | 20 +--
.../test/WorldMap.test.ts | 141 +++++++++++++++++++--
.../src/dashboard/components/SliceHeader/index.tsx | 6 +-
.../components/gridComponents/Chart/Chart.tsx | 2 +
.../src/explore/components/ChartPills.tsx | 6 +-
.../useExploreAdditionalActionsMenu/index.tsx | 2 +
6 files changed, 157 insertions(+), 20 deletions(-)
diff --git
a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts
b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts
index f5c873b2ef2..e6c7bc2679d 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts
+++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts
@@ -244,18 +244,20 @@ function WorldMap(element: HTMLElement, props:
WorldMapProps): void {
},
];
}
- onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
- drillToDetail: drillToDetailFilters,
- crossFilter: getCrossFilterDataMask(source),
- drillBy: { filters: drillByFilters, groupbyFieldName: 'entity' },
- });
+ if (onContextMenu) {
+ onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
+ drillToDetail: drillToDetailFilters,
+ crossFilter: getCrossFilterDataMask(source),
+ drillBy: { filters: drillByFilters, groupbyFieldName: 'entity' },
+ });
+ }
};
const map = new Datamap({
element,
width,
height,
- data: processedData,
+ data: mapData,
fills: {
defaultFill: theme.colorBorder,
},
@@ -268,6 +270,7 @@ function WorldMap(element: HTMLElement, props:
WorldMapProps): void {
highlightFillColor: color,
highlightBorderWidth: 1,
popupTemplate: (geo, d) =>
+ d &&
`<div class="hoverinfo"><strong>${d.name}</strong><br>${formatter(
d.m1,
)}</div>`,
@@ -298,7 +301,8 @@ function WorldMap(element: HTMLElement, props:
WorldMapProps): void {
.selectAll('.datamaps-subunit')
.on('contextmenu', handleContextMenu)
.on('click', handleClick)
- .on('mouseover', function onMouseOver() {
+ // Use namespaced events to avoid overriding Datamaps' default tooltip
handlers
+ .on('mouseover.fillPreserve', function onMouseOver() {
if (inContextMenu) {
return;
}
@@ -311,7 +315,7 @@ function WorldMap(element: HTMLElement, props:
WorldMapProps): void {
// Store original fill color for restoration
element.attr('data-original-fill', originalFill);
})
- .on('mouseout', function onMouseOut() {
+ .on('mouseout.fillPreserve', function onMouseOut() {
if (inContextMenu) {
return;
}
diff --git
a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts
b/superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts
index 096c558bc63..5a53aab9e82 100644
---
a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts
+++
b/superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts
@@ -77,8 +77,13 @@ const mockSvg = {
style: jest.fn().mockReturnThis(),
};
+// Store the last Datamap config for assertions
+let lastDatamapConfig: Record<string, unknown> | null = null;
+
jest.mock('datamaps/dist/datamaps.all.min', () =>
jest.fn().mockImplementation(config => {
+ // Store config for test assertions
+ lastDatamapConfig = config;
// Call the done callback immediately to simulate Datamap initialization
if (config.done) {
config.done({
@@ -158,9 +163,11 @@ test('sets up mouseover and mouseout handlers on
countries', () => {
expect(mockSvg.selectAll).toHaveBeenCalledWith('.datamaps-subunit');
const onCalls = mockSvg.on.mock.calls;
- // Find mouseover and mouseout handler registrations
- const hasMouseover = onCalls.some(call => call[0] === 'mouseover');
- const hasMouseout = onCalls.some(call => call[0] === 'mouseout');
+ // Find mouseover and mouseout handler registrations (namespaced events)
+ const hasMouseover = onCalls.some(
+ call => call[0] === 'mouseover.fillPreserve',
+ );
+ const hasMouseout = onCalls.some(call => call[0] ===
'mouseout.fillPreserve');
expect(hasMouseover).toBe(true);
expect(hasMouseout).toBe(true);
@@ -199,9 +206,9 @@ test('stores original fill color on mouseover', () => {
jest.spyOn(d3 as any, 'select').mockReturnValue(mockD3Selection as any);
- // Capture the mouseover handler
+ // Capture the mouseover handler (namespaced event)
mockSvg.on.mockImplementation((event: string, handler: MouseEventHandler) =>
{
- if (event === 'mouseover') {
+ if (event === 'mouseover.fillPreserve') {
mouseoverHandler = handler;
}
return mockSvg;
@@ -254,9 +261,9 @@ test('restores original fill color on mouseout for country
with data', () => {
jest.spyOn(d3 as any, 'select').mockReturnValue(mockD3Selection as any);
- // Capture the mouseout handler
+ // Capture the mouseout handler (namespaced event)
mockSvg.on.mockImplementation((event: string, handler: MouseEventHandler) =>
{
- if (event === 'mouseout') {
+ if (event === 'mouseout.fillPreserve') {
mouseoutHandler = handler;
}
return mockSvg;
@@ -310,8 +317,9 @@ test('restores default fill color on mouseout for country
with no data', () => {
jest.spyOn(d3 as any, 'select').mockReturnValue(mockD3Selection as any);
+ // Capture the mouseout handler (namespaced event)
mockSvg.on.mockImplementation((event: string, handler: MouseEventHandler) =>
{
- if (event === 'mouseout') {
+ if (event === 'mouseout.fillPreserve') {
mouseoutHandler = handler;
}
return mockSvg;
@@ -352,11 +360,12 @@ test('does not handle mouse events when inContextMenu is
true', () => {
jest.spyOn(d3 as any, 'select').mockReturnValue(mockD3Selection as any);
+ // Capture namespaced event handlers
mockSvg.on.mockImplementation((event: string, handler: MouseEventHandler) =>
{
- if (event === 'mouseover') {
+ if (event === 'mouseover.fillPreserve') {
mouseoverHandler = handler;
}
- if (event === 'mouseout') {
+ if (event === 'mouseout.fillPreserve') {
mouseoutHandler = handler;
}
return mockSvg;
@@ -387,3 +396,115 @@ test('does not handle mouse events when inContextMenu is
true', () => {
expect(fillChangeCalls.length).toBe(0);
expect(fillStyleChangeCalls.length).toBe(0);
});
+
+test('does not throw error when onContextMenu is undefined', () => {
+ const propsWithoutContextMenu = {
+ ...baseProps,
+ onContextMenu: undefined,
+ };
+
+ // Should not throw
+ expect(() => {
+ WorldMap(container, propsWithoutContextMenu as any);
+ }).not.toThrow();
+});
+
+test('calls onContextMenu when provided and right-click occurs', () => {
+ const mockOnContextMenu = jest.fn();
+ const propsWithContextMenu = {
+ ...baseProps,
+ onContextMenu: mockOnContextMenu,
+ };
+
+ let contextMenuHandler: ((source: any) => void) | undefined;
+
+ mockSvg.on.mockImplementation((event: string, handler: any) => {
+ if (event === 'contextmenu') {
+ contextMenuHandler = handler;
+ }
+ return mockSvg;
+ });
+
+ // Mock d3.event
+ (d3 as any).event = {
+ preventDefault: jest.fn(),
+ clientX: 100,
+ clientY: 200,
+ };
+
+ WorldMap(container, propsWithContextMenu);
+
+ expect(contextMenuHandler).toBeDefined();
+ contextMenuHandler!({ country: 'USA' });
+
+ expect(mockOnContextMenu).toHaveBeenCalledWith(100, 200, expect.any(Object));
+});
+
+test('initializes Datamap with keyed object data for tooltip support', () => {
+ WorldMap(container, baseProps);
+
+ // Verify data is an object (not an array) keyed by country codes
+ expect(Array.isArray(lastDatamapConfig?.data)).toBe(false);
+ expect(typeof lastDatamapConfig?.data).toBe('object');
+
+ const data = lastDatamapConfig?.data as Record<string, unknown>;
+
+ // Verify the data is keyed by country code
+ expect(data).toHaveProperty('USA');
+ expect(data).toHaveProperty('CAN');
+
+ // Verify the keyed data contains the expected properties for tooltips
+ expect(data.USA).toMatchObject({
+ country: 'USA',
+ name: 'United States',
+ m1: 100,
+ m2: 200,
+ });
+ expect(data.CAN).toMatchObject({
+ country: 'CAN',
+ name: 'Canada',
+ m1: 50,
+ m2: 100,
+ });
+});
+
+test('popupTemplate returns tooltip HTML when country data exists', () => {
+ WorldMap(container, baseProps);
+
+ const geographyConfig = lastDatamapConfig?.geographyConfig as Record<
+ string,
+ unknown
+ >;
+ const popupTemplate = geographyConfig?.popupTemplate as (
+ geo: unknown,
+ d: unknown,
+ ) => string;
+
+ const mockGeo = { properties: { name: 'United States' } };
+ const mockCountryData = { name: 'United States', m1: 100 };
+
+ const tooltipHtml = popupTemplate(mockGeo, mockCountryData);
+
+ expect(tooltipHtml).toContain('United States');
+ expect(tooltipHtml).toContain('hoverinfo');
+});
+
+test('popupTemplate handles null/undefined country data gracefully', () => {
+ WorldMap(container, baseProps);
+
+ const geographyConfig = lastDatamapConfig?.geographyConfig as Record<
+ string,
+ unknown
+ >;
+ const popupTemplate = geographyConfig?.popupTemplate as (
+ geo: unknown,
+ d: unknown,
+ ) => string | undefined;
+
+ const mockGeo = { properties: { name: 'Antarctica' } };
+
+ // When hovering over a country with no data, 'd' will be undefined
+ const tooltipHtml = popupTemplate(mockGeo, undefined);
+
+ expect(tooltipHtml).toBeFalsy();
+});
diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
index a0a6f702a4a..77da178b246 100644
--- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
@@ -205,7 +205,11 @@ const SliceHeader = forwardRef<HTMLDivElement,
SliceHeaderProps>(
const sqlRowCount =
countFromSecondQuery != null
? countFromSecondQuery
- : Number(firstQueryResponse?.sql_rowcount ?? 0);
+ : Number(
+ firstQueryResponse?.sql_rowcount ??
+ firstQueryResponse?.rowcount ??
+ 0,
+ );
const canExplore = !editMode && supersetCanExplore;
const showRowLimitWarning =
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.tsx
b/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.tsx
index fa015ff4991..0dda657a02d 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.tsx
@@ -499,6 +499,8 @@ const Chart = (props: ChartProps) => {
} else if ((queriesResponse?.[0] as JsonObject)?.sql_rowcount != null) {
actualRowCount = (queriesResponse![0] as JsonObject)
.sql_rowcount as number;
+ } else if ((queriesResponse?.[0] as JsonObject)?.rowcount != null) {
+ actualRowCount = (queriesResponse![0] as JsonObject).rowcount as
number;
} else {
actualRowCount = (exportFormData as JsonObject)?.row_limit as
| number
diff --git a/superset-frontend/src/explore/components/ChartPills.tsx
b/superset-frontend/src/explore/components/ChartPills.tsx
index a3d26de0a57..7794cf147d5 100644
--- a/superset-frontend/src/explore/components/ChartPills.tsx
+++ b/superset-frontend/src/explore/components/ChartPills.tsx
@@ -77,7 +77,11 @@ export const ChartPills = forwardRef(
const actualRowCount =
isTableChart && countFromSecondQuery != null
? countFromSecondQuery
- : Number(firstQueryResponse?.sql_rowcount ?? 0);
+ : Number(
+ firstQueryResponse?.sql_rowcount ??
+ firstQueryResponse?.rowcount ??
+ 0,
+ );
return (
<div ref={ref}>
diff --git
a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx
b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx
index 1fbf54ede75..db491c134ba 100644
---
a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx
+++
b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx
@@ -311,6 +311,8 @@ export const useExploreAdditionalActionsMenu = (
actualRowCount = queriesResponse[1].data[0].rowcount;
} else if (queriesResponse && queriesResponse[0]?.sql_rowcount != null) {
actualRowCount = queriesResponse[0].sql_rowcount;
+ } else if (queriesResponse && queriesResponse[0]?.rowcount != null) {
+ actualRowCount = queriesResponse[0].rowcount;
} else {
actualRowCount = latestQueryFormData?.row_limit;
}