Skip to content

Commit 97c80da

Browse files
committed
Add simple statistics interface
The statistics customization tab disappeared with the plot customization dialog, leaving no way to choose which statistics to show. Reintroduce the controls in the plot sidebar, letting the user select statistics and whether bands are filled.
1 parent bace0fa commit 97c80da

5 files changed

Lines changed: 207 additions & 4 deletions

File tree

src/ert/gui/plotting/plot_window.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@
5858
from .utils.plot_color_palettes import TABLEAU_10_COLOR_CYCLE
5959
from .utils.plot_types import ObservationPlotLocations
6060
from .utils.qt_creator import create_group_box, create_group_layout, create_side_panel
61-
from .utils.statistics_style import DEFAULT_ENABLED_STATISTICS
6261
from .widgets.data_type_keys_widget import DataTypeKeysWidget
6362
from .widgets.everest_control_selection_widget import EverestControlSelectionWidget
6463
from .widgets.plot_controls import (
6564
BoxplotOptions,
6665
EverestControlsPlotOptions,
6766
GeneralPlotOptions,
67+
StatisticsOptions,
6868
)
6969
from .widgets.plot_ensemble_selection_widget import EnsembleSelectionWidget
7070
from .widgets.plot_widget import Plotter, PlotWidget
@@ -301,6 +301,7 @@ def __init__(
301301
self._general_options.axisLabelEditRequested.connect(self._edit_axis_label)
302302
self._general_options.titleEditRequested.connect(self._edit_title)
303303
self._boxplot_options = BoxplotOptions(self.update_plot)
304+
self._statistics_options = StatisticsOptions(self.update_plot)
304305

305306
right_container = QWidget()
306307
right_layout = create_group_layout(
@@ -310,13 +311,15 @@ def __init__(
310311
self._everest_controls_plot_options.get_widget(),
311312
self._everest_controls_group,
312313
self._boxplot_options.get_widget(),
314+
self._statistics_options.get_widget(),
313315
]
314316
)
315317
right_container.setLayout(right_layout)
316318

317319
self._everest_controls_group.setVisible(False)
318320
self._everest_controls_plot_options.get_widget().setVisible(False)
319321
self._boxplot_options.get_widget().setVisible(False)
322+
self._statistics_options.get_widget().setVisible(False)
320323
self._data_type_keys_widget.selectDefault()
321324

322325
splitter = QSplitter(Qt.Orientation.Horizontal)
@@ -384,6 +387,7 @@ def update_plot(self, layer: int | None = None) -> None:
384387
self._boxplot_options.get_widget().setVisible(
385388
plot_widget.name in {MISFITS, CROSS_ENSEMBLE_STATISTICS}
386389
)
390+
self._statistics_options.get_widget().setVisible(plot_widget.name == STATISTICS)
387391
self._general_options.get_widget().setVisible(plot_widget.name != STD_DEV)
388392

389393
is_gradient_plot = plot_widget.name == EVEREST_GRADIENTS_PLOT
@@ -543,9 +547,7 @@ def fetch_data(
543547
plot_config.set_x_label(self._x_labels.get(key_def.key))
544548
plot_config.set_y_label(self._y_labels.get(key_def.key))
545549
if selected_tab == STATISTICS:
546-
plot_config.set_statistics_options(
547-
DEFAULT_ENABLED_STATISTICS, fill_bands=False
548-
)
550+
self._statistics_options.apply_to(plot_config)
549551
plot_config.set_legend_enabled(self._general_options.legend_checkbox_state)
550552
plot_config.set_grid_enabled(self._general_options.grid_checkbox_state)
551553
plot_config.set_line_color_cycle(self._general_options.get_color_cycle())

src/ert/gui/plotting/utils/qt_creator.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from PyQt6.QtWidgets import (
66
QCheckBox,
77
QGroupBox,
8+
QHBoxLayout,
89
QLabel,
10+
QSpinBox,
911
QVBoxLayout,
1012
QWidget,
1113
)
@@ -57,3 +59,33 @@ def create_checkbox_with_tooltip(
5759
checkbox.stateChanged.connect(connection_point)
5860
log_plot_option_usage_once(checkbox.clicked, logger, name)
5961
return checkbox
62+
63+
64+
def create_spinbox_with_tooltip(
65+
name: str,
66+
tooltip: str,
67+
connection_point: Callable[..., object],
68+
*,
69+
minimum: int,
70+
maximum: int,
71+
initial_value: int,
72+
logger: Logger,
73+
) -> QSpinBox:
74+
spinbox = QSpinBox()
75+
spinbox.setObjectName(f"{name.lower().replace(' ', '_')}_spinbox")
76+
spinbox.setToolTip(tooltip)
77+
spinbox.setRange(minimum, maximum)
78+
spinbox.setValue(initial_value)
79+
spinbox.valueChanged.connect(connection_point)
80+
log_plot_option_usage_once(spinbox.valueChanged, logger, name)
81+
return spinbox
82+
83+
84+
def create_labeled_row(label: str, widget: QWidget) -> QWidget:
85+
row = QWidget()
86+
layout = QHBoxLayout(row)
87+
layout.setContentsMargins(0, 0, 0, 0)
88+
layout.addWidget(QLabel(label))
89+
layout.addWidget(widget)
90+
layout.addStretch()
91+
return row

src/ert/gui/plotting/widgets/plot_controls/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
from .everest_controls_plot_options import EverestControlsPlotOptions
44
from .general_options import GeneralPlotOptions
55
from .plot_color_palette_selector import PlotColorPaletteSelector
6+
from .statistics_options import StatisticsOptions
67

78
__all__ = [
89
"BoxplotOptions",
910
"CustomPaletteDialog",
1011
"EverestControlsPlotOptions",
1112
"GeneralPlotOptions",
1213
"PlotColorPaletteSelector",
14+
"StatisticsOptions",
1315
]
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from collections.abc import Callable
5+
from typing import TYPE_CHECKING
6+
7+
from PyQt6.QtWidgets import QGroupBox
8+
9+
from ert.gui.plotting.utils.qt_creator import (
10+
create_checkbox_with_tooltip,
11+
create_group_box,
12+
create_group_layout,
13+
create_labeled_row,
14+
create_spinbox_with_tooltip,
15+
)
16+
from ert.gui.plotting.utils.statistics_style import (
17+
DEFAULT_ENABLED_STATISTICS,
18+
STATISTICS,
19+
)
20+
21+
if TYPE_CHECKING:
22+
from ert.gui.plotting.utils import PlotConfig
23+
24+
logger = logging.getLogger(__name__)
25+
26+
27+
class StatisticsOptions:
28+
"""Owns the statistics selection, which persists across keys."""
29+
30+
def __init__(self, connection_point: Callable[..., object]) -> None:
31+
self._toggles = {
32+
statistic: create_checkbox_with_tooltip(
33+
style.label,
34+
f"Show or hide the {style.label} "
35+
+ ("band" if style.is_band else "line"),
36+
connection_point,
37+
initial_checked=statistic in DEFAULT_ENABLED_STATISTICS,
38+
logger=logger,
39+
)
40+
for statistic, style in STATISTICS.items()
41+
}
42+
self._area_toggle = create_checkbox_with_tooltip(
43+
"Area",
44+
"Draw the standard deviation, min/max and percentile ranges as a "
45+
"filled area instead of a pair of lines",
46+
connection_point,
47+
initial_checked=False,
48+
logger=logger,
49+
)
50+
self._std_dev_factor = create_spinbox_with_tooltip(
51+
"Std dev multiplier",
52+
"Choose the number of standard deviations to plot",
53+
connection_point,
54+
minimum=1,
55+
maximum=3,
56+
initial_value=1,
57+
logger=logger,
58+
)
59+
60+
self._statistics_options = create_group_box(
61+
"Statistics options",
62+
create_group_layout(
63+
[
64+
*self._toggles.values(),
65+
self._area_toggle,
66+
create_labeled_row("Std dev multiplier", self._std_dev_factor),
67+
]
68+
),
69+
)
70+
71+
def apply_to(self, plot_config: PlotConfig) -> None:
72+
plot_config.set_standard_deviation_factor(self._std_dev_factor.value())
73+
plot_config.set_statistics_options(
74+
self._enabled_statistics(),
75+
fill_bands=self._area_toggle.isChecked(),
76+
)
77+
78+
def _enabled_statistics(self) -> set[str]:
79+
return {
80+
statistic
81+
for statistic, checkbox in self._toggles.items()
82+
if checkbox.isChecked()
83+
}
84+
85+
def get_widget(self) -> QGroupBox:
86+
return self._statistics_options
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from unittest.mock import Mock
2+
3+
from ert.gui.plotting.utils import PlotConfig
4+
from ert.gui.plotting.utils.statistics_style import STATISTICS
5+
from ert.gui.plotting.widgets.plot_controls.statistics_options import StatisticsOptions
6+
7+
8+
def test_that_statistics_options_have_expected_default_toggle_states(qtbot):
9+
options = StatisticsOptions(Mock())
10+
qtbot.addWidget(options.get_widget())
11+
12+
checked = {
13+
statistic
14+
for statistic, checkbox in options._toggles.items()
15+
if checkbox.isChecked()
16+
}
17+
18+
assert checked == {"mean", "p10-p90"}
19+
assert not options._area_toggle.isChecked()
20+
assert options._std_dev_factor.value() == 1
21+
22+
23+
def test_that_unchecked_statistics_are_hidden_in_the_plot_config(qtbot):
24+
options = StatisticsOptions(Mock())
25+
qtbot.addWidget(options.get_widget())
26+
plot_config = PlotConfig()
27+
28+
options.apply_to(plot_config)
29+
30+
for statistic in STATISTICS:
31+
style = plot_config.get_statistics_style(statistic)
32+
if statistic in {"mean", "p10-p90"}:
33+
assert style.is_visible()
34+
else:
35+
assert not style.is_visible()
36+
37+
38+
def test_that_enabling_area_switches_band_statistics_to_a_filled_style(qtbot):
39+
options = StatisticsOptions(Mock())
40+
qtbot.addWidget(options.get_widget())
41+
42+
line_config = PlotConfig()
43+
options.apply_to(line_config)
44+
assert line_config.get_statistics_style("p10-p90").line_style == "--"
45+
46+
options._area_toggle.setChecked(True)
47+
area_config = PlotConfig()
48+
options.apply_to(area_config)
49+
assert area_config.get_statistics_style("p10-p90").line_style == "#"
50+
51+
52+
def test_that_the_std_dev_multiplier_is_applied_to_the_plot_config(qtbot):
53+
options = StatisticsOptions(Mock())
54+
qtbot.addWidget(options.get_widget())
55+
options._std_dev_factor.setValue(3)
56+
plot_config = PlotConfig()
57+
58+
options.apply_to(plot_config)
59+
60+
assert plot_config.get_standard_deviation_factor() == 3
61+
62+
63+
def test_that_toggling_a_statistic_invokes_the_connection_point(qtbot):
64+
connection_point = Mock()
65+
options = StatisticsOptions(connection_point)
66+
qtbot.addWidget(options.get_widget())
67+
68+
options._toggles["p50"].setChecked(True)
69+
70+
connection_point.assert_called()
71+
72+
73+
def test_that_applying_options_does_not_invoke_the_connection_point(qtbot):
74+
connection_point = Mock()
75+
options = StatisticsOptions(connection_point)
76+
qtbot.addWidget(options.get_widget())
77+
connection_point.reset_mock()
78+
79+
options.apply_to(PlotConfig())
80+
81+
connection_point.assert_not_called()

0 commit comments

Comments
 (0)