Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/ert/gui/plotting/plot_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
BoxplotOptions,
EverestControlsPlotOptions,
GeneralPlotOptions,
StatisticsOptions,
)
from .widgets.plot_ensemble_selection_widget import EnsembleSelectionWidget
from .widgets.plot_widget import Plotter, PlotWidget
Expand Down Expand Up @@ -300,6 +301,7 @@ def __init__(
self._general_options.axisLabelEditRequested.connect(self._edit_axis_label)
self._general_options.titleEditRequested.connect(self._edit_title)
self._boxplot_options = BoxplotOptions(self.update_plot)
self._statistics_options = StatisticsOptions(self.update_plot)

right_container = QWidget()
right_layout = create_group_layout(
Expand All @@ -309,13 +311,15 @@ def __init__(
self._everest_controls_plot_options.get_widget(),
self._everest_controls_group,
self._boxplot_options.get_widget(),
self._statistics_options.get_widget(),
]
)
right_container.setLayout(right_layout)

self._everest_controls_group.setVisible(False)
self._everest_controls_plot_options.get_widget().setVisible(False)
self._boxplot_options.get_widget().setVisible(False)
self._statistics_options.get_widget().setVisible(False)
self._data_type_keys_widget.selectDefault()

splitter = QSplitter(Qt.Orientation.Horizontal)
Expand Down Expand Up @@ -383,6 +387,7 @@ def update_plot(self, layer: int | None = None) -> None:
self._boxplot_options.get_widget().setVisible(
plot_widget.name in {MISFITS, CROSS_ENSEMBLE_STATISTICS}
)
self._statistics_options.get_widget().setVisible(plot_widget.name == STATISTICS)
self._general_options.get_widget().setVisible(plot_widget.name != STD_DEV)

is_gradient_plot = plot_widget.name == EVEREST_GRADIENTS_PLOT
Expand Down Expand Up @@ -539,9 +544,7 @@ def fetch_data(

plot_config = PlotConfig(title=key_def.key)
if selected_tab == STATISTICS:
plot_config.set_statistics_styles_for_dimensionality(
key_def.dimensionality
)
self._statistics_options.apply_to(plot_config)
plot_config.set_title(self._titles.get(key_def.key, key_def.key))
plot_config.set_x_label(self._x_labels.get(key_def.key))
plot_config.set_y_label(self._y_labels.get(key_def.key))
Expand Down
45 changes: 19 additions & 26 deletions src/ert/gui/plotting/utils/plot_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

from .plot_color_palettes import TABLEAU_10_COLOR_CYCLE
from .plot_style import PlotStyle
from .statistics_style import (
BAND_AREA_STYLE,
STATISTICS,
)


class PlotConfig:
Expand Down Expand Up @@ -63,39 +67,28 @@ def __init__(
self._grid_enabled = True

self._statistics_style = {
"mean": PlotStyle("Mean", line_style=""),
"p50": PlotStyle("P50", line_style=""),
"min-max": PlotStyle("Min/Max", line_style=""),
"p10-p90": PlotStyle("P10-P90", line_style=""),
"p33-p67": PlotStyle("P33-P67", line_style=""),
"std": PlotStyle("Std dev", line_style=""),
statistic: PlotStyle(style.label, line_style="")
for statistic, style in STATISTICS.items()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this needed? could we not in line 85 just do:

for statistic, style, _ in n STATISTICS.items():

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's shorthand for:

self._statistics_style = {}

for statistic, style in STATISTICS.items():
    self._statistics_style[statistic] = PlotStyle(
        style.label,
        line_style="",
    )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant more, why do we need self._statistics_style = {}, could we not just use STATISTICS.items()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

statistics_style are the mutable style elements in the config file which is changed by the options.

STATISTICS.items() are just the defaults styles and the rules.

At least the line styles are changed with the band option. On/off switches as well.

}

self._std_dev_factor = 1 # sigma 1 is default std dev

self.flip_response_axis = False
self.flip_observation_axis = False

def set_statistics_styles_for_dimensionality(self, dimensionality: int) -> None:
mean_style = self._statistics_style["mean"]
p10p90_style = self._statistics_style["p10-p90"]
std_style = self._statistics_style["std"]

mean_style.line_style = ""
mean_style.marker = ""
p10p90_style.line_style = ""
p10p90_style.marker = ""
std_style.line_style = ""
std_style.marker = ""

if dimensionality == 2:
mean_style.line_style = "-"
p10p90_style.line_style = "--"
elif dimensionality == 1:
mean_style.line_style = "-"
mean_style.marker = "o"
std_style.line_style = "--"
std_style.marker = "D"
def set_statistics_options(
self,
enabled_statistics: set[str],
*,
fill_bands: bool,
) -> None:
for statistic, style in self._statistics_style.items():
if statistic not in enabled_statistics:
style.line_style = ""
elif fill_bands and STATISTICS[statistic].is_band:
style.line_style = BAND_AREA_STYLE
else:
style.line_style = STATISTICS[statistic].line_style

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the section I meant why we couldn't use STATISTICS.items() directly, but I see now it overwrites


def get_number_of_colors(self) -> int:
return len(self._line_color_cycle_colors)
Expand Down
32 changes: 32 additions & 0 deletions src/ert/gui/plotting/utils/qt_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from PyQt6.QtWidgets import (
QCheckBox,
QGroupBox,
QHBoxLayout,
QLabel,
QSpinBox,
QVBoxLayout,
QWidget,
)
Expand Down Expand Up @@ -57,3 +59,33 @@ def create_checkbox_with_tooltip(
checkbox.stateChanged.connect(connection_point)
log_plot_option_usage_once(checkbox.clicked, logger, name)
return checkbox


def create_spinbox_with_tooltip(
name: str,
tooltip: str,
connection_point: Callable[..., object],
*,
minimum: int,
maximum: int,
initial_value: int,
logger: Logger,
) -> QSpinBox:
spinbox = QSpinBox()
spinbox.setObjectName(f"{name.lower().replace(' ', '_')}_spinbox")
spinbox.setToolTip(tooltip)
spinbox.setRange(minimum, maximum)
spinbox.setValue(initial_value)
spinbox.valueChanged.connect(connection_point)
log_plot_option_usage_once(spinbox.valueChanged, logger, name)
return spinbox


def create_labeled_row(label: str, widget: QWidget) -> QWidget:
row = QWidget()
layout = QHBoxLayout(row)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(QLabel(label))
layout.addWidget(widget)
layout.addStretch()
return row
23 changes: 23 additions & 0 deletions src/ert/gui/plotting/utils/statistics_style.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations

from typing import NamedTuple


class StatisticStyle(NamedTuple):
label: str
line_style: str
is_band: bool


STATISTICS = {
"mean": StatisticStyle("Mean", "-", is_band=False),
"p50": StatisticStyle("P50", "--", is_band=False),
"std": StatisticStyle("Std dev", ":", is_band=True),
"min-max": StatisticStyle("Min/Max", ":", is_band=True),
"p10-p90": StatisticStyle("P10-P90", "--", is_band=True),
"p33-p67": StatisticStyle("P33-P67", "-.", is_band=True),
}

BAND_AREA_STYLE = "#"

DEFAULT_ENABLED_STATISTICS = {"mean", "p10-p90"}
2 changes: 2 additions & 0 deletions src/ert/gui/plotting/widgets/plot_controls/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
from .everest_controls_plot_options import EverestControlsPlotOptions
from .general_options import GeneralPlotOptions
from .plot_color_palette_selector import PlotColorPaletteSelector
from .statistics_options import StatisticsOptions

__all__ = [
"BoxplotOptions",
"CustomPaletteDialog",
"EverestControlsPlotOptions",
"GeneralPlotOptions",
"PlotColorPaletteSelector",
"StatisticsOptions",
]
86 changes: 86 additions & 0 deletions src/ert/gui/plotting/widgets/plot_controls/statistics_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from __future__ import annotations

import logging
from collections.abc import Callable
from typing import TYPE_CHECKING

from PyQt6.QtWidgets import QGroupBox

from ert.gui.plotting.utils.qt_creator import (
create_checkbox_with_tooltip,
create_group_box,
create_group_layout,
create_labeled_row,
create_spinbox_with_tooltip,
)
from ert.gui.plotting.utils.statistics_style import (
DEFAULT_ENABLED_STATISTICS,
STATISTICS,
)

if TYPE_CHECKING:
from ert.gui.plotting.utils import PlotConfig

logger = logging.getLogger(__name__)


class StatisticsOptions:
"""Owns the statistics selection, which persists across keys."""

def __init__(self, connection_point: Callable[..., object]) -> None:
self._toggles = {
statistic: create_checkbox_with_tooltip(
style.label,
f"Show or hide the {style.label} "
+ ("band" if style.is_band else "line"),
connection_point,
initial_checked=statistic in DEFAULT_ENABLED_STATISTICS,
logger=logger,
)
for statistic, style in STATISTICS.items()
}
self._area_toggle = create_checkbox_with_tooltip(
"Area",
"Draw the standard deviation, min/max and percentile ranges as a "
"filled area instead of a pair of lines",
connection_point,
initial_checked=False,
logger=logger,
)
self._std_dev_factor = create_spinbox_with_tooltip(
"Std dev multiplier",
"Choose the number of standard deviations to plot",
connection_point,
minimum=1,
maximum=3,
initial_value=1,
logger=logger,
)

self._statistics_options = create_group_box(
"Statistics options",
create_group_layout(
[
*self._toggles.values(),
self._area_toggle,
create_labeled_row("Std dev multiplier", self._std_dev_factor),
]
),
)

def apply_to(self, plot_config: PlotConfig) -> None:
plot_config.set_standard_deviation_factor(self._std_dev_factor.value())
plot_config.set_statistics_options(
self._enabled_statistics(),
fill_bands=self._area_toggle.isChecked(),
)

def _enabled_statistics(self) -> set[str]:
return {
statistic
for statistic, checkbox in self._toggles.items()
if checkbox.isChecked()
}

def get_widget(self) -> QGroupBox:
return self._statistics_options
49 changes: 26 additions & 23 deletions tests/ert/unit_tests/gui/plottery/test_plot_style.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import math

import pytest

from ert.gui.plotting.utils import PlotConfig, PlotStyle
from ert.gui.plotting.utils.plot_tools import ConditionalAxisFormatter
from ert.gui.plotting.utils.statistics_style import STATISTICS


def test_conditional_axis_formatter():
Expand Down Expand Up @@ -205,39 +208,39 @@ def test_plot_config():
) != copy_of_plot_config.get_statistics_style("std")


def test_that_two_dimensional_keys_draw_mean_and_p10_p90_as_lines():
def test_that_an_enabled_statistic_gets_its_configured_line_style():
plot_config = PlotConfig()

plot_config.set_statistics_options({"mean", "p10-p90"}, fill_bands=False)

assert plot_config.get_statistics_style("mean").line_style == "-"
assert plot_config.get_statistics_style("p10-p90").line_style == "--"


def test_that_a_deselected_statistic_has_neither_line_style_nor_marker():
plot_config = PlotConfig()
plot_config.set_statistics_styles_for_dimensionality(1)

plot_config.set_statistics_styles_for_dimensionality(2)
plot_config.set_statistics_options(set(), fill_bands=False)

mean_style = plot_config.get_statistics_style("mean")
assert mean_style.line_style == "-"
assert not mean_style.line_style
assert not mean_style.marker
assert not mean_style.is_visible()

p10p90_style = plot_config.get_statistics_style("p10-p90")
assert p10p90_style.line_style == "--"
assert not p10p90_style.marker

std_style = plot_config.get_statistics_style("std")
assert not std_style.line_style
assert not std_style.marker
def test_that_filling_bands_draws_selected_band_statistics_as_areas():
plot_config = PlotConfig()

plot_config.set_statistics_options({"mean", "p33-p67"}, fill_bands=True)

def test_that_one_dimensional_keys_draw_mean_and_std_with_markers():
plot_config = PlotConfig()
plot_config.set_statistics_styles_for_dimensionality(2)
assert plot_config.get_statistics_style("p33-p67").line_style == "#"
assert plot_config.get_statistics_style("mean").line_style == "-"

plot_config.set_statistics_styles_for_dimensionality(1)

mean_style = plot_config.get_statistics_style("mean")
assert mean_style.line_style == "-"
assert mean_style.marker == "o"
@pytest.mark.parametrize("statistic", sorted(STATISTICS))
def test_that_every_statistic_is_visible_when_enabled(statistic):
plot_config = PlotConfig()

std_style = plot_config.get_statistics_style("std")
assert std_style.line_style == "--"
assert std_style.marker == "D"
plot_config.set_statistics_options({statistic}, fill_bands=False)

p10p90_style = plot_config.get_statistics_style("p10-p90")
assert not p10p90_style.line_style
assert not p10p90_style.marker
assert plot_config.get_statistics_style(statistic).is_visible()
Loading
Loading