Skip to content

Commit 1e2c387

Browse files
committed
Add SyncPlotDialog class for synchronized plotting in a dialog interface
1 parent b5fb277 commit 1e2c387

File tree

4 files changed

+164
-58
lines changed

4 files changed

+164
-58
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
* Added `AnnotatedXRange` and `AnnotatedYRange` items
1919
* These items provide X and Y range selection with an annotation label
2020
* They can be created using `make.annotated_xrange` and `make.annotated_yrange` functions
21+
* New `SyncPlotDialog` class:
22+
* This class provides a dialog for displaying synchronized plots.
23+
* This is a complementary class to `SyncPlotWindow`, providing a modal dialog interface for synchronized plotting.
2124

2225
🧹 API cleanup: removed deprecated update methods (use `update_item` instead)
2326

plotpy/plot/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
PlotWindow,
1313
SubplotWidget,
1414
SyncPlotWindow,
15+
SyncPlotDialog,
1516
set_widget_title_icon,
1617
)

plotpy/plot/plotwidget.py

Lines changed: 131 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -889,57 +889,33 @@ def add_plot(
889889
self.manager.add_plot(plot, plot_id)
890890

891891

892-
class SyncPlotWindow(QW.QMainWindow):
893-
"""Window for showing plots, optionally synchronized
894-
895-
Args:
896-
parent: parent widget
897-
toolbar: show/hide toolbar
898-
options: plot options
899-
panels: additionnal panels
900-
auto_tools: If True, the plot tools are automatically registered.
901-
If False, the user must register the tools manually.
902-
title: The window title
903-
icon: The window icon
904-
size: The window size (width, height). Defaults to None (no resize)
905-
906-
Usage: first, create a window, then add plots to it, then call the
907-
:py:meth:`.SyncPlotWindow.finalize_configuration` method to add panels and
908-
eventually register tools.
909-
910-
Example::
911-
912-
from plotpy.plot import BasePlot, SyncPlotWindow
913-
win = SyncPlotWindow(title="My window")
914-
plot = BasePlot()
915-
win.add_plot(plot)
916-
win.finalize_configuration()
917-
win.show()
918-
"""
892+
class BaseSyncPlot:
893+
"""Base class for synchronized plot windows and dialogs"""
919894

920895
def __init__(
921896
self,
922-
parent: QWidget | None = None,
923897
toolbar: bool = True,
924898
options: PlotOptions | dict[str, Any] | None = None,
925899
auto_tools: bool = True,
926900
title: str = "PlotPy",
927901
icon: str = "plotpy.svg",
928902
size: tuple[int, int] | None = None,
929903
) -> None:
930-
super().__init__(parent)
931-
set_widget_title_icon(self, title, icon, size)
932904
self.manager = PlotManager(None)
933905
self.manager.set_main(self)
934906
self.subplotwidget = SubplotWidget(self.manager, parent=self, options=options)
935-
self.setCentralWidget(self.subplotwidget)
936907
self.toolbar = QW.QToolBar(_("Tools"), self)
937908
self.toolbar.setVisible(toolbar)
938909
self.manager.add_toolbar(self.toolbar, "default")
939910
self.toolbar.setMovable(True)
940911
self.toolbar.setFloatable(True)
941-
self.addToolBar(self.toolbar)
942912
self.auto_tools = auto_tools
913+
set_widget_title_icon(self, title, icon, size)
914+
# Note: setup_layout() is called by subclasses after Qt widget initialization
915+
916+
def setup_layout(self) -> None:
917+
"""Setup the layout - to be implemented by subclasses"""
918+
raise NotImplementedError("Subclasses must implement `setup_layout` method")
943919

944920
def get_toolbar(self) -> QW.QToolBar:
945921
"""Return main toolbar
@@ -969,11 +945,6 @@ def rescale_plots(self) -> None:
969945
for plot in self.subplotwidget.plots:
970946
plot.do_autoscale()
971947

972-
def showEvent(self, event): # pylint: disable=C0103
973-
"""Reimplement Qt method"""
974-
super().showEvent(event)
975-
QC.QTimer.singleShot(0, self.rescale_plots)
976-
977948
def add_plot(
978949
self,
979950
row: int,
@@ -995,10 +966,15 @@ def add_plot(
995966
plot_id = str(len(self.subplotwidget.plots) + 1)
996967
self.subplotwidget.add_plot(plot, row, col, plot_id)
997968
if sync and len(self.subplotwidget.plots) > 1:
998-
syncaxis = self.manager.synchronize_axis
999-
for i_plot in range(len(self.subplotwidget.plots) - 1):
1000-
syncaxis(X_BOTTOM, [plot_id, f"{i_plot + 1}"])
1001-
syncaxis(Y_LEFT, [plot_id, f"{i_plot + 1}"])
969+
self._synchronize_with_existing_plots(plot_id)
970+
971+
def _synchronize_with_existing_plots(self, plot_id: str) -> None:
972+
"""Synchronize the new plot with existing plots"""
973+
syncaxis = self.manager.synchronize_axis
974+
for i_plot in range(len(self.subplotwidget.plots) - 1):
975+
existing_plot_id = f"{i_plot + 1}"
976+
syncaxis(X_BOTTOM, [plot_id, existing_plot_id])
977+
syncaxis(Y_LEFT, [plot_id, existing_plot_id])
1002978

1003979
def get_plots(self) -> list[BasePlot]:
1004980
"""Return the plots
@@ -1007,3 +983,117 @@ def get_plots(self) -> list[BasePlot]:
1007983
list[BasePlot]: The plots
1008984
"""
1009985
return self.subplotwidget.get_plots()
986+
987+
988+
class SyncPlotWindow(QW.QMainWindow, BaseSyncPlot):
989+
"""Window for showing plots, optionally synchronized
990+
991+
Args:
992+
parent: parent widget
993+
toolbar: show/hide toolbar
994+
options: plot options
995+
panels: additionnal panels
996+
auto_tools: If True, the plot tools are automatically registered.
997+
If False, the user must register the tools manually.
998+
title: The window title
999+
icon: The window icon
1000+
size: The window size (width, height). Defaults to None (no resize)
1001+
1002+
Usage: first, create a window, then add plots to it, then call the
1003+
:py:meth:`.SyncPlotWindow.finalize_configuration` method to add panels and
1004+
eventually register tools.
1005+
1006+
Example::
1007+
1008+
from plotpy.plot import BasePlot, SyncPlotWindow
1009+
win = SyncPlotWindow(title="My window")
1010+
plot = BasePlot()
1011+
win.add_plot(plot)
1012+
win.finalize_configuration()
1013+
win.show()
1014+
"""
1015+
1016+
def __init__(
1017+
self,
1018+
parent: QWidget | None = None,
1019+
toolbar: bool = True,
1020+
options: PlotOptions | dict[str, Any] | None = None,
1021+
auto_tools: bool = True,
1022+
title: str = "PlotPy",
1023+
icon: str = "plotpy.svg",
1024+
size: tuple[int, int] | None = None,
1025+
) -> None:
1026+
self.subplotwidget: SubplotWidget
1027+
self.toolbar: QW.QToolBar
1028+
QW.QMainWindow.__init__(self, parent)
1029+
BaseSyncPlot.__init__(self, toolbar, options, auto_tools, title, icon, size)
1030+
self.setup_layout()
1031+
1032+
def showEvent(self, event): # pylint: disable=C0103
1033+
"""Reimplement Qt method"""
1034+
super().showEvent(event)
1035+
QC.QTimer.singleShot(0, self.rescale_plots)
1036+
1037+
def setup_layout(self) -> None:
1038+
"""Setup the main window layout"""
1039+
self.setCentralWidget(self.subplotwidget)
1040+
self.addToolBar(self.toolbar)
1041+
1042+
1043+
class SyncPlotDialog(QW.QDialog, BaseSyncPlot):
1044+
"""Dialog for showing plots, optionally synchronized
1045+
1046+
Args:
1047+
parent: parent widget
1048+
toolbar: show/hide toolbar
1049+
options: plot options
1050+
auto_tools: If True, the plot tools are automatically registered.
1051+
If False, the user must register the tools manually.
1052+
title: The window title
1053+
icon: The window icon
1054+
size: The window size (width, height). Defaults to None (no resize)
1055+
1056+
Usage: first, create a dialog, then add plots to it, then call the
1057+
:py:meth:`.SyncPlotDialog.finalize_configuration` method to add panels and
1058+
eventually register tools.
1059+
1060+
Example::
1061+
1062+
from plotpy.plot import BasePlot, SyncPlotDialog
1063+
dlg = SyncPlotDialog(title="My dialog")
1064+
plot = BasePlot()
1065+
dlg.add_plot(plot)
1066+
dlg.finalize_configuration()
1067+
dlg.exec()
1068+
"""
1069+
1070+
def __init__(
1071+
self,
1072+
parent: QWidget | None = None,
1073+
toolbar: bool = True,
1074+
options: PlotOptions | dict[str, Any] | None = None,
1075+
auto_tools: bool = True,
1076+
title: str = "PlotPy",
1077+
icon: str = "plotpy.svg",
1078+
size: tuple[int, int] | None = None,
1079+
) -> None:
1080+
self.subplotwidget: SubplotWidget
1081+
self.toolbar: QW.QToolBar
1082+
QW.QDialog.__init__(self, parent)
1083+
BaseSyncPlot.__init__(self, toolbar, options, auto_tools, title, icon, size)
1084+
self.setup_layout()
1085+
self.setWindowFlags(QC.Qt.Window)
1086+
1087+
def showEvent(self, event): # pylint: disable=C0103
1088+
"""Reimplement Qt method"""
1089+
super().showEvent(event)
1090+
QC.QTimer.singleShot(0, self.rescale_plots)
1091+
1092+
def setup_layout(self) -> None:
1093+
"""Setup the dialog layout"""
1094+
self.plot_layout = QW.QGridLayout()
1095+
self.plot_layout.addWidget(self.subplotwidget)
1096+
layout = QW.QVBoxLayout()
1097+
layout.addWidget(self.toolbar)
1098+
layout.addLayout(self.plot_layout)
1099+
self.setLayout(layout)

plotpy/tests/widgets/test_syncplot.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@
88
from __future__ import annotations
99

1010
import numpy as np
11-
from guidata.qthelpers import qt_app_context
11+
from guidata.env import execenv
12+
from guidata.qthelpers import exec_dialog, qt_app_context
1213
from qtpy import QtGui as QG
14+
from qtpy import QtWidgets as QW
1315

1416
from plotpy.builder import make
1517
from plotpy.plot import BasePlot, PlotOptions
16-
from plotpy.plot.plotwidget import SyncPlotWindow
18+
from plotpy.plot.plotwidget import SyncPlotDialog, SyncPlotWindow
1719
from plotpy.tests.data import gen_2d_gaussian
1820

1921

20-
def plot(plot_type, *itemlists):
21-
"""Plot items in SyncPlotDialog"""
22-
win = SyncPlotWindow(
23-
title="Window for showing plots, optionally synchronized",
22+
def show_with(cls: type[SyncPlotDialog] | type[SyncPlotWindow], plot_type, *itemlists):
23+
"""Show plot items in SyncPlotWindow or SyncPlotDialog"""
24+
widget = cls(
25+
title=f"{cls.__name__}: showing plots, optionally synchronized ({plot_type})",
2426
options=PlotOptions(type=plot_type),
2527
)
2628
row, col = 0, 0
@@ -30,16 +32,22 @@ def plot(plot_type, *itemlists):
3032
plot.add_item(item)
3133
plot.set_axis_font("left", QG.QFont("Courier"))
3234
plot.set_items_readonly(False)
33-
win.add_plot(row, col, plot, sync=True)
35+
widget.add_plot(row, col, plot, sync=True)
3436
col += 1
3537
if col == 2:
3638
row += 1
3739
col = 0
38-
win.finalize_configuration()
40+
widget.finalize_configuration()
3941
if plot_type == "image":
40-
win.manager.get_contrast_panel().show()
41-
win.resize(800, 600)
42-
win.show()
42+
widget.get_manager().get_contrast_panel().show()
43+
widget.resize(800, 600)
44+
if cls is SyncPlotWindow:
45+
widget.show()
46+
if not execenv.unattended:
47+
app = QW.QApplication.instance()
48+
app.exec()
49+
else:
50+
exec_dialog(widget)
4351

4452

4553
def test_syncplot_curves():
@@ -49,9 +57,8 @@ def test_syncplot_curves():
4957
y = np.sin(np.sin(np.sin(x)))
5058
x2 = np.linspace(-10, 10, 20)
5159
y2 = np.sin(np.sin(np.sin(x2)))
52-
with qt_app_context(exec_loop=True):
53-
plot(
54-
"curve",
60+
with qt_app_context():
61+
itemlists = [
5562
[
5663
make.curve(x, y, color="b"),
5764
make.label(
@@ -70,21 +77,26 @@ def test_syncplot_curves():
7077
make.label("Absolute position", "R", (0, 0), "R"),
7178
make.legend("TR"),
7279
],
73-
)
80+
]
81+
for cls in (SyncPlotWindow, SyncPlotDialog):
82+
show_with(cls, "curve", *itemlists)
7483

7584

7685
def test_syncplot_images():
7786
"""Test plot synchronization: images"""
7887
img1 = gen_2d_gaussian(20, np.uint8, x0=-10.0, y0=-10.0, mu=7.0, sigma=10.0)
7988
img2 = gen_2d_gaussian(20, np.uint8, x0=-10.0, y0=-10.0, mu=5.0, sigma=8.0)
8089
img3 = gen_2d_gaussian(20, np.uint8, x0=-10.0, y0=-10.0, mu=3.0, sigma=6.0)
81-
with qt_app_context(exec_loop=True):
90+
with qt_app_context():
8291

8392
def makeim(data):
8493
"""Make image item"""
8594
return make.image(data, interpolation="nearest")
8695

87-
plot("image", [makeim(img1)], [makeim(img2)], [makeim(img3)])
96+
itemlists = [[makeim(img1)], [makeim(img2)], [makeim(img3)]]
97+
98+
for cls in (SyncPlotWindow, SyncPlotDialog):
99+
show_with(cls, "image", *itemlists)
88100

89101

90102
if __name__ == "__main__":

0 commit comments

Comments
 (0)