Commit 2f295d23 authored by Alice Russell's avatar Alice Russell
Browse files

Re #27856 apply changes to all plots on tiled colorplots

This commit changes settings to apply to all plots if there is one colorbar and multiple colorfill plots.
The settings changed are:
- all the settings in the images tab in figure options excluding the label
- normalization (right-click on plot)
- the colorbar min and max settings when you double click the colorbar
parent 76a41cb8
......@@ -10,10 +10,11 @@
import datetime
import numpy as np
from matplotlib.collections import PolyCollection
from matplotlib.collections import PolyCollection, QuadMesh
from matplotlib.container import ErrorbarContainer
from matplotlib.colors import LogNorm
from matplotlib.ticker import LogLocator
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from scipy.interpolate import interp1d
import mantid.api
......@@ -1039,3 +1040,28 @@ def update_colorbar_scale(figure, image, scale, vmin, vmax):
"as the range between min value and max value is too large")
figure.subplots_adjust(wspace=0.5, hspace=0.5)
figure.colorbar(image, ax=figure.axes, ticks=locator, pad=0.06)
def get_images_from_figure(figure):
"""Return a list of images in the given figure excluding any colorbar images"""
axes = figure.get_axes()
images = []
for ax in axes:
images += ax.images + [col for col in ax.collections if isinstance(col, QuadMesh)
or isinstance(col, Poly3DCollection)]
# remove any colorbar images
for img in images:
if img.colorbar:
images.remove(img.colorbar.solids)
return images
def get_axes_from_figure(figure):
"""Return a list of axes in the given figure excluding any colorbar axes"""
images = get_images_from_figure(figure)
axes = []
for im in images:
axes.append(im.axes)
return axes
......@@ -674,52 +674,58 @@ class FigureInteraction(object):
return
self._toggle_normalization(ax)
def _toggle_normalization(self, ax):
waterfall = isinstance(ax, MantidAxes) and ax.is_waterfall()
if waterfall:
x, y = ax.waterfall_x_offset, ax.waterfall_y_offset
has_fill = ax.waterfall_has_fill()
if has_fill:
line_colour_fill = datafunctions.waterfall_fill_is_line_colour(ax)
if line_colour_fill:
fill_colour = None
else:
fill_colour = datafunctions.get_waterfall_fills(ax)[0].get_facecolor()
def _toggle_normalization(self, selected_ax):
if figure_type(self.canvas.figure) == FigureType.Image and len(self.canvas.figure.get_axes()) > 1:
axes = datafunctions.get_axes_from_figure(self.canvas.figure)
else:
axes = [selected_ax]
for ax in axes:
waterfall = isinstance(ax, MantidAxes) and ax.is_waterfall()
if waterfall:
x, y = ax.waterfall_x_offset, ax.waterfall_y_offset
has_fill = ax.waterfall_has_fill()
if has_fill:
line_colour_fill = datafunctions.waterfall_fill_is_line_colour(ax)
if line_colour_fill:
fill_colour = None
else:
fill_colour = datafunctions.get_waterfall_fills(ax)[0].get_facecolor()
ax.update_waterfall(0, 0)
ax.update_waterfall(0, 0)
# The colorbar can get screwed up with ragged workspaces and log scales as they go
# through the normalisation toggle.
# Set it to Linear and change it back after if necessary, since there's no reason
# to duplicate the handling.
colorbar_log = False
if ax.images:
colorbar_log = isinstance(ax.images[-1].norm, LogNorm)
if colorbar_log:
self._change_colorbar_axes(Normalize)
# The colorbar can get screwed up with ragged workspaces and log scales as they go
# through the normalisation toggle.
# Set it to Linear and change it back after if necessary, since there's no reason
# to duplicate the handling.
colorbar_log = False
if ax.images:
colorbar_log = isinstance(ax.images[-1].norm, LogNorm)
if colorbar_log:
self._change_colorbar_axes(Normalize)
self._change_plot_normalization(ax)
self._change_plot_normalization(ax)
if ax.lines: # Relim causes issues with colour plots, which have no lines.
ax.relim()
if ax.lines: # Relim causes issues with colour plots, which have no lines.
ax.relim()
if ax.images: # Colour bar limits are wrong if workspace is ragged. Set them manually.
colorbar_min = np.nanmin(ax.images[-1].get_array())
colorbar_max = np.nanmax(ax.images[-1].get_array())
for image in ax.images:
image.set_clim(colorbar_min, colorbar_max)
if colorbar_log: # If it had a log scaled colorbar before, put it back.
self._change_colorbar_axes(LogNorm)
if ax.images: # Colour bar limits are wrong if workspace is ragged. Set them manually.
colorbar_min = np.nanmin(ax.images[-1].get_array())
colorbar_max = np.nanmax(ax.images[-1].get_array())
for image in ax.images:
image.set_clim(colorbar_min, colorbar_max)
if colorbar_log: # If it had a log scaled colorbar before, put it back.
self._change_colorbar_axes(LogNorm)
ax.autoscale()
ax.autoscale()
datafunctions.set_initial_dimensions(ax)
if waterfall:
ax.update_waterfall(x, y)
datafunctions.set_initial_dimensions(ax)
if waterfall:
ax.update_waterfall(x, y)
if has_fill:
ax.set_waterfall_fill(True, fill_colour)
if has_fill:
ax.set_waterfall_fill(True, fill_colour)
self.canvas.draw()
......@@ -749,7 +755,7 @@ class FigureInteraction(object):
if figure_type(self.canvas.figure) in [FigureType.Image, FigureType.Contour]:
arg_set_copy.pop('specNum')
for ws_artist in ax.tracked_workspaces[workspace.name()]:
if ws_artist.spec_num == arg_set.get('specNum'):
if ws_artist.spec_num == arg_set_copy.get('specNum'):
ws_artist.is_normalized = not is_normalized
# This check is to prevent the contour lines being re-plotted using the colorfill plot args.
......
......@@ -10,7 +10,7 @@
# std imports
# 3rdparty imports
from mantid.plots.datafunctions import update_colorbar_scale
from mantid.plots.datafunctions import update_colorbar_scale, get_images_from_figure
from mantidqt.plotting.figuretype import FigureType, figure_type
from mantidqt.utils.qt import load_ui
from matplotlib.collections import QuadMesh
......@@ -210,16 +210,27 @@ class ColorbarAxisEditor(AxisEditor):
self.ui.gridBox.hide()
self.images = self.canvas.figure.gca().images
if len(self.images) == 0:
self.images = [col for col in self.canvas.figure.gca().collections if isinstance(col, QuadMesh)
or isinstance(col, Poly3DCollection)]
self.images=[]
images = get_images_from_figure(canvas.figure)
# If there are an equal number of plots and colorbars so apply changes to plot with the selected colorbar
# Otherwise apply changes to all the plots in the figure
if len(images) != len(self.canvas.figure.axes)/2:
self.images = images
else:
# apply changes to selected axes
for img in images:
if img.colorbar and img.colorbar.ax == axes:
self.images.append(img)
self.create_model()
def changes_accepted(self):
self.ui.errors.hide()
if len(self.images) == 0:
raise RuntimeError("Cannot find any plot linked to this colorbar")
limit_min, limit_max = float(self.ui.editor_min.text()), float(self.ui.editor_max.text())
scale = LogNorm if self.ui.logBox.isChecked() else Normalize
......@@ -228,12 +239,14 @@ class ColorbarAxisEditor(AxisEditor):
raise ValueError("Limits must be positive\nwhen scale is logarithmic.")
self.lim_setter(limit_min, limit_max)
update_colorbar_scale(self.canvas.figure, self.images[0], scale, limit_min, limit_max)
for img in self.images:
update_colorbar_scale(self.canvas.figure, img, scale, limit_min, limit_max)
def create_model(self):
memento = AxisEditorModel()
self._memento = memento
memento.min, memento.max = self.images[0].get_clim()
if len(self.images) > 0:
memento.min, memento.max = self.images[0].get_clim()
memento.log = isinstance(self.images[0].norm, LogNorm)
memento.grid = False
......
......@@ -26,7 +26,7 @@ from mantid.plots import MantidAxes
from unittest.mock import MagicMock, PropertyMock, call, patch
from mantid.simpleapi import CreateWorkspace
from mantidqt.plotting.figuretype import FigureType
from mantidqt.plotting.functions import plot, pcolormesh_from_names
from mantidqt.plotting.functions import plot, pcolormesh_from_names, plot_contour, pcolormesh
from mantidqt.utils.qt.testing import start_qapplication
from workbench.plotting.figureinteraction import FigureInteraction, LogNorm
......@@ -199,7 +199,7 @@ class FigureInteractionTest(unittest.TestCase):
def test_toggle_normalization_with_errorbars(self):
self._test_toggle_normalization(errorbars_on=True, plot_kwargs={'distribution': True})
def test_correct_yunit_label_when_overplotting_after_normaliztion_toggle(self):
def test_correct_yunit_label_when_overplotting_after_normalization_toggle(self):
# The earlier version of Matplotlib on RHEL throws an error when performing the second
# plot in this test, if the lines have errorbars. The error occurred when it attempted
# to draw an interactive legend. Plotting without errors still fulfills the purpose of this
......@@ -402,44 +402,6 @@ class FigureInteractionTest(unittest.TestCase):
self.assertTrue(isinstance(fig.axes[0].images[-1].norm, LogNorm))
# Private methods
def _create_mock_fig_manager_to_accept_right_click(self):
fig_manager = MagicMock()
canvas = MagicMock()
type(canvas).buttond = PropertyMock(return_value={Qt.RightButton: 3})
fig_manager.canvas = canvas
return fig_manager
def _create_mock_right_click(self):
mouse_event = MagicMock(inaxes=MagicMock(spec=MantidAxes, collections = [], creation_args = [{}]))
type(mouse_event).button = PropertyMock(return_value=3)
return mouse_event
def _test_toggle_normalization(self, errorbars_on, plot_kwargs):
fig = plot([self.ws], spectrum_nums=[1], errors=errorbars_on,
plot_kwargs=plot_kwargs)
mock_canvas = MagicMock(figure=fig)
fig_manager_mock = MagicMock(canvas=mock_canvas)
fig_interactor = FigureInteraction(fig_manager_mock)
# Earlier versions of matplotlib do not store the data assciated with a
# line with high precision and hence we need to set a lower tolerance
# when making comparisons of this data
if matplotlib.__version__ < "2":
decimal_tol = 1
else:
decimal_tol = 7
ax = fig.axes[0]
fig_interactor._toggle_normalization(ax)
assert_almost_equal(ax.lines[0].get_xdata(), [15, 25])
assert_almost_equal(ax.lines[0].get_ydata(), [0.2, 0.3], decimal=decimal_tol)
self.assertEqual("Counts ($\\AA$)$^{-1}$", ax.get_ylabel())
fig_interactor._toggle_normalization(ax)
assert_almost_equal(ax.lines[0].get_xdata(), [15, 25])
assert_almost_equal(ax.lines[0].get_ydata(), [2, 3], decimal=decimal_tol)
self.assertEqual("Counts", ax.get_ylabel())
@patch('workbench.plotting.figureinteraction.QMenu', autospec=True)
@patch('workbench.plotting.figureinteraction.figure_type', autospec=True)
def test_right_click_gives_marker_menu_when_hovering_over_one(self, mocked_figure_type, mocked_qmenu_cls):
......@@ -626,7 +588,6 @@ class FigureInteractionTest(unittest.TestCase):
self.assertEqual(1, self.interactor.redraw_annotations.call_count)
def test_toggle_normalisation_on_contour_plot_maintains_contour_line_colour(self):
from mantidqt.plotting.functions import plot_contour
from mantid.plots.legend import convert_color_to_hex
ws = CreateWorkspace(DataX=[1, 2, 3, 4, 2, 4, 6, 8], DataY=[2] * 8, NSpec=2, OutputWorkspace="test_ws")
fig = plot_contour([ws])
......@@ -642,6 +603,62 @@ class FigureInteractionTest(unittest.TestCase):
self.assertTrue(all(convert_color_to_hex(col.get_color()[0]) == "#ff9900"
for col in fig.get_axes()[0].collections))
def test_toggle_normalisation_applies_to_all_images_if_one_colorbar(self):
fig = pcolormesh([self.ws, self.ws])
mock_canvas = MagicMock(figure=fig)
fig_manager_mock = MagicMock(canvas=mock_canvas)
fig_interactor = FigureInteraction(fig_manager_mock)
# there should be 3 axes, 2 colorplots and 1 colorbar
self.assertEqual(3, len(fig.axes))
fig.axes[0].tracked_workspaces.values()
self.assertTrue(fig.axes[0].tracked_workspaces['ws'][0].is_normalized)
self.assertTrue(fig.axes[1].tracked_workspaces['ws'][0].is_normalized)
fig_interactor._toggle_normalization(fig.axes[0])
self.assertFalse(fig.axes[0].tracked_workspaces['ws'][0].is_normalized)
self.assertFalse(fig.axes[1].tracked_workspaces['ws'][0].is_normalized)
# Private methods
def _create_mock_fig_manager_to_accept_right_click(self):
fig_manager = MagicMock()
canvas = MagicMock()
type(canvas).buttond = PropertyMock(return_value={Qt.RightButton: 3})
fig_manager.canvas = canvas
return fig_manager
def _create_mock_right_click(self):
mouse_event = MagicMock(inaxes=MagicMock(spec=MantidAxes, collections = [], creation_args = [{}]))
type(mouse_event).button = PropertyMock(return_value=3)
return mouse_event
def _test_toggle_normalization(self, errorbars_on, plot_kwargs):
fig = plot([self.ws], spectrum_nums=[1], errors=errorbars_on,
plot_kwargs=plot_kwargs)
mock_canvas = MagicMock(figure=fig)
fig_manager_mock = MagicMock(canvas=mock_canvas)
fig_interactor = FigureInteraction(fig_manager_mock)
# Earlier versions of matplotlib do not store the data assciated with a
# line with high precision and hence we need to set a lower tolerance
# when making comparisons of this data
if matplotlib.__version__ < "2":
decimal_tol = 1
else:
decimal_tol = 7
ax = fig.axes[0]
fig_interactor._toggle_normalization(ax)
assert_almost_equal(ax.lines[0].get_xdata(), [15, 25])
assert_almost_equal(ax.lines[0].get_ydata(), [0.2, 0.3], decimal=decimal_tol)
self.assertEqual("Counts ($\\AA$)$^{-1}$", ax.get_ylabel())
fig_interactor._toggle_normalization(ax)
assert_almost_equal(ax.lines[0].get_xdata(), [15, 25])
assert_almost_equal(ax.lines[0].get_ydata(), [2, 3], decimal=decimal_tol)
self.assertEqual("Counts", ax.get_ylabel())
if __name__ == '__main__':
unittest.main()
......@@ -16,8 +16,13 @@ import matplotlib
matplotlib.use('AGG') # noqa
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
from unittest.mock import MagicMock
from mantid.api import WorkspaceFactory
from mantidqt.utils.qt.testing import start_qapplication
from workbench.plotting.propertiesdialog import XAxisEditor, YAxisEditor
from mantidqt.plotting.functions import pcolormesh
from workbench.plotting.propertiesdialog import XAxisEditor, YAxisEditor, ColorbarAxisEditor
@start_qapplication
......@@ -46,6 +51,27 @@ class PropertiesDialogTest(unittest.TestCase):
self.assertEqual(xEditor._memento.log, False)
self.assertEqual(yEditor._memento.log, True)
def test_changes_apply_to_all_colorfill_plots_if_one_colorbar(self):
ws = WorkspaceFactory.Instance().create("Workspace2D", NVectors=1, YLength=5, XLength=5)
fig = pcolormesh([ws, ws])
# there should be 3 axes: 2 colorfill plots and 1 colorbar
self.assertEqual(3, len(fig.axes))
colorbarEditor = ColorbarAxisEditor(fig.canvas, fig.axes[2])
min_value = 1.0
max_value = 2.0
colorbarEditor.ui.editor_min.text = MagicMock(return_value=min_value)
colorbarEditor.ui.editor_max.text = MagicMock(return_value=max_value)
colorbarEditor.ui.logBox.isChecked = MagicMock(return_value=True)
colorbarEditor.changes_accepted()
for ax in range(2):
self.assertEqual(min_value, fig.axes[ax].collections[0].norm.vmin)
self.assertEqual(max_value, fig.axes[ax].collections[0].norm.vmax)
self.assertTrue(isinstance(fig.axes[ax].collections[0].norm, LogNorm))
if __name__ == '__main__':
unittest.main()
......@@ -20,6 +20,8 @@ try:
except ImportError:
from matplotlib.cm import jet as DEFAULT_CMAP
from matplotlib.colors import Normalize
# local imports
from mantid.api import AnalysisDataService, MatrixWorkspace
from mantid.plots.plotfunctions import manage_workspace_names, figure_title, plot,\
......@@ -195,16 +197,14 @@ def pcolormesh(workspaces, fig=None):
workspaces_len = len(workspaces)
fig, axes, nrows, ncols = create_subplots(workspaces_len, fig=fig)
plots = []
row_idx, col_idx = 0, 0
for subplot_idx in range(nrows * ncols):
ax = axes[row_idx][col_idx]
if subplot_idx < workspaces_len:
ws = workspaces[subplot_idx]
pcm = pcolormesh_on_axis(ax, ws)
if pcm: # Colour bar limits are wrong if workspace is ragged. Set them manually.
colorbar_min = np.nanmin(pcm.get_array())
colorbar_max = np.nanmax(pcm.get_array())
pcm.set_clim(colorbar_min, colorbar_max)
plots.append(pcm)
if col_idx < ncols - 1:
col_idx += 1
else:
......@@ -214,6 +214,13 @@ def pcolormesh(workspaces, fig=None):
# nothing here
ax.axis('off')
# Colour bar limits are wrong if workspace is ragged. Set them manually.
# If there are multiple plots limits are the min and max of all the plots
colorbar_min = min(np.nanmin(plot.get_array()) for plot in plots)
colorbar_max = max(np.nanmax(plot.get_array()) for plot in plots)
for plot in plots:
plot.set_clim(colorbar_min, colorbar_max)
# Adjust locations to ensure the plots don't overlap
fig.subplots_adjust(wspace=SUBPLOT_WSPACE, hspace=SUBPLOT_HSPACE)
fig.colorbar(pcm, ax=axes.ravel().tolist(), pad=0.06)
......
......@@ -8,7 +8,7 @@
from mantid.plots.datafunctions import update_colorbar_scale
from mantidqt.utils.qt import block_signals
from mantidqt.widgets.plotconfigdialog import generate_ax_name, get_images_from_fig
from mantidqt.widgets.plotconfigdialog import generate_ax_name, get_images_from_fig, get_colorbars_from_fig
from mantidqt.widgets.plotconfigdialog.imagestabwidget import ImageProperties
from mantidqt.widgets.plotconfigdialog.imagestabwidget.view import ImagesTabWidgetView, SCALES
......@@ -34,13 +34,19 @@ class ImagesTabWidgetPresenter:
def apply_properties(self):
props = self.view.get_properties()
image = self.get_selected_image()
# if only one colorbar apply settings to all images
if len(get_colorbars_from_fig(self.fig)) == 1:
images = self.image_names_dict.values()
else:
images = [self.get_selected_image()]
self.set_selected_image_label(props.label)
image.set_cmap(props.colormap)
if props.interpolation:
image.set_interpolation(props.interpolation)
update_colorbar_scale(self.fig, image, SCALES[props.scale], props.vmin, props.vmax)
for image in images:
image.set_cmap(props.colormap)
if props.interpolation:
image.set_interpolation(props.interpolation)
update_colorbar_scale(self.fig, image, SCALES[props.scale], props.vmin, props.vmax)
if props.vmin > props.vmax:
self.view.max_min_value_warning.setVisible(True)
......
......@@ -17,6 +17,7 @@ from numpy import linspace, random
from mantid.plots import MantidAxes # register mantid projection # noqa
from unittest.mock import Mock
from mantid.simpleapi import CreateWorkspace
from mantidqt.plotting.functions import pcolormesh
from mantidqt.widgets.plotconfigdialog.imagestabwidget import ImageProperties
from mantidqt.widgets.plotconfigdialog.imagestabwidget.presenter import ImagesTabWidgetPresenter
......@@ -186,6 +187,25 @@ class ImagesTabWidgetPresenterTest(unittest.TestCase):
presenter.view.populate_select_image_combo_box.assert_called_once_with(
[img_name])
def test_apply_properties_applies_to_all_images_if_multiple_colorfill_plots_and_one_colorbar(self):
fig = pcolormesh([self.ws, self.ws])
props = {'label': 'New Label',
'colormap': 'jet',
'vmin': 0,
'vmax': 2,
'scale': 'Linear',
'interpolation': 'None'}
mock_view = Mock(get_selected_image_name=lambda: '(0, 0)',
get_properties=lambda: ImageProperties(props))
presenter = self._generate_presenter(fig=fig, view=mock_view)
presenter.apply_properties()
for ax in range(2):
image = fig.axes[ax].images[0]
self.assertEqual('jet', image.cmap.name)
self.assertEqual(0, image.norm.vmin)
self.assertEqual(2, image.norm.vmax)
self.assertTrue(isinstance(image.norm, Normalize))
if __name__ == '__main__':
unittest.main()
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment