Skip to content

Commit cfc2510

Browse files
fix: [on-5092] resolve issue with diagram tooltip (#2210)
1 parent e1dc496 commit cfc2510

5 files changed

Lines changed: 431 additions & 130 deletions

File tree

app/src/app/[lng]/[inventory]/InventoryResultTab/EmissionBySectorChart.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,12 @@ function CustomCombinedBarLayer<D extends BarDatum>({
165165
radius: 16,
166166
});
167167
return (
168-
<g key={year}>
168+
<g
169+
key={year}
170+
onMouseEnter={handleMouseEnter}
171+
onMouseMove={handleMouseMove}
172+
onMouseLeave={handleMouseLeave}
173+
>
169174
{bars
170175
.filter((bar) => bar.index !== topSegment.index)
171176
.map((bar: any, index) => {
@@ -201,9 +206,6 @@ function CustomCombinedBarLayer<D extends BarDatum>({
201206
pointerEvents: "all",
202207
transition: "transform 0.3s ease, fill 0.3s ease",
203208
}}
204-
onMouseEnter={handleMouseEnter}
205-
onMouseMove={handleMouseMove}
206-
onMouseLeave={handleMouseLeave}
207209
/>
208210
</g>
209211
);
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
2+
import { Box } from '@chakra-ui/react';
3+
4+
interface TooltipState {
5+
isVisible: boolean;
6+
content: ReactNode | null;
7+
position: { x: number; y: number };
8+
anchor: 'top' | 'right' | 'bottom' | 'left';
9+
}
10+
11+
interface TooltipContextValue {
12+
showTooltipFromEvent: (content: ReactNode, event: React.MouseEvent, anchor?: 'top' | 'right' | 'bottom' | 'left') => void;
13+
hideTooltip: () => void;
14+
tooltipState: TooltipState;
15+
}
16+
17+
const TooltipContext = createContext<TooltipContextValue | undefined>(undefined);
18+
19+
interface TooltipProviderProps {
20+
children: ReactNode;
21+
}
22+
23+
export const TooltipProvider: React.FC<TooltipProviderProps> = ({ children }) => {
24+
const [tooltipState, setTooltipState] = useState<TooltipState>({
25+
isVisible: false,
26+
content: null,
27+
position: { x: 0, y: 0 },
28+
anchor: 'top'
29+
});
30+
31+
const showTooltipFromEvent = useCallback((
32+
content: ReactNode,
33+
event: React.MouseEvent,
34+
anchor: 'top' | 'right' | 'bottom' | 'left' = 'top'
35+
) => {
36+
const x = event.clientX;
37+
const y = event.clientY;
38+
39+
setTooltipState({
40+
isVisible: true,
41+
content,
42+
position: { x, y },
43+
anchor
44+
});
45+
}, []);
46+
47+
const hideTooltip = useCallback(() => {
48+
setTooltipState(prev => ({
49+
...prev,
50+
isVisible: false,
51+
content: null
52+
}));
53+
}, []);
54+
55+
return (
56+
<TooltipContext.Provider value={{ showTooltipFromEvent, hideTooltip, tooltipState }}>
57+
{children}
58+
{tooltipState.isVisible && tooltipState.content && (
59+
<TooltipPortal
60+
content={tooltipState.content}
61+
position={tooltipState.position}
62+
anchor={tooltipState.anchor}
63+
/>
64+
)}
65+
</TooltipContext.Provider>
66+
);
67+
};
68+
69+
interface TooltipPortalProps {
70+
content: ReactNode;
71+
position: { x: number; y: number };
72+
anchor: 'top' | 'right' | 'bottom' | 'left';
73+
}
74+
75+
const TooltipPortal: React.FC<TooltipPortalProps> = ({ content, position, anchor }) => {
76+
const getTooltipPosition = () => {
77+
const offset = 10;
78+
const tooltipWidth = 420; // Based on TooltipCard minW="420px"
79+
const tooltipHeight = 300; // Estimated height
80+
const viewportWidth = window.innerWidth;
81+
const viewportHeight = window.innerHeight;
82+
83+
let finalAnchor = anchor;
84+
let x = position.x;
85+
let y = position.y;
86+
87+
// Adjust anchor and position based on viewport boundaries
88+
switch (anchor) {
89+
case 'right':
90+
// Check if tooltip would go beyond right edge
91+
if (x + offset + tooltipWidth > viewportWidth) {
92+
finalAnchor = 'left';
93+
}
94+
// Check if tooltip would go beyond bottom edge
95+
if (y + tooltipHeight / 2 > viewportHeight) {
96+
y = viewportHeight - tooltipHeight / 2 - offset;
97+
}
98+
// Check if tooltip would go beyond top edge
99+
if (y - tooltipHeight / 2 < 0) {
100+
y = tooltipHeight / 2 + offset;
101+
}
102+
break;
103+
104+
case 'left':
105+
// Check if tooltip would go beyond left edge
106+
if (x - offset - tooltipWidth < 0) {
107+
finalAnchor = 'right';
108+
}
109+
// Check vertical boundaries
110+
if (y + tooltipHeight / 2 > viewportHeight) {
111+
y = viewportHeight - tooltipHeight / 2 - offset;
112+
}
113+
if (y - tooltipHeight / 2 < 0) {
114+
y = tooltipHeight / 2 + offset;
115+
}
116+
break;
117+
118+
case 'bottom':
119+
// Check if tooltip would go beyond bottom edge
120+
if (y + offset + tooltipHeight > viewportHeight) {
121+
finalAnchor = 'top';
122+
}
123+
// Check horizontal boundaries
124+
if (x + tooltipWidth / 2 > viewportWidth) {
125+
x = viewportWidth - tooltipWidth / 2 - offset;
126+
}
127+
if (x - tooltipWidth / 2 < 0) {
128+
x = tooltipWidth / 2 + offset;
129+
}
130+
break;
131+
132+
case 'top':
133+
default:
134+
// Check if tooltip would go beyond top edge
135+
if (y - offset - tooltipHeight < 0) {
136+
finalAnchor = 'bottom';
137+
}
138+
// Check horizontal boundaries
139+
if (x + tooltipWidth / 2 > viewportWidth) {
140+
x = viewportWidth - tooltipWidth / 2 - offset;
141+
}
142+
if (x - tooltipWidth / 2 < 0) {
143+
x = tooltipWidth / 2 + offset;
144+
}
145+
break;
146+
}
147+
148+
// Return position based on final anchor
149+
switch (finalAnchor) {
150+
case 'right':
151+
return {
152+
left: x + offset,
153+
top: y,
154+
transform: 'translateY(-50%)'
155+
};
156+
case 'left':
157+
return {
158+
left: x - offset - tooltipWidth,
159+
top: y,
160+
transform: 'translateY(-50%)'
161+
};
162+
case 'bottom':
163+
return {
164+
left: x,
165+
top: y + offset,
166+
transform: 'translateX(-50%)'
167+
};
168+
case 'top':
169+
default:
170+
return {
171+
left: x,
172+
top: y - offset - tooltipHeight,
173+
transform: 'translateX(-50%)'
174+
};
175+
}
176+
};
177+
178+
return (
179+
<Box
180+
position="fixed"
181+
zIndex={9999}
182+
pointerEvents="none"
183+
style={getTooltipPosition()}
184+
>
185+
{content}
186+
</Box>
187+
);
188+
};
189+
190+
export const useCustomTooltip = (): TooltipContextValue => {
191+
const context = useContext(TooltipContext);
192+
if (context === undefined) {
193+
throw new Error('useCustomTooltip must be used within a TooltipProvider');
194+
}
195+
return context;
196+
};

app/src/app/[lng]/[inventory]/InventoryResultTab/EmissionsForecast/EmissionsForecastCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const EmissionsForecastCard = ({
2525
}) => {
2626
const [isExplanationModalOpen, setIsExplanationModalOpen] = useState(false);
2727

28+
2829
return (
2930
<>
3031
<GrowthRatesExplanationModal
@@ -66,7 +67,6 @@ export const EmissionsForecastCard = ({
6667
paddingRight={0}
6768
height="600px"
6869
width="100%"
69-
overflow="hidden"
7070
>
7171
<EmissionsForecastChart forecast={forecast} t={t} />
7272
</CardBody>

0 commit comments

Comments
 (0)