Skip to content

Commit e788243

Browse files
committed
Add simple statistics interface
Both settings only had controls in the removed customization dialog, even though the plotters still read them from PlotConfig. Add them to the sidebar so the statistics and distribution plots regain the options they lost.
1 parent 5b4d067 commit e788243

5 files changed

Lines changed: 231 additions & 0 deletions

File tree

src/ert/gui/plotting/plot_window.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
HISTOGRAM,
4646
MISFITS,
4747
SHARED_PLOT_MAP,
48+
STATISTICS,
4849
STD_DEV,
4950
)
5051
from ert.gui.utils import is_everest_application
@@ -61,8 +62,10 @@
6162
from .widgets.everest_control_selection_widget import EverestControlSelectionWidget
6263
from .widgets.plot_controls import (
6364
BoxplotOptions,
65+
DistributionOptions,
6466
EverestControlsPlotOptions,
6567
GeneralPlotOptions,
68+
StatisticsOptions,
6669
)
6770
from .widgets.plot_ensemble_selection_widget import EnsembleSelectionWidget
6871
from .widgets.plot_widget import Plotter, PlotWidget
@@ -306,6 +309,8 @@ def __init__(
306309
self._general_options.axisLabelEditRequested.connect(self._edit_axis_label)
307310
self._general_options.titleEditRequested.connect(self._edit_title)
308311
self._boxplot_options = BoxplotOptions(self.update_plot)
312+
self._statistics_options = StatisticsOptions(self.update_plot)
313+
self._distribution_options = DistributionOptions(self.update_plot)
309314

310315
right_container = QWidget()
311316
right_layout = create_group_layout(
@@ -315,13 +320,17 @@ def __init__(
315320
self._everest_controls_plot_options.get_widget(),
316321
self._everest_controls_group,
317322
self._boxplot_options.get_widget(),
323+
self._statistics_options.get_widget(),
324+
self._distribution_options.get_widget(),
318325
]
319326
)
320327
right_container.setLayout(right_layout)
321328

322329
self._everest_controls_group.setVisible(False)
323330
self._everest_controls_plot_options.get_widget().setVisible(False)
324331
self._boxplot_options.get_widget().setVisible(False)
332+
self._statistics_options.get_widget().setVisible(False)
333+
self._distribution_options.get_widget().setVisible(False)
325334
self._data_type_keys_widget.selectDefault()
326335

327336
splitter = QSplitter(Qt.Orientation.Horizontal)
@@ -390,6 +399,10 @@ def update_plot(self, layer: int | None = None) -> None:
390399
self._boxplot_options.get_widget().setVisible(
391400
plot_widget.name in {MISFITS, CROSS_ENSEMBLE_STATISTICS}
392401
)
402+
self._statistics_options.get_widget().setVisible(plot_widget.name == STATISTICS)
403+
self._distribution_options.get_widget().setVisible(
404+
plot_widget.name == DISTRIBUTION
405+
)
393406
self._general_options.get_widget().setVisible(plot_widget.name != STD_DEV)
394407

395408
is_gradient_plot = plot_widget.name == EVEREST_GRADIENTS_PLOT
@@ -548,6 +561,8 @@ def fetch_data(
548561
plot_config.set_title(self._titles.get(key, key))
549562
plot_config.set_x_label(self._x_labels.get(key))
550563
plot_config.set_y_label(self._y_labels.get(key))
564+
self._statistics_options.apply_to(plot_config)
565+
self._distribution_options.apply_to(plot_config)
551566
plot_config.set_legend_enabled(self._general_options.legend_checkbox_state)
552567
plot_config.set_grid_enabled(self._general_options.grid_checkbox_state)
553568
plot_config.set_line_color_cycle(self._general_options.get_color_cycle())

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
from PyQt6.QtWidgets import (
66
QCheckBox,
77
QGroupBox,
8+
QHBoxLayout,
89
QLabel,
10+
QSpinBox,
11+
QToolButton,
912
QVBoxLayout,
1013
QWidget,
1114
)
@@ -42,6 +45,48 @@ def create_group_box(title: str, layout: QVBoxLayout) -> QGroupBox:
4245
return group_box
4346

4447

48+
def create_collapsible_group_box(
49+
title: str, layout: QVBoxLayout, *, expanded: bool = True
50+
) -> QGroupBox:
51+
"""Create a group box whose contents can be folded away by its header.
52+
53+
The header is a disclosure arrow rather than a check, so it does not read
54+
as a switch that turns the section off. Collapsing only hides the
55+
contents; the widgets keep their values.
56+
"""
57+
name = title.lower().replace(" ", "_")
58+
59+
content = QWidget()
60+
content.setObjectName(f"{name}_content")
61+
content.setLayout(layout)
62+
content.setVisible(expanded)
63+
64+
header = QToolButton()
65+
header.setObjectName(f"{name}_header")
66+
header.setText(title)
67+
header.setCheckable(True)
68+
header.setChecked(expanded)
69+
header.setAutoRaise(True)
70+
header.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
71+
header.setArrowType(Qt.ArrowType.DownArrow if expanded else Qt.ArrowType.RightArrow)
72+
header.setStyleSheet("QToolButton { border: none; font-style: italic; }")
73+
74+
def toggle_contents(checked: bool) -> None:
75+
header.setArrowType(
76+
Qt.ArrowType.DownArrow if checked else Qt.ArrowType.RightArrow
77+
)
78+
content.setVisible(checked)
79+
80+
header.toggled.connect(toggle_contents)
81+
82+
outer_layout = QVBoxLayout()
83+
outer_layout.setContentsMargins(0, 0, 0, 0)
84+
outer_layout.addWidget(header)
85+
outer_layout.addWidget(content)
86+
87+
return create_group_box("", outer_layout)
88+
89+
4590
def create_checkbox_with_tooltip(
4691
name: str,
4792
tooltip: str,
@@ -57,3 +102,33 @@ def create_checkbox_with_tooltip(
57102
checkbox.stateChanged.connect(connection_point)
58103
log_plot_option_usage_once(checkbox.clicked, logger, name)
59104
return checkbox
105+
106+
107+
def create_spinbox_with_tooltip(
108+
name: str,
109+
tooltip: str,
110+
connection_point: Callable[..., object],
111+
*,
112+
minimum: int,
113+
maximum: int,
114+
initial_value: int,
115+
logger: Logger,
116+
) -> QSpinBox:
117+
spinbox = QSpinBox()
118+
spinbox.setObjectName(f"{name.lower().replace(' ', '_')}_spinbox")
119+
spinbox.setToolTip(tooltip)
120+
spinbox.setRange(minimum, maximum)
121+
spinbox.setValue(initial_value)
122+
spinbox.valueChanged.connect(connection_point)
123+
log_plot_option_usage_once(spinbox.valueChanged, logger, name)
124+
return spinbox
125+
126+
127+
def create_labeled_row(label: str, widget: QWidget) -> QWidget:
128+
row = QWidget()
129+
layout = QHBoxLayout(row)
130+
layout.setContentsMargins(0, 0, 0, 0)
131+
layout.addWidget(QLabel(label))
132+
layout.addWidget(widget)
133+
layout.addStretch()
134+
return row
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
from .boxplot_options import BoxplotOptions
22
from .custom_palette_dialog import CustomPaletteDialog
3+
from .distribution_options import DistributionOptions
34
from .everest_controls_plot_options import EverestControlsPlotOptions
45
from .general_options import GeneralPlotOptions
56
from .plot_color_palette_selector import PlotColorPaletteSelector
7+
from .statistics_options import StatisticsOptions
68

79
__all__ = [
810
"BoxplotOptions",
911
"CustomPaletteDialog",
12+
"DistributionOptions",
1013
"EverestControlsPlotOptions",
1114
"GeneralPlotOptions",
1215
"PlotColorPaletteSelector",
16+
"StatisticsOptions",
1317
]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
)
14+
15+
if TYPE_CHECKING:
16+
from ert.gui.plotting.utils import PlotConfig
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
class DistributionOptions:
22+
def __init__(self, connection_point: Callable[..., object]) -> None:
23+
self._connection_lines_toggle = create_checkbox_with_tooltip(
24+
"Connection lines",
25+
"Draw lines between the realizations of neighbouring ensembles",
26+
connection_point,
27+
initial_checked=False,
28+
logger=logger,
29+
)
30+
31+
self._distribution_options = create_group_box(
32+
"Distribution options",
33+
create_group_layout([self._connection_lines_toggle]),
34+
)
35+
36+
def apply_to(self, plot_config: PlotConfig) -> None:
37+
plot_config.set_distribution_line_enabled(
38+
self._connection_lines_toggle.isChecked()
39+
)
40+
41+
def get_widget(self) -> QGroupBox:
42+
return self._distribution_options
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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_collapsible_group_box,
12+
create_group_layout,
13+
create_labeled_row,
14+
create_spinbox_with_tooltip,
15+
)
16+
17+
if TYPE_CHECKING:
18+
from ert.gui.plotting.utils import PlotConfig
19+
20+
logger = logging.getLogger(__name__)
21+
22+
# (statistic, label, tooltip, checked by default)
23+
_STATISTICS = [
24+
("mean", "Mean", "Show or hide the mean", True),
25+
("p50", "P50", "Show or hide the P50 line", False),
26+
("std", "Std dev", "Show or hide the standard deviation band", False),
27+
("min-max", "Min/Max", "Show or hide the min/max band", False),
28+
("p10-p90", "P10-P90", "Show or hide the P10-P90 band", True),
29+
("p33-p67", "P33-P67", "Show or hide the P33-P67 band", False),
30+
]
31+
32+
_BAND_STATISTICS = frozenset({"std", "min-max", "p10-p90", "p33-p67"})
33+
34+
_AREA_LINE_STYLE = "#"
35+
_BAND_LINE_STYLE = "--"
36+
37+
_LINE_STYLE_WHEN_ENABLED = {"mean": "-", "p50": "--"}
38+
39+
40+
class StatisticsOptions:
41+
def __init__(self, connection_point: Callable[..., object]) -> None:
42+
self._toggles = {
43+
statistic: create_checkbox_with_tooltip(
44+
label, tooltip, connection_point, initial_checked=checked, logger=logger
45+
)
46+
for statistic, label, tooltip, checked in _STATISTICS
47+
}
48+
self._area_toggle = create_checkbox_with_tooltip(
49+
"Area",
50+
"Draw the standard deviation, min/max and percentile ranges as a "
51+
"filled area instead of a pair of lines",
52+
connection_point,
53+
initial_checked=False,
54+
logger=logger,
55+
)
56+
self._std_dev_factor = create_spinbox_with_tooltip(
57+
"Std dev multiplier",
58+
"Choose which standard deviation to plot",
59+
connection_point,
60+
minimum=1,
61+
maximum=3,
62+
initial_value=1,
63+
logger=logger,
64+
)
65+
66+
self._statistics_options = create_collapsible_group_box(
67+
"Statistics options",
68+
create_group_layout(
69+
[
70+
*self._toggles.values(),
71+
self._area_toggle,
72+
create_labeled_row("Std dev multiplier", self._std_dev_factor),
73+
]
74+
),
75+
)
76+
77+
def apply_to(self, plot_config: PlotConfig) -> None:
78+
"""Enable or disable each statistic according to its checkbox."""
79+
plot_config.set_standard_deviation_factor(self._std_dev_factor.value())
80+
fill_area = self._area_toggle.isChecked()
81+
for statistic, checkbox in self._toggles.items():
82+
style = plot_config.get_statistics_style(statistic)
83+
if not checkbox.isChecked():
84+
style.line_style = ""
85+
style.marker = ""
86+
elif statistic in _BAND_STATISTICS:
87+
style.line_style = _AREA_LINE_STYLE if fill_area else _BAND_LINE_STYLE
88+
style.marker = ""
89+
elif not style.is_visible():
90+
style.line_style = _LINE_STYLE_WHEN_ENABLED[statistic]
91+
style.marker = ""
92+
plot_config.set_statistics_style(statistic, style)
93+
94+
def get_widget(self) -> QGroupBox:
95+
return self._statistics_options

0 commit comments

Comments
 (0)