diff --git a/src/ert/gui/plotting/customization_dialog/__init__.py b/src/ert/gui/plotting/customization_dialog/__init__.py deleted file mode 100644 index a3cb47316e8..00000000000 --- a/src/ert/gui/plotting/customization_dialog/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .customization_view import CustomizationView -from .customize_plot_dialog import PlotCustomizer - -__all__ = ["CustomizationView", "PlotCustomizer"] diff --git a/src/ert/gui/plotting/customization_dialog/customization_view.py b/src/ert/gui/plotting/customization_dialog/customization_view.py deleted file mode 100644 index a4cb954e12f..00000000000 --- a/src/ert/gui/plotting/customization_dialog/customization_view.py +++ /dev/null @@ -1,172 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable -from typing import TYPE_CHECKING, Any - -from PyQt6.QtWidgets import ( - QCheckBox, - QFormLayout, - QHBoxLayout, - QSpacerItem, - QSpinBox, - QWidget, -) - -from ert.gui.plotting.widgets import ClearableLineEdit - -from .style_chooser import STYLESET_DEFAULT, StyleChooser - -if TYPE_CHECKING: - from ert.gui.plotting.utils import PlotConfig - - -class CustomizationView(QWidget): - def __init__(self) -> None: - QWidget.__init__(self) - - self._layout = QFormLayout() - self.setLayout(self._layout) - self._widgets: dict[str, QWidget] = {} - - def add_row(self, title: str, widget: QWidget) -> None: - self._layout.addRow(title, widget) - - def add_line_edit( - self, - attribute_name: str, - title: str, - tool_tip: str | None = None, - placeholder: str = "", - ) -> None: - self[attribute_name] = ClearableLineEdit(placeholder=placeholder) - self.add_row(title, self[attribute_name]) - - if tool_tip is not None: - self[attribute_name].setToolTip(tool_tip) - - def getter(self: Any) -> str | None: - value: str | None = str(self[attribute_name].text()) - if not value: - value = None - return value - - def setter(self: Any, value: str | None) -> None: - if value is None: - value = "" - self[attribute_name].setText(str(value)) - - self.update_property(attribute_name, getter, setter) - - def add_check_box( - self, attribute_name: str, title: str, tool_tip: str | None = None - ) -> None: - self[attribute_name] = QCheckBox() - self.add_row(title, self[attribute_name]) - - if tool_tip is not None: - self[attribute_name].setToolTip(tool_tip) - - def getter(self: Any) -> bool: - return self[attribute_name].isChecked() - - def setter(self: Any, value: bool) -> None: - self[attribute_name].setChecked(value) - - self.update_property(attribute_name, getter, setter) - - def add_integer_selection_box( - self, - attribute_name: str, - title: str, - tool_tip: str | None = None, - min_value: int = 1, - max_value: int = 10, - single_step: int = 1, - ) -> QSpinBox: - sb = QSpinBox() - self[attribute_name] = sb - sb.setMaximumHeight(25) - sb_layout = QHBoxLayout() - sb_layout.addWidget(sb) - sb_layout.addStretch() - self.add_row(title, sb_layout) # type: ignore - - if tool_tip is not None: - sb.setToolTip(tool_tip) - - sb.setMinimum(min_value) - sb.setMaximum(max_value) - sb.setSingleStep(single_step) - - def getter(self: Any) -> QWidget: - return self[attribute_name].value() - - def setter(self: Any, value: QWidget) -> None: - self[attribute_name].setValue(value) - - self.update_property(attribute_name, getter, setter) - return sb - - def add_style_chooser( - self, - attribute_name: str, - title: str, - tool_tip: str | None = None, - line_style_set: str = STYLESET_DEFAULT, - ) -> None: - style_chooser = StyleChooser(line_style_set=line_style_set) - self[attribute_name] = style_chooser - self.add_row(title, self[attribute_name]) - - if tool_tip is not None: - self[attribute_name].setToolTip(tool_tip) - - def getter(self: Any) -> QWidget: - return self[attribute_name].get_style() - - def setter(self: Any, style: QWidget) -> None: - self[attribute_name].setStyle(style) - - self.update_property(attribute_name, getter, setter) - - def update_property( - self, - attribute_name: str, - getter: Callable[[Any], Any], - setter: Callable[[Any, Any], None], - ) -> None: - setattr(self.__class__, attribute_name, property(getter, setter)) - - def add_spacing(self, pixels: int = 10) -> None: - self._layout.addItem(QSpacerItem(1, pixels)) - - def add_heading(self, title: str) -> None: - self.add_spacing(10) - self._layout.addRow(title, None) - self.add_spacing(1) - - def __getitem__(self, item: str) -> QWidget: - return self._widgets[item] - - def __setitem__(self, key: str, value: QWidget) -> None: - self._widgets[key] = value - - def apply_customization(self, plot_config: PlotConfig) -> None: - raise NotImplementedError( - f"Class '{self.__class__.__name__}' has not implemented " - "the apply_customization() function!" - ) - - def revert_customization(self, plot_config: PlotConfig) -> None: - raise NotImplementedError( - f"Class '{self.__class__.__name__}' has not implemented " - "the revert_customization() function!" - ) - - -class WidgetProperty: - def __get__(self, instance: Any, owner: Any) -> Any: - raise UserWarning("Property is invalid!") - - def __set__(self, instance: Any, value: Any) -> Any: - raise UserWarning("Property is invalid!") diff --git a/src/ert/gui/plotting/customization_dialog/customize_plot_dialog.py b/src/ert/gui/plotting/customization_dialog/customize_plot_dialog.py deleted file mode 100644 index 3cce8ae9495..00000000000 --- a/src/ert/gui/plotting/customization_dialog/customize_plot_dialog.py +++ /dev/null @@ -1,350 +0,0 @@ -from __future__ import annotations - -import logging -from collections.abc import Iterable, Iterator -from typing import TYPE_CHECKING, override - -from PyQt6.QtCore import QObject, QSignalBlocker, Qt -from PyQt6.QtCore import pyqtSignal as Signal -from PyQt6.QtCore import pyqtSlot as Slot -from PyQt6.QtGui import QKeyEvent -from PyQt6.QtWidgets import ( - QDialog, - QHBoxLayout, - QLayout, - QListWidget, - QListWidgetItem, - QMenu, - QPushButton, - QTabWidget, - QToolButton, - QVBoxLayout, - QWidget, - QWidgetAction, -) - -from ert.gui.icon_utils import load_icon -from ert.gui.plotting.plot_api import PlotApiKeyDefinition -from ert.gui.plotting.utils import PlotConfig, PlotConfigFactory, PlotConfigHistory -from ert.gui.plotting.widgets import CopyStyleToDialog -from ert.gui.utils import is_everest_application - -from .statistics_customization_view import StatisticsCustomizationView -from .style_customization_view import StyleCustomizationView - -if TYPE_CHECKING: - from .customization_view import CustomizationView - -logger = logging.getLogger(__name__) - -DEFAULT_PLOTCONFIG_KEYNAME = "__default_key__" - - -class PlotCustomizer(QObject): - settingsChanged = Signal() - - def __init__( - self, parent: QWidget | None, key_defs: list[PlotApiKeyDefinition] - ) -> None: - super().__init__() - self._is_everest = is_everest_application() - self._plot_config_key = None - self._previous_key = None - self._plot_configs: dict[str | None, PlotConfigHistory] = { - None: PlotConfigHistory( - DEFAULT_PLOTCONFIG_KEYNAME, PlotConfig(plot_settings=None, title=None) - ) - } - - self._customization_dialog = CustomizePlotDialog( - "Customize", parent, key_defs, key=self._plot_config_key - ) - - self._customization_dialog.add_tab("style", "Style", StyleCustomizationView()) - if not self._is_everest: - self._customization_dialog.add_tab( - "statistics", "Statistics", StatisticsCustomizationView() - ) - - self._customization_dialog.applySettings.connect(self.apply_customization) - self._customization_dialog.undoSettings.connect(self.undo_customization) - self._customization_dialog.redoSettings.connect(self.redo_customization) - self._customization_dialog.resetSettings.connect(self.reset_customization) - self._customization_dialog.copySettings.connect(self.copy_customization) - self._customization_dialog.copySettingsToOthers.connect( - self.copy_customization_to - ) - self._revert_customization(self.get_plot_config()) - - def _get_plot_config_history(self) -> PlotConfigHistory: - return self._plot_configs[self._plot_config_key] - - def undo_customization(self) -> None: - history = self._get_plot_config_history() - history.undo_changes() - self._revert_customization(history.get_plot_config()) - - def redo_customization(self) -> None: - history = self._get_plot_config_history() - history.redo_changes() - self._revert_customization(history.get_plot_config()) - - def reset_customization(self) -> None: - history = self._get_plot_config_history() - history.reset_changes() - self._revert_customization(history.get_plot_config()) - - def apply_customization(self) -> None: - history = self._get_plot_config_history() - plot_config = history.get_plot_config() - if self._customization_dialog is not None: - for customization_view in self._customization_dialog: - customization_view.apply_customization(plot_config) - - self.update_plot_config(plot_config) - - def update_plot_config(self, plot_config: PlotConfig) -> None: - history = self._get_plot_config_history() - history.apply_changes(plot_config) - self._emit_changed_signal() - - def _revert_customization( - self, plot_config: PlotConfig, *, emit: bool = True - ) -> None: - if self._customization_dialog is not None: - for customization_view in self._customization_dialog: - customization_view.revert_customization(plot_config) - - self._emit_changed_signal(emit=emit) - - def _emit_changed_signal(self, *, emit: bool = True) -> None: - history = self._get_plot_config_history() - self._customization_dialog.set_undo_redo_copy_state( - undo=history.is_undo_possible(), - redo=history.is_redo_possible(), - copy=self.is_copy_possible(), - ) - - if emit: - self.settingsChanged.emit() - - def is_copy_possible(self) -> bool: - return len(self._plot_configs) > 2 - - def copy_customization_to(self, keys: Iterable[str]) -> None: - """copies the plotconfig of the current key, to a set of other keys""" - history = self._get_plot_config_history() - - for key in keys: - if key not in self._plot_configs: - self._plot_configs[key] = PlotConfigHistory( - DEFAULT_PLOTCONFIG_KEYNAME, - PlotConfig(None, title=None), - ) - source_config = history.get_plot_config() - source_config.set_title(key) - - self._plot_configs[key].apply_changes(source_config) - - self._customization_dialog.add_copyable_key(key) - - self._emit_changed_signal(emit=True) - - def copy_customization(self, key: str | None) -> None: - key = str(key) - if self.is_copy_possible(): - source_config = self._plot_configs[key].get_plot_config() - source_config.set_title(None) - - history = self._get_plot_config_history() - history.apply_changes(source_config) - - self._revert_customization(history.get_plot_config()) - - def toggle_customization_dialog(self) -> None: - if self._customization_dialog.isVisible(): - self._customization_dialog.hide() - else: - self._customization_dialog.show() - - def switch_plot_config_history(self, key_def: PlotApiKeyDefinition) -> None: - if key_def is None: - return - key = key_def.key - if key != self._plot_config_key: - if key not in self._plot_configs: - self._plot_configs[key] = PlotConfigHistory( - key, PlotConfigFactory.create_plot_config_for_key(key_def) - ) - self._customization_dialog.add_copyable_key(key) - self._customization_dialog.current_plot_key_changed(key) - self._previous_key = self._plot_config_key - self._plot_config_key = key - self._revert_customization(self.get_plot_config(), emit=False) - - def get_plot_config(self) -> PlotConfig: - return self._get_plot_config_history().get_plot_config() - - -class CustomizePlotDialog(QDialog): - applySettings = Signal() - undoSettings = Signal() - redoSettings = Signal() - resetSettings = Signal() - copySettings = Signal(str) - copySettingsToOthers = Signal(list) - tabChanged = Signal(int) - - def __init__( - self, - title: str | None, - parent: QWidget | None, - key_defs: list[PlotApiKeyDefinition], - key: str | None = "", - ) -> None: - QDialog.__init__(self, parent) - if title is not None: - self.setWindowTitle(title) - - self.current_key = key - self._key_defs = key_defs - - self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False) - self.setWindowFlag(Qt.WindowType.WindowCloseButtonHint, False) - - self._tab_map: dict[str, CustomizationView] = {} - self._tab_order: list[str] = [] - - layout = QVBoxLayout() - - self._tabs = QTabWidget() - self._tabs.currentChanged.connect(self.log_tabs) - layout.addWidget(self._tabs) - layout.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize) - - self._button_layout = QHBoxLayout() - - self._reset_button = QToolButton() - self._reset_button.setIcon(load_icon("format_color_reset.svg")) - self._reset_button.setToolTip("Reset all settings back to default") - self._reset_button.clicked.connect(self.resetSettings) - self._reset_button.clicked.connect(lambda: self.log_fn("Reset")) - - self._undo_button = QToolButton() - self._undo_button.setIcon(load_icon("undo.svg")) - self._undo_button.setToolTip("Undo") - self._undo_button.clicked.connect(self.undoSettings) - self._undo_button.clicked.connect(lambda: self.log_fn("Undo")) - - self._redo_button = QToolButton() - self._redo_button.setIcon(load_icon("redo.svg")) - self._redo_button.setToolTip("Redo") - self._redo_button.clicked.connect(self.redoSettings) - self._redo_button.clicked.connect(lambda: self.log_fn("Redo")) - self._redo_button.setEnabled(False) - - self._copy_from_button = QToolButton() - self._copy_from_button.setIcon(load_icon("download.svg")) - self._copy_from_button.setToolTip("Copy settings from another key") - self._copy_from_button.setPopupMode( - QToolButton.ToolButtonPopupMode.InstantPopup - ) - self._copy_from_button.setEnabled(False) - - self._copy_to_button = QToolButton() - self._copy_to_button.setIcon(load_icon("upload.svg")) - self._copy_to_button.setToolTip("Copy current plot settings to other keys") - self._copy_to_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) - self._copy_to_button.clicked.connect(self.initiate_copy_style_to_dialog) - self._copy_to_button.clicked.connect(lambda: self.log_fn("Copy to")) - self._copy_to_button.setEnabled(True) - - tool_menu = QMenu(self._copy_from_button) - self._popup_list = QListWidget(tool_menu) - self._popup_list.setSortingEnabled(True) - self._popup_list.itemClicked.connect(self.key_selected) - action = QWidgetAction(tool_menu) - action.setDefaultWidget(self._popup_list) - tool_menu.addAction(action) - self._copy_from_button.setMenu(tool_menu) - tool_menu.aboutToShow.connect(lambda: self.log_fn("Copy from")) - - self._apply_button = QPushButton("Apply") - self._apply_button.setToolTip("Apply the new settings") - self._apply_button.clicked.connect(self.applySettings) - self._apply_button.setDefault(True) - self._apply_button.clicked.connect(lambda: self.log_fn("Apply")) - - self._close_button = QPushButton("Close") - self._close_button.setToolTip("Hide this dialog") - self._close_button.clicked.connect(self.hide) - - self._button_layout.addWidget(self._reset_button) - self._button_layout.addStretch() - self._button_layout.addWidget(self._undo_button) - self._button_layout.addWidget(self._redo_button) - self._button_layout.addWidget(self._copy_from_button) - self._button_layout.addWidget(self._copy_to_button) - self._button_layout.addStretch() - self._button_layout.addWidget(self._apply_button) - self._button_layout.addWidget(self._close_button) - - layout.addStretch() - layout.addLayout(self._button_layout) - - self.setLayout(layout) - - def initiate_copy_style_to_dialog(self) -> None: - dialog = CopyStyleToDialog(self, self.current_key, self._key_defs) - if dialog.exec(): - self.copySettingsToOthers.emit(dialog.getSelectedKeys()) - - def add_copyable_key(self, key: str) -> None: - if not self._popup_list.findItems(key, Qt.MatchFlag.MatchExactly): - self._popup_list.addItem(key) - - def key_selected(self, list_widget_item: QListWidgetItem) -> None: - self.copySettings.emit(str(list_widget_item.text())) - - def current_plot_key_changed(self, new_key: str | None) -> None: - self.current_key = new_key - - def log_fn(self, action: str) -> None: - logger.info(f"Customization dialog action: {action}") - - @override - def keyPressEvent(self, a0: QKeyEvent | None) -> None: - # Hide when pressing Escape instead of QDialog.keyPressEvent(KeyEscape) - # which closes the dialog - if a0 and a0.key() == Qt.Key.Key_Escape: - self.hide() - else: - QDialog.keyPressEvent(self, a0) - - def add_tab( - self, attribute_name: str, title: str, widget: CustomizationView - ) -> None: - with QSignalBlocker(self._tabs): - self._tabs.addTab(widget, title) - self._tab_map[attribute_name] = widget - self._tab_order.append(attribute_name) - - def __getitem__(self, item: str) -> CustomizationView: - return self._tab_map[item] - - def __iter__(self) -> Iterator[CustomizationView]: - for attribute_name in self._tab_order: - yield self._tab_map[attribute_name] - - def set_undo_redo_copy_state( - self, *, undo: bool, redo: bool, copy: bool = False - ) -> None: - self._undo_button.setEnabled(undo) - self._redo_button.setEnabled(redo) - self._copy_from_button.setEnabled(copy) - - @Slot(int) - def log_tabs(self, index: int) -> None: - tab_title = self._tabs.tabText(index) - - self.log_fn(tab_title) diff --git a/src/ert/gui/plotting/customization_dialog/statistics_customization_view.py b/src/ert/gui/plotting/customization_dialog/statistics_customization_view.py deleted file mode 100644 index edad15c6881..00000000000 --- a/src/ert/gui/plotting/customization_dialog/statistics_customization_view.py +++ /dev/null @@ -1,157 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, cast, override - -from PyQt6.QtWidgets import QComboBox, QHBoxLayout - -from .customization_view import CustomizationView, WidgetProperty -from .style_chooser import STYLESET_AREA, StyleChooser - -if TYPE_CHECKING: - from ert.gui.plotting.utils import PlotConfig - - -class StatisticsCustomizationView(CustomizationView): - mean_style = WidgetProperty() - p50_style = WidgetProperty() - std_style = WidgetProperty() - min_max_style = WidgetProperty() - p10_p90_style = WidgetProperty() - p33_p67_style = WidgetProperty() - std_dev_factor = WidgetProperty() - distribution_lines = WidgetProperty() - - def __init__(self) -> None: - CustomizationView.__init__(self) - - self._presets = [ - "Statistics default", - "Cross ensemble statistics default", - "Overview", - "All statistics", - ] - - self.add_row("Presets", self.create_presets()) - self.add_spacing(10) - layout = QHBoxLayout() - self.add_row("", layout) # type: ignore - self.add_style_chooser( - "mean_style", "Mean", "Line and marker style for the mean line." - ) - self.add_style_chooser( - "p50_style", "P50", "Line and marker style for the P50 line." - ) - self.add_style_chooser( - "std_style", - "Std dev", - "Line and marker style for the unbiased standard deviation lines.", - line_style_set=STYLESET_AREA, - ) - self.add_style_chooser( - "min_max_style", - "Min/max", - "Line and marker style for the min/max lines.", - line_style_set=STYLESET_AREA, - ) - self.add_style_chooser( - "p10_p90_style", - "P10-P90", - "Line and marker style for the P10-P90 lines.", - line_style_set=STYLESET_AREA, - ) - self.add_style_chooser( - "p33_p67_style", - "P33-P67", - "Line and marker style for the P33-P67 lines.", - line_style_set=STYLESET_AREA, - ) - self.add_spacing() - - self.add_integer_selection_box( - "std_dev_factor", - "Std dev multiplier", - "Choose which standard deviation to plot", - max_value=3, - ) - - self.add_check_box( - "distribution_lines", - "Connection lines", - "Toggle distribution connection lines visibility.", - ) - - style = cast(StyleChooser, self["mean_style"]) - style.create_label_layout(layout) - - def create_presets(self) -> QComboBox: - preset_combo = QComboBox() - for preset in self._presets: - preset_combo.addItem(preset) - - preset_combo.currentIndexChanged.connect(self.preset_selected) - return preset_combo - - def preset_selected(self, index: int) -> None: - if index == 0: # Default - self.update_style("mean_style", "-", None) - self.update_style("p50_style", None, None) - self.update_style("std_style", None, None) - self.update_style("min_max_style", None, None) - self.update_style("p10_p90_style", "--", None) - self.update_style("p33_p67_style", None, None) - elif index == 1: # CCS Default - self.update_style("mean_style", "-", "o") - self.update_style("p50_style", None, None) - self.update_style("std_style", "--", "D") - self.update_style("min_max_style", None, None) - self.update_style("p10_p90_style", None, None) - self.update_style("p33_p67_style", None, None) - elif index == 2: # Overview - self.update_style("mean_style", None, None) - self.update_style("p50_style", None, None) - self.update_style("std_style", None, None) - self.update_style("min_max_style", "#", None) - self.update_style("p10_p90_style", None, None) - self.update_style("p33_p67_style", None, None) - elif index == 3: # All statistics - self.update_style("mean_style", "-", None) - self.update_style("p50_style", "--", "x") - self.update_style("std_style", ":", None) - self.update_style("min_max_style", "--", None) - self.update_style("p10_p90_style", "#", None) - self.update_style("p33_p67_style", "#", None) - - def update_style( - self, - attribute_name: str, - line_style: str | None, - marker_style: str | None, - ) -> None: - style = getattr(self, attribute_name) - style.line_style = line_style - style.marker = marker_style - setattr(self, attribute_name, style) - - @override - def apply_customization(self, plot_config: PlotConfig) -> None: - plot_config.set_statistics_style("mean", self.mean_style) - plot_config.set_statistics_style("p50", self.p50_style) - plot_config.set_statistics_style("std", self.std_style) - plot_config.set_statistics_style("min-max", self.min_max_style) - plot_config.set_statistics_style("p10-p90", self.p10_p90_style) - plot_config.set_statistics_style("p33-p67", self.p33_p67_style) - - plot_config.set_standard_deviation_factor(self.std_dev_factor) - plot_config.set_distribution_line_enabled(self.distribution_lines) - - @override - def revert_customization(self, plot_config: PlotConfig) -> None: - self.mean_style = plot_config.get_statistics_style("mean") - self.p50_style = plot_config.get_statistics_style("p50") - self.std_style = plot_config.get_statistics_style("std") - self.min_max_style = plot_config.get_statistics_style("min-max") - self.p10_p90_style = plot_config.get_statistics_style("p10-p90") - self.p33_p67_style = plot_config.get_statistics_style("p33-p67") - - self.std_dev_factor = plot_config.get_standard_deviation_factor() - self.distribution_lines = plot_config.is_distribution_line_enabled() diff --git a/src/ert/gui/plotting/customization_dialog/style_chooser.py b/src/ert/gui/plotting/customization_dialog/style_chooser.py deleted file mode 100644 index eaf8ac3c316..00000000000 --- a/src/ert/gui/plotting/customization_dialog/style_chooser.py +++ /dev/null @@ -1,209 +0,0 @@ -from collections.abc import Iterator -from typing import override - -from PyQt6.QtWidgets import ( - QComboBox, - QDoubleSpinBox, - QHBoxLayout, - QLabel, - QLayout, - QWidget, -) - -from ert.gui.plotting.utils import PlotStyle - -STYLE_OFF = ("Off", None) -STYLE_AREA = ("Area", "#") -STYLE_SOLID = ("Solid", "-") -STYLE_DASHED = ("Dashed", "--") -STYLE_DOTTED = ("Dotted", ":") -STYLE_DASH_DOTTED = ("Dash dotted", "-.") - -STYLESET_DEFAULT = "default" -STYLESET_AREA = "area" -STYLESET_TOGGLE = "toggle_only" - -STYLES = { - STYLESET_DEFAULT: [ - STYLE_OFF, - STYLE_SOLID, - STYLE_DASHED, - STYLE_DOTTED, - STYLE_DASH_DOTTED, - ], - STYLESET_AREA: [ - STYLE_OFF, - STYLE_AREA, - STYLE_SOLID, - STYLE_DASHED, - STYLE_DOTTED, - STYLE_DASH_DOTTED, - ], - STYLESET_TOGGLE: [STYLE_OFF, STYLE_SOLID], -} - -MARKER_OFF = ("Off", None) -MARKER_X = ("X", "x") -MARKER_CIRCLE = ("Circle", "o") -MARKER_POINT = ("Point", ".") -MARKER_PIXEL = ("Pixel", ",") -MARKER_PLUS = ("Plus", "+") -MARKER_STAR = ("Star", "*") -MARKER_DIAMOND = ("Diamond", "D") -MARKER_PENTAGON = ("Pentagon", "p") -MARKER_SQUARE = ("Square", "s") -MARKER_HLINE = ("H Line", "_") -MARKER_VLINE = ("V Line", "|") -MARKER_OCTAGON = ("Octagon", "8") -MARKER_HEXAGON1 = ("Hexagon 1", "h") -MARKER_HEXAGON2 = ("Hexagon 2", "H") - -MARKERS: list[tuple[str, str | None]] = [ - MARKER_OFF, - MARKER_X, - MARKER_CIRCLE, - MARKER_POINT, - MARKER_STAR, - MARKER_DIAMOND, - MARKER_PLUS, - MARKER_PENTAGON, - MARKER_SQUARE, - MARKER_OCTAGON, - MARKER_HEXAGON1, - MARKER_HEXAGON2, -] - - -class StyleChooser(QWidget): - def __init__(self, line_style_set: str = STYLESET_DEFAULT) -> None: - QWidget.__init__(self) - self._style = PlotStyle("StyleChooser internal style") - - self._styles: list[tuple[str, str | None]] = ( - STYLES["default"] - if line_style_set not in STYLES - else STYLES[line_style_set] - ) - - self.setMinimumWidth(140) - self.setMaximumHeight(25) - - layout = QHBoxLayout() - - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(2) - - self.line_chooser = QComboBox() - self.line_chooser.setToolTip("Select line style.") - for style in self._styles: - self.line_chooser.addItem(*style) - - self.marker_chooser = QComboBox() - self.marker_chooser.setToolTip("Select marker style.") - for marker in MARKERS: - self.marker_chooser.addItem(*marker) - - self.thickness_spinner = QDoubleSpinBox() - self.thickness_spinner.setToolTip("Line thickness") - self.thickness_spinner.setMinimum(0.1) - self.thickness_spinner.setDecimals(1) - self.thickness_spinner.setSingleStep(0.1) - - self.size_spinner = QDoubleSpinBox() - self.size_spinner.setToolTip("Marker size") - self.size_spinner.setMinimum(0.1) - self.size_spinner.setDecimals(1) - self.size_spinner.setSingleStep(0.1) - - # the text content of the spinner varies, but shouldn't push the control - # out of boundaries - self.line_chooser.setMinimumWidth(110) - layout.addWidget(self.line_chooser) - layout.addWidget(self.thickness_spinner) - layout.addWidget(self.marker_chooser) - layout.addWidget(self.size_spinner) - - self.setLayout(layout) - - self.line_chooser.currentIndexChanged.connect(self._update_style) - self.marker_chooser.currentIndexChanged.connect(self._update_style) - self.thickness_spinner.valueChanged.connect(self._update_style) - self.size_spinner.valueChanged.connect(self._update_style) - - self._update_line_style_and_marker( - self._style.line_style, - self._style.marker, - self._style.width, - self._style.size, - ) - self._layout = layout - - def get_item_sizes(self) -> tuple[int, ...]: - def _iter() -> Iterator[int]: - for i in range(4): - item = self._layout.itemAt(i) - assert item is not None - yield item.sizeHint().width() - - return tuple(_iter()) - - def _find_line_style_index(self, line_style: str) -> int: - for index, style in enumerate(self._styles): - if (style[1] == line_style) or (style[1] is None and not line_style): - return index - return -1 - - @staticmethod - def _find_marker_style_index(marker: str) -> int: - for index, style in enumerate(MARKERS): - if (style[1] == marker) or (style[1] is None and not marker): - return index - return -1 - - def _update_line_style_and_marker( - self, line_style: str, marker: str, thickness: float, size: float - ) -> None: - self.line_chooser.setCurrentIndex(self._find_line_style_index(line_style)) - self.marker_chooser.setCurrentIndex(self._find_marker_style_index(marker)) - self.thickness_spinner.setValue(thickness) - self.size_spinner.setValue(size) - - def _update_style(self) -> None: - self.marker_chooser.setEnabled(self.line_chooser.currentText() != "Area") - - line_style: str = self.line_chooser.itemData(self.line_chooser.currentIndex()) - marker_style: str = self.marker_chooser.itemData( - self.marker_chooser.currentIndex() - ) - thickness = float(self.thickness_spinner.value()) - size = float(self.size_spinner.value()) - - self._style.line_style = line_style - self._style.marker = marker_style - self._style.width = thickness - self._style.size = size - - @override - def setStyle(self, style: PlotStyle) -> None: # type: ignore - self._style.copy_style_from(style) - self._update_line_style_and_marker( - style.line_style, style.marker, style.width, style.size - ) - - def get_style(self) -> PlotStyle: - style = PlotStyle("Generated style from StyleChooser") - style.copy_style_from(self._style) - return style - - def create_label_layout(self, layout: QLayout | None = None) -> QLayout: - if layout is None: - layout = QHBoxLayout() - - titles = ["Line style", "Width", "Marker style", "Size"] - sizes = self.get_item_sizes() - for title, size in zip(titles, sizes, strict=False): - label = QLabel(title) - label.setFixedWidth(size) - layout.addWidget(label) - - return layout diff --git a/src/ert/gui/plotting/customization_dialog/style_customization_view.py b/src/ert/gui/plotting/customization_dialog/style_customization_view.py deleted file mode 100644 index 77a962c4ce3..00000000000 --- a/src/ert/gui/plotting/customization_dialog/style_customization_view.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import TYPE_CHECKING, cast, override - -from PyQt6.QtWidgets import QHBoxLayout - -from .customization_view import CustomizationView, WidgetProperty -from .style_chooser import STYLESET_TOGGLE, StyleChooser - -if TYPE_CHECKING: - from ert.gui.plotting.utils import PlotConfig - - -class StyleCustomizationView(CustomizationView): - default_style = WidgetProperty() - history_style = WidgetProperty() - observations_style = WidgetProperty() - - def __init__(self) -> None: - CustomizationView.__init__(self) - - layout = QHBoxLayout() - - self.add_row("", layout) # type: ignore - self.add_style_chooser( - "default_style", "Default", "Line and marker style for default lines." - ) - self.add_style_chooser( - "history_style", "History", "Line and marker style for the history line." - ) - self.add_style_chooser( - "observations_style", - "Observation", - "Line and marker style for the observation line.", - line_style_set=STYLESET_TOGGLE, - ) - - style = cast(StyleChooser, self["default_style"]) - style.create_label_layout(layout) - - self.add_spacing(10) - - @override - def apply_customization(self, plot_config: "PlotConfig") -> None: - plot_config.set_default_style(self.default_style) - plot_config.set_history_style(self.history_style) - plot_config.set_observations_style(self.observations_style) - - @override - def revert_customization(self, plot_config: "PlotConfig") -> None: - self.default_style = plot_config.default_style() - self.history_style = plot_config.history_style() - self.observations_style = plot_config.observations_style() diff --git a/src/ert/gui/plotting/plot_window.py b/src/ert/gui/plotting/plot_window.py index 6a74718e504..3fbff6d1a84 100644 --- a/src/ert/gui/plotting/plot_window.py +++ b/src/ert/gui/plotting/plot_window.py @@ -51,10 +51,10 @@ from ert.services import ServerBootFail from ert.utils import log_duration -from .customization_dialog import PlotCustomizer from .plot_api import EnsembleObject, PlotApi, PlotApiKeyDefinition -from .utils import PlotConfig, PlotContext +from .utils import PlotConfigFactory, PlotContext from .utils.observation_locations import transform_observation_locations +from .utils.plot_color_palettes import TABLEAU_10_COLOR_CYCLE from .utils.plot_types import ObservationPlotLocations from .utils.qt_creator import create_group_box, create_group_layout, create_side_panel from .widgets.data_type_keys_widget import DataTypeKeysWidget @@ -206,8 +206,9 @@ def __init__( self._key_definitions = [] QApplication.restoreOverrideCursor() - self._plot_customizer = PlotCustomizer(self, self._key_definitions) - self._plot_customizer.settingsChanged.connect(self.keySelected) + self._titles: dict[str, str] = {} + self._x_labels: dict[str, str | None] = {} + self._y_labels: dict[str, str | None] = {} self._central_tab = QTabWidget() central_widget = QWidget() @@ -266,7 +267,7 @@ def __init__( self._ensemble_selection_widget = EnsembleSelectionWidget( plot_case_objects, - self._plot_customizer.get_plot_config().get_number_of_colors(), + len(TABLEAU_10_COLOR_CYCLE), ) self._ensemble_selection_widget.ensembleSelectionChanged.connect( @@ -543,9 +544,10 @@ def fetch_data( except BaseException as e: handle_exception(e) - plot_config = PlotConfig.create_copy( - self._plot_customizer.get_plot_config() - ) + plot_config = PlotConfigFactory.create_plot_config_for_key(key_def) + 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)) plot_config.set_legend_enabled(self._general_options.legend_checkbox_state) plot_config.set_grid_enabled(self._general_options.grid_checkbox_state) plot_config.set_line_color_cycle(self._general_options.get_color_cycle()) @@ -656,7 +658,6 @@ def add_plot_widget( enabled: bool = True, ) -> None: plot_widget = PlotWidget(name, plotter) - plot_widget.customizationTriggered.connect(self.toggle_customize_dialog) plot_widget.axisLabelEditRequested.connect(self._edit_axis_label) plot_widget.titleEditRequested.connect(self._edit_title) plot_widget.layer_index_changed.connect(self.layer_index_changed) @@ -669,11 +670,14 @@ def _edit_axis_label(self, axis: str) -> None: label_names = {"x": "x-label", "y": "y-label"} if axis not in label_names: raise ValueError(f"Unknown axis '{axis}'. Expected 'x' or 'y'.") + key_def = self.getSelectedKey() + if key_def is None: + return label_name = label_names[axis] title = f"Edit {label_name}" prompt = f"New {label_name}:" - plot_config = self._plot_customizer.get_plot_config() - current_label = plot_config.x_label() if axis == "x" else plot_config.y_label() + labels = self._x_labels if axis == "x" else self._y_labels + current_label = labels.get(key_def.key) if current_label is None: current_widget = self._central_tab.currentWidget() if isinstance(current_widget, PlotWidget) and current_widget._figure.axes: @@ -689,30 +693,23 @@ def _edit_axis_label(self, axis: str) -> None: if not accepted: return new_label: str | None = new_label_text or None - if axis == "x": - plot_config.set_x_label(new_label) - else: - plot_config.set_y_label(new_label) - self._plot_customizer.update_plot_config(plot_config) + labels[key_def.key] = new_label + self.update_plot() def _edit_title(self) -> None: + key_def = self.getSelectedKey() + if key_def is None: + return title = "Edit title" - plot_config = self._plot_customizer.get_plot_config() new_title, accepted = self._general_options.get_text_input( title, "New title:", - plot_config.title(), + self._titles.get(key_def.key, key_def.key), ) if not accepted: return - if new_title: - plot_config.set_title(new_title) - else: - key_def = self.getSelectedKey() - if key_def is None: - return - plot_config.set_title(key_def.key) - self._plot_customizer.update_plot_config(plot_config) + self._titles[key_def.key] = new_title or key_def.key + self.update_plot() @showWaitCursorWhileWaiting def keySelected(self) -> None: @@ -720,7 +717,6 @@ def keySelected(self) -> None: if key_def is None: self._show_no_data_message() return - self._plot_customizer.switch_plot_config_history(key_def) is_everest_specific_widget = key_def.metadata.get("data_origin") in { "everest_objectives", @@ -835,9 +831,6 @@ def everest_available_widget_selection( self._prev_key_origin = key_def.metadata.get("data_origin") self.update_plot() - def toggle_customize_dialog(self) -> None: - self._plot_customizer.toggle_customization_dialog() - def add_plot_widgets_from_plot_map( self, plot_map: dict[str, Callable[[], Plotter]] ) -> None: diff --git a/src/ert/gui/plotting/utils/__init__.py b/src/ert/gui/plotting/utils/__init__.py index 6aeccb9d62f..678330b590f 100644 --- a/src/ert/gui/plotting/utils/__init__.py +++ b/src/ert/gui/plotting/utils/__init__.py @@ -9,7 +9,6 @@ from .plot_config import PlotConfig from .plot_config_factory import PlotConfigFactory -from .plot_config_history import PlotConfigHistory from .plot_context import PlotContext from .plot_style import PlotStyle from .plot_tools import ConditionalAxisFormatter, PlotTools @@ -28,7 +27,6 @@ "ObservationPlotLocations", "PlotConfig", "PlotConfigFactory", - "PlotConfigHistory", "PlotContext", "PlotStyle", "PlotTools", diff --git a/src/ert/gui/plotting/utils/plot_config_history.py b/src/ert/gui/plotting/utils/plot_config_history.py deleted file mode 100644 index 9213639ac9c..00000000000 --- a/src/ert/gui/plotting/utils/plot_config_history.py +++ /dev/null @@ -1,44 +0,0 @@ -from .plot_config import PlotConfig - - -class PlotConfigHistory: - """A Class for tracking changes to a PlotConfig class (supports undo, redo - and reset) - """ - - def __init__(self, name: str, initial: PlotConfig) -> None: - super().__init__() - self._name: str = name - self._initial: PlotConfig = PlotConfig.create_copy(initial) - self._undo_history: list[PlotConfig] = [] - self._redo_history: list[PlotConfig] = [] - self._current = PlotConfig.create_copy(self._initial) - - def is_undo_possible(self) -> bool: - return len(self._undo_history) > 0 - - def is_redo_possible(self) -> bool: - return len(self._redo_history) > 0 - - def apply_changes(self, plot_config: PlotConfig) -> None: - self._undo_history.append(self._current) - copy = PlotConfig.create_copy(self._current) - copy.copy_config_from(plot_config) - self._current = copy - del self._redo_history[:] - - def reset_changes(self) -> None: - self.apply_changes(self._initial) - - def undo_changes(self) -> None: - if self.is_undo_possible(): - self._redo_history.append(self._current) - self._current = self._undo_history.pop() - - def redo_changes(self) -> None: - if self.is_redo_possible(): - self._undo_history.append(self._current) - self._current = self._redo_history.pop() - - def get_plot_config(self) -> PlotConfig: - return PlotConfig.create_copy(self._current) diff --git a/src/ert/gui/plotting/widgets/__init__.py b/src/ert/gui/plotting/widgets/__init__.py index 95a9fa9a6d4..cc9dca8cf7d 100644 --- a/src/ert/gui/plotting/widgets/__init__.py +++ b/src/ert/gui/plotting/widgets/__init__.py @@ -1,5 +1,4 @@ from .clearable_line_edit import ClearableLineEdit -from .copy_style_to_dialog import CopyStyleToDialog from .custom_date_edit import CustomDateEdit from .data_type_keys_widget import DataTypeKeysWidget from .everest_control_selection_widget import EverestControlSelectionWidget @@ -11,7 +10,6 @@ __all__ = [ "ClearableLineEdit", - "CopyStyleToDialog", "CustomDateEdit", "DataTypeKeysWidget", "EnsembleSelectListWidget", diff --git a/src/ert/gui/plotting/widgets/copy_style_to_dialog.py b/src/ert/gui/plotting/widgets/copy_style_to_dialog.py deleted file mode 100644 index a07ce3b8830..00000000000 --- a/src/ert/gui/plotting/widgets/copy_style_to_dialog.py +++ /dev/null @@ -1,81 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -from PyQt6.QtWidgets import ( - QDialog, - QFormLayout, - QHBoxLayout, - QPushButton, - QToolButton, - QWidget, -) - -from ert.gui.ertwidgets import CheckList, SelectableListModel -from ert.gui.icon_utils import load_icon - -from .filter_popup import FilterPopup - -if TYPE_CHECKING: - from ert.gui.plotting.plot_api import PlotApiKeyDefinition - - -class CopyStyleToDialog(QDialog): - def __init__( - self, - parent: QWidget | None, - current_key: Any, - key_defs: list[PlotApiKeyDefinition], - ) -> None: - QWidget.__init__(self, parent) - self.setMinimumWidth(450) - self.setMinimumHeight(200) - self._dynamic = False - self.setWindowTitle(f"Copy the style of {current_key} to other keys") - self.activateWindow() - - self._key_defs = key_defs - self._active_filters: dict[str, bool] = {} - - layout = QFormLayout(self) - - self._filter_popup = FilterPopup(self, key_defs) - self._filter_popup.filterSettingsChanged.connect(self.filterSettingsChanged) - - filter_popup_button = QToolButton() - filter_popup_button.setIcon(load_icon("filter_list.svg")) - filter_popup_button.clicked.connect(self._filter_popup.show) - - self._list_model = SelectableListModel([k.key for k in key_defs]) - self._list_model.unselectAll() - - self._cl = CheckList(self._list_model, custom_filter_button=filter_popup_button) - - layout.addWidget(self._cl) - - apply_button = QPushButton("Apply") - apply_button.clicked.connect(self.accept) - apply_button.setDefault(True) - - close_button = QPushButton("Close") - close_button.setToolTip("Hide this dialog") - close_button.clicked.connect(self.reject) - - button_layout = QHBoxLayout() - button_layout.addStretch() - button_layout.addWidget(apply_button) - button_layout.addWidget(close_button) - - layout.addRow(button_layout) - - def getSelectedKeys(self) -> list[str]: - return self._list_model.getSelectedItems() - - def filterSettingsChanged(self, filter_settings: dict[str, bool]) -> None: - self._active_filters = filter_settings - filtered = [ - k.key - for k in self._key_defs - if filter_settings.get(k.metadata.get("data_origin", ""), True) - ] - self._list_model.setItems(filtered) diff --git a/src/ert/gui/plotting/customization_dialog/color_chooser.py b/src/ert/gui/plotting/widgets/plot_controls/color_chooser.py similarity index 100% rename from src/ert/gui/plotting/customization_dialog/color_chooser.py rename to src/ert/gui/plotting/widgets/plot_controls/color_chooser.py diff --git a/src/ert/gui/plotting/widgets/plot_controls/custom_palette_dialog.py b/src/ert/gui/plotting/widgets/plot_controls/custom_palette_dialog.py index 8ff42d6994a..f1c1e37ee4a 100644 --- a/src/ert/gui/plotting/widgets/plot_controls/custom_palette_dialog.py +++ b/src/ert/gui/plotting/widgets/plot_controls/custom_palette_dialog.py @@ -10,9 +10,9 @@ QWidget, ) -from ert.gui.plotting.customization_dialog.color_chooser import ColorBox from ert.gui.plotting.utils.logging_utils import log_plot_option_usage_once from ert.gui.plotting.utils.plot_color_palettes import MINIMUM_COLOR_CYCLE_LENGTH +from ert.gui.plotting.widgets.plot_controls.color_chooser import ColorBox logger = logging.getLogger(__name__) diff --git a/src/ert/gui/plotting/widgets/plot_controls/observation_color.py b/src/ert/gui/plotting/widgets/plot_controls/observation_color.py index e464c617f79..e436741b8ac 100644 --- a/src/ert/gui/plotting/widgets/plot_controls/observation_color.py +++ b/src/ert/gui/plotting/widgets/plot_controls/observation_color.py @@ -3,9 +3,9 @@ from PyQt6.QtWidgets import QCheckBox, QHBoxLayout, QLabel, QWidget -from ert.gui.plotting.customization_dialog.color_chooser import ColorBox from ert.gui.plotting.utils.logging_utils import log_plot_option_usage_once from ert.gui.plotting.utils.plot_config import PlotConfig +from ert.gui.plotting.widgets.plot_controls.color_chooser import ColorBox logger = logging.getLogger(__name__) diff --git a/src/ert/gui/plotting/widgets/plot_widget.py b/src/ert/gui/plotting/widgets/plot_widget.py index f7cac11a062..6daa4029d57 100644 --- a/src/ert/gui/plotting/widgets/plot_widget.py +++ b/src/ert/gui/plotting/widgets/plot_widget.py @@ -17,7 +17,7 @@ from PyQt6.QtCore import QStringListModel, Qt from PyQt6.QtCore import pyqtSignal as Signal from PyQt6.QtCore import pyqtSlot as Slot -from PyQt6.QtGui import QAction, QCursor +from PyQt6.QtGui import QCursor from PyQt6.QtWidgets import ( QComboBox, QToolTip, @@ -26,7 +26,6 @@ QWidgetAction, ) -from ert.gui.icon_utils import load_icon from ert.gui.plotting.plot_api import EnsembleObject, PlotApiKeyDefinition from ert.gui.plotting.utils.plot_types import ObservationPlotLocations @@ -55,7 +54,6 @@ def plot( class CustomNavigationToolbar(NavigationToolbar2QT): - customizationTriggered = Signal() layer_index_changed = Signal(int) def __init__( @@ -67,25 +65,13 @@ def __init__( ) -> None: super().__init__(canvas, parent, coordinates) # type: ignore - gear = load_icon("edit.svg") - customize_action = QAction(gear, "Customize", self) - customize_action.setToolTip("Customize plot settings") - customize_action.triggered.connect(self.customizationTriggered) - customize_action.triggered.connect( - lambda: self.logToolbarUsage(customize_action.text()) - ) - layer_combobox = QComboBox() self._model = QStringListModel() layer_combobox.setModel(self._model) layer_combobox.currentIndexChanged.connect(self.layer_index_changed) for action in self.actions(): - if str(action.text()).lower() == "subplots": - self.removeAction(action) - - if str(action.text()).lower() == "customize": - self.insertAction(action, customize_action) + if str(action.text()).lower() in {"subplots", "customize"}: self.removeAction(action) # insert the layer widget before the coordinates widget @@ -120,7 +106,6 @@ def updateLayerWidget(self, layers: int) -> None: class PlotWidget(QWidget): - customizationTriggered = Signal() axisLabelEditRequested = Signal(str) titleEditRequested = Signal() layer_index_changed = Signal(int) @@ -153,7 +138,6 @@ def __init__( vbox = QVBoxLayout() vbox.addWidget(self._canvas) self._toolbar = CustomNavigationToolbar(self._canvas, self) - self._toolbar.customizationTriggered.connect(self.customizationTriggered) self._toolbar.layer_index_changed.connect(self.layer_index_changed) self.updateLayerWidget.connect(self._toolbar.updateLayerWidget) self.resetLayerWidget.connect(self._toolbar.resetLayerWidget) diff --git a/tests/ert/ui_tests/gui/test_plot_customization.py b/tests/ert/ui_tests/gui/test_plot_customization.py deleted file mode 100644 index 59eb05f3ab4..00000000000 --- a/tests/ert/ui_tests/gui/test_plot_customization.py +++ /dev/null @@ -1,33 +0,0 @@ -import logging - -from ert.gui.plotting.customization_dialog.customize_plot_dialog import ( - CustomizePlotDialog, -) -from ert.gui.plotting.customization_dialog.statistics_customization_view import ( - StatisticsCustomizationView, -) -from ert.gui.plotting.customization_dialog.style_customization_view import ( - StyleCustomizationView, -) - - -def test_that_first_tab_is_not_logged_when_opening_customize_plot_dialog(qtbot, caplog): - caplog.set_level( - logging.INFO, - logger="ert.gui.plotting.customization_dialog.customize_plot_dialog", - ) - - plot = CustomizePlotDialog(title="Test Plot", parent=None, key_defs=[]) - plot.add_tab("style", "Style", StyleCustomizationView()) - plot.add_tab("statistics", "Statistics", StatisticsCustomizationView()) - qtbot.addWidget(plot) - - plot.show() - assert "Customization dialog action: Style" not in caplog.text - - plot._tabs.setCurrentIndex(1) - assert "Customization dialog action: Statistics" in caplog.text - - plot._tabs.setCurrentIndex(0) - assert "Customization dialog action: Style" in caplog.text - assert len(caplog.records) == 2 diff --git a/tests/ert/unit_tests/gui/plottery/test_plot_config_history.py b/tests/ert/unit_tests/gui/plottery/test_plot_config_history.py deleted file mode 100644 index ee1cb7982ab..00000000000 --- a/tests/ert/unit_tests/gui/plottery/test_plot_config_history.py +++ /dev/null @@ -1,37 +0,0 @@ -from ert.gui.plotting.utils import PlotConfig, PlotConfigHistory - - -def test_plot_config_history(): - test_pc = PlotConfig(title="test_1") - history = PlotConfigHistory("test", test_pc) - - assert history.get_plot_config().title() == test_pc.title() - assert history.get_plot_config() != test_pc - - assert not history.is_undo_possible() - assert not history.is_redo_possible() - - history.apply_changes(PlotConfig(title="test_2")) - assert history.is_undo_possible() - assert not history.is_redo_possible() - assert history.get_plot_config().title() == "test_2" - - history.undo_changes() - assert not history.is_undo_possible() - assert history.is_redo_possible() - assert history.get_plot_config().title() == "test_1" - - history.redo_changes() - assert history.is_undo_possible() - assert not history.is_redo_possible() - assert history.get_plot_config().title() == "test_2" - - history.reset_changes() - assert history.is_undo_possible() - assert not history.is_redo_possible() - assert history.get_plot_config().title() == "test_1" - - history.undo_changes() - assert history.is_undo_possible() - assert history.is_redo_possible() - assert history.get_plot_config().title() == "test_2" diff --git a/tests/ert/unit_tests/gui/tools/plot/test_plot_window.py b/tests/ert/unit_tests/gui/tools/plot/test_plot_window.py index e532f36ee85..0aa50a830c5 100644 --- a/tests/ert/unit_tests/gui/tools/plot/test_plot_window.py +++ b/tests/ert/unit_tests/gui/tools/plot/test_plot_window.py @@ -10,6 +10,7 @@ from PyQt6.QtWidgets import QApplication, QCheckBox, QLabel, QPushButton, QToolTip from pytestqt.qtbot import QtBot +from ert.config.breakthrough_config import BreakthroughConfig from ert.config.distribution import RawSettings from ert.config.gen_kw_config import DataSource, GenKwConfig from ert.gui.plotting.ert_plots.gaussian_kde import plotGaussianKDE @@ -1039,6 +1040,9 @@ def _create_plot_window_for_text_edit( monkeypatch.setattr("ert.gui.plotting.plot_window.PlotApi", mock_plot_api_cls) plot_window = PlotWindow(config_file="", ens_path=Path(), parent=None) qtbot.addWidget(plot_window) + plot_window.getSelectedKey = MagicMock( + return_value=MagicMock(key="some_key", dimensionality=1, metadata={}) + ) return plot_window @@ -1086,12 +1090,8 @@ def test_that_sidebar_axis_label_edit_uses_configured_or_visible_label( plot_window = _create_plot_window_for_text_edit(qtbot, monkeypatch) get_text_input = MagicMock(return_value=("", False)) plot_window._general_options.get_text_input = get_text_input - plot_config = plot_window._plot_customizer.get_plot_config() - if axis == "x": - plot_config.set_x_label(configured_label) - else: - plot_config.set_y_label(configured_label) - plot_window._plot_customizer.update_plot_config(plot_config) + labels = plot_window._x_labels if axis == "x" else plot_window._y_labels + labels["some_key"] = configured_label current_widget = plot_window._central_tab.currentWidget() assert isinstance(current_widget, PlotWidget) if visible_label is not None: @@ -1140,36 +1140,19 @@ def test_that_axis_label_edit_updates_or_preserves_persistent_config( plot_window = _create_plot_window_for_text_edit(qtbot, monkeypatch) get_text_input = MagicMock(return_value=(new_label, accepted)) plot_window._general_options.get_text_input = get_text_input - plot_config = plot_window._plot_customizer.get_plot_config() - if axis == "x": - plot_config.set_x_label(current_label) - else: - plot_config.set_y_label(current_label) - plot_window._plot_customizer.update_plot_config(plot_config) + plot_window.update_plot = MagicMock() + labels = plot_window._x_labels if axis == "x" else plot_window._y_labels + labels["some_key"] = current_label plot_window._edit_axis_label(axis) expected_label = (new_label or None) if accepted else current_label - persisted_config = plot_window._plot_customizer.get_plot_config() - assert ( - persisted_config.x_label() if axis == "x" else persisted_config.y_label() - ) == expected_label + assert labels["some_key"] == expected_label get_text_input.assert_called_once_with( f"Edit {axis}-label", f"New {axis}-label:", current_label ) -def _create_plot_window_for_title_edit( - qtbot: QtBot, - monkeypatch: pytest.MonkeyPatch, -) -> PlotWindow: - plot_window = _create_plot_window_for_text_edit(qtbot, monkeypatch) - plot_window.getSelectedKey = MagicMock( - return_value=MagicMock(key="some_key", dimensionality=1, metadata={}) - ) - return plot_window - - @pytest.mark.parametrize( ("dialog_value", "expected_title", "accepted"), [ @@ -1190,19 +1173,15 @@ def test_that_title_edit_updates_or_preserves_persistent_config( expected_title: str, accepted: bool, ) -> None: - plot_window = _create_plot_window_for_title_edit(qtbot, monkeypatch) + plot_window = _create_plot_window_for_text_edit(qtbot, monkeypatch) get_text_input = MagicMock(return_value=(dialog_value, accepted)) plot_window._general_options.get_text_input = get_text_input plot_window.update_plot = MagicMock() - plot_window._plot_customizer._emit_changed_signal = MagicMock() - plot_config = plot_window._plot_customizer.get_plot_config() - plot_config.set_title("Existing title") - plot_window._plot_customizer.update_plot_config(plot_config) + plot_window._titles["some_key"] = "Existing title" plot_window._edit_title() - persisted_config = plot_window._plot_customizer.get_plot_config() - assert persisted_config.title() == expected_title + assert plot_window._titles["some_key"] == expected_title get_text_input.assert_called_once_with("Edit title", "New title:", "Existing title") @@ -1258,15 +1237,61 @@ def test_that_clearing_custom_title_restores_key_title_when_rendering( plot_window._general_options.get_text_input = MagicMock(return_value=("", True)) plot_window.update_plot() - plot_config = plot_window._plot_customizer.get_plot_config() - plot_config.set_title("Custom title") - plot_window._plot_customizer.update_plot_config(plot_config) + plot_window._titles["some_key"] = "Custom title" plot_window._edit_title() plot_widget = plot_window._central_tab.currentWidget() assert plot_widget._figure.axes[0].get_title() == "some_key" +@pytest.mark.slow +def test_that_breakthrough_response_title_keeps_the_breakthrough_prefix( + qtbot: QtBot, + monkeypatch: pytest.MonkeyPatch, +) -> None: + mock_plot_api_cls = MagicMock(spec=PlotApi) + mock_plot_api = MagicMock(spec=PlotApi) + mock_plot_api_cls.return_value = mock_plot_api + + storage_version = "0.0" + mock_plot_api.api_version = storage_version + mock_plot_api.parameters_api_key_defs = [] + mock_plot_api.responses_api_key_defs = [ + PlotApiKeyDefinition( + "BREAKTHROUGH:WWCT:OP1", + index_type=None, + metadata={"data_origin": "summary"}, + observations=False, + dimensionality=2, + response=BreakthroughConfig(), + ) + ] + mock_plot_api.get_all_ensembles.return_value = [ + EnsembleObject( + "ensemble", + "ensemble", + False, + "experiment", + "2026-01-01T00:00:00", + ) + ] + mock_plot_api.data_for_response.return_value = pd.DataFrame({0: [1.0, 2.0, 3.0]}) + mock_plot_api.has_history_data.return_value = False + + monkeypatch.setattr( + "ert.gui.plotting.plot_window.get_storage_api_version", + lambda: storage_version, + ) + monkeypatch.setattr("ert.gui.plotting.plot_window.PlotApi", mock_plot_api_cls) + + plot_window = PlotWindow(config_file="", ens_path=Path(), parent=None) + qtbot.addWidget(plot_window) + plot_window.update_plot() + + plot_widget = cast(PlotWidget, plot_window._central_tab.currentWidget()) + assert plot_widget._figure.axes[0].get_title() == "BREAKTHROUGH:WWCT:OP1" + + def test_that_resetting_axis_label_restores_histogram_default_label( qtbot: QtBot, ) -> None: