diff --git a/glue/viewers/histogram/qt/options_widget.py b/glue/viewers/histogram/qt/options_widget.py index a46bf25f1..4c2562105 100644 --- a/glue/viewers/histogram/qt/options_widget.py +++ b/glue/viewers/histogram/qt/options_widget.py @@ -22,8 +22,8 @@ def __init__(self, viewer_state, session, parent=None): self._connections = autoconnect_callbacks_to_qt(viewer_state, self.ui) self._connections_axes = autoconnect_callbacks_to_qt(viewer_state, self.ui.axes_editor.ui) - connect_kwargs = {'legend_alpha': dict(value_range=(0, 1))} - self._connections_legend = autoconnect_callbacks_to_qt(viewer_state, self.ui.legend_editor.ui, connect_kwargs) + connect_kwargs = {'alpha': dict(value_range=(0, 1))} + self._connections_legend = autoconnect_callbacks_to_qt(viewer_state.legend, self.ui.legend_editor.ui, connect_kwargs) self.viewer_state = viewer_state diff --git a/glue/viewers/histogram/qt/tests/test_data_viewer.py b/glue/viewers/histogram/qt/tests/test_data_viewer.py index f7efd4d95..001f28fc6 100644 --- a/glue/viewers/histogram/qt/tests/test_data_viewer.py +++ b/glue/viewers/histogram/qt/tests/test_data_viewer.py @@ -672,7 +672,7 @@ def test_legend(self): viewer_state = self.viewer.state self.viewer.add_data(self.data) - self.viewer.state.show_legend = True + self.viewer.state.legend.visible = True handles, labels, handler_dict = self.viewer.get_handles_legend() assert len(handles) == 1 diff --git a/glue/viewers/histogram/qt/tests/test_python_export.py b/glue/viewers/histogram/qt/tests/test_python_export.py index 57d892537..987fd0050 100644 --- a/glue/viewers/histogram/qt/tests/test_python_export.py +++ b/glue/viewers/histogram/qt/tests/test_python_export.py @@ -33,7 +33,7 @@ def test_simple_visual(self, tmpdir): self.assert_same(tmpdir) def test_simple_visual_legend(self, tmpdir): - self.viewer.state.show_legend = True + 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) @@ -51,7 +51,7 @@ def test_subset(self, tmpdir): self.assert_same(tmpdir) def test_subset_legend(self, tmpdir): - self.viewer.state.show_legend = True + self.viewer.state.legend.visible = True self.data_collection.new_subset_group('mysubset', self.data.id['a'] > 0.5) self.assert_same(tmpdir) diff --git a/glue/viewers/image/qt/options_widget.py b/glue/viewers/image/qt/options_widget.py index e4c2a1c10..3a005f3bc 100644 --- a/glue/viewers/image/qt/options_widget.py +++ b/glue/viewers/image/qt/options_widget.py @@ -23,8 +23,8 @@ def __init__(self, viewer_state, session, parent=None): self._connections = autoconnect_callbacks_to_qt(viewer_state, self.ui) self._connections_axes = autoconnect_callbacks_to_qt(viewer_state, self.ui.axes_editor.ui) - connect_kwargs = {'legend_alpha': dict(value_range=(0, 1))} - self._connections_legend = autoconnect_callbacks_to_qt(viewer_state, self.ui.legend_editor.ui, connect_kwargs) + connect_kwargs = {'alpha': dict(value_range=(0, 1))} + self._connections_legend = autoconnect_callbacks_to_qt(viewer_state.legend, self.ui.legend_editor.ui, connect_kwargs) self.viewer_state = viewer_state diff --git a/glue/viewers/image/qt/tests/test_data_viewer.py b/glue/viewers/image/qt/tests/test_data_viewer.py index d304aa074..c607ddf40 100644 --- a/glue/viewers/image/qt/tests/test_data_viewer.py +++ b/glue/viewers/image/qt/tests/test_data_viewer.py @@ -726,7 +726,7 @@ def test_legend(self): viewer_state = self.viewer.state self.viewer.add_data(self.image1) - self.viewer.state.show_legend = True + self.viewer.state.legend.visible = True handles, labels, handler_dict = self.viewer.get_handles_legend() assert len(handles) == 1 diff --git a/glue/viewers/image/qt/tests/test_python_export.py b/glue/viewers/image/qt/tests/test_python_export.py index d3b67946f..3ee339619 100644 --- a/glue/viewers/image/qt/tests/test_python_export.py +++ b/glue/viewers/image/qt/tests/test_python_export.py @@ -47,7 +47,7 @@ def test_simple_att(self, tmpdir): self.assert_same(tmpdir) def test_simple_visual(self, tmpdir): - self.viewer.state.show_legend = True + 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 @@ -72,7 +72,7 @@ def test_subset(self, tmpdir): self.assert_same(tmpdir) def test_subset_legend(self, tmpdir): - self.viewer.state.show_legend = True + self.viewer.state.legend.visible = True self.data_collection.new_subset_group('mysubset', self.data.id['cube'] > 0.5) self.assert_same(tmpdir, tol=0.15) # transparency and such diff --git a/glue/viewers/matplotlib/qt/legend_editor.ui b/glue/viewers/matplotlib/qt/legend_editor.ui index 38af3c12c..e991e2865 100644 --- a/glue/viewers/matplotlib/qt/legend_editor.ui +++ b/glue/viewers/matplotlib/qt/legend_editor.ui @@ -32,6 +32,29 @@ 5 + + + + + 0 + 0 + + + + enable + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + @@ -43,7 +66,7 @@ - + @@ -55,15 +78,15 @@ - - - - + + + + Qt::Horizontal - - + + 0 @@ -71,19 +94,15 @@ - enable + title Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - Qt::Horizontal - - + + @@ -96,29 +115,65 @@ - + - - + + + + box color + + - - + + - + 0 0 - title + - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + text color + + + + + + + + + + + + + + box edge + + + QColorBox + QLabel +
glue.utils.qt.colors
+
+
diff --git a/glue/viewers/matplotlib/qt/tests/test_data_viewer.py b/glue/viewers/matplotlib/qt/tests/test_data_viewer.py index a2a72ba0a..87e9af8e0 100644 --- a/glue/viewers/matplotlib/qt/tests/test_data_viewer.py +++ b/glue/viewers/matplotlib/qt/tests/test_data_viewer.py @@ -625,7 +625,7 @@ def test_legend(self): # no legend by default assert self.viewer.axes.get_legend() is None - self.viewer.state.show_legend = True + self.viewer.state.legend.visible = True # a legend appears legend = self.viewer.axes.get_legend() @@ -644,7 +644,7 @@ def test_legend(self): # The next set of test check that the legend does not create extra draws ! def test_legend_single_draw(self): # Make sure that the number of draws is kept to a minimum - self.viewer.show_legend = True + self.viewer.state.legend.visible = True self.init_draw_count() self.init_subset() assert self.draw_count == 0 @@ -652,7 +652,7 @@ def test_legend_single_draw(self): assert self.draw_count == 1 def test_legend_numerical_data_changed(self): - self.viewer.show_legend = True + self.viewer.state.legend.visible = True self.init_draw_count() self.init_subset() assert self.draw_count == 0 diff --git a/glue/viewers/matplotlib/state.py b/glue/viewers/matplotlib/state.py index 94d1f70e6..7905e5410 100644 --- a/glue/viewers/matplotlib/state.py +++ b/glue/viewers/matplotlib/state.py @@ -1,7 +1,10 @@ from echo import CallbackProperty, SelectionCallbackProperty, keep_in_sync, delay_callback +from matplotlib.colors import to_rgba + from glue.core.message import LayerArtistUpdatedMessage +from glue.core.state_objects import State from glue.viewers.common.state import ViewerState, LayerState from glue.utils import defer_draw, avoid_circular @@ -34,13 +37,70 @@ def notify(self, *args, **kwargs): VALID_WEIGHTS = ['light', 'normal', 'medium', 'semibold', 'bold', 'heavy', 'black'] -VALID_LOCATIONS = ['best', + +VALID_LOCATIONS = ['draggable', 'best', 'upper right', 'upper left', 'lower left', 'lower right', 'center left', 'center right', 'lower center', 'upper center'] +class MatplotlibLegendState(State): + """The legend state""" + + visible = DeferredDrawCallbackProperty(False, docstring="Whether to show the legend") + + location = DeferredDrawSelectionCallbackProperty(0, docstring="The location of the legend in the axis") + + title = DeferredDrawCallbackProperty("", docstring='The title of the legend') + fontsize = DeferredDrawCallbackProperty(10, docstring='The font size of the title') + + alpha = DeferredDrawCallbackProperty(0.6, docstring='Transparency of the legend frame') + frame_color = DeferredDrawCallbackProperty("#ffffff", docstring='Frame color of the legend') + show_edge = DeferredDrawCallbackProperty(True, docstring="Whether to show the edge of the frame ") + text_color = DeferredDrawCallbackProperty("#000000", docstring='Text color of the legend') + + def __init__(self, *args, **kwargs): + MatplotlibLegendState.location.set_choices(self, VALID_LOCATIONS) + + super().__init__(*args, **kwargs) + self._set_color_choices() + + def _set_color_choices(self): + from glue.config import settings + + self.frame_color = settings.BACKGROUND_COLOR + self.text_color = settings.FOREGROUND_COLOR + + @property + def edge_color(self): + if self.show_edge: + return to_rgba(self.text_color, self.alpha) + else: + return None + + @property + def draggable(self): + return self.location == 'draggable' + + @property + def mpl_location(self): + if self.location == 'draggable': + return 'best' + else: + return self.location + + def update_axes_settings_from(self, state): + self.visible = state.show_legend + self.loc_and_drag = state.loc_and_drag + self.alpha = state.alpha + self.title = state.title + self.fontsize = state.fontsize + self.frame_color = state.frame_color + self.show_edge = state.show_edge + self.text_color = state.text_color + + class MatplotlibDataViewerState(ViewerState): """ A base class that includes common attributes for viewers based on @@ -72,21 +132,15 @@ class MatplotlibDataViewerState(ViewerState): x_ticklabel_size = DeferredDrawCallbackProperty(8, docstring='Size of the x-axis tick labels') y_ticklabel_size = DeferredDrawCallbackProperty(8, docstring='Size of the y-axis tick labels') - show_legend = DeferredDrawCallbackProperty(False, docstring="Whether to show the legend") - legend_location = DeferredDrawSelectionCallbackProperty(0, docstring="The location of the legend in the axis") - legend_alpha = DeferredDrawCallbackProperty(0.8, docstring='Transparency of the legend frame') - legend_title = DeferredDrawCallbackProperty("", docstring='Transparency of the legend frame') - legend_fontsize = DeferredDrawCallbackProperty(10, docstring='Transparency of the legend frame') - def __init__(self, *args, **kwargs): self._axes_aspect_ratio = None MatplotlibDataViewerState.x_axislabel_weight.set_choices(self, VALID_WEIGHTS) MatplotlibDataViewerState.y_axislabel_weight.set_choices(self, VALID_WEIGHTS) - MatplotlibDataViewerState.legend_location.set_choices(self, VALID_LOCATIONS) super(MatplotlibDataViewerState, self).__init__(*args, **kwargs) + self.legend = MatplotlibLegendState(*args, **kwargs) self.add_callback('aspect', self._adjust_limits_aspect, priority=10000) self.add_callback('x_min', self._adjust_limits_aspect_x, priority=10000) @@ -181,17 +235,15 @@ def _adjust_limits_aspect(self, *args, **kwargs): self.y_max = y_max def update_axes_settings_from(self, state): + # axis self.x_axislabel_size = state.x_axislabel_size self.y_axislabel_size = state.y_axislabel_size self.x_axislabel_weight = state.x_axislabel_weight self.y_axislabel_weight = state.y_axislabel_weight self.x_ticklabel_size = state.x_ticklabel_size self.y_ticklabel_size = state.y_ticklabel_size - self.show_legend = state.show_legend - self.legend_location = state.legend_location - self.legend_alpha = state.legend_alpha - self.legend_title = state.legend_title - self.legend_fontsize = state.legend_fontsize + # legend + self.legend.update_axes_settings_from(state.legend) @defer_draw def _notify_global(self, *args, **kwargs): diff --git a/glue/viewers/matplotlib/tests/test_state.py b/glue/viewers/matplotlib/tests/test_state.py new file mode 100755 index 000000000..0c07cb68b --- /dev/null +++ b/glue/viewers/matplotlib/tests/test_state.py @@ -0,0 +1,27 @@ +from ..state import MatplotlibLegendState +from glue.config import settings + +from matplotlib.colors import to_rgba + + +class TestMatplotlibLegendState: + def setup_method(self, method): + self.state = MatplotlibLegendState() + + def test_draggable(self): + self.state.location = 'draggable' + assert self.state.draggable + assert self.state.mpl_location == 'best' + + def test_no_draggable(self): + self.state.location = 'lower left' + assert not self.state.draggable + assert self.state.mpl_location == 'lower left' + + def test_no_edge(self): + self.state.show_edge = False + assert self.state.edge_color is None + + def test_default_color(self): + assert self.state.frame_color == settings.BACKGROUND_COLOR + assert self.state.edge_color == to_rgba(settings.FOREGROUND_COLOR, self.state.alpha) diff --git a/glue/viewers/matplotlib/viewer.py b/glue/viewers/matplotlib/viewer.py index 0b8b116de..b068ec30d 100644 --- a/glue/viewers/matplotlib/viewer.py +++ b/glue/viewers/matplotlib/viewer.py @@ -3,6 +3,7 @@ import numpy as np from matplotlib.patches import Rectangle +from matplotlib.artist import setp as msetp from glue.viewers.matplotlib.mpl_axes import update_appearance_from_settings from echo import delay_callback @@ -52,11 +53,13 @@ SCRIPT_LEGEND = """ ax.legend(legend_handles, legend_labels, handler_map=legend_handler_dict, - loc='{legend_location}', # location - framealpha={legend_alpha:.2f}, # opacity of the frame - title='{legend_title}', # title of the legend - title_fontsize={legend_fontsize}, # fontsize of the title - fontsize={legend_fontsize} # fontsize of the labels + loc='{location}', # location + framealpha={alpha:.2f}, # opacity of the frame + title='{title}', # title of the legend + title_fontsize={fontsize}, # fontsize of the title + fontsize={fontsize}, # fontsize of the labels + facecolor='{frame_color}', + edgecolor={edge_color} ) """.strip() @@ -115,11 +118,14 @@ def setup_callbacks(self): self.state.add_callback('x_ticklabel_size', self.update_x_ticklabel) self.state.add_callback('y_ticklabel_size', self.update_y_ticklabel) - self.state.add_callback('show_legend', self.draw_legend) - self.state.add_callback('legend_location', self.draw_legend) - self.state.add_callback('legend_alpha', self.draw_legend) - self.state.add_callback('legend_title', self.draw_legend) - self.state.add_callback('legend_fontsize', self.draw_legend) + self.state.legend.add_callback('visible', self.draw_legend) + self.state.legend.add_callback('location', self.draw_legend) + self.state.legend.add_callback('alpha', self.update_legend) + self.state.legend.add_callback('title', self.draw_legend) + self.state.legend.add_callback('fontsize', self.draw_legend) + self.state.legend.add_callback('frame_color', self.update_legend) + self.state.legend.add_callback('show_edge', self.update_legend) + self.state.legend.add_callback('text_color', self.update_legend) self.update_x_axislabel() self.update_y_axislabel() @@ -153,6 +159,7 @@ def update_y_ticklabel(self, *event): self.redraw() def get_handles_legend(self): + """Collect the handles and labels from each layer artist.""" handles = [] labels = [] handler_dict = {} @@ -165,21 +172,35 @@ def get_handles_legend(self): handler_dict[handle] = handler return handles, labels, handler_dict + def _update_legend_visual(self, legend): + """Update the legend colors and opacity. No redraw.""" + msetp(legend.get_title(), color=self.state.legend.text_color) + msetp(legend.get_texts(), color=self.state.legend.text_color) + msetp(legend.get_frame(), + alpha=self.state.legend.alpha, + facecolor=self.state.legend.frame_color, + edgecolor=self.state.legend.edge_color + ) + + def update_legend(self, *args): + """Update the legend colors and opacity.""" + legend = self.axes.get_legend() + if legend is not None: + self._update_legend_visual(legend) + self.redraw() + def draw_legend(self, *args): - if self.state.show_legend: + if self.state.legend.visible: handles, labels, handler_map = self.get_handles_legend() + kwargs = dict(loc=self.state.legend.mpl_location, + title=self.state.legend.title, + title_fontsize=self.state.legend.fontsize, + fontsize=self.state.legend.fontsize) if handler_map is not None: - self.axes.legend( - handles, labels, handler_map=handler_map, - loc=self.state.legend_location, framealpha=self.state.legend_alpha, - title=self.state.legend_title, title_fontsize=self.state.legend_fontsize, - fontsize=self.state.legend_fontsize) - else: - self.axes.legend( - handles, labels, - loc=self.state.legend_location, framealpha=self.state.legend_alpha, - title=self.state.legend_title, title_fontsize=self.state.legend_fontsize, - fontsize=self.state.legend_fontsize) + kwargs["handler_map"] = handler_map + legend = self.axes.legend(handles, labels, **kwargs) + self._update_legend_visual(legend) + legend.set_draggable(self.state.legend.draggable) else: legend = self.axes.get_legend() if legend is not None: @@ -294,8 +315,10 @@ def _script_footer(self): return [], SCRIPT_FOOTER.format(**state_dict) def _script_legend(self): - state_dict = self.state.as_dict() + state_dict = self.state.legend.as_dict() + state_dict['location'] = self.state.legend.mpl_location + state_dict['edge_color'] = self.state.legend.edge_color legend_str = SCRIPT_LEGEND.format(**state_dict) - if not self.state.show_legend: + if not self.state.legend.visible: legend_str = indent(legend_str, "# ") return [], legend_str diff --git a/glue/viewers/profile/qt/options_widget.py b/glue/viewers/profile/qt/options_widget.py index 5c1c95b8f..261f16cf3 100644 --- a/glue/viewers/profile/qt/options_widget.py +++ b/glue/viewers/profile/qt/options_widget.py @@ -26,8 +26,8 @@ def __init__(self, viewer_state, session, parent=None): self._connections = autoconnect_callbacks_to_qt(viewer_state, self.ui) self._connections_axes = autoconnect_callbacks_to_qt(viewer_state, self.ui.axes_editor.ui) - connect_kwargs = {'legend_alpha': dict(value_range=(0, 1))} - self._connections_legend = autoconnect_callbacks_to_qt(viewer_state, self.ui.legend_editor.ui, connect_kwargs) + connect_kwargs = {'alpha': dict(value_range=(0, 1))} + self._connections_legend = autoconnect_callbacks_to_qt(viewer_state.legend, self.ui.legend_editor.ui, connect_kwargs) self.viewer_state = viewer_state diff --git a/glue/viewers/profile/qt/tests/test_python_export.py b/glue/viewers/profile/qt/tests/test_python_export.py index df24bd6d0..84e0428ba 100644 --- a/glue/viewers/profile/qt/tests/test_python_export.py +++ b/glue/viewers/profile/qt/tests/test_python_export.py @@ -31,7 +31,7 @@ def test_simple(self, tmpdir): self.assert_same(tmpdir) def test_simple_legend(self, tmpdir): - self.viewer.state.show_legend = True + self.viewer.state.legend.visible = True self.assert_same(tmpdir) def test_color(self, tmpdir): @@ -72,7 +72,7 @@ def test_subset(self, tmpdir): self.assert_same(tmpdir) def test_subset_legend(self, tmpdir): - self.viewer.state.show_legend = True + 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) diff --git a/glue/viewers/scatter/qt/options_widget.py b/glue/viewers/scatter/qt/options_widget.py index 7988893a6..1f32f6e3c 100644 --- a/glue/viewers/scatter/qt/options_widget.py +++ b/glue/viewers/scatter/qt/options_widget.py @@ -22,8 +22,8 @@ def __init__(self, viewer_state, session, parent=None): self._connections = autoconnect_callbacks_to_qt(viewer_state, self.ui) self._connections_axes = autoconnect_callbacks_to_qt(viewer_state, self.ui.axes_editor.ui) - connect_kwargs = {'legend_alpha': dict(value_range=(0, 1))} - self._connections_legend = autoconnect_callbacks_to_qt(viewer_state, self.ui.legend_editor.ui, connect_kwargs) + connect_kwargs = {'alpha': dict(value_range=(0, 1))} + self._connections_legend = autoconnect_callbacks_to_qt(viewer_state.legend, self.ui.legend_editor.ui, connect_kwargs) self.viewer_state = viewer_state diff --git a/glue/viewers/scatter/qt/tests/test_data_viewer.py b/glue/viewers/scatter/qt/tests/test_data_viewer.py index 5b20e02bd..ccf97c4f0 100644 --- a/glue/viewers/scatter/qt/tests/test_data_viewer.py +++ b/glue/viewers/scatter/qt/tests/test_data_viewer.py @@ -681,7 +681,7 @@ def test_legend(self): viewer_state = self.viewer.state self.viewer.add_data(self.data) - self.viewer.state.show_legend = True + viewer_state.legend.visible = True handles, labels, handler_dict = self.viewer.get_handles_legend() assert len(handles) == 1 diff --git a/glue/viewers/scatter/qt/tests/test_python_export.py b/glue/viewers/scatter/qt/tests/test_python_export.py index c8606ffae..2a644d318 100644 --- a/glue/viewers/scatter/qt/tests/test_python_export.py +++ b/glue/viewers/scatter/qt/tests/test_python_export.py @@ -32,7 +32,7 @@ def test_simple(self, tmpdir): self.assert_same(tmpdir) def test_simple_legend(self, tmpdir): - self.viewer.state.show_legend = True + self.viewer.state.legend.visible = True self.assert_same(tmpdir) def test_simple_nofill(self, tmpdir): @@ -47,7 +47,7 @@ def test_simple_visual(self, tmpdir): self.assert_same(tmpdir) def test_simple_visual_legend(self, tmpdir): - self.viewer.state.show_legend = True + 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 @@ -76,7 +76,7 @@ def test_size_mode(self, tmpdir): self.assert_same(tmpdir) def test_size_mode_legend(self, tmpdir): - self.viewer.state.show_legend = True + 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 @@ -129,7 +129,7 @@ def test_errorbarxy(self, tmpdir): self.assert_same(tmpdir) def test_errorbarxy_legend(self, tmpdir): - self.viewer.state.show_legend = True + 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 @@ -200,7 +200,7 @@ def test_density_map_cmap_with_subset(self, tmpdir): self.assert_same(tmpdir) def test_density_map_cmap_with_subset_legend(self, tmpdir): - self.viewer.state.show_legend = True + 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'