-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathplotwidget.py
More file actions
1009 lines (848 loc) · 33.3 KB
/
plotwidget.py
File metadata and controls
1009 lines (848 loc) · 33.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8
from __future__ import annotations
import abc
import warnings
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from guidata.configtools import get_icon
from guidata.qthelpers import win32_fix_title_bar_background
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
from qtpy.QtWidgets import QWidget # only to help intersphinx find QWidget
from plotpy.config import _
from plotpy.constants import X_BOTTOM, Y_LEFT, PlotType
from plotpy.plot.base import BasePlot, BasePlotOptions
from plotpy.plot.manager import PlotManager
if TYPE_CHECKING:
from plotpy.panels.base import PanelWidget
def configure_plot_splitter(qsplit: QW.QSplitter, decreasing_size: bool = True) -> None:
"""Configure a QSplitter for plot widgets
Args:
qsplit: QSplitter instance
decreasing_size: Specify if child widgets can be resized down
to size 0 by the user. Defaults to True.
"""
qsplit.setChildrenCollapsible(False)
qsplit.setHandleWidth(4)
if decreasing_size:
qsplit.setStretchFactor(0, 1)
qsplit.setStretchFactor(1, 0)
qsplit.setSizes([2, 1])
else:
qsplit.setStretchFactor(0, 0)
qsplit.setStretchFactor(1, 1)
qsplit.setSizes([1, 2])
@dataclass
class PlotOptions(BasePlotOptions):
"""Plot options
Args:
title: The plot title
xlabel: (bottom axis title, top axis title) or bottom axis title only
ylabel: (left axis title, right axis title) or left axis title only
zlabel: The Z-axis label
xunit: (bottom axis unit, top axis unit) or bottom axis unit only
yunit: (left axis unit, right axis unit) or left axis unit only
zunit: The Z-axis unit
yreverse: If True, the Y-axis is reversed
aspect_ratio: The plot aspect ratio
lock_aspect_ratio: If True, the aspect ratio is locked
curve_antialiasing: If True, the curve antialiasing is enabled
gridparam: The grid parameters
section: The plot configuration section name ("plot", by default)
type: The plot type ("auto", "manual", "curve" or "image")
axes_synchronised: If True, the axes are synchronised
force_colorbar_enabled: If True, the colorbar is always enabled
no_image_analysis_widgets: If True, the image analysis widgets are not added
show_contrast: If True, the contrast adjustment panel is visible
show_itemlist: If True, the itemlist panel is visible
show_xsection: If True, the X-axis cross section panel is visible
show_ysection: If True, the Y-axis cross section panel is visible
xsection_pos: The X-axis cross section panel position ("top" or "bottom")
ysection_pos: The Y-axis cross section panel position ("left" or "right")
"""
no_image_analysis_widgets: bool = False
show_contrast: bool = False
show_itemlist: bool = False
show_xsection: bool = False
show_ysection: bool = False
xsection_pos: str = "top"
ysection_pos: str = "right"
def __post_init__(self) -> None:
"""Check arguments"""
super().__post_init__()
# Check xsection_pos and ysection_pos
if self.xsection_pos not in ["top", "bottom"]:
raise ValueError("xsection_pos must be 'top' or 'bottom'")
if self.ysection_pos not in ["left", "right"]:
raise ValueError("ysection_pos must be 'left' or 'right'")
# Show warning if no image analysis widgets is True and type is not manual
if self.no_image_analysis_widgets and self.type != "manual":
warnings.warn(
"no_image_analysis_widgets is True but type is not 'manual' "
"(option ignored)",
RuntimeWarning,
)
class BasePlotWidget(QW.QSplitter):
"""Base class for plot widgets
This widget is a ``QSplitter`` that contains a plot, several panels and a toolbar.
It also includes tools (as in the :py:mod:`.tools` module) for manipulating the
plot, its items and its panels. All these elements are managed by a
:py:class:`.PlotManager` object. The plot manager is accessible through the
:py:attr:`.PlotWidget.manager` attribute.
Args:
parent: The parent widget
options: Plot options
"""
def __init__(
self,
parent: QWidget = None,
options: PlotOptions | dict[str, Any] | None = None,
) -> None:
super().__init__(parent)
self.manager: PlotManager | None = None
if isinstance(options, dict):
options = PlotOptions(**options)
self.options = options = options if options is not None else PlotOptions()
self.setSizePolicy(QW.QSizePolicy.Expanding, QW.QSizePolicy.Expanding)
self.plot = self.create_plot()
# Avoid circular import
# pylint: disable=import-outside-toplevel
from plotpy.panels.itemlist import PlotItemList
self.itemlist = PlotItemList(self)
self.itemlist.setVisible(options.show_itemlist)
# For CURVE plots, or MANUAL plots with the ``no_image_analysis_widgets``
# option, don't add splitters and widgets dedicated to images since
# they make the widget more "laggy" when resized.
if options.type == PlotType.CURVE or (
options.type == PlotType.MANUAL and options.no_image_analysis_widgets
):
self.__set_curve_layout()
else:
self.__set_image_layout()
configure_plot_splitter(self)
# ---- Private API -----------------------------------------------------------------
def __set_curve_layout(self) -> None:
"""Set the layout for curve only plots"""
self.setOrientation(QC.Qt.Orientation.Horizontal)
self.addWidget(self.plot)
self.addWidget(self.itemlist)
self.xcsw = None
self.ycsw = None
self.contrast = None
def __set_image_layout(self) -> None:
"""Set the layout for image only plots"""
self.setOrientation(QC.Qt.Orientation.Vertical)
self.sub_splitter = QW.QSplitter(QC.Qt.Orientation.Horizontal, self)
# Avoid circular import
# pylint: disable=import-outside-toplevel
from plotpy.panels.csection.cswidget import XCrossSection, YCrossSection
self.ycsw = YCrossSection(
self,
position=self.options.ysection_pos,
xsection_pos=self.options.xsection_pos,
)
self.ycsw.setVisible(self.options.show_ysection)
self.xcsw = XCrossSection(self)
self.xcsw.setVisible(self.options.show_xsection)
self.xcsw.SIG_VISIBILITY_CHANGED.connect(self.__xcsw_is_visible)
self.xcsw.SIG_RESIZED.connect(self.__adjust_ycsw_height)
self.xcsw_splitter = QW.QSplitter(QC.Qt.Orientation.Vertical, self)
if self.options.xsection_pos == "top":
self.xcsw_splitter.addWidget(self.xcsw)
self.xcsw_splitter.addWidget(self.plot)
else:
self.xcsw_splitter.addWidget(self.plot)
self.xcsw_splitter.addWidget(self.xcsw)
self.ycsw_splitter = QW.QSplitter(QC.Qt.Orientation.Horizontal, self)
if self.options.ysection_pos == "left":
self.ycsw_splitter.addWidget(self.ycsw)
self.ycsw_splitter.addWidget(self.xcsw_splitter)
else:
self.ycsw_splitter.addWidget(self.xcsw_splitter)
self.ycsw_splitter.addWidget(self.ycsw)
configure_plot_splitter(
self.xcsw_splitter, decreasing_size=self.options.xsection_pos == "bottom"
)
configure_plot_splitter(
self.ycsw_splitter, decreasing_size=self.options.ysection_pos == "right"
)
self.sub_splitter.addWidget(self.ycsw_splitter)
self.sub_splitter.addWidget(self.itemlist)
# Contrast adjustment (Levels histogram)
# Avoid circular import
# pylint: disable=import-outside-toplevel
from plotpy.panels.contrastadjustment import ContrastAdjustment
self.contrast = ContrastAdjustment(self)
self.contrast.setVisible(self.options.show_contrast)
self.addWidget(self.contrast)
configure_plot_splitter(self.sub_splitter)
def __adjust_ycsw_height(self, height: int | None = None) -> None:
"""Adjust the Y-axis cross section panel height
Args:
height: The height (in pixels) to set. If None, the height is adjusted
to the current height of the widget.
"""
if height is None:
height = self.xcsw.height() - self.ycsw.toolbar.height()
self.ycsw.adjust_height(height)
def __xcsw_is_visible(self, state: bool) -> None:
"""Callback when the X-axis cross section panel visibility changes
Args:
state: The new visibility state of the X-axis cross section panel
"""
if state:
self.__adjust_ycsw_height()
else:
self.__adjust_ycsw_height(0)
# ---- Public API ------------------------------------------------------------------
def create_plot(self) -> BasePlot:
"""Create the plot, which is the main widget of the base plot widget.
In subclasses, this method can be overriden to create a custom plot object,
as for example multiple plots in a grid layout."""
return BasePlot(parent=self, options=self.options)
def add_panels_to_manager(self) -> None:
"""Add the panels to the plot manager
Raises:
RuntimeError: If the plot manager is not defined
"""
if self.manager is None:
raise RuntimeError("Plot manager is not defined")
self.manager.add_panel(self.itemlist)
if self.xcsw is not None:
self.manager.add_panel(self.xcsw)
if self.ycsw is not None:
self.manager.add_panel(self.ycsw)
if self.contrast is not None:
self.manager.add_panel(self.contrast)
def register_tools(self) -> None:
"""Register the plotting tools according to the plot type
Raises:
RuntimeError: If the plot manager is not defined
"""
if self.manager is None:
raise RuntimeError("Plot manager is not defined")
if self.options.type == PlotType.CURVE:
self.manager.register_all_curve_tools()
elif self.options.type == PlotType.IMAGE:
self.manager.register_all_image_tools()
elif self.options.type == PlotType.AUTO:
self.manager.register_all_tools()
def register_annotation_tools(self) -> None:
"""Register the annotation tools according to the plot type
Raises:
RuntimeError: If the plot manager is not defined
"""
if self.manager is None:
raise RuntimeError("Plot manager is not defined")
if self.options.type == PlotType.CURVE:
self.manager.register_curve_annotation_tools()
elif self.options.type == PlotType.IMAGE:
self.manager.register_image_annotation_tools()
elif self.options.type == PlotType.AUTO:
self.manager.register_all_annotation_tools()
class PlotWidget(BasePlotWidget):
"""Plot widget with integrated plot manager, toolbar, tools and panels
Args:
parent: Parent widget
toolbar: Show/hide toolbar
options: Plot options
panels: Additionnal panels
auto_tools: If True, the plot tools are automatically registered.
If False, the user must register the tools manually.
"""
def __init__(
self,
parent: QWidget | None = None,
toolbar: bool = False,
options: PlotOptions | dict[str, Any] | None = None,
panels: tuple[PanelWidget] | None = None,
auto_tools: bool = True,
) -> None:
super().__init__(parent, options)
self.manager = PlotManager(self)
self.toolbar: QW.QToolBar | None = None
self.configure_manager(panels, toolbar)
if auto_tools:
self.register_tools()
# ---- Public API ------------------------------------------------------------------
def get_plot(self) -> BasePlot:
"""Return the plot object
Returns:
BasePlot: The plot object
"""
return self.plot
def get_toolbar(self) -> QW.QToolBar:
"""Return main toolbar
Returns:
The plot widget main toolbar
"""
return self.toolbar
def get_manager(self) -> PlotManager:
"""Return the plot manager
Returns:
The plot widget manager
"""
return self.manager
def configure_manager(
self,
panels: tuple[PanelWidget] | None = None,
toolbar: bool = True,
) -> None:
"""Configure the plot manager
Args:
panels: additionnal panels (list, tuple). Defaults to None.
toolbar: [description]. Defaults to True.
"""
self.manager.add_plot(self.get_plot())
self.add_panels_to_manager()
if panels is not None:
for panel in panels:
self.manager.add_panel(panel)
self.toolbar = QW.QToolBar(_("Tools"))
if toolbar:
self.manager.add_toolbar(self.toolbar, "default")
else:
self.toolbar.hide()
def set_widget_title_icon(
widget: QWidget, title: str, icon: QG.QIcon, size: tuple[int, int] | None = None
) -> None:
"""Setups the widget title and icon
Args:
title: The window title
icon: The window icon
size: The window size (width, height). Defaults to None (no resize)
"""
win32_fix_title_bar_background(widget)
widget.setWindowTitle(title)
if isinstance(icon, str):
icon = get_icon(icon)
if icon is not None:
widget.setWindowIcon(icon)
widget.setMinimumSize(320, 240)
if size is not None:
widget.resize(*size)
def add_widget_to_grid_layout(
layout: QW.QGridLayout,
widget: QWidget,
row: int | None = None,
column: int | None = None,
rowspan: int | None = None,
columnspan: int | None = None,
) -> None:
"""Add widget to the grid layout
Args:
layout: The layout
widget: The widget to add
row: The row index
column: The column index
rowspan: The row span
columnspan: The column span
If row, column, rowspan and columnspan are None, the widget is added to the grid
layout after the previous one (i.e. same as the method addWidget without options).
If row is not None, column must be not None and the widget is added to the grid
layout at the specified row and column. If rowspan is not None, the widget is
added to the grid layout with the specified row span. If columnspan is not None,
the widget is added to the grid layout with the specified column span.
"""
if row is None and column is None and rowspan is None and columnspan is None:
layout.addWidget(widget)
else:
if row is None or column is None:
raise ValueError("row and column must be specified")
if rowspan is None and columnspan is None:
layout.addWidget(widget, row, column)
elif rowspan is None:
layout.addWidget(widget, row, column, 1, columnspan)
elif columnspan is None:
layout.addWidget(widget, row, column, rowspan, 1)
else:
layout.addWidget(widget, row, column, rowspan, columnspan)
class AbstractPlotDialogWindow(abc.ABC):
"""Abstract base class for plot dialog and plot window"""
@abc.abstractmethod
def get_plot(self) -> BasePlot | None:
"""Return the plot object
Returns:
BasePlot: The plot object
"""
@abc.abstractmethod
def get_toolbar(self) -> QW.QToolBar:
"""Return main toolbar
Returns:
The plot widget main toolbar
"""
@abc.abstractmethod
def get_manager(self) -> PlotManager:
"""Return the plot manager
Returns:
The plot widget manager
"""
@abc.abstractmethod
def setup_widget(
self,
toolbar: bool = False,
options: PlotOptions | dict[str, Any] | None = None,
panels: list[PanelWidget] | None = None,
auto_tools: bool = False,
) -> None:
"""Setup the widget
Args:
toolbar: If True, the plot toolbar is displayed
options: Plot options
panels: The panels to add to the plot
auto_tools: If True, the plot tools are automatically registered.
If False, the user must register the tools manually.
"""
@abc.abstractmethod
def add_widget(
self,
widget: QWidget,
row: int | None = None,
column: int | None = None,
rowspan: int | None = None,
columnspan: int | None = None,
) -> None:
"""Add widget to the widget main layout
Args:
widget: The widget to add
row: The row index
column: The column index
rowspan: The row span
columnspan: The column span
"""
@abc.abstractmethod
def populate_plot_layout(self) -> None:
"""Populate the plot layout"""
@abc.abstractmethod
def setup_layout(self) -> None:
"""Setup the widget layout"""
@abc.abstractmethod
def register_tools(self):
"""
Register the plotting tools: the base implementation of this method
register tools according to the plot type (curve, image, etc.)
This method may be overriden to provide a fully customized set of tools
"""
@abc.abstractmethod
def register_annotation_tools(self):
"""
Register the annotation tools: the base implementation of this method
register tools according to the plot type (curve, image, etc.)
This method may be overriden to provide a fully customized set of tools
"""
class PlotDialogMeta(type(QW.QDialog), abc.ABCMeta):
"""Mixed metaclass to avoid conflicts"""
class PlotDialog(QW.QDialog, AbstractPlotDialogWindow, metaclass=PlotDialogMeta):
"""Plotting dialog box with integrated plot manager
Args:
parent: parent widget
toolbar: show/hide toolbar
options: plot options
panels: additionnal panels
auto_tools: If True, the plot tools are automatically registered.
If False, the user must register the tools manually.
title: The window title
icon: The window icon
edit: If True, the plot is editable
size: The window size (width, height). Defaults to None (no resize)
"""
def __init__(
self,
parent: QWidget | None = None,
toolbar: bool = False,
options: PlotOptions | dict[str, Any] | None = None,
panels: list[PanelWidget] | None = None,
auto_tools: bool = True,
title: str = "PlotPy",
icon: str = "plotpy.svg",
edit: bool = False,
size: tuple[int, int] | None = None,
) -> None:
super().__init__(parent)
set_widget_title_icon(self, title, icon, size)
self.edit = edit
self.button_box = None
self.button_layout = None
self.plot_layout = QW.QGridLayout()
self.plot_widget: PlotWidget = None
self.manager: PlotManager = None
self.setup_widget(toolbar, options, panels, auto_tools)
self.setWindowFlags(QC.Qt.Window)
def get_plot(self) -> BasePlot | None:
"""Return the plot object
Returns:
BasePlot: The plot object
"""
if self.plot_widget is not None:
return self.plot_widget.get_plot()
return None
def get_toolbar(self) -> QW.QToolBar:
"""Return main toolbar
Returns:
The plot widget main toolbar
"""
if self.plot_widget is not None:
return self.plot_widget.get_toolbar()
return None
def get_manager(self) -> PlotManager:
"""Return the plot manager
Returns:
The plot widget manager
"""
if self.plot_widget is not None:
return self.plot_widget.get_manager()
return None
def setup_widget(
self,
toolbar: bool = False,
options: PlotOptions | dict[str, Any] | None = None,
panels: list[PanelWidget] | None = None,
auto_tools: bool = False,
) -> None:
"""Setup the widget
Args:
toolbar: If True, the plot toolbar is displayed
options: Plot options
panels: The panels to add to the plot
auto_tools: If True, the plot tools are automatically registered.
If False, the user must register the tools manually.
"""
self.plot_widget = PlotWidget(self, toolbar, options, panels, auto_tools=False)
self.manager = self.plot_widget.manager
self.populate_plot_layout()
self.setup_layout()
if auto_tools:
self.register_tools()
def add_widget(
self,
widget: QWidget,
row: int | None = None,
column: int | None = None,
rowspan: int | None = None,
columnspan: int | None = None,
) -> None:
"""Add widget to the widget main layout
Args:
widget: The widget to add
row: The row index
column: The column index
rowspan: The row span
columnspan: The column span
"""
add_widget_to_grid_layout(
self.plot_layout, widget, row, column, rowspan, columnspan
)
def populate_plot_layout(self) -> None:
"""Populate the plot layout"""
self.add_widget(self.plot_widget)
def setup_layout(self) -> None:
"""Setup the widget layout"""
vlayout = QW.QVBoxLayout(self)
vlayout.addWidget(self.plot_widget.toolbar)
vlayout.addLayout(self.plot_layout)
self.setLayout(vlayout)
if self.edit:
self.button_layout = QW.QHBoxLayout()
self.install_button_layout()
vlayout.addLayout(self.button_layout)
def install_button_layout(self) -> None:
"""Install standard buttons (OK, Cancel) in dialog button box layout
This method may be overriden to customize the button box
"""
bbox = QW.QDialogButtonBox(QW.QDialogButtonBox.Ok | QW.QDialogButtonBox.Cancel)
bbox.accepted.connect(self.accept)
bbox.rejected.connect(self.reject)
self.button_layout.addWidget(bbox)
self.button_box = bbox
def register_tools(self):
"""Register the plotting tools: the base implementation of this method
register tools according to the plot type (curve, image, etc.)
This method may be overriden to provide a fully customized set of tools
"""
self.plot_widget.register_tools()
def register_annotation_tools(self):
"""Register the annotation tools: the base implementation of this method
register tools according to the plot type (curve, image, etc.)
This method may be overriden to provide a fully customized set of tools
"""
self.plot_widget.register_annotation_tools()
class PlotWindowMeta(type(QW.QMainWindow), abc.ABCMeta):
"""Mixed metaclass to avoid conflicts"""
class PlotWindow(QW.QMainWindow, AbstractPlotDialogWindow, metaclass=PlotWindowMeta):
"""Plotting window with integrated plot manager
Args:
parent: parent widget
toolbar: show/hide toolbar
options: plot options
panels: additionnal panels
auto_tools: If True, the plot tools are automatically registered.
If False, the user must register the tools manually.
title: The window title
icon: The window icon
size: The window size (width, height). Defaults to None (no resize)
"""
def __init__(
self,
parent: QWidget | None = None,
toolbar: bool = False,
options: PlotOptions | dict[str, Any] | None = None,
panels: list[PanelWidget] | None = None,
auto_tools: bool = True,
title: str = "PlotPy",
icon: str = "plotpy.svg",
size: tuple[int, int] | None = None,
) -> None:
super().__init__(parent)
set_widget_title_icon(self, title, icon, size)
self.plot_layout = QW.QGridLayout()
self.plot_widget: PlotWidget = None
self.manager: PlotManager = None
self.setup_widget(toolbar, options, panels, auto_tools)
def get_plot(self) -> BasePlot | None:
"""Return the plot object
Returns:
BasePlot: The plot object
"""
if self.plot_widget is not None:
return self.plot_widget.get_plot()
return None
def get_toolbar(self) -> QW.QToolBar:
"""Return main toolbar
Returns:
The plot widget main toolbar
"""
if self.plot_widget is not None:
return self.plot_widget.get_toolbar()
return None
def get_manager(self) -> PlotManager:
"""Return the plot manager
Returns:
The plot widget manager
"""
if self.plot_widget is not None:
return self.plot_widget.get_manager()
return None
def setup_widget(
self,
toolbar: bool = False,
options: PlotOptions | dict[str, Any] | None = None,
panels: list[PanelWidget] | None = None,
auto_tools: bool = False,
) -> None:
"""Setup the widget
Args:
toolbar: If True, the plot toolbar is displayed
options: Plot options
panels: The panels to add to the plot
auto_tools: If True, the plot tools are automatically registered.
If False, the user must register the tools manually.
"""
self.plot_widget = PlotWidget(self, toolbar, options, panels, auto_tools=False)
self.manager = self.plot_widget.manager
self.populate_plot_layout()
self.setup_layout()
if auto_tools:
self.register_tools()
def add_widget(
self,
widget: QWidget,
row: int | None = None,
column: int | None = None,
rowspan: int | None = None,
columnspan: int | None = None,
) -> None:
"""Add widget to the widget main layout
Args:
widget: The widget to add
row: The row index
column: The column index
rowspan: The row span
columnspan: The column span
"""
add_widget_to_grid_layout(
self.plot_layout, widget, row, column, rowspan, columnspan
)
def populate_plot_layout(self) -> None:
"""Populate the plot layout"""
self.add_widget(self.plot_widget)
def setup_layout(self) -> None:
"""Setup the widget layout"""
self.addToolBar(self.plot_widget.toolbar)
widget = QWidget()
widget.setLayout(self.plot_layout)
self.setCentralWidget(widget)
def register_tools(self):
"""
Register the plotting tools: the base implementation of this method
register tools according to the plot type (curve, image, etc.)
This method may be overriden to provide a fully customized set of tools
"""
self.plot_widget.register_tools()
def register_annotation_tools(self):
"""Register the annotation tools: the base implementation of this method
register tools according to the plot type (curve, image, etc.)
This method may be overriden to provide a fully customized set of tools
"""
self.plot_widget.register_annotation_tools()
def closeEvent(self, event) -> None:
"""Reimplement the close event to close all panels
Args:
event: The close event
"""
# Closing panels (necessary if at least one of these panels has no
# parent widget: otherwise, this panel will stay open after the main
# window has been closed which is not the expected behavior)
for panel in self.manager.panels:
self.manager.get_panel(panel).close()
QW.QMainWindow.closeEvent(self, event)
class SubplotWidget(BasePlotWidget):
"""Construct a Widget that helps managing several plots
together handled by the same manager
Since the plots must be added to the manager before the panels
the add_itemlist method can be called after having declared
all the subplots
Args:
manager: The plot manager
parent: The parent widget
options: Plot options
"""
def __init__(
self,
manager: PlotManager,
parent: QWidget | None = None,
options: PlotOptions | dict[str, Any] | None = None,
) -> None:
self.plotlayout: QW.QGridLayout | None = None
super().__init__(parent, options=options)
self.manager = manager
self.plots: list[BasePlot] = []
def create_plot(self) -> QWidget:
"""Create the plot, which is the main widget of the plot widget"""
main = QWidget()
self.plotlayout = QW.QGridLayout()
main.setLayout(self.plotlayout)
return main
def get_plots(self) -> list[BasePlot]:
"""Return the plots
Returns:
list[BasePlot]: The plots
"""
return self.plots
def add_plot(
self, plot: BasePlot, i: int = 0, j: int = 0, plot_id: str | None = None
) -> None:
"""Add a plot to the grid of plots
Args:
plot: The plot to add
i: The row index
j: The column index
plot_id: The plot id
"""
self.plotlayout.addWidget(plot, i, j)
self.plots.append(plot)
if plot_id is None:
plot_id = id(plot)
self.manager.add_plot(plot, plot_id)
class SyncPlotWindow(QW.QMainWindow):
"""Window for showing plots, optionally synchronized
Args:
parent: parent widget
toolbar: show/hide toolbar
options: plot options
panels: additionnal panels
auto_tools: If True, the plot tools are automatically registered.
If False, the user must register the tools manually.
title: The window title
icon: The window icon
size: The window size (width, height). Defaults to None (no resize)
Usage: first, create a window, then add plots to it, then call the
:py:meth:`.SyncPlotWindow.finalize_configuration` method to add panels and
eventually register tools.
Example::
from plotpy.plot import BasePlot, SyncPlotWindow
win = SyncPlotWindow(title="My window")
plot = BasePlot()
win.add_plot(plot)
win.finalize_configuration()
win.show()
"""
def __init__(
self,
parent: QWidget | None = None,
toolbar: bool = True,
options: PlotOptions | dict[str, Any] | None = None,
auto_tools: bool = True,
title: str = "PlotPy",
icon: str = "plotpy.svg",
size: tuple[int, int] | None = None,
) -> None:
super().__init__(parent)
set_widget_title_icon(self, title, icon, size)
self.manager = PlotManager(None)
self.manager.set_main(self)
self.subplotwidget = SubplotWidget(self.manager, parent=self, options=options)
self.setCentralWidget(self.subplotwidget)
self.toolbar = QW.QToolBar(_("Tools"), self)
self.toolbar.setVisible(toolbar)
self.manager.add_toolbar(self.toolbar, "default")
self.toolbar.setMovable(True)
self.toolbar.setFloatable(True)
self.addToolBar(self.toolbar)
self.auto_tools = auto_tools
def get_toolbar(self) -> QW.QToolBar:
"""Return main toolbar
Returns:
The plot widget main toolbar
"""
return self.toolbar
def get_manager(self) -> PlotManager:
"""Return the plot manager
Returns:
The plot widget manager
"""
return self.manager
def finalize_configuration(self) -> None:
"""Configure plot manager and register all tools"""
self.subplotwidget.add_panels_to_manager()
if self.auto_tools:
self.subplotwidget.register_tools()
def rescale_plots(self) -> None:
"""Rescale all plots"""
QW.QApplication.instance().processEvents()
for plot in self.subplotwidget.plots:
plot.do_autoscale()
def showEvent(self, event): # pylint: disable=C0103
"""Reimplement Qt method"""
super().showEvent(event)
QC.QTimer.singleShot(0, self.rescale_plots)
def add_plot(
self,
row: int,
col: int,
plot: BasePlot,
sync: bool = False,
plot_id: str | None = None,
) -> None:
"""Add plot to window
Args:
row: The row index
col: The column index
plot: The plot to add
sync: If True, the axes are synchronized
plot_id: The plot id
"""
if plot_id is None:
plot_id = str(len(self.subplotwidget.plots) + 1)
self.subplotwidget.add_plot(plot, row, col, plot_id)
if sync and len(self.subplotwidget.plots) > 1:
syncaxis = self.manager.synchronize_axis
for i_plot in range(len(self.subplotwidget.plots) - 1):
syncaxis(X_BOTTOM, [plot_id, f"{i_plot + 1}"])