Skip to content

Commit b23a469

Browse files
committed
Add support for labels on-hover for bar plots
1 parent 0a6bf58 commit b23a469

3 files changed

Lines changed: 87 additions & 14 deletions

File tree

src/ert/gui/tools/plot/plottery/plot_context.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from enum import StrEnum
34
from typing import TYPE_CHECKING, ClassVar
45

56
if TYPE_CHECKING:
@@ -10,6 +11,12 @@
1011
from .plot_config import PlotConfig
1112

1213

14+
class PlotType(StrEnum):
15+
LINE = "LINE"
16+
BAR = "BAR"
17+
SCATTER = "SCATTER"
18+
19+
1320
class PlotContext:
1421
UNKNOWN_AXIS = None
1522
VALUE_AXIS = "VALUE"
@@ -25,9 +32,6 @@ class PlotContext:
2532
INDEX_AXIS,
2633
VALUE_AXIS,
2734
]
28-
PLOT_TYPES: ClassVar[list[str]] = [
29-
"BAR",
30-
]
3135

3236
def __init__(
3337
self,
@@ -51,7 +55,7 @@ def __init__(
5155
self._log_scale = False
5256
self._extended_plot_information = False
5357

54-
self._plot_type: str | None = None
58+
self._plot_type: PlotType | None = None
5559

5660
@property
5761
def flip_response_axis(self) -> bool:
@@ -116,15 +120,11 @@ def y_axis(self, value: str) -> None:
116120
self._y_axis = value
117121

118122
@property
119-
def plot_type(self) -> str | None:
123+
def plot_type(self) -> PlotType | None:
120124
return self._plot_type
121125

122126
@plot_type.setter
123-
def plot_type(self, value: str) -> None:
124-
if value not in PlotContext.PLOT_TYPES:
125-
raise UserWarning(
126-
f"Plot type: '{value}' is not one of: {PlotContext.PLOT_TYPES}"
127-
)
127+
def plot_type(self, value: PlotType) -> None:
128128
self._plot_type = value
129129

130130
def setXLabel(self, value: str) -> None:

src/ert/gui/tools/plot/plottery/plots/everest_gradients_plot.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import numpy as np
66
import pandas as pd
77

8+
from ert.gui.tools.plot.plottery.plot_context import PlotType
9+
810
from .plot_tools import ConditionalAxisFormatter, PlotTools
911

1012
if TYPE_CHECKING:
@@ -50,7 +52,7 @@ def plot(
5052

5153
plot_context.y_axis = plot_context.VALUE_AXIS
5254
plot_context.x_axis = plot_context.INDEX_AXIS
53-
plot_context.plot_type = "BAR"
55+
plot_context.plot_type = PlotType.BAR
5456
plot_context.deactivateDateSupport()
5557

5658
response_key = key_def.key if key_def else "Response"
@@ -85,6 +87,8 @@ def plot(
8587
n_controls = len(self.selected_controls)
8688
bar_width = 0.8 / n_controls
8789

90+
bar_containers = []
91+
8892
for i, control in enumerate(self.selected_controls):
8993
color = colors[i % len(colors)][0]
9094
values = []
@@ -105,6 +109,8 @@ def plot(
105109
alpha=0.7,
106110
)
107111
config.addLegendItem(control, bars[0])
112+
bar_containers.append(bars)
113+
108114
axes.axhline(0, color="black", linewidth=1.0, alpha=0.3)
109115
axes.set_xticks(pos)
110116
axes.set_xticklabels([f"Batch {b}" for b in batch_ids], rotation=0)
@@ -113,6 +119,16 @@ def plot(
113119
axes.spines["left"].set_visible(False)
114120
axes.spines["top"].set_visible(False)
115121

122+
bar_labels = [
123+
f"batch {batch}\n{control}"
124+
for batch in batch_ids
125+
for control in self.selected_controls
126+
]
127+
128+
PlotTools.labels_on_hover(
129+
axes, plot_context, figure, (bar_containers, bar_labels)
130+
)
131+
116132
PlotTools.finalizePlot(
117133
plot_context,
118134
figure,

src/ert/gui/tools/plot/plottery/plots/plot_tools.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from __future__ import annotations
22

33
import math
4-
from typing import TYPE_CHECKING
4+
from typing import TYPE_CHECKING, Any
55

66
import matplotlib.ticker as mticker
7+
from matplotlib.backend_bases import Event
8+
from matplotlib.text import Annotation
9+
10+
from ert.gui.tools.plot.plottery.plot_context import PlotType
711

812
if TYPE_CHECKING:
913
from datetime import date
@@ -33,7 +37,7 @@ def __init__(
3337
self,
3438
low: float = 1e-3,
3539
high: float = 1e4,
36-
precision: float = 0,
40+
precision: int = 0,
3741
) -> None:
3842
self.low = low
3943
self.high = high
@@ -61,7 +65,7 @@ class PlotTools:
6165
def showGrid(axes: Axes, plot_context: PlotContext) -> None:
6266
config = plot_context.plotConfig()
6367
if config.isGridEnabled():
64-
if plot_context.plot_type == "BAR":
68+
if plot_context.plot_type == PlotType.BAR:
6569
axes.grid(axis="y", color="black", alpha=0.1)
6670
else:
6771
axes.grid(visible=True, color="black", alpha=0.4)
@@ -163,3 +167,56 @@ def __setupLabels(
163167

164168
if config.yLabel() is None:
165169
config.setYLabel(default_y_label)
170+
171+
@staticmethod
172+
def labels_on_hover(
173+
axes: Axes,
174+
plot_context: PlotContext,
175+
figure: Figure,
176+
data: tuple[list[Any], list[str]],
177+
) -> None:
178+
annot = axes.annotate(
179+
"",
180+
xy=(0, 0),
181+
xytext=(-40, 10),
182+
textcoords="offset points",
183+
bbox={"boxstyle": "round,pad=0.5", "fc": "pink", "alpha": 0.7},
184+
arrowprops={"arrowstyle": "->", "connectionstyle": "arc3,rad=0"},
185+
)
186+
annot.set_visible(False)
187+
188+
if plot_context.plot_type == PlotType.BAR:
189+
figure.canvas.mpl_connect(
190+
"motion_notify_event",
191+
lambda event: PlotTools.__on_hover_bar_group(
192+
event, axes, annot, figure, data
193+
),
194+
)
195+
else:
196+
raise NotImplementedError(
197+
f"Hover labels not implemented for: {plot_context.plot_type}"
198+
)
199+
200+
@staticmethod
201+
def __on_hover_bar_group(
202+
event: Event,
203+
axes: Axes,
204+
annot: Annotation,
205+
figure: Figure,
206+
data: tuple[list[Any], list[str]],
207+
) -> None:
208+
if event.inaxes == axes: # type: ignore
209+
for bar_container_idx, bars in enumerate(data[0]):
210+
for bar_idx, bar in enumerate(bars):
211+
if bar.contains(event)[0]:
212+
value = bar.get_height()
213+
214+
annot.xy = (bar.get_x() + bar.get_width() / 2, value)
215+
idx = bar_idx * len(data[0]) + bar_container_idx
216+
annot.set_text(f"{data[1][idx]}\nValue: {value:.3g}")
217+
annot.set_visible(True)
218+
figure.canvas.draw_idle()
219+
return
220+
221+
annot.set_visible(False)
222+
figure.canvas.draw_idle()

0 commit comments

Comments
 (0)