Source code for sas.qtgui.Plotting.PlotterBase

import numpy

from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets, QtPrintSupport

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar

import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib import rcParams

from packaging import version

DEFAULT_CMAP = mpl.cm.jet
from sas.qtgui.Plotting.PlotterData import Data1D

from sas.qtgui.Plotting.ScaleProperties import ScaleProperties
from sas.qtgui.Plotting.WindowTitle import WindowTitle
from sas.qtgui.Plotting.Binder import BindArtist
import sas.qtgui.Utilities.GuiUtils as GuiUtils
import sas.qtgui.Plotting.PlotHelper as PlotHelper


[docs]class PlotterBase(QtWidgets.QWidget):
[docs] def __init__(self, parent=None, manager=None, quickplot=False): super(PlotterBase, self).__init__(parent) # Required for the communicator self.manager = manager self.quickplot = quickplot # Set auto layout so x/y axis captions don't get cut off rcParams.update({'figure.autolayout': True}) #plt.style.use('ggplot') #plt.style.use('seaborn-darkgrid') # a figure instance to plot on self.figure = plt.figure() # Define canvas for the figure to be placed on self.canvas = FigureCanvas(self.figure) # Simple window for data display self.txt_widget = QtWidgets.QTextEdit(None) # Set the layout and place the canvas widget in it. layout = QtWidgets.QVBoxLayout() # FIXME setMargin -> setContentsMargins in qt5 with 4 args #layout.setContentsMargins(0) layout.addWidget(self.canvas) # 1D plotter defaults self.current_plot = 111 self._data = [] # Original 1D/2D object self._xscale = 'log' self._yscale = 'log' self.qx_data = [] self.qy_data = [] self.color = 0 self.symbol = 0 self.grid_on = False self.scale = 'linear' self.x_label = "log10(x)" self.y_label = "log10(y)" # Mouse click related self._scale_xlo = None self._scale_xhi = None self._scale_ylo = None self._scale_yhi = None self.x_click = None self.y_click = None self.event_pos = None self.leftdown = False self.gotLegend = 0 self.show_legend = True # Annotations self.selectedText = None self.textList = [] # Create Artist and bind it self.connect = BindArtist(self.figure) # Pre-define the Scale properties dialog self.properties = ScaleProperties(self, init_scale_x=self.x_label, init_scale_y=self.y_label) # default color map self.cmap = DEFAULT_CMAP # Add the axes object -> subplot # TODO: self.ax will have to be tracked and exposed # to enable subplot specific operations self.ax = self.figure.add_subplot(self.current_plot) # Remove this, DAMMIT self.axes = [self.ax] # Set the background color to white self.canvas.figure.set_facecolor('#FFFFFF') # Canvas event handlers self.canvas.mpl_connect('button_release_event', self.onMplMouseUp) self.canvas.mpl_connect('button_press_event', self.onMplMouseDown) self.canvas.mpl_connect('motion_notify_event', self.onMplMouseMotion) self.canvas.mpl_connect('pick_event', self.onMplPick) self.canvas.mpl_connect('scroll_event', self.onMplWheel) self.contextMenu = QtWidgets.QMenu(self) self.toolbar = NavigationToolbar(self.canvas, self) cid = self.canvas.mpl_connect('resize_event', self.onResize) layout.addWidget(self.toolbar) if not quickplot: # Add the toolbar # self.toolbar.show() self.toolbar.hide() # hide for the time being # Notify PlotHelper about the new plot self.upatePlotHelper() else: self.toolbar.hide() self.setLayout(layout)
@property def data(self): """ data getter """ return self._data @data.setter def data(self, data=None): """ Pure virtual data setter """ raise NotImplementedError("Data setter must be implemented in derived class.")
[docs] def title(self, title=""): """ title setter """ self._title = title # Set the object name to satisfy the Squish object picker self.canvas.setObjectName(title)
@property def item(self): ''' getter for this plot's QStandardItem ''' return self._item @item.setter def item(self, item=None): ''' setter for this plot's QStandardItem ''' self._item = item @property def xLabel(self, xlabel=""): """ x-label setter """ return self.x_label @xLabel.setter def xLabel(self, xlabel=""): """ x-label setter """ self.x_label = r'$%s$'% xlabel if xlabel else "" @property def yLabel(self, ylabel=""): """ y-label setter """ return self.y_label @yLabel.setter def yLabel(self, ylabel=""): """ y-label setter """ self.y_label = r'$%s$'% ylabel if ylabel else "" @property def yscale(self): """ Y-axis scale getter """ return self._yscale @yscale.setter def yscale(self, scale='linear'): """ Y-axis scale setter """ if version.parse(mpl.__version__) < version.parse("3.3"): self.ax.set_yscale(scale, nonposy='clip') if scale != 'linear' else self.ax.set_yscale(scale) else: self.ax.set_yscale(scale, nonpositive='clip') if scale != 'linear' else self.ax.set_yscale(scale) self._yscale = scale @property def xscale(self): """ X-axis scale getter """ return self._xscale @xscale.setter def xscale(self, scale='linear'): """ X-axis scale setter """ self.ax.cla() if version.parse(mpl.__version__) < version.parse("3.3"): self.ax.set_xscale(scale, nonposx='clip') if scale != 'linear' else self.ax.set_xscale(scale) else: self.ax.set_xscale(scale, nonpositive='clip') if scale != 'linear' else self.ax.set_xscale(scale) self._xscale = scale @property def showLegend(self): """ Legend visibility getter """ return self.show_legend @showLegend.setter def showLegend(self, show=True): """ Legend visibility setter """ self.show_legend = show
[docs] def update(self): self.figure.canvas.draw()
[docs] def draw(self): self.figure.canvas.draw()
[docs] def upatePlotHelper(self): """ Notify the plot helper about the new plot """ # Notify the helper PlotHelper.addPlot(self) # Notify the listeners about a new graph self.manager.communicator.activeGraphsSignal.emit([self, False])
[docs] def defaultContextMenu(self): """ Content of the dialog-universal context menu: Save, Print and Copy """ # Actions self.contextMenu.clear() self.actionSaveImage = self.contextMenu.addAction("Save Image") self.actionPrintImage = self.contextMenu.addAction("Print Image") self.actionCopyToClipboard = self.contextMenu.addAction("Copy to Clipboard") self.contextMenu.addSeparator() # Define the callbacks self.actionSaveImage.triggered.connect(self.onImageSave) self.actionPrintImage.triggered.connect(self.onImagePrint) self.actionCopyToClipboard.triggered.connect(self.onClipboardCopy)
[docs] def createContextMenu(self): """ Define common context menu and associated actions for the MPL widget """ raise NotImplementedError("Context menu method must be implemented in derived class.")
[docs] def createContextMenuQuick(self): """ Define context menu and associated actions for the quickplot MPL widget """ raise NotImplementedError("Context menu method must be implemented in derived class.")
[docs] def onResize(self, event): """ Redefine default resize event """ pass
[docs] def contextMenuEvent(self, event): """ Display the context menu """ if not self.quickplot: self.createContextMenu() else: self.createContextMenuQuick() event_pos = event.pos() self.contextMenu.exec_(self.canvas.mapToGlobal(event_pos))
[docs] def onMplMouseUp(self, event): """ Mouse button up callback """ pass
[docs] def onMplMouseDown(self, event): """ Mouse button down callback """ pass
[docs] def onMplMouseMotion(self, event): """ Mouse motion callback """ pass
[docs] def onMplPick(self, event): """ Mouse pick callback """ pass
[docs] def onMplWheel(self, event): """ Mouse wheel scroll callback """ pass
[docs] def clean(self): """ Redraw the graph """ self.figure.delaxes(self.ax) self.ax = self.figure.add_subplot(self.current_plot)
[docs] def plot(self, marker=None, linestyle=None): """ PURE VIRTUAL Plot the content of self._data """ raise NotImplementedError("Plot method must be implemented in derived class.")
[docs] def closeEvent(self, event): """ Overwrite the close event adding helper notification """ self.clearQRangeSliders() # Please remove me from your database. PlotHelper.deletePlot(PlotHelper.idOfPlot(self)) # Notify the listeners self.manager.communicator.activeGraphsSignal.emit([self, True]) event.accept()
[docs] def clearQRangeSliders(self): # Destroy the Q-range sliders in 1D plots if hasattr(self, 'sliders') and isinstance(self.sliders, dict): for slider in self.sliders.values(): slider.clear() self.sliders = {}
[docs] def onImageSave(self): """ Use the internal MPL method for saving to file """ if not hasattr(self, "toolbar"): self.toolbar = NavigationToolbar(self.canvas, self) self.toolbar.save_figure()
[docs] def onImagePrint(self): """ Display printer dialog and print the MPL widget area """ # Define the printer printer = QtPrintSupport.QPrinter() # Display the print dialog dialog = QtPrintSupport.QPrintDialog(printer) dialog.setModal(True) dialog.setWindowTitle("Print") if dialog.exec_() != QtWidgets.QDialog.Accepted: return painter = QtGui.QPainter(printer) # Grab the widget screenshot pmap = QtGui.QPixmap(self.size()) self.render(pmap) # Create a label with pixmap drawn printLabel = QtWidgets.QLabel() printLabel.setPixmap(pmap) # Print the label printLabel.render(painter) painter.end()
[docs] def onClipboardCopy(self): """ Copy MPL widget area to buffer """ bmp = QtWidgets.QApplication.clipboard() pixmap = QtGui.QPixmap(self.canvas.size()) self.canvas.render(pixmap) bmp.setPixmap(pixmap)
[docs] def onGridToggle(self): """ Add/remove grid lines from MPL plot """ self.grid_on = (not self.grid_on) self.ax.grid(self.grid_on) self.canvas.draw_idle()
[docs] def onWindowsTitle(self): """ Show a dialog allowing chart title customisation """ current_title = self.windowTitle() titleWidget = WindowTitle(self, new_title=current_title) result = titleWidget.exec_() if result != QtWidgets.QDialog.Accepted: return title = titleWidget.title() self.setWindowTitle(title) # Notify the listeners about a new graph title self.manager.communicator.activeGraphName.emit((current_title, title))
[docs] def onToggleMenu(self): """ Toggle navigation menu visibility in the chart """ if self.toolbar.isVisible(): self.toolbar.hide() else: self.toolbar.show()
[docs] def offset_graph(self): """ Zoom and offset the graph to the last known settings """ for ax in self.axes: if self._scale_xhi is not None and self._scale_xlo is not None: ax.set_xlim(self._scale_xlo, self._scale_xhi) if self._scale_yhi is not None and self._scale_ylo is not None: ax.set_ylim(self._scale_ylo, self._scale_yhi)
[docs] def onDataInfo(self, plot_data): """ Displays data info text window for the selected plot """ if isinstance(plot_data, Data1D): text_to_show = GuiUtils.retrieveData1d(plot_data) else: text_to_show = GuiUtils.retrieveData2d(plot_data) # Hardcoded sizes to enable full width rendering with default font self.txt_widget.resize(420,600) self.txt_widget.clear() self.txt_widget.setReadOnly(True) self.txt_widget.setWindowFlags(QtCore.Qt.Window) self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico")) self.txt_widget.setWindowTitle("Data Info: %s" % plot_data.filename) self.txt_widget.insertPlainText(text_to_show) self.txt_widget.show() # Move the slider all the way up, if present vertical_scroll_bar = self.txt_widget.verticalScrollBar() vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
[docs] def onSavePoints(self, plot_data): """ Saves plot data to a file """ if isinstance(plot_data, Data1D): GuiUtils.saveData1D(plot_data) else: GuiUtils.saveData2D(plot_data)