diff --git a/src/bedit_gui/commands/simulation_plot_commands.py b/src/bedit_gui/commands/simulation_plot_commands.py index 333cc0e..a1b4a73 100644 --- a/src/bedit_gui/commands/simulation_plot_commands.py +++ b/src/bedit_gui/commands/simulation_plot_commands.py @@ -77,3 +77,21 @@ class ChangeSimulationPlotSignalsCommand(QUndoCommand): def undo(self) -> None: self.root.plot_tabs[self.index].signals = list(self.old_signals) self.changed(self.index) + + +class ChangeSimulationPlotXAxisCommand(QUndoCommand): + def __init__(self, root: SimulationRoot, index: int, x_axis: str | None, changed: ChangedCallback) -> None: + super().__init__("Change plot x axis") + self.root = root + self.index = index + self.old_x_axis = root.plot_tabs[index].x_axis + self.x_axis = x_axis + self.changed = changed + + def redo(self) -> None: + self.root.plot_tabs[self.index].x_axis = self.x_axis + self.changed(self.index) + + def undo(self) -> None: + self.root.plot_tabs[self.index].x_axis = self.old_x_axis + self.changed(self.index) diff --git a/src/bedit_gui/controllers/simulation_plot_controller.py b/src/bedit_gui/controllers/simulation_plot_controller.py index 1346861..4552d38 100644 --- a/src/bedit_gui/controllers/simulation_plot_controller.py +++ b/src/bedit_gui/controllers/simulation_plot_controller.py @@ -4,7 +4,7 @@ from PySide6.QtCore import QObject, QPoint, Qt from PySide6.QtGui import QUndoStack from PySide6.QtWidgets import QInputDialog, QMenu, QTreeWidgetItem, QWidget -from bedit_gui.commands.simulation_plot_commands import AddSimulationPlotTabCommand, ChangeSimulationPlotSignalsCommand, RemoveSimulationPlotTabCommand, RenameSimulationPlotTabCommand +from bedit_gui.commands.simulation_plot_commands import AddSimulationPlotTabCommand, ChangeSimulationPlotSignalsCommand, ChangeSimulationPlotXAxisCommand, RemoveSimulationPlotTabCommand, RenameSimulationPlotTabCommand from bedit_gui.controllers.simulation_file_controller import SimulationFileController from bedit_gui.services.application_logging import get_logger from bedit_gui.simulation_models import SimulationPlotTab @@ -29,6 +29,8 @@ class SimulationPlotController(QObject): tabs.tabBar().setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) tabs.tabBar().customContextMenuRequested.connect(self._show_tab_menu) tree.itemChanged.connect(self._signal_changed) + tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + tree.customContextMenuRequested.connect(self._show_signal_menu) files.runtime_changed.connect(self.load_root) window.ui.actionUndo.triggered.connect(self.undo) @@ -201,6 +203,25 @@ class SimulationPlotController(QObject): elif selected is remove_action: self.remove_tab(index) + def _show_signal_menu(self, position: QPoint) -> None: + root = self.files.root + tree = self.window.ui.simulationTree + item = tree.itemAt(position) + signal = item.data(0, Qt.ItemDataRole.UserRole) if item is not None else None + if root is None or signal is None or not 0 <= self._active_tab < len(root.plot_tabs): + return + tab = root.plot_tabs[self._active_tab] + menu = QMenu(tree) + use_signal = menu.addAction("Use as x axis") + use_signal.setEnabled(tab.x_axis != signal) + use_time = menu.addAction("Use time as x axis") + use_time.setEnabled(tab.x_axis is not None) + selected = menu.exec(tree.viewport().mapToGlobal(position)) + if selected is use_signal: + self.undo_stack.push(ChangeSimulationPlotXAxisCommand(root, self._active_tab, signal, self._selection_changed)) + elif selected is use_time: + self.undo_stack.push(ChangeSimulationPlotXAxisCommand(root, self._active_tab, None, self._selection_changed)) + def _available_signals(self) -> list[str]: root = self.files.root if root is None: @@ -220,7 +241,8 @@ class SimulationPlotController(QObject): return widget = self.window.ui.resultsTabWidget.widget(index) if isinstance(widget, SimulationPlotWidget): - widget.set_plot(root.results, root.plot_tabs[index].signals) + tab = root.plot_tabs[index] + widget.set_plot(root.results, tab.signals, tab.x_axis) def _set_undo_text(self, command: str) -> None: self.window.ui.actionUndo.setText(f"Undo {command}" if command else "Undo") diff --git a/src/bedit_gui/simulation_models.py b/src/bedit_gui/simulation_models.py index 57f19b6..b57b8cd 100644 --- a/src/bedit_gui/simulation_models.py +++ b/src/bedit_gui/simulation_models.py @@ -29,16 +29,17 @@ class CompiledModel: class SimulationPlotTab: name: str signals: list[str] = field(default_factory=list) + x_axis: str | None = None @classmethod def from_data(cls, data: Mapping[str, Any]) -> SimulationPlotTab: raw_signals = data.get("signals", []) if not isinstance(raw_signals, list): raise TypeError("plot tab signals must be a list") - return cls(name=str(data.get("name", "Plot")), signals=[str(signal) for signal in raw_signals]) + return cls(name=str(data.get("name", "Plot")), signals=[str(signal) for signal in raw_signals], x_axis=str(data["x_axis"]) if data.get("x_axis") is not None else None) def to_data(self) -> dict[str, Any]: - return {"name": self.name, "signals": self.signals} + return {"name": self.name, "signals": self.signals, "x_axis": self.x_axis} @dataclass diff --git a/src/bedit_gui/views/simulation_plot_widget.py b/src/bedit_gui/views/simulation_plot_widget.py index 57a054f..67cd0b7 100644 --- a/src/bedit_gui/views/simulation_plot_widget.py +++ b/src/bedit_gui/views/simulation_plot_widget.py @@ -1,6 +1,6 @@ from __future__ import annotations -from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg, NavigationToolbar2QT from matplotlib.figure import Figure from PySide6.QtWidgets import QVBoxLayout, QWidget @@ -12,31 +12,35 @@ class SimulationPlotWidget(QWidget): super().__init__(parent) self.figure = Figure(layout="constrained") self.canvas = FigureCanvasQTAgg(self.figure) + self.toolbar = NavigationToolbar2QT(self.canvas, self) self.axes = self.figure.add_subplot() layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.toolbar) layout.addWidget(self.canvas) - def set_plot(self, results: list[SimulationResult], signals: list[str]) -> None: + def set_plot(self, results: list[SimulationResult], signals: list[str], x_axis: str | None = None) -> None: self.axes.clear() for signal in signals: - times: list[float] = [] + x_values: list[float] = [] values: list[float] = [] sample_offset = 0 for result in results: result_values = result.data.get(signal) if result_values is None: continue - result_times = result.data.get("time") - if result_times is not None and len(result_times) == len(result_values): - times.extend(result_times) + result_x = result.data.get(x_axis or "time") + if result_x is not None and len(result_x) == len(result_values): + x_values.extend(result_x) + elif x_axis is not None: + continue else: - times.extend(range(sample_offset, sample_offset + len(result_values))) + x_values.extend(range(sample_offset, sample_offset + len(result_values))) values.extend(result_values) sample_offset += len(result_values) if values: - self.axes.plot(times, values, label=signal) - self.axes.set_xlabel("Time") + self.axes.plot(x_values, values, label=signal) + self.axes.set_xlabel(x_axis or "Time") self.axes.grid(True) if self.axes.lines: self.axes.legend() diff --git a/untitled.besim.json b/untitled.besim.json index 46bab51..86196a6 100644 --- a/untitled.besim.json +++ b/untitled.besim.json @@ -7908,13 +7908,22 @@ "signals": [ "j0.e", "j1.f" - ] + ], + "x_axis": null }, { "name": "R power", "signals": [ "R.power" - ] + ], + "x_axis": null + }, + { + "name": "Eh idk?", + "signals": [ + "j1.f" + ], + "x_axis": "j0.e" } ] }