Skip to content

Commit cff9218

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.
1 parent 91d37b4 commit cff9218

9 files changed

Lines changed: 505 additions & 33 deletions

File tree

src/ert/gui/plotting/plot_window.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@
6262
from .widgets.everest_control_selection_widget import EverestControlSelectionWidget
6363
from .widgets.plot_controls import (
6464
BoxplotOptions,
65+
DistributionOptions,
6566
EverestControlsPlotOptions,
6667
GeneralPlotOptions,
68+
StatisticsOptions,
6769
)
6870
from .widgets.plot_ensemble_selection_widget import EnsembleSelectionWidget
6971
from .widgets.plot_widget import Plotter, PlotWidget
@@ -300,6 +302,8 @@ def __init__(
300302
self._general_options.axisLabelEditRequested.connect(self._edit_axis_label)
301303
self._general_options.titleEditRequested.connect(self._edit_title)
302304
self._boxplot_options = BoxplotOptions(self.update_plot)
305+
self._statistics_options = StatisticsOptions(self.update_plot)
306+
self._distribution_options = DistributionOptions(self.update_plot)
303307

304308
right_container = QWidget()
305309
right_layout = create_group_layout(
@@ -309,13 +313,17 @@ def __init__(
309313
self._everest_controls_plot_options.get_widget(),
310314
self._everest_controls_group,
311315
self._boxplot_options.get_widget(),
316+
self._statistics_options.get_widget(),
317+
self._distribution_options.get_widget(),
312318
]
313319
)
314320
right_container.setLayout(right_layout)
315321

316322
self._everest_controls_group.setVisible(False)
317323
self._everest_controls_plot_options.get_widget().setVisible(False)
318324
self._boxplot_options.get_widget().setVisible(False)
325+
self._statistics_options.get_widget().setVisible(False)
326+
self._distribution_options.get_widget().setVisible(False)
319327
self._data_type_keys_widget.selectDefault()
320328

321329
splitter = QSplitter(Qt.Orientation.Horizontal)
@@ -383,6 +391,10 @@ def update_plot(self, layer: int | None = None) -> None:
383391
self._boxplot_options.get_widget().setVisible(
384392
plot_widget.name in {MISFITS, CROSS_ENSEMBLE_STATISTICS}
385393
)
394+
self._statistics_options.get_widget().setVisible(plot_widget.name == STATISTICS)
395+
self._distribution_options.get_widget().setVisible(
396+
plot_widget.name == DISTRIBUTION
397+
)
386398
self._general_options.get_widget().setVisible(plot_widget.name != STD_DEV)
387399

388400
is_gradient_plot = plot_widget.name == EVEREST_GRADIENTS_PLOT
@@ -538,13 +550,12 @@ def fetch_data(
538550
handle_exception(e)
539551

540552
plot_config = PlotConfig(title=key_def.key)
541-
if selected_tab == STATISTICS:
542-
plot_config.set_statistics_styles_for_dimensionality(
543-
key_def.dimensionality
544-
)
545553
plot_config.set_title(self._titles.get(key_def.key, key_def.key))
546554
plot_config.set_x_label(self._x_labels.get(key_def.key))
547555
plot_config.set_y_label(self._y_labels.get(key_def.key))
556+
self._statistics_options.set_dimensionality(key_def.dimensionality)
557+
self._statistics_options.apply_to(plot_config)
558+
self._distribution_options.apply_to(plot_config)
548559
plot_config.set_legend_enabled(self._general_options.legend_checkbox_state)
549560
plot_config.set_grid_enabled(self._general_options.grid_checkbox_state)
550561
plot_config.set_line_color_cycle(self._general_options.get_color_cycle())

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

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55

66
from .plot_color_palettes import TABLEAU_10_COLOR_CYCLE
77
from .plot_style import PlotStyle
8+
from .statistics_style import (
9+
BAND_AREA_STYLE,
10+
BAND_STATISTICS,
11+
STATISTIC_LABELS,
12+
STATISTICS_STYLE_PRESETS,
13+
)
814

915

1016
class PlotConfig:
@@ -63,12 +69,8 @@ def __init__(
6369
self._grid_enabled = True
6470

6571
self._statistics_style = {
66-
"mean": PlotStyle("Mean", line_style=""),
67-
"p50": PlotStyle("P50", line_style=""),
68-
"min-max": PlotStyle("Min/Max", line_style=""),
69-
"p10-p90": PlotStyle("P10-P90", line_style=""),
70-
"p33-p67": PlotStyle("P33-P67", line_style=""),
71-
"std": PlotStyle("Std dev", line_style=""),
72+
statistic: PlotStyle(label, line_style="")
73+
for statistic, label in STATISTIC_LABELS.items()
7274
}
7375

7476
self._std_dev_factor = 1 # sigma 1 is default std dev
@@ -77,25 +79,27 @@ def __init__(
7779
self.flip_observation_axis = False
7880

7981
def set_statistics_styles_for_dimensionality(self, dimensionality: int) -> None:
80-
mean_style = self._statistics_style["mean"]
81-
p10p90_style = self._statistics_style["p10-p90"]
82-
std_style = self._statistics_style["std"]
83-
84-
mean_style.line_style = ""
85-
mean_style.marker = ""
86-
p10p90_style.line_style = ""
87-
p10p90_style.marker = ""
88-
std_style.line_style = ""
89-
std_style.marker = ""
90-
91-
if dimensionality == 2:
92-
mean_style.line_style = "-"
93-
p10p90_style.line_style = "--"
94-
elif dimensionality == 1:
95-
mean_style.line_style = "-"
96-
mean_style.marker = "o"
97-
std_style.line_style = "--"
98-
std_style.marker = "D"
82+
preset = STATISTICS_STYLE_PRESETS.get(dimensionality, {})
83+
for statistic, style in self._statistics_style.items():
84+
style.line_style, style.marker = preset.get(statistic, ("", ""))
85+
86+
def set_statistics_options(
87+
self,
88+
dimensionality: int,
89+
enabled_statistics: set[str],
90+
*,
91+
fill_bands: bool,
92+
) -> None:
93+
"""Apply the user's selections on top of the dimensionality styles."""
94+
self.set_statistics_styles_for_dimensionality(dimensionality)
95+
96+
for statistic, style in self._statistics_style.items():
97+
if statistic not in enabled_statistics:
98+
style.line_style = ""
99+
style.marker = ""
100+
elif fill_bands and dimensionality == 2 and statistic in BAND_STATISTICS:
101+
style.line_style = BAND_AREA_STYLE
102+
style.marker = ""
99103

100104
def get_number_of_colors(self) -> int:
101105
return len(self._line_color_cycle_colors)

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

Lines changed: 71 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,44 @@ 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, name: str, *, expanded: bool = True
50+
) -> QGroupBox:
51+
"""Create a group box whose contents can be folded away by its header.
52+
53+
Collapsing only hides the contents; the widgets keep their values.
54+
"""
55+
content = QWidget()
56+
content.setObjectName(f"{name}_content")
57+
content.setLayout(layout)
58+
content.setVisible(expanded)
59+
60+
header = QToolButton()
61+
header.setObjectName(f"{name}_header")
62+
header.setText(title)
63+
header.setCheckable(True)
64+
header.setChecked(expanded)
65+
header.setAutoRaise(True)
66+
header.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
67+
header.setArrowType(Qt.ArrowType.DownArrow if expanded else Qt.ArrowType.RightArrow)
68+
header.setStyleSheet("QToolButton { border: none; font-style: italic; }")
69+
70+
def toggle_contents(checked: bool) -> None:
71+
header.setArrowType(
72+
Qt.ArrowType.DownArrow if checked else Qt.ArrowType.RightArrow
73+
)
74+
content.setVisible(checked)
75+
76+
header.toggled.connect(toggle_contents)
77+
78+
outer_layout = QVBoxLayout()
79+
outer_layout.setContentsMargins(0, 0, 0, 0)
80+
outer_layout.addWidget(header)
81+
outer_layout.addWidget(content)
82+
83+
return create_group_box("", outer_layout)
84+
85+
4586
def create_checkbox_with_tooltip(
4687
name: str,
4788
tooltip: str,
@@ -57,3 +98,33 @@ def create_checkbox_with_tooltip(
5798
checkbox.stateChanged.connect(connection_point)
5899
log_plot_option_usage_once(checkbox.clicked, logger, name)
59100
return checkbox
101+
102+
103+
def create_spinbox_with_tooltip(
104+
name: str,
105+
tooltip: str,
106+
connection_point: Callable[..., object],
107+
*,
108+
minimum: int,
109+
maximum: int,
110+
initial_value: int,
111+
logger: Logger,
112+
) -> QSpinBox:
113+
spinbox = QSpinBox()
114+
spinbox.setObjectName(f"{name.lower().replace(' ', '_')}_spinbox")
115+
spinbox.setToolTip(tooltip)
116+
spinbox.setRange(minimum, maximum)
117+
spinbox.setValue(initial_value)
118+
spinbox.valueChanged.connect(connection_point)
119+
log_plot_option_usage_once(spinbox.valueChanged, logger, name)
120+
return spinbox
121+
122+
123+
def create_labeled_row(label: str, widget: QWidget) -> QWidget:
124+
row = QWidget()
125+
layout = QHBoxLayout(row)
126+
layout.setContentsMargins(0, 0, 0, 0)
127+
layout.addWidget(QLabel(label))
128+
layout.addWidget(widget)
129+
layout.addStretch()
130+
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

0 commit comments

Comments
 (0)