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;
     }

Reply via email to