Unverified Commit 329a8ac7 authored by Gagik Vardanyan's avatar Gagik Vardanyan Committed by GitHub
Browse files

Merge pull request #28739 from mantidproject/27856_incorrectly_tied_colorfill_plots

Changes on all plots apply to Colorbar on Tiled Colorfill plots
parents b2b8f9b2 385b56e3
......@@ -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,26 @@ 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()
all_images = []
for ax in axes:
all_images += ax.images + [col for col in ax.collections if isinstance(col, QuadMesh)
or isinstance(col, Poly3DCollection)]
# remove any colorbar images
colorbars = [img.colorbar.solids for img in all_images if img.colorbar]
images = [img for img in all_images if img not in colorbars]
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 = [img.axes for img in images]
return axes
......@@ -72,6 +72,7 @@ Bugfixes
- `plt.show()` now shows the most recently created figure.
- Removed error when changing the normalisation of a ragged workspace with a log scaled colorbar.
- The SavePlot1D algorithm can now be run in Workbench.
- Changing the settings on tiled colorbars now applys to all the plots if there is only one colorbar.
- Colorfill plots now correctly use the workspace name as the plot title.
- Overplotting no longer resets the axes scales.
- Fixed a bug with the peak cursor immediately resetting to the default cursor when trying to add a peak.
......
......@@ -675,52 +675,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()
......@@ -750,7 +756,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,12 +10,10 @@
# 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
from matplotlib.colors import LogNorm, Normalize
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from mpl_toolkits.mplot3d.axes3d import Axes3D
from qtpy.QtGui import QDoubleValidator, QIcon
from qtpy.QtWidgets import QDialog, QWidget
......@@ -210,16 +208,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 +237,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):
......@@ -635,7 +597,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])
......@@ -651,6 +612,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()
......@@ -195,16 +195,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 +212,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(pt.get_array()) for pt in plots)
colorbar_max = max(np.nanmax(pt.get_array()) for pt in plots)
for pt in plots:
pt.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,26 @@ 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: 'ws: (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()
Markdown is supported
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