Skip to content

Commit 2f74e7b

Browse files
fix(tooltip): fixing tooltip issue
1 parent a5b0612 commit 2f74e7b

File tree

2 files changed

+93
-13
lines changed

2 files changed

+93
-13
lines changed

superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ function WorldMap(element: HTMLElement, props: WorldMapProps): void {
257257
element,
258258
width,
259259
height,
260-
data: processedData,
260+
data: mapData,
261261
fills: {
262262
defaultFill: theme.colorBorder,
263263
},
@@ -270,6 +270,7 @@ function WorldMap(element: HTMLElement, props: WorldMapProps): void {
270270
highlightFillColor: color,
271271
highlightBorderWidth: 1,
272272
popupTemplate: (geo, d) =>
273+
d &&
273274
`<div class="hoverinfo"><strong>${d.name}</strong><br>${formatter(
274275
d.m1,
275276
)}</div>`,
@@ -300,7 +301,8 @@ function WorldMap(element: HTMLElement, props: WorldMapProps): void {
300301
.selectAll('.datamaps-subunit')
301302
.on('contextmenu', handleContextMenu)
302303
.on('click', handleClick)
303-
.on('mouseover', function onMouseOver() {
304+
// Use namespaced events to avoid overriding Datamaps' default tooltip handlers
305+
.on('mouseover.fillPreserve', function onMouseOver() {
304306
if (inContextMenu) {
305307
return;
306308
}
@@ -313,7 +315,7 @@ function WorldMap(element: HTMLElement, props: WorldMapProps): void {
313315
// Store original fill color for restoration
314316
element.attr('data-original-fill', originalFill);
315317
})
316-
.on('mouseout', function onMouseOut() {
318+
.on('mouseout.fillPreserve', function onMouseOut() {
317319
if (inContextMenu) {
318320
return;
319321
}

superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,13 @@ const mockSvg = {
7777
style: jest.fn().mockReturnThis(),
7878
};
7979

80+
// Store the last Datamap config for assertions
81+
let lastDatamapConfig: Record<string, unknown> | null = null;
82+
8083
jest.mock('datamaps/dist/datamaps.all.min', () =>
8184
jest.fn().mockImplementation(config => {
85+
// Store config for test assertions
86+
lastDatamapConfig = config;
8287
// Call the done callback immediately to simulate Datamap initialization
8388
if (config.done) {
8489
config.done({
@@ -158,9 +163,11 @@ test('sets up mouseover and mouseout handlers on countries', () => {
158163
expect(mockSvg.selectAll).toHaveBeenCalledWith('.datamaps-subunit');
159164
const onCalls = mockSvg.on.mock.calls;
160165

161-
// Find mouseover and mouseout handler registrations
162-
const hasMouseover = onCalls.some(call => call[0] === 'mouseover');
163-
const hasMouseout = onCalls.some(call => call[0] === 'mouseout');
166+
// Find mouseover and mouseout handler registrations (namespaced events)
167+
const hasMouseover = onCalls.some(
168+
call => call[0] === 'mouseover.fillPreserve',
169+
);
170+
const hasMouseout = onCalls.some(call => call[0] === 'mouseout.fillPreserve');
164171

165172
expect(hasMouseover).toBe(true);
166173
expect(hasMouseout).toBe(true);
@@ -199,9 +206,9 @@ test('stores original fill color on mouseover', () => {
199206

200207
jest.spyOn(d3 as any, 'select').mockReturnValue(mockD3Selection as any);
201208

202-
// Capture the mouseover handler
209+
// Capture the mouseover handler (namespaced event)
203210
mockSvg.on.mockImplementation((event: string, handler: MouseEventHandler) => {
204-
if (event === 'mouseover') {
211+
if (event === 'mouseover.fillPreserve') {
205212
mouseoverHandler = handler;
206213
}
207214
return mockSvg;
@@ -254,9 +261,9 @@ test('restores original fill color on mouseout for country with data', () => {
254261

255262
jest.spyOn(d3 as any, 'select').mockReturnValue(mockD3Selection as any);
256263

257-
// Capture the mouseout handler
264+
// Capture the mouseout handler (namespaced event)
258265
mockSvg.on.mockImplementation((event: string, handler: MouseEventHandler) => {
259-
if (event === 'mouseout') {
266+
if (event === 'mouseout.fillPreserve') {
260267
mouseoutHandler = handler;
261268
}
262269
return mockSvg;
@@ -310,8 +317,9 @@ test('restores default fill color on mouseout for country with no data', () => {
310317

311318
jest.spyOn(d3 as any, 'select').mockReturnValue(mockD3Selection as any);
312319

320+
// Capture the mouseout handler (namespaced event)
313321
mockSvg.on.mockImplementation((event: string, handler: MouseEventHandler) => {
314-
if (event === 'mouseout') {
322+
if (event === 'mouseout.fillPreserve') {
315323
mouseoutHandler = handler;
316324
}
317325
return mockSvg;
@@ -352,11 +360,12 @@ test('does not handle mouse events when inContextMenu is true', () => {
352360

353361
jest.spyOn(d3 as any, 'select').mockReturnValue(mockD3Selection as any);
354362

363+
// Capture namespaced event handlers
355364
mockSvg.on.mockImplementation((event: string, handler: MouseEventHandler) => {
356-
if (event === 'mouseover') {
365+
if (event === 'mouseover.fillPreserve') {
357366
mouseoverHandler = handler;
358367
}
359-
if (event === 'mouseout') {
368+
if (event === 'mouseout.fillPreserve') {
360369
mouseoutHandler = handler;
361370
}
362371
return mockSvg;
@@ -430,3 +439,72 @@ test('calls onContextMenu when provided and right-click occurs', () => {
430439

431440
expect(mockOnContextMenu).toHaveBeenCalledWith(100, 200, expect.any(Object));
432441
});
442+
443+
test('initializes Datamap with keyed object data for tooltip support', () => {
444+
WorldMap(container, baseProps);
445+
446+
// Verify data is an object (not an array) keyed by country codes
447+
expect(Array.isArray(lastDatamapConfig?.data)).toBe(false);
448+
expect(typeof lastDatamapConfig?.data).toBe('object');
449+
450+
const data = lastDatamapConfig?.data as Record<string, unknown>;
451+
452+
// Verify the data is keyed by country code
453+
expect(data).toHaveProperty('USA');
454+
expect(data).toHaveProperty('CAN');
455+
456+
// Verify the keyed data contains the expected properties for tooltips
457+
expect(data.USA).toMatchObject({
458+
country: 'USA',
459+
name: 'United States',
460+
m1: 100,
461+
m2: 200,
462+
});
463+
expect(data.CAN).toMatchObject({
464+
country: 'CAN',
465+
name: 'Canada',
466+
m1: 50,
467+
m2: 100,
468+
});
469+
});
470+
471+
test('popupTemplate returns tooltip HTML when country data exists', () => {
472+
WorldMap(container, baseProps);
473+
474+
const geographyConfig = lastDatamapConfig?.geographyConfig as Record<
475+
string,
476+
unknown
477+
>;
478+
const popupTemplate = geographyConfig?.popupTemplate as (
479+
geo: unknown,
480+
d: unknown,
481+
) => string;
482+
483+
const mockGeo = { properties: { name: 'United States' } };
484+
const mockCountryData = { name: 'United States', m1: 100 };
485+
486+
const tooltipHtml = popupTemplate(mockGeo, mockCountryData);
487+
488+
expect(tooltipHtml).toContain('United States');
489+
expect(tooltipHtml).toContain('hoverinfo');
490+
});
491+
492+
test('popupTemplate handles null/undefined country data gracefully', () => {
493+
WorldMap(container, baseProps);
494+
495+
const geographyConfig = lastDatamapConfig?.geographyConfig as Record<
496+
string,
497+
unknown
498+
>;
499+
const popupTemplate = geographyConfig?.popupTemplate as (
500+
geo: unknown,
501+
d: unknown,
502+
) => string | undefined;
503+
504+
const mockGeo = { properties: { name: 'Antarctica' } };
505+
506+
// When hovering over a country with no data, 'd' will be undefined
507+
const tooltipHtml = popupTemplate(mockGeo, undefined);
508+
509+
expect(tooltipHtml).toBeFalsy();
510+
});

0 commit comments

Comments
 (0)