-
Notifications
You must be signed in to change notification settings - Fork 0
/
custom_tooltip.py
161 lines (130 loc) · 4.19 KB
/
custom_tooltip.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
from collections.abc import Callable
from typing import Optional
from matplotlib import pyplot as plt
from matplotlib.artist import Artist
from matplotlib.axes import Axes
from matplotlib.backend_bases import MouseEvent
from matplotlib.figure import Figure
from matplotlib.patheffects import withSimplePatchShadow
from matplotlib.text import Text
from matplotlib.transforms import IdentityTransform
import pandas as pd
import mpl_utils
# Added in #603
class Blitter:
def __init__(self, fig: Figure = None):
fig = fig or plt.gcf()
self.fig = fig
self.canvas = fig.canvas
self.background = None
self.capture_background()
fig.canvas.mpl_connect("draw_event", self.capture_background)
def capture_background(self, _=None):
self.background = self.canvas.copy_from_bbox(self.fig.bbox)
def blit(self, artist: Artist):
self.canvas.restore_region(self.background)
self.fig.draw_artist(artist)
self.canvas.blit()
def _default_get_text(event: MouseEvent):
return f"x={event.xdata:g}\ny={event.ydata:g}"
# Added in #602
class add_custom_tooltip(mpl_utils.EventsMixin):
def __init__(
self,
ax: Axes = None,
get_text: Callable[[MouseEvent], Optional[str]] = _default_get_text,
use_blit=True,
):
ax = ax or plt.gca()
super().__init__(ax)
self.ax = ax
self.fig = ax.figure
self.get_text = get_text
if use_blit and self.fig.canvas.supports_blit:
self.blitter = Blitter(self.fig)
else:
self.blitter = None
self.tooltip: Text = self.fig.text(
x=0,
y=0,
s="",
transform=IdentityTransform(),
bbox=dict(
alpha=0.8,
path_effects=[withSimplePatchShadow(offset=(2, -2))],
),
linespacing=1.4,
multialignment="left",
visible=False,
animated=use_blit,
)
self.fig.canvas.mpl_connect("motion_notify_event", self.on_mouse_move)
ax._custom_tooltip_ref = self
def on_mouse_move(self, event: MouseEvent):
if event.button:
self.hide_tooltip()
return
if event.inaxes != self.ax:
return
text = self.get_text(event)
if text:
is_left = event.x < self.fig.bbox.width / 2
is_bottom = event.y < self.fig.bbox.height / 2
self.tooltip.set(
text=text,
x=event.x + (15 if is_left else -15),
y=event.y + (15 if is_bottom else -15),
ha="left" if is_left else "right",
va="bottom" if is_bottom else "top",
visible=True,
)
self.render()
else:
self.hide_tooltip()
def on_leave(self, _):
if self.tooltip.get_visible():
self.hide_tooltip()
def hide_tooltip(self):
self.tooltip.set_visible(False)
self.render()
def render(self):
if self.blitter:
self.blitter.blit(self.tooltip)
else:
self.fig.canvas.draw_idle()
if __name__ == "__main__":
def get_text(event: MouseEvent):
year = mpl_utils.get_closest_x(event)
has_match = False
text = mpl_utils.bold(f"{year:g}")
for line in event.inaxes.lines:
if line.contains(event)[0]:
has_match = True
country = line.get_label()
value = mpl_utils.get_y_at_x(line, year)
text += f"\n{country}: {value:.2f}"
if has_match:
return text
mpl_utils.setup()
fig, ax = plt.subplots(num="Custom tooltip", clear=True)
mpl_utils.clear_events()
df = pd.read_csv("../data/crop-data.csv")
chart_df = df.pivot_table(
index="Year",
columns="Country",
# columns=["Country", "Crop"],
values="Yield",
)
# Mock missing data
chart_df.iloc[20:25, 60:65] = 0
ax.plot(
chart_df,
label=chart_df.columns,
color="C0",
linestyle="-",
alpha=0.3,
)
self = add_custom_tooltip(
ax=ax,
get_text=get_text,
)