diff --git a/glue/core/coordinates.py b/glue/core/coordinates.py index 63f280daa..7eac0d11a 100644 --- a/glue/core/coordinates.py +++ b/glue/core/coordinates.py @@ -209,6 +209,10 @@ def __setgluestate__(cls, rec, context): units=rec['units'], labels=rec['labels']) + @property + def axis_correlation_matrix(self): + return self._matrix[:-1, :-1] != 0 + # Kept for backward-compatibility WCSCoordinates = WCS diff --git a/glue/viewers/histogram/tests/test_python_export.py b/glue/viewers/histogram/tests/test_python_export.py new file mode 100644 index 000000000..07cb695ae --- /dev/null +++ b/glue/viewers/histogram/tests/test_python_export.py @@ -0,0 +1,61 @@ +from astropy.utils import NumpyRNGContext + +from glue.core import Data, DataCollection +from glue.core.application_base import Application +from glue.viewers.histogram.viewer import SimpleHistogramViewer +from glue.viewers.matplotlib.tests.test_python_export import BaseTestExportPython, random_with_nan + + +class TestExportPython(BaseTestExportPython): + + def setup_method(self, method): + + with NumpyRNGContext(12345): + self.data = Data(**dict((name, random_with_nan(100, nan_index=idx + 1)) for idx, name in enumerate('abcdefgh'))) + self.data_collection = DataCollection([self.data]) + self.app = Application(self.data_collection) + self.viewer = self.app.new_data_viewer(SimpleHistogramViewer) + self.viewer.add_data(self.data) + self.viewer.state.x_att = self.data.id['a'] + + def teardown_method(self, method): + self.viewer = None + self.app = None + + def test_simple(self, tmpdir): + self.assert_same(tmpdir) + + def test_simple_visual(self, tmpdir): + self.viewer.state.layers[0].color = 'blue' + self.viewer.state.layers[0].alpha = 0.5 + self.assert_same(tmpdir) + + def test_simple_visual_legend(self, tmpdir): + self.viewer.state.legend.visible = True + self.viewer.state.layers[0].color = 'blue' + self.viewer.state.layers[0].alpha = 0.5 + self.assert_same(tmpdir) + + def test_cumulative(self, tmpdir): + self.viewer.state.cumulative = True + self.assert_same(tmpdir) + + def test_normalize(self, tmpdir): + self.viewer.state.normalize = True + self.assert_same(tmpdir) + + def test_subset(self, tmpdir): + self.data_collection.new_subset_group('mysubset', self.data.id['a'] > 0.5) + self.assert_same(tmpdir) + + def test_subset_legend(self, tmpdir): + self.viewer.state.legend.visible = True + self.data_collection.new_subset_group('mysubset', self.data.id['a'] > 0.5) + self.assert_same(tmpdir) + + def test_empty(self, tmpdir): + self.viewer.state.x_min = 10 + self.viewer.state.x_max = 11 + self.viewer.state.hist_x_min = 10 + self.viewer.state.hist_x_max = 11 + self.assert_same(tmpdir) diff --git a/glue/viewers/image/state.py b/glue/viewers/image/state.py index 9258646b9..59f5db95e 100644 --- a/glue/viewers/image/state.py +++ b/glue/viewers/image/state.py @@ -257,7 +257,7 @@ def _on_yatt_change(self, *args): self.y_att_world = self.y_att @defer_draw - def _on_xatt_world_change(self, *args): + def _on_xatt_world_change(self, *args, forced=False): if self.x_att_world is not None: @@ -279,8 +279,11 @@ def _on_xatt_world_change(self, *args): else: self.x_att = self.x_att_world + if not forced: + self._on_yatt_world_change(forced=True) + @defer_draw - def _on_yatt_world_change(self, *args): + def _on_yatt_world_change(self, *args, forced=False): if self.y_att_world is not None: @@ -302,6 +305,9 @@ def _on_yatt_world_change(self, *args): else: self.y_att = self.y_att_world + if not forced: + self._on_xatt_world_change(forced=True) + def _set_reference_data(self): if self.reference_data is None: for layer in self.layers: diff --git a/glue/viewers/image/tests/test_python_export.py b/glue/viewers/image/tests/test_python_export.py new file mode 100644 index 000000000..fff360a5e --- /dev/null +++ b/glue/viewers/image/tests/test_python_export.py @@ -0,0 +1,124 @@ +import pytest +import numpy as np +import matplotlib.pyplot as plt +from astropy.utils import NumpyRNGContext +from astropy.wcs import WCS + +from glue.core import Data, DataCollection +from glue.core.coordinates import AffineCoordinates +from glue.core.application_base import Application +from glue.viewers.image.viewer import SimpleImageViewer +from glue.viewers.matplotlib.tests.test_python_export import BaseTestExportPython + + +class TestExportPython(BaseTestExportPython): + + def setup_method(self, method): + + with NumpyRNGContext(12345): + self.data = Data(cube=np.random.random((30, 50, 20))) + # Create data versions with WCS and affine coordinates + matrix = np.array([[2, 0, 0, -1], [0, 2, 1, 2], [0, 3, 1, -2], [0, 0, 0, 1]]) + affine = AffineCoordinates(matrix, units=['Mm', 'Mm', 'km'], labels=['xw', 'yw', 'zw']) + + self.data_wcs = Data(label='cube', cube=self.data['cube'], coords=WCS(naxis=3)) + self.data_affine = Data(label='cube', cube=self.data['cube'], coords=affine) + self.data_collection = DataCollection([self.data, self.data_wcs, self.data_affine]) + self.app = Application(self.data_collection) + self.viewer = self.app.new_data_viewer(SimpleImageViewer) + self.viewer.add_data(self.data) + # FIXME: On some platforms, using an integer label size + # causes some of the labels to be non-deterministically + # shifted by one pixel, so we pick a non-round font size + # to avoid this. + self.viewer.state.x_ticklabel_size = 8.21334111 + self.viewer.state.y_ticklabel_size = 8.21334111 + + def teardown_method(self, method): + self.viewer = None + self.app = None + + def assert_same(self, tmpdir, tol=0.1): + BaseTestExportPython.assert_same(self, tmpdir, tol=tol) + + def viewer_load(self, coords): + if coords is not None: + self.viewer.add_data(getattr(self, f'data_{coords}')) + self.viewer.remove_data(self.data) + + @pytest.mark.parametrize('coords', [None, 'wcs', 'affine']) + def test_simple(self, tmpdir, coords): + self.viewer_load(coords) + self.assert_same(tmpdir) + + @pytest.mark.parametrize('coords', [None, 'wcs', 'affine']) + def test_simple_legend(self, tmpdir, coords): + self.viewer_load(coords) + self.viewer.state.show_legend = True + self.assert_same(tmpdir) + + @pytest.mark.parametrize('coords', [None, 'wcs', 'affine']) + def test_simple_att(self, tmpdir, coords): + self.viewer_load(coords) + self.viewer.state.x_att = self.viewer.state.reference_data.pixel_component_ids[1] + self.viewer.state.y_att = self.viewer.state.reference_data.pixel_component_ids[0] + if coords == 'affine': + pytest.xfail('Known issue with axis label rendering') + self.assert_same(tmpdir) + + @pytest.mark.parametrize('coords', [None, 'wcs', 'affine']) + def test_simple_visual(self, tmpdir, coords): + self.viewer_load(coords) + self.viewer.state.legend.visible = True + self.viewer.state.layers[0].cmap = plt.cm.RdBu + self.viewer.state.layers[0].v_min = 0.2 + self.viewer.state.layers[0].v_max = 0.8 + self.viewer.state.layers[0].stretch = 'sqrt' + self.viewer.state.layers[0].stretch = 'sqrt' + self.viewer.state.layers[0].contrast = 0.9 + self.viewer.state.layers[0].bias = 0.6 + self.assert_same(tmpdir) + + @pytest.mark.parametrize('coords', [None, 'wcs', 'affine']) + def test_slice(self, tmpdir, coords): + self.viewer_load(coords) + self.viewer.state.x_att = self.viewer.state.reference_data.pixel_component_ids[1] + self.viewer.state.y_att = self.viewer.state.reference_data.pixel_component_ids[0] + self.viewer.state.slices = (2, 3, 4) + if coords == 'affine': + pytest.xfail('Known issue with axis label rendering') + self.assert_same(tmpdir) + + @pytest.mark.parametrize('coords', [None, 'wcs', 'affine']) + def test_aspect(self, tmpdir, coords): + self.viewer_load(coords) + self.viewer.state.aspect = 'auto' + self.assert_same(tmpdir) + + @pytest.mark.parametrize('coords', [None, 'wcs', 'affine']) + def test_subset(self, tmpdir, coords): + self.viewer_load(coords) + self.data_collection.new_subset_group('mysubset', self.data.id['cube'] > 0.5) + self.assert_same(tmpdir) + + @pytest.mark.parametrize('coords', [None, 'wcs', 'affine']) + def test_subset_legend(self, tmpdir, coords): + self.viewer_load(coords) + self.data_collection.new_subset_group('mysubset', + self.viewer.state.reference_data.id['cube'] > 0.5) + self.viewer.state.legend.visible = True + self.assert_same(tmpdir, tol=0.15) # transparency and such + + @pytest.mark.parametrize('coords', [None, 'wcs', 'affine']) + def test_subset_slice(self, tmpdir, coords): + self.viewer_load(coords) + self.data_collection.new_subset_group('mysubset', self.data.id['cube'] > 0.5) + self.test_slice(tmpdir, coords) + + @pytest.mark.parametrize('coords', [None, 'wcs', 'affine']) + def test_subset_transposed(self, tmpdir, coords): + self.viewer_load(coords) + self.data_collection.new_subset_group('mysubset', self.data.id['cube'] > 0.5) + self.viewer.state.x_att = self.data.pixel_component_ids[0] + self.viewer.state.y_att = self.data.pixel_component_ids[1] + self.assert_same(tmpdir) diff --git a/glue/viewers/matplotlib/tests/test_python_export.py b/glue/viewers/matplotlib/tests/test_python_export.py new file mode 100644 index 000000000..24213c8f2 --- /dev/null +++ b/glue/viewers/matplotlib/tests/test_python_export.py @@ -0,0 +1,62 @@ +import os +import sys +import pytest +import subprocess + +from glue.config import settings + +import numpy as np + +from matplotlib.testing.compare import compare_images + +__all__ = ['random_with_nan', 'BaseTestExportPython'] + + +def random_with_nan(nsamples, nan_index): + x = np.random.random(nsamples) + x[nan_index] = np.nan + return x + + +class BaseTestExportPython: + + def assert_same(self, tmpdir, tol=0.1): + + os.chdir(tmpdir.strpath) + + expected = tmpdir.join('expected.png').strpath + script = tmpdir.join('actual.py').strpath + actual = tmpdir.join('glue_plot.png').strpath + + self.viewer.axes.figure.savefig(expected) + + self.viewer.export_as_script(script) + subprocess.call([sys.executable, script]) + + msg = compare_images(expected, actual, tol=tol) + + if msg: + + from base64 import b64encode + + print("SCRIPT:") + with open(script, 'r') as f: + print(f.read()) + + print("EXPECTED:") + with open(expected, 'rb') as f: + print(b64encode(f.read()).decode()) + + print("ACTUAL:") + with open(actual, 'rb') as f: + print(b64encode(f.read()).decode()) + + pytest.fail(msg, pytrace=False) + + def test_color_settings(self, tmpdir): + settings.FOREGROUND_COLOR = '#a51d2d' + settings.BACKGROUND_COLOR = '#99c1f1' + self.viewer._update_appearance_from_settings() + self.assert_same(tmpdir) + settings.reset_defaults() + self.viewer._update_appearance_from_settings() diff --git a/glue/viewers/profile/tests/test_python_export.py b/glue/viewers/profile/tests/test_python_export.py new file mode 100644 index 000000000..589a41288 --- /dev/null +++ b/glue/viewers/profile/tests/test_python_export.py @@ -0,0 +1,87 @@ +from astropy.utils import NumpyRNGContext + +from glue.core import Data, DataCollection +from glue.core.application_base import Application +from glue.viewers.profile.viewer import SimpleProfileViewer +from glue.viewers.matplotlib.tests.test_python_export import BaseTestExportPython, random_with_nan +from glue.viewers.profile.tests.test_state import SimpleCoordinates + + +class TestExportPython(BaseTestExportPython): + + def setup_method(self, method): + + self.data = Data(label='d1') + self.data.coords = SimpleCoordinates() + with NumpyRNGContext(12345): + self.data['x'] = random_with_nan(48, 5).reshape((6, 4, 2)) + self.data['y'] = random_with_nan(48, 12).reshape((6, 4, 2)) + self.data_collection = DataCollection([self.data]) + self.app = Application(self.data_collection) + self.viewer = self.app.new_data_viewer(SimpleProfileViewer) + self.viewer.add_data(self.data) + # Make legend location deterministic + self.viewer.state.legend.location = 'lower left' + + def teardown_method(self, method): + self.viewer = None + self.app = None + + def test_simple(self, tmpdir): + self.assert_same(tmpdir) + + def test_simple_legend(self, tmpdir): + self.viewer.state.legend.visible = True + self.assert_same(tmpdir) + + def test_color(self, tmpdir): + self.viewer.state.layers[0].color = '#ac0567' + self.assert_same(tmpdir) + + def test_linewidth(self, tmpdir): + self.viewer.state.layers[0].linewidth = 7.25 + self.assert_same(tmpdir) + + def test_max(self, tmpdir): + self.viewer.state.function = 'maximum' + self.assert_same(tmpdir) + + def test_min(self, tmpdir): + self.viewer.state.function = 'minimum' + self.assert_same(tmpdir) + + def test_mean(self, tmpdir): + self.viewer.state.function = 'mean' + self.assert_same(tmpdir) + + def test_median(self, tmpdir): + self.viewer.state.function = 'median' + self.assert_same(tmpdir) + + def test_sum(self, tmpdir): + self.viewer.state.function = 'sum' + self.assert_same(tmpdir) + + def test_normalization(self, tmpdir): + self.viewer.state.normalize = True + self.assert_same(tmpdir) + + def test_subset(self, tmpdir): + self.viewer.state.function = 'mean' + self.data_collection.new_subset_group('mysubset', self.data.id['x'] > 0.25) + self.assert_same(tmpdir) + + def test_subset_legend(self, tmpdir): + self.viewer.state.legend.visible = True + self.viewer.state.function = 'mean' + self.viewer.state.layers[0].linewidth = 7.25 + self.data_collection.new_subset_group('mysubset', self.data.id['x'] > 0.25) + self.assert_same(tmpdir) + + def test_xatt(self, tmpdir): + self.viewer.x_att = self.data.pixel_component_ids[1] + self.assert_same(tmpdir) + + def test_profile_att(self, tmpdir): + self.viewer.layers[0].state.attribute = self.data.id['y'] + self.assert_same(tmpdir) diff --git a/glue/viewers/scatter/tests/test_python_export.py b/glue/viewers/scatter/tests/test_python_export.py new file mode 100644 index 000000000..c89d4919a --- /dev/null +++ b/glue/viewers/scatter/tests/test_python_export.py @@ -0,0 +1,299 @@ +from itertools import product + +import numpy as np +import matplotlib.pyplot as plt +from astropy.utils import NumpyRNGContext + +from glue.core import Data, DataCollection +from glue.core.application_base import Application +from glue.viewers.scatter.viewer import SimpleScatterViewer +from glue.viewers.matplotlib.tests.test_python_export import BaseTestExportPython, random_with_nan + + +class TestExportPython(BaseTestExportPython): + + def setup_method(self, method): + + with NumpyRNGContext(12345): + self.data = Data(**dict((name, random_with_nan(100, nan_index=idx + 1)) for idx, name in enumerate('abcdefgh'))) + self.data['angle'] = np.random.uniform(0, 360, 100) + self.data_collection = DataCollection([self.data]) + self.app = Application(self.data_collection) + self.viewer = self.app.new_data_viewer(SimpleScatterViewer) + self.viewer.add_data(self.data) + self.viewer.state.x_att = self.data.id['a'] + self.viewer.state.y_att = self.data.id['b'] + + def teardown_method(self, method): + self.viewer = None + self.app = None + + def test_simple(self, tmpdir): + self.assert_same(tmpdir) + + def test_simple_legend(self, tmpdir): + self.viewer.state.legend.visible = True + self.assert_same(tmpdir) + + def test_simple_nofill(self, tmpdir): + self.viewer.state.layers[0].fill = False + self.viewer.state.layers[0].size_scaling = 10 + self.assert_same(tmpdir) + + def test_simple_visual(self, tmpdir): + self.viewer.state.layers[0].color = 'blue' + self.viewer.state.layers[0].markersize = 30 + self.viewer.state.layers[0].alpha = 0.5 + self.assert_same(tmpdir) + + def test_simple_visual_legend(self, tmpdir): + self.viewer.state.legend.visible = True + self.viewer.state.layers[0].color = 'blue' + self.viewer.state.layers[0].markersize = 30 + self.viewer.state.layers[0].alpha = 0.5 + self.assert_same(tmpdir) + + def test_cmap_mode(self, tmpdir): + self.viewer.state.layers[0].cmap_mode = 'Linear' + self.viewer.state.layers[0].cmap_att = self.data.id['c'] + self.viewer.state.layers[0].cmap = plt.cm.BuGn + self.viewer.state.layers[0].cmap_vmin = 0.2 + self.viewer.state.layers[0].cmap_vmax = 0.7 + self.viewer.state.layers[0].alpha = 0.8 + self.assert_same(tmpdir) + + def test_cmap_mode_nofill(self, tmpdir): + self.viewer.state.layers[0].fill = False + self.test_cmap_mode(tmpdir) + + def test_size_mode(self, tmpdir): + self.viewer.state.layers[0].size_mode = 'Linear' + self.viewer.state.layers[0].size_att = self.data.id['d'] + self.viewer.state.layers[0].size_vmin = 0.1 + self.viewer.state.layers[0].size_vmax = 0.8 + self.viewer.state.layers[0].size_scaling = 0.4 + self.viewer.state.layers[0].alpha = 0.7 + self.assert_same(tmpdir) + + def test_size_mode_legend(self, tmpdir): + self.viewer.state.legend.visible = True + self.viewer.state.layers[0].size_mode = 'Linear' + self.viewer.state.layers[0].size_att = self.data.id['d'] + self.viewer.state.layers[0].size_vmin = 0.1 + self.viewer.state.layers[0].size_vmax = 0.8 + self.viewer.state.layers[0].size_scaling = 0.4 + self.viewer.state.layers[0].alpha = 0.7 + self.assert_same(tmpdir) + + def test_size_mode_nofill(self, tmpdir): + self.viewer.state.layers[0].fill = False + self.test_size_mode(tmpdir) + + def test_line(self, tmpdir): + self.viewer.state.layers[0].line_visible = True + self.viewer.state.layers[0].linewidth = 10 + self.viewer.state.layers[0].linestype = 'dashed' + self.viewer.state.layers[0].color = 'orange' + self.viewer.state.layers[0].alpha = 0.7 + self.viewer.state.layers[0].markersize = 100 + self.assert_same(tmpdir, tol=5) + + def test_line_cmap(self, tmpdir): + self.viewer.state.layers[0].cmap_mode = 'Linear' + self.viewer.state.layers[0].cmap_vmin = 0.2 + self.viewer.state.layers[0].cmap_vmax = 0.7 + self.viewer.state.layers[0].cmap = plt.cm.BuGn + self.test_line(tmpdir) + + def test_errorbarx(self, tmpdir): + self.viewer.state.layers[0].xerr_visible = True + self.viewer.state.layers[0].xerr_att = self.data.id['e'] + self.viewer.state.layers[0].color = 'purple' + self.viewer.state.layers[0].alpha = 0.5 + self.assert_same(tmpdir) + + def test_errorbary(self, tmpdir): + self.viewer.state.layers[0].yerr_visible = True + self.viewer.state.layers[0].yerr_att = self.data.id['f'] + self.viewer.state.layers[0].color = 'purple' + self.viewer.state.layers[0].alpha = 0.5 + self.assert_same(tmpdir) + + def test_errorbarxy(self, tmpdir): + self.viewer.state.layers[0].xerr_visible = True + self.viewer.state.layers[0].xerr_att = self.data.id['e'] + self.viewer.state.layers[0].yerr_visible = True + self.viewer.state.layers[0].yerr_att = self.data.id['f'] + self.viewer.state.layers[0].color = 'purple' + self.viewer.state.layers[0].alpha = 0.5 + self.assert_same(tmpdir) + + def test_errorbarxy_legend(self, tmpdir): + self.viewer.state.legend.visible = True + self.viewer.state.layers[0].xerr_visible = True + self.viewer.state.layers[0].xerr_att = self.data.id['e'] + self.viewer.state.layers[0].yerr_visible = True + self.viewer.state.layers[0].yerr_att = self.data.id['f'] + self.viewer.state.layers[0].color = 'purple' + self.viewer.state.layers[0].alpha = 0.5 + self.assert_same(tmpdir) + + def test_errorbarxy_cmap(self, tmpdir): + self.viewer.state.layers[0].cmap_mode = 'Linear' + self.viewer.state.layers[0].cmap_vmin = 0.2 + self.viewer.state.layers[0].cmap_vmax = 0.7 + self.viewer.state.layers[0].cmap = plt.cm.BuGn + self.test_errorbarxy(tmpdir) + + def _vector_common(self, tmpdir): + self.viewer.state.layers[0].vector_visible = True + self.viewer.state.layers[0].vy_att = self.data.id['g'] + self.viewer.state.layers[0].vector_arrowhead = True + self.viewer.state.layers[0].vector_origin = 'tail' + self.viewer.state.layers[0].vector_scaling = 1.5 + self.viewer.state.layers[0].color = 'teal' + self.viewer.state.layers[0].alpha = 0.9 + self.assert_same(tmpdir, tol=1) + + def test_vector_cartesian(self, tmpdir): + self.viewer.state.layers[0].vector_mode = 'Cartesian' + self.viewer.state.layers[0].vx_att = self.data.id['h'] + self._vector_common(tmpdir) + + def test_vector_polar(self, tmpdir): + self.viewer.state.layers[0].vector_mode = 'Polar' + self.viewer.state.layers[0].vx_att = self.data.id['angle'] + self._vector_common(tmpdir) + + def test_vector_cartesian_cmap(self, tmpdir): + self.viewer.state.layers[0].cmap_mode = 'Linear' + self.viewer.state.layers[0].cmap_vmin = 0.2 + self.viewer.state.layers[0].cmap_vmax = 0.7 + self.viewer.state.layers[0].cmap = plt.cm.BuGn + self.test_vector_cartesian(tmpdir) + + def test_vector_cartesian_xflip(self, tmpdir): + # Regression test for a bug that caused vectors to not be flipped + self.viewer.state.layers[0].vector_mode = 'Cartesian' + self.viewer.state.layers[0].vx_att = self.data.id['h'] + self.viewer.state.flip_x() + self._vector_common(tmpdir) + + def test_subset(self, tmpdir): + self.data_collection.new_subset_group('mysubset', self.data.id['a'] > 0.5) + self.assert_same(tmpdir) + + def test_density_map_with_subset(self, tmpdir): + self.viewer.state.dpi = 2 + self.viewer.state.layers[0].density_map = True + self.data_collection.new_subset_group('mysubset', self.data.id['a'] > 0.5) + self.assert_same(tmpdir) + + def test_density_map_cmap_with_subset(self, tmpdir): + self.viewer.state.dpi = 2 + self.viewer.state.layers[0].density_map = True + self.viewer.state.layers[0].cmap_mode = 'Linear' + self.viewer.state.layers[0].cmap_vmin = 0.2 + self.viewer.state.layers[0].cmap_vmax = 0.7 + self.viewer.state.layers[0].cmap = plt.cm.BuGn + self.data_collection.new_subset_group('mysubset', self.data.id['a'] > 0.5) + self.assert_same(tmpdir) + + def test_density_map_cmap_with_subset_legend(self, tmpdir): + self.viewer.state.legend.visible = True + self.viewer.state.dpi = 2 + self.viewer.state.layers[0].density_map = True + self.viewer.state.layers[0].cmap_mode = 'Linear' + self.viewer.state.layers[0].cmap_vmin = 0.2 + self.viewer.state.layers[0].cmap_vmax = 0.7 + self.viewer.state.layers[0].cmap = plt.cm.BuGn + self.data_collection.new_subset_group('mysubset', self.data.id['a'] > 0.5) + self.assert_same(tmpdir) + + def test_cmap_mode_change(self, tmpdir): + # Regression test for a bug that caused scatter markers to not change + # color when going from Linear to Fixed mode + self.viewer.state.layers[0].size_mode = 'Linear' + self.viewer.state.layers[0].cmap_mode = 'Linear' + self.viewer.state.layers[0].cmap_mode = 'Fixed' + self.assert_same(tmpdir) + + def test_density_map_change(self, tmpdir): + # Regression test for a bug that caused the density map to still + # be visible if using color-coding with the density map then + # switching to markers. + self.viewer.state.layers[0].density_map = True + self.viewer.state.layers[0].cmap_mode = 'Linear' + self.viewer.state.layers[0].cmap = plt.cm.BuGn + self.viewer.state.layers[0].density_map = False + self.assert_same(tmpdir) + + def test_simple_polar_plot_degrees(self, tmpdir): + self.viewer.state.plot_mode = 'polar' + self.viewer.state.angle_unit = 'degrees' + self.viewer.state.x_att = self.data.id['c'] + self.viewer.state.y_att = self.data.id['d'] + self.assert_same(tmpdir) + + def test_simple_polar_plot_radians(self, tmpdir): + self.viewer.state.plot_mode = 'polar' + self.viewer.state.angle_unit = 'radians' + self.viewer.state.x_att = self.data.id['c'] + self.viewer.state.y_att = self.data.id['d'] + self.assert_same(tmpdir) + + def _fullsphere_common_test(self, tmpdir): + # Note that all the full-sphere projections have the same bounds, + # so we can use the same sets of min/max values + x_bounds = self.viewer.state.x_min, self.viewer.state.x_max + y_bounds = self.viewer.state.y_min, self.viewer.state.y_max + for order in product([True, False], repeat=2): + self.viewer.state.x_min, self.viewer.state.x_max = sorted(x_bounds, reverse=order[0]) + self.viewer.state.y_min, self.viewer.state.y_max = sorted(y_bounds, reverse=order[1]) + self.viewer.state.plot_mode = 'aitoff' + self.viewer.state.x_att = self.data.id['c'] + self.viewer.state.y_att = self.data.id['d'] + self.assert_same(tmpdir) + self.viewer.state.plot_mode = 'hammer' + self.viewer.state.x_att = self.data.id['e'] + self.viewer.state.y_att = self.data.id['f'] + self.assert_same(tmpdir) + self.viewer.state.plot_mode = 'lambert' + self.viewer.state.x_att = self.data.id['g'] + self.viewer.state.y_att = self.data.id['h'] + self.assert_same(tmpdir) + self.viewer.state.plot_mode = 'mollweide' + self.viewer.state.x_att = self.data.id['a'] + self.viewer.state.y_att = self.data.id['b'] + self.assert_same(tmpdir) + + def test_full_sphere_degrees(self, tmpdir): + self.viewer.state.angle_unit = 'degrees' + self._fullsphere_common_test(tmpdir) + + def test_full_sphere_radians(self, tmpdir): + self.viewer.state.angle_unit = 'radians' + self._fullsphere_common_test(tmpdir) + + def test_cmap_size_noncartesian(self, tmpdir): + self.viewer.state.layers[0].size_mode = 'Linear' + self.viewer.state.layers[0].cmap_mode = 'Linear' + for proj in ['polar', 'aitoff', 'hammer', 'lambert', 'mollweide']: + self.viewer.state.plot_mode = proj + self.assert_same(tmpdir) + + def test_vectors_noncartesian(self, tmpdir): + for proj in ['polar', 'aitoff', 'hammer', 'lambert', 'mollweide']: + self.viewer.state.plot_mode = proj + self._vector_common(tmpdir) + + def test_errorbarxy_noncartesian(self, tmpdir): + self.viewer.state.layers[0].xerr_visible = True + self.viewer.state.layers[0].xerr_att = self.data.id['e'] + self.viewer.state.layers[0].yerr_visible = True + self.viewer.state.layers[0].yerr_att = self.data.id['f'] + self.viewer.state.layers[0].color = 'purple' + self.viewer.state.layers[0].alpha = 0.5 + for proj in ['polar', 'aitoff', 'hammer', 'lambert', 'mollweide']: + self.viewer.state.plot_mode = proj + self.assert_same(tmpdir) diff --git a/glue/viewers/scatter/viewer.py b/glue/viewers/scatter/viewer.py index c986fd706..76289a996 100644 --- a/glue/viewers/scatter/viewer.py +++ b/glue/viewers/scatter/viewer.py @@ -1,3 +1,5 @@ +from echo import delay_callback + from glue.core.subset import roi_to_subset_state from glue.core.util import update_ticks from glue.core.roi_pretransforms import FullSphereLongitudeTransform, ProjectionMplTransform, RadianTransform @@ -63,13 +65,12 @@ def _update_polar_ticks(self, *args): def _update_projection(self, *args): self.figure.delaxes(self.axes) _, self.axes = init_mpl(self.figure, projection=self.state.plot_mode) - self.remove_all_toolbars() - self.initialize_toolbar() for layer in self.layers: layer._set_axes(self.axes) - layer.state.vector_mode = 'Cartesian' - layer.state._update_points_mode() - layer.update() + with delay_callback(layer.state, 'vector_mode'): + layer.state.vector_mode = 'Cartesian' + layer.state._update_points_mode() + layer.update() self.axes.callbacks.connect('xlim_changed', self.limits_from_mpl) self.axes.callbacks.connect('ylim_changed', self.limits_from_mpl) self.update_x_axislabel() @@ -189,6 +190,13 @@ class SimpleScatterViewer(MatplotlibScatterMixin, SimpleMatplotlibViewer): _data_artist_cls = ScatterLayerArtist _subset_artist_cls = ScatterLayerArtist - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, session, parent=None, state=None): + proj = None if not state or not state.plot_mode else state.plot_mode + SimpleMatplotlibViewer.__init__(self, session, parent=parent, state=state, projection=proj) MatplotlibScatterMixin.setup_callbacks(self) + + def limits_to_mpl(self, *args): + # These projections throw errors if we try to set the limits + if self.state.plot_mode in ['aitoff', 'hammer', 'lambert', 'mollweide']: + return + super().limits_to_mpl(*args)