From f3c90d0d95a92e517c4ff3d03bba5a423176396d Mon Sep 17 00:00:00 2001 From: Feda Curic Date: Fri, 22 May 2026 13:37:07 +0200 Subject: [PATCH] Allow hiding observations for field parameter plots Show observation controls for field plots with observation locations, while hiding unsupported observation and history controls for GEN_KW and surface keys. --- .../plot/customize/customization_view.py | 19 ++++ .../plot/customize/customize_plot_dialog.py | 5 + .../customize/default_customization_view.py | 14 +++ .../customize/style_customization_view.py | 11 +++ .../gui/tools/plot/plottery/plots/std_dev.py | 5 +- .../ui_tests/gui/test_plot_customization.py | 93 +++++++++++++++++++ .../gui/plottery/test_stddev_plot.py | 21 +++++ 7 files changed, 167 insertions(+), 1 deletion(-) diff --git a/src/ert/gui/tools/plot/customize/customization_view.py b/src/ert/gui/tools/plot/customize/customization_view.py index 5908873a572..d6b2158beb6 100644 --- a/src/ert/gui/tools/plot/customize/customization_view.py +++ b/src/ert/gui/tools/plot/customize/customization_view.py @@ -17,6 +17,7 @@ from .style_chooser import STYLESET_DEFAULT, StyleChooser if TYPE_CHECKING: + from ert.gui.tools.plot.plot_api import PlotApiKeyDefinition from ert.gui.tools.plot.plottery import PlotConfig @@ -27,6 +28,7 @@ def __init__(self) -> None: self._layout = QFormLayout() self.setLayout(self._layout) self._widgets: dict[str, QWidget] = {} + self._row_widgets: dict[str, QWidget] = {} def add_row(self, title: str, widget: QWidget) -> None: self._layout.addRow(title, widget) @@ -40,6 +42,7 @@ def add_line_edit( ) -> None: self[attribute_name] = ClearableLineEdit(placeholder=placeholder) self.add_row(title, self[attribute_name]) + self._row_widgets[attribute_name] = self[attribute_name] if tool_tip is not None: self[attribute_name].setToolTip(tool_tip) @@ -62,6 +65,7 @@ def add_check_box( ) -> None: self[attribute_name] = QCheckBox() self.add_row(title, self[attribute_name]) + self._row_widgets[attribute_name] = self[attribute_name] if tool_tip is not None: self[attribute_name].setToolTip(tool_tip) @@ -90,6 +94,7 @@ def add_integer_selection_box( sb_layout.addWidget(sb) sb_layout.addStretch() self.add_row(title, sb_layout) # type: ignore + self._row_widgets[attribute_name] = sb if tool_tip is not None: sb.setToolTip(tool_tip) @@ -117,6 +122,7 @@ def add_style_chooser( style_chooser = StyleChooser(line_style_set=line_style_set) self[attribute_name] = style_chooser self.add_row(title, self[attribute_name]) + self._row_widgets[attribute_name] = self[attribute_name] if tool_tip is not None: self[attribute_name].setToolTip(tool_tip) @@ -140,6 +146,16 @@ def update_property( def add_spacing(self, pixels: int = 10) -> None: self._layout.addItem(QSpacerItem(1, pixels)) + def set_row_visible(self, attribute_name: str, visible: bool) -> None: + widget = self._row_widgets.get(attribute_name) + if widget is None: + return + + label = self._layout.labelForField(widget) + if label is not None: + label.setVisible(visible) + widget.setVisible(visible) + def add_heading(self, title: str) -> None: self.add_spacing(10) self._layout.addRow(title, None) @@ -163,6 +179,9 @@ def revert_customization(self, plot_config: PlotConfig) -> None: "the revert_customization() function!" ) + def set_key_definition(self, key_def: PlotApiKeyDefinition) -> None: + pass + class WidgetProperty: def __get__(self, instance: Any, owner: Any) -> Any: diff --git a/src/ert/gui/tools/plot/customize/customize_plot_dialog.py b/src/ert/gui/tools/plot/customize/customize_plot_dialog.py index f044c7f9d16..1e5fe2dbde3 100644 --- a/src/ert/gui/tools/plot/customize/customize_plot_dialog.py +++ b/src/ert/gui/tools/plot/customize/customize_plot_dialog.py @@ -185,6 +185,7 @@ def switch_plot_config_history(self, key_def: PlotApiKeyDefinition) -> None: ) self._customization_dialog.add_copyable_key(key) self._customization_dialog.current_plot_key_changed(key) + self._customization_dialog.set_key_definition(key_def) self._previous_key = self._plot_config_key self._plot_config_key = key self._revert_customization(self.get_plot_config(), emit=False) @@ -320,6 +321,10 @@ def key_selected(self, list_widget_item: QListWidgetItem) -> None: def current_plot_key_changed(self, new_key: str | None) -> None: self.current_key = new_key + def set_key_definition(self, key_def: PlotApiKeyDefinition) -> None: + for customization_view in self: + customization_view.set_key_definition(key_def) + def log_fn(self, action: str) -> None: logger.info(f"Customization dialog action: {action}") diff --git a/src/ert/gui/tools/plot/customize/default_customization_view.py b/src/ert/gui/tools/plot/customize/default_customization_view.py index 5cde2ba1ff7..db537290b24 100644 --- a/src/ert/gui/tools/plot/customize/default_customization_view.py +++ b/src/ert/gui/tools/plot/customize/default_customization_view.py @@ -7,9 +7,14 @@ from .customization_view import CustomizationView, WidgetProperty if TYPE_CHECKING: + from ert.gui.tools.plot.plot_api import PlotApiKeyDefinition from ert.gui.tools.plot.plottery import PlotConfig +def _supports_observations(key_def: "PlotApiKeyDefinition") -> bool: + return key_def.observations or key_def.metadata.get("data_origin") == "field" + + def _label_msg(label: str) -> str: return ( f"Set to empty to use the default {label}.\n" @@ -92,3 +97,12 @@ def revert_customization(self, plot_config: "PlotConfig") -> None: if not self.is_everest: self.history = plot_config.is_history_enabled() self.observations = plot_config.is_observations_enabled() + + @override + def set_key_definition(self, key_def: "PlotApiKeyDefinition") -> None: + if self.is_everest: + return + + is_response = key_def.response is not None + self.set_row_visible("history", is_response) + self.set_row_visible("observations", _supports_observations(key_def)) diff --git a/src/ert/gui/tools/plot/customize/style_customization_view.py b/src/ert/gui/tools/plot/customize/style_customization_view.py index 79fc4f188d4..15f91c1c512 100644 --- a/src/ert/gui/tools/plot/customize/style_customization_view.py +++ b/src/ert/gui/tools/plot/customize/style_customization_view.py @@ -8,6 +8,7 @@ from .style_chooser import STYLESET_TOGGLE, StyleChooser if TYPE_CHECKING: + from ert.gui.tools.plot.plot_api import PlotApiKeyDefinition from ert.gui.tools.plot.plottery import PlotConfig @@ -58,7 +59,9 @@ def __init__(self) -> None: ) self._observations_color_box = self.create_color_box("observations_color") + self["observations_color"] = self._observations_color_box self.add_row("Observations color", self._observations_color_box) + self._row_widgets["observations_color"] = self._observations_color_box self.update_property( "observations_color", StyleCustomizationView.get_observations_color, @@ -107,3 +110,11 @@ def revert_customization(self, plot_config: "PlotConfig") -> None: self.observations_style = plot_config.observations_style() self.observations_color = plot_config.observations_color() self.color_cycle = plot_config.line_color_cycle() + + @override + def set_key_definition(self, key_def: "PlotApiKeyDefinition") -> None: + is_response = key_def.response is not None + + self.set_row_visible("history_style", is_response) + self.set_row_visible("observations_style", key_def.observations) + self.set_row_visible("observations_color", key_def.observations) diff --git a/src/ert/gui/tools/plot/plottery/plots/std_dev.py b/src/ert/gui/tools/plot/plottery/plots/std_dev.py index 68afe4c370d..ff284d34614 100644 --- a/src/ert/gui/tools/plot/plottery/plots/std_dev.py +++ b/src/ert/gui/tools/plot/plottery/plots/std_dev.py @@ -63,7 +63,10 @@ def plot( im = ax_heat.imshow(data, cmap="viridis", aspect="equal") - if obs_loc is not None: + if ( + obs_loc is not None + and plot_context.plotConfig().is_observations_enabled() + ): xs = obs_loc[:, 0] - 0.5 ys = obs_loc[:, 1] - 0.5 diff --git a/tests/ert/ui_tests/gui/test_plot_customization.py b/tests/ert/ui_tests/gui/test_plot_customization.py index b6ad9da274c..c8b88dd2bc7 100644 --- a/tests/ert/ui_tests/gui/test_plot_customization.py +++ b/tests/ert/ui_tests/gui/test_plot_customization.py @@ -1,9 +1,13 @@ import logging +import pytest + from ert.gui.tools.plot.customize.customize_plot_dialog import CustomizePlotDialog from ert.gui.tools.plot.customize.default_customization_view import ( DefaultCustomizationView, ) +from ert.gui.tools.plot.customize.style_customization_view import StyleCustomizationView +from ert.gui.tools.plot.plot_api import PlotApiKeyDefinition def test_that_first_tab_is_not_logged_when_opening_customize_plot_dialog(qtbot, caplog): @@ -26,3 +30,92 @@ def test_that_first_tab_is_not_logged_when_opening_customize_plot_dialog(qtbot, plot._tabs.setCurrentIndex(0) assert "Customization dialog action: General" in caplog.text assert len(caplog.records) == 2 + + +@pytest.mark.parametrize( + ( + "key_def", + "history_visible", + "observations_visible", + "observations_style_visible", + ), + [ + ( + PlotApiKeyDefinition( + key="MY_PARAM", + index_type=None, + observations=False, + dimensionality=1, + metadata={"data_origin": "gen_kw"}, + ), + False, + False, + False, + ), + ( + PlotApiKeyDefinition( + key="MY_FIELD", + index_type=None, + observations=False, + dimensionality=3, + metadata={"data_origin": "field"}, + ), + False, + True, + False, + ), + ( + PlotApiKeyDefinition( + key="MY_SURFACE", + index_type=None, + observations=False, + dimensionality=3, + metadata={"data_origin": "surface"}, + ), + False, + False, + False, + ), + ( + PlotApiKeyDefinition( + key="WWCT:OP_1", + index_type="VALUE", + observations=True, + dimensionality=2, + metadata={"data_origin": "summary"}, + response=object(), # type: ignore[arg-type] + ), + True, + True, + True, + ), + ], +) +def test_observation_and_history_settings_visibility( + qtbot, + key_def, + history_visible, + observations_visible, + observations_style_visible, +): + general_view = DefaultCustomizationView() + style_view = StyleCustomizationView() + qtbot.addWidget(general_view) + qtbot.addWidget(style_view) + + general_view.set_key_definition(key_def) + style_view.set_key_definition(key_def) + + assert general_view["history"].isVisibleTo(general_view) is history_visible + assert ( + general_view["observations"].isVisibleTo(general_view) is observations_visible + ) + assert style_view["history_style"].isVisibleTo(style_view) is history_visible + assert ( + style_view["observations_style"].isVisibleTo(style_view) + is observations_style_visible + ) + assert ( + style_view["observations_color"].isVisibleTo(style_view) + is observations_style_visible + ) diff --git a/tests/ert/unit_tests/gui/plottery/test_stddev_plot.py b/tests/ert/unit_tests/gui/plottery/test_stddev_plot.py index 1642081fccb..3a5c5d7e2c3 100644 --- a/tests/ert/unit_tests/gui/plottery/test_stddev_plot.py +++ b/tests/ert/unit_tests/gui/plottery/test_stddev_plot.py @@ -53,6 +53,27 @@ def test_stddev_plot_shows_boxplot(plot_context: PlotContext): ) +def test_stddev_plot_observation_locations_follow_observation_setting( + plot_context: PlotContext, +): + rng = np.random.default_rng() + figure = Figure() + std_dev_data = rng.random((5, 5)) + obs_loc = np.array([[1, 2], [3, 4]]) + plot_context.plotConfig.return_value.set_observations_enabled(False) + + StdDevPlot().plot( + figure, + plot_context, + {}, + {}, + {"id": std_dev_data}, + obs_loc, + ) + + assert len(figure.axes[0].collections) == 0 + + def test_that_stddev_plot_does_not_crash_and_returns_early_when_no_ensembles(): figure = Figure() context = Mock(spec=PlotContext)