diff --git a/qt/python/mantidqt/widgets/colorbar/__init__.py b/qt/python/mantidqt/widgets/colorbar/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1d6beeb60af03d5f011fc43d4e12d4bdfddaea04 --- /dev/null +++ b/qt/python/mantidqt/widgets/colorbar/__init__.py @@ -0,0 +1,21 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + +# This file is part of the mantid workbench. +# +# +# Can be launch from python by _e.g_ +# +# +# from mantidqt.widgets.colorbar.colorbar import ColorbarWidget +# from qtpy.QtWidgets import QApplication +# import matplotlib.pyplot as plt +# ax=plt.imshow([[0,1],[2,100]]) +# app = QApplication([]) +# window = ColorbarWidget() +# window.set_mappable(ax) +# window.show() +# app.exec_() diff --git a/qt/python/mantidqt/widgets/colorbar/colorbar.py b/qt/python/mantidqt/widgets/colorbar/colorbar.py new file mode 100644 index 0000000000000000000000000000000000000000..c54b5893cea00106cb4881a274b60fa5434e9720 --- /dev/null +++ b/qt/python/mantidqt/widgets/colorbar/colorbar.py @@ -0,0 +1,181 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + +# This file is part of the mantid workbench. +# +# +from __future__ import (absolute_import, division, print_function) +from qtpy.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QComboBox, QCheckBox, QLabel +from qtpy.QtCore import Signal +from qtpy.QtGui import QDoubleValidator +from matplotlib.colorbar import Colorbar +from matplotlib.figure import Figure +from mantidqt.MPLwidgets import FigureCanvas +from matplotlib.colors import Normalize, SymLogNorm, PowerNorm + + +NORM_OPTS = ["Linear", "SymmetricLog10", "Power"] + + +class ColorbarWidget(QWidget): + colorbarChanged = Signal() # The parent should simply redraw their canvas + + def __init__(self, parent=None): + super(ColorbarWidget, self).__init__(parent) + + self.setWindowTitle("Colorbar") + self.setMaximumWidth(200) + + self.dval = QDoubleValidator() + + self.cmin = QLineEdit() + self.cmin_value = 0 + self.cmin.setMaximumWidth(100) + self.cmin.editingFinished.connect(self.clim_changed) + self.cmin_layout = QHBoxLayout() + self.cmin_layout.addStretch() + self.cmin_layout.addWidget(self.cmin) + self.cmin_layout.addStretch() + + self.cmax = QLineEdit() + self.cmax_value = 1 + self.cmax.setMaximumWidth(100) + self.cmax.editingFinished.connect(self.clim_changed) + self.cmin.setValidator(self.dval) + self.cmax.setValidator(self.dval) + self.cmax_layout = QHBoxLayout() + self.cmax_layout.addStretch() + self.cmax_layout.addWidget(self.cmax) + self.cmax_layout.addStretch() + + self.norm_layout = QHBoxLayout() + self.norm = QComboBox() + self.norm.addItems(NORM_OPTS) + self.norm.currentIndexChanged.connect(self.norm_changed) + + self.powerscale = QLineEdit() + self.powerscale_value = 2 + self.powerscale.setText("2") + self.powerscale.setValidator(QDoubleValidator(0.001,100,3)) + self.powerscale.setMaximumWidth(50) + self.powerscale.editingFinished.connect(self.norm_changed) + self.powerscale.hide() + self.powerscale_label = QLabel("n=") + self.powerscale_label.hide() + + self.norm_layout.addStretch() + self.norm_layout.addWidget(self.norm) + self.norm_layout.addStretch() + self.norm_layout.addWidget(self.powerscale_label) + self.norm_layout.addWidget(self.powerscale) + + self.autoscale = QCheckBox("Autoscaling") + self.autoscale.setChecked(True) + self.autoscale.stateChanged.connect(self.update_clim) + + self.canvas = FigureCanvas(Figure()) + if parent: + # Set facecolor to match parent + self.canvas.figure.set_facecolor(parent.palette().window().color().getRgbF()) + self.ax = self.canvas.figure.add_axes([0.4,0.05,0.2,0.9]) + + # layout + self.layout = QVBoxLayout(self) + self.layout.addLayout(self.cmax_layout) + self.layout.addWidget(self.canvas, stretch=1) + self.layout.addLayout(self.cmin_layout) + self.layout.addLayout(self.norm_layout) + self.layout.addWidget(self.autoscale) + + def set_mappable(self, mappable): + """ + When a new plot is created this method should be called with the new mappable + """ + self.ax.clear() + self.colorbar = Colorbar(ax=self.ax, mappable=mappable) + self.cmin_value, self.cmax_value = self.colorbar.get_clim() + self.update_clim_text() + self.redraw() + + def norm_changed(self): + """ + Called when a different normalization is selected + """ + idx = self.norm.currentIndex() + if NORM_OPTS[idx] == 'Power': + self.powerscale.show() + self.powerscale_label.show() + else: + self.powerscale.hide() + self.powerscale_label.hide() + self.colorbar.mappable.set_norm(self.get_norm()) + self.colorbarChanged.emit() + + def get_norm(self): + """ + This will create a matplotlib.colors.Normalize from selected idx, limits and powerscale + """ + idx = self.norm.currentIndex() + if self.autoscale.isChecked(): + cmin = cmax = None + else: + cmin = self.cmin_value + cmax = self.cmax_value + if NORM_OPTS[idx] == 'Power': + if self.powerscale.hasAcceptableInput(): + self.powerscale_value = float(self.powerscale.text()) + return PowerNorm(gamma=self.powerscale_value, vmin=cmin, vmax=cmax) + elif NORM_OPTS[idx] == "SymmetricLog10": + return SymLogNorm(1e-8 if cmin is None else max(1e-8, abs(cmin)*1e-3), + vmin=cmin, vmax=cmax) + else: + return Normalize(vmin=cmin, vmax=cmax) + + def clim_changed(self): + """ + Called when either the min or max is changed. Will unset the autoscale. + """ + self.autoscale.blockSignals(True) + self.autoscale.setChecked(False) + self.autoscale.blockSignals(False) + self.update_clim() + + def update_clim(self): + """ + This will update the clim of the plot based on min, max, and autoscale + """ + if self.autoscale.isChecked(): + data = self.colorbar.mappable.get_array() + try: + self.cmin_value = data[~data.mask].min() + self.cmax_value = data[~data.mask].max() + except ValueError: + # all values mask + pass + self.update_clim_text() + else: + if self.cmin.hasAcceptableInput(): + self.cmin_value = float(self.cmin.text()) + if self.cmax.hasAcceptableInput(): + self.cmax_value = float(self.cmax.text()) + self.colorbar.set_clim(self.cmin_value, self.cmax_value) + self.redraw() + + def update_clim_text(self): + """ + Update displayed limit values based on stored ones + """ + self.cmin.setText("{:.4}".format(self.cmin_value)) + self.cmax.setText("{:.4}".format(self.cmax_value)) + + def redraw(self): + """ + Redraws the colobar and emits signal to cause the parent to redraw + """ + self.colorbar.update_ticks() + self.colorbar.draw_all() + self.canvas.draw_idle() + self.colorbarChanged.emit() diff --git a/qt/python/mantidqt/widgets/sliceviewer/view.py b/qt/python/mantidqt/widgets/sliceviewer/view.py index 8adc2106509853e52a0d2c822031a2b5713dc6e3..de7297573d0c4d86d08abb7b10dfafe778c180c8 100644 --- a/qt/python/mantidqt/widgets/sliceviewer/view.py +++ b/qt/python/mantidqt/widgets/sliceviewer/view.py @@ -8,11 +8,12 @@ # # from __future__ import (absolute_import, division, print_function) -from qtpy.QtWidgets import QWidget, QVBoxLayout +from qtpy.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout from qtpy.QtCore import Qt from mantidqt.MPLwidgets import FigureCanvas, NavigationToolbar2QT as NavigationToolbar from matplotlib.figure import Figure from .dimensionwidget import DimensionWidget +from mantidqt.widgets.colorbar.colorbar import ColorbarWidget class SliceViewerView(QWidget): @@ -30,11 +31,17 @@ class SliceViewerView(QWidget): self.dimensions.dimensionsChanged.connect(self.presenter.new_plot) self.dimensions.valueChanged.connect(self.presenter.update_plot_data) - # MPL figure + # MPL figure + colorbar + self.mpl_layout = QHBoxLayout() self.fig = Figure() + self.fig.set_facecolor(self.palette().window().color().getRgbF()) self.fig.set_tight_layout(True) self.canvas = FigureCanvas(self.fig) self.ax = self.fig.add_subplot(111, projection='mantid') + self.mpl_layout.addWidget(self.canvas) + self.colorbar = ColorbarWidget(self) + self.colorbar.colorbarChanged.connect(self.canvas.draw_idle) + self.mpl_layout.addWidget(self.colorbar) # MPL toolbar self.mpl_toolbar = NavigationToolbar(self.canvas, self) @@ -43,7 +50,7 @@ class SliceViewerView(QWidget): self.layout = QVBoxLayout(self) self.layout.addWidget(self.dimensions) self.layout.addWidget(self.mpl_toolbar) - self.layout.addWidget(self.canvas, stretch=1) + self.layout.addLayout(self.mpl_layout, stretch=1) self.show() @@ -52,23 +59,19 @@ class SliceViewerView(QWidget): clears the plot and creates a new one using the workspace """ self.ax.clear() - try: - self.colorbar.remove() - except AttributeError: - pass - self.im = self.ax.imshow(ws, origin='lower', **kwargs) + self.im = self.ax.imshow(ws, origin='lower', aspect='auto', + norm=self.colorbar.get_norm(), **kwargs) self.ax.set_title('') - self.colorbar = self.fig.colorbar(self.im) + self.colorbar.set_mappable(self.im) self.mpl_toolbar.update() # clear nav stack - self.fig.canvas.draw_idle() + self.canvas.draw_idle() def update_plot_data(self, data): """ This just updates the plot data without creating a new plot """ self.im.set_data(data.T) - self.im.set_clim(data.min(), data.max()) - self.fig.canvas.draw_idle() + self.colorbar.update_clim() def closeEvent(self, event): self.deleteLater()