mirror of
https://github.com/apache/superset.git
synced 2026-06-05 07:39:19 +00:00
fix(bugs): fixing bugs for world map chart (#38030)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user