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 onResize(self, event):
"""
Redefine default resize event
"""
pass
[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 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)