From aff19cef7e72d3077086dac5760d8ec9ae1c69c6 Mon Sep 17 00:00:00 2001 From: Fabien Georget Date: Thu, 21 May 2020 11:45:47 +0200 Subject: [PATCH 1/6] Legend background and foreground colors can be modified - Change in visual properties of the legend do not require to get the handles anymore - Background and foreground can be changed - Defaults for these properties are taken from the settings - The legend is also set to be draggable --- glue/viewers/matplotlib/qt/legend_editor.ui | 41 +++++++++++++++++++ glue/viewers/matplotlib/state.py | 14 +++++++ glue/viewers/matplotlib/viewer.py | 45 +++++++++++++++------ 3 files changed, 87 insertions(+), 13 deletions(-) diff --git a/glue/viewers/matplotlib/qt/legend_editor.ui b/glue/viewers/matplotlib/qt/legend_editor.ui index 38af3c12c..aae300653 100644 --- a/glue/viewers/matplotlib/qt/legend_editor.ui +++ b/glue/viewers/matplotlib/qt/legend_editor.ui @@ -117,8 +117,49 @@ + + + + frame color + + + + + + + text color + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + QColorBox + QLabel +
glue.utils.qt.colors
+
+
diff --git a/glue/viewers/matplotlib/state.py b/glue/viewers/matplotlib/state.py index 94d1f70e6..0a4fcd505 100644 --- a/glue/viewers/matplotlib/state.py +++ b/glue/viewers/matplotlib/state.py @@ -77,6 +77,8 @@ class MatplotlibDataViewerState(ViewerState): 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') + legend_frame_color = DeferredDrawCallbackProperty("#FFFFFF", docstring='Background color of the legend') + legend_text_color = DeferredDrawCallbackProperty("#000000", docstring='Text color of the legend') def __init__(self, *args, **kwargs): @@ -86,7 +88,9 @@ def __init__(self, *args, **kwargs): MatplotlibDataViewerState.y_axislabel_weight.set_choices(self, VALID_WEIGHTS) MatplotlibDataViewerState.legend_location.set_choices(self, VALID_LOCATIONS) + super(MatplotlibDataViewerState, self).__init__(*args, **kwargs) + self._set_color_choices() self.add_callback('aspect', self._adjust_limits_aspect, priority=10000) self.add_callback('x_min', self._adjust_limits_aspect_x, priority=10000) @@ -94,6 +98,12 @@ def __init__(self, *args, **kwargs): self.add_callback('y_min', self._adjust_limits_aspect_y, priority=10000) self.add_callback('y_max', self._adjust_limits_aspect_y, priority=10000) + def _set_color_choices(self): + from glue.config import settings + + self.legend_frame_color = settings.BACKGROUND_COLOR + self.legend_text_color = settings.FOREGROUND_COLOR + def _set_axes_aspect_ratio(self, value): """ Set the aspect ratio of the axes in which the visualization is shown. @@ -181,17 +191,21 @@ 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 + # legend 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 + self.legend_frame_color = state.legend_frame_color + self.legend_text_color = state.legend_text_color @defer_draw def _notify_global(self, *args, **kwargs): diff --git a/glue/viewers/matplotlib/viewer.py b/glue/viewers/matplotlib/viewer.py index 0b8b116de..9a26e7832 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 @@ -117,9 +118,12 @@ def setup_callbacks(self): 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_alpha', self.update_legend) self.state.add_callback('legend_title', self.draw_legend) - self.state.add_callback('legend_fontsize', self.draw_legend) + self.state.add_callback('legend_fontsize', self.update_legend) + self.state.add_callback('legend_frame_color', self.update_legend) + self.state.add_callback('legend_text_color', self.update_legend) + self.update_x_axislabel() self.update_y_axislabel() @@ -165,21 +169,36 @@ def get_handles_legend(self): handler_dict[handle] = handler return handles, labels, handler_dict + def _update_legend_visual(self, legend): + 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 + ) + + def update_legend(self, *args): + 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: handles, labels, handler_map = self.get_handles_legend() + kwargs = dict(loc=self.state.legend_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(True) + else: legend = self.axes.get_legend() if legend is not None: From 32c2a6a1f0e2cd9e564e89a94a55dddb17f2a76c Mon Sep 17 00:00:00 2001 From: Fabien Georget Date: Thu, 21 May 2020 14:56:27 +0200 Subject: [PATCH 2/6] Add customization of legend box edge Add option for draggable legend --- glue/viewers/matplotlib/qt/legend_editor.ui | 88 ++++++++++++--------- glue/viewers/matplotlib/state.py | 10 ++- glue/viewers/matplotlib/viewer.py | 40 ++++++---- 3 files changed, 83 insertions(+), 55 deletions(-) diff --git a/glue/viewers/matplotlib/qt/legend_editor.ui b/glue/viewers/matplotlib/qt/legend_editor.ui index aae300653..8feafab7b 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 + + + + + + + + + + @@ -55,15 +78,15 @@ - - - - + + + + Qt::Horizontal - - + + 0 @@ -71,19 +94,15 @@ - enable + title Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - Qt::Horizontal - - + + @@ -98,56 +117,51 @@ - - + + + + box color + + - - + + - + 0 0 - title - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + - - + + - frame color + - + text color - - - - - 0 - 0 - - + + - - + + - + box edge diff --git a/glue/viewers/matplotlib/state.py b/glue/viewers/matplotlib/state.py index 0a4fcd505..1f1f94bc8 100644 --- a/glue/viewers/matplotlib/state.py +++ b/glue/viewers/matplotlib/state.py @@ -34,7 +34,7 @@ def notify(self, *args, **kwargs): VALID_WEIGHTS = ['light', 'normal', 'medium', 'semibold', 'bold', 'heavy', 'black'] -VALID_LOCATIONS = ['best', +VALID_LOCATIONS = ['best (draggable)', 'best', 'upper right', 'upper left', 'lower left', 'lower right', 'center left', 'center right', @@ -75,9 +75,10 @@ class MatplotlibDataViewerState(ViewerState): 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') - legend_frame_color = DeferredDrawCallbackProperty("#FFFFFF", docstring='Background color of the legend') + legend_title = DeferredDrawCallbackProperty("", docstring='The title of the legend') + legend_fontsize = DeferredDrawCallbackProperty(10, docstring='The font size of the title') + legend_frame_color = DeferredDrawCallbackProperty("#ffffff", docstring='Frame color of the legend') + legend_show_frame_edge = DeferredDrawCallbackProperty(True, docstring="Whether to show the edge of the frame ") legend_text_color = DeferredDrawCallbackProperty("#000000", docstring='Text color of the legend') def __init__(self, *args, **kwargs): @@ -205,6 +206,7 @@ def update_axes_settings_from(self, state): self.legend_title = state.legend_title self.legend_fontsize = state.legend_fontsize self.legend_frame_color = state.legend_frame_color + self.legend_show_edge = state.legend_show_edge self.legend_text_color = state.legend_text_color @defer_draw diff --git a/glue/viewers/matplotlib/viewer.py b/glue/viewers/matplotlib/viewer.py index 9a26e7832..748955831 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.colors import to_rgba from matplotlib.artist import setp as msetp from glue.viewers.matplotlib.mpl_axes import update_appearance_from_settings @@ -120,11 +121,11 @@ def setup_callbacks(self): self.state.add_callback('legend_location', self.draw_legend) self.state.add_callback('legend_alpha', self.update_legend) self.state.add_callback('legend_title', self.draw_legend) - self.state.add_callback('legend_fontsize', self.update_legend) + self.state.add_callback('legend_fontsize', self.draw_legend) self.state.add_callback('legend_frame_color', self.update_legend) + self.state.add_callback('legend_show_frame_edge', self.update_legend) self.state.add_callback('legend_text_color', self.update_legend) - self.update_x_axislabel() self.update_y_axislabel() self.update_x_ticklabel() @@ -157,6 +158,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 = {} @@ -170,35 +172,45 @@ def get_handles_legend(self): return handles, labels, handler_dict def _update_legend_visual(self, legend): - 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 - ) + """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) + if self.state.legend_show_frame_edge: + edge_color = to_rgba(self.state.legend_text_color, self.state.legend_alpha) + else: + edge_color = 'none' + msetp(legend.get_frame(), + alpha=self.state.legend_alpha, + facecolor=self.state.legend_frame_color, + edgecolor=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: handles, labels, handler_map = self.get_handles_legend() - kwargs = dict(loc=self.state.legend_location, + loc = self.state.legend_location + if loc == 'best (draggable)': + loc = 'best' + draggable = True + else: + draggable = False + kwargs = dict(loc=loc, title=self.state.legend_title, title_fontsize=self.state.legend_fontsize, - fontsize=self.state.legend_fontsize, - ) + fontsize=self.state.legend_fontsize) if handler_map is not None: kwargs["handler_map"] = handler_map legend = self.axes.legend( handles, labels, **kwargs) self._update_legend_visual(legend) - legend.set_draggable(True) - + legend.set_draggable(draggable) else: legend = self.axes.get_legend() if legend is not None: From 4106105ed70839fec9cb1b838e97ce42d41da0a0 Mon Sep 17 00:00:00 2001 From: Fabien Georget Date: Sun, 24 May 2020 20:47:50 +0200 Subject: [PATCH 3/6] Separate legend state from the main matplotlib viewers Solve's legend inconsistency between qt interface and glue interface --- glue/viewers/histogram/qt/options_widget.py | 4 +- .../histogram/qt/tests/test_data_viewer.py | 2 +- .../histogram/qt/tests/test_python_export.py | 4 +- glue/viewers/image/qt/options_widget.py | 4 +- .../image/qt/tests/test_data_viewer.py | 2 +- .../image/qt/tests/test_python_export.py | 4 +- glue/viewers/matplotlib/qt/legend_editor.ui | 16 ++-- .../matplotlib/qt/tests/test_data_viewer.py | 6 +- glue/viewers/matplotlib/state.py | 85 +++++++++++++------ glue/viewers/matplotlib/viewer.py | 66 +++++++------- glue/viewers/profile/qt/options_widget.py | 4 +- .../profile/qt/tests/test_python_export.py | 4 +- glue/viewers/scatter/qt/options_widget.py | 4 +- .../scatter/qt/tests/test_data_viewer.py | 2 +- .../scatter/qt/tests/test_python_export.py | 10 +-- 15 files changed, 124 insertions(+), 93 deletions(-) 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 8feafab7b..993c40265 100644 --- a/glue/viewers/matplotlib/qt/legend_editor.ui +++ b/glue/viewers/matplotlib/qt/legend_editor.ui @@ -49,7 +49,7 @@ - + @@ -66,7 +66,7 @@ - + @@ -79,7 +79,7 @@ - + Qt::Horizontal @@ -102,7 +102,7 @@ - + @@ -115,7 +115,7 @@ - + @@ -125,7 +125,7 @@ - + 0 @@ -138,7 +138,7 @@ - + @@ -152,7 +152,7 @@ - + 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 1f1f94bc8..2db927b24 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 @@ -41,6 +44,62 @@ def notify(self, *args, **kwargs): 'lower center', 'upper center'] +class MatplotlibLegendState(State): + """The legend state""" + + visible = DeferredDrawCallbackProperty(False, docstring="Whether to show the legend") + + loc_and_drag = 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.loc_and_drag.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.loc_and_drag.endswith('(draggable)') + + @property + def location(self): + if self.loc_and_drag.endswith('(draggable)'): + return self.loc_and_drag[:-12] + else: + return self.loc_and_drag + + + 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,14 +131,6 @@ 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='The title of the legend') - legend_fontsize = DeferredDrawCallbackProperty(10, docstring='The font size of the title') - legend_frame_color = DeferredDrawCallbackProperty("#ffffff", docstring='Frame color of the legend') - legend_show_frame_edge = DeferredDrawCallbackProperty(True, docstring="Whether to show the edge of the frame ") - legend_text_color = DeferredDrawCallbackProperty("#000000", docstring='Text color of the legend') def __init__(self, *args, **kwargs): @@ -87,11 +138,9 @@ def __init__(self, *args, **kwargs): 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._set_color_choices() + 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) @@ -99,11 +148,6 @@ def __init__(self, *args, **kwargs): self.add_callback('y_min', self._adjust_limits_aspect_y, priority=10000) self.add_callback('y_max', self._adjust_limits_aspect_y, priority=10000) - def _set_color_choices(self): - from glue.config import settings - - self.legend_frame_color = settings.BACKGROUND_COLOR - self.legend_text_color = settings.FOREGROUND_COLOR def _set_axes_aspect_ratio(self, value): """ @@ -200,14 +244,7 @@ def update_axes_settings_from(self, state): self.x_ticklabel_size = state.x_ticklabel_size self.y_ticklabel_size = state.y_ticklabel_size # legend - 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 - self.legend_frame_color = state.legend_frame_color - self.legend_show_edge = state.legend_show_edge - self.legend_text_color = state.legend_text_color + self.legend.update_axes_settings_from(state.legend) @defer_draw def _notify_global(self, *args, **kwargs): diff --git a/glue/viewers/matplotlib/viewer.py b/glue/viewers/matplotlib/viewer.py index 748955831..8ac480778 100644 --- a/glue/viewers/matplotlib/viewer.py +++ b/glue/viewers/matplotlib/viewer.py @@ -54,11 +54,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() @@ -117,14 +119,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.update_legend) - self.state.add_callback('legend_title', self.draw_legend) - self.state.add_callback('legend_fontsize', self.draw_legend) - self.state.add_callback('legend_frame_color', self.update_legend) - self.state.add_callback('legend_show_frame_edge', self.update_legend) - self.state.add_callback('legend_text_color', self.update_legend) + self.state.legend.add_callback('visible', self.draw_legend) + self.state.legend.add_callback('loc_and_drag', 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() @@ -173,16 +175,12 @@ def get_handles_legend(self): 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) - if self.state.legend_show_frame_edge: - edge_color = to_rgba(self.state.legend_text_color, self.state.legend_alpha) - else: - edge_color = 'none' + 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=edge_color + alpha=self.state.legend.alpha, + facecolor=self.state.legend.frame_color, + edgecolor=self.state.legend.edge_color ) def update_legend(self, *args): @@ -193,24 +191,18 @@ def update_legend(self, *args): 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() - loc = self.state.legend_location - if loc == 'best (draggable)': - loc = 'best' - draggable = True - else: - draggable = False - kwargs = dict(loc=loc, - title=self.state.legend_title, - title_fontsize=self.state.legend_fontsize, - fontsize=self.state.legend_fontsize) + kwargs = dict(loc=self.state.legend.location, + title=self.state.legend.title, + title_fontsize=self.state.legend.fontsize, + fontsize=self.state.legend.fontsize) if handler_map is not None: kwargs["handler_map"] = handler_map legend = self.axes.legend( handles, labels, **kwargs) self._update_legend_visual(legend) - legend.set_draggable(draggable) + legend.set_draggable(self.state.legend.draggable) else: legend = self.axes.get_legend() if legend is not None: @@ -325,8 +317,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.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' From 68973c5624fa2effa7997af8534edb18b2b1d99d Mon Sep 17 00:00:00 2001 From: Fabien Georget Date: Mon, 25 May 2020 21:00:07 +0200 Subject: [PATCH 4/6] Legend: bugfix, codestyle and naming fix --- glue/viewers/matplotlib/state.py | 21 ++++++++++----------- glue/viewers/matplotlib/viewer.py | 10 ++++------ 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/glue/viewers/matplotlib/state.py b/glue/viewers/matplotlib/state.py index 2db927b24..04328d6ad 100644 --- a/glue/viewers/matplotlib/state.py +++ b/glue/viewers/matplotlib/state.py @@ -37,7 +37,8 @@ def notify(self, *args, **kwargs): VALID_WEIGHTS = ['light', 'normal', 'medium', 'semibold', 'bold', 'heavy', 'black'] -VALID_LOCATIONS = ['best (draggable)', 'best', + +VALID_LOCATIONS = ['draggable', 'best', 'upper right', 'upper left', 'lower left', 'lower right', 'center left', 'center right', @@ -49,7 +50,7 @@ class MatplotlibLegendState(State): visible = DeferredDrawCallbackProperty(False, docstring="Whether to show the legend") - loc_and_drag = DeferredDrawSelectionCallbackProperty(0, docstring="The location of the legend in the axis") + 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') @@ -60,7 +61,7 @@ class MatplotlibLegendState(State): text_color = DeferredDrawCallbackProperty("#000000", docstring='Text color of the legend') def __init__(self, *args, **kwargs): - MatplotlibLegendState.loc_and_drag.set_choices(self, VALID_LOCATIONS) + MatplotlibLegendState.location.set_choices(self, VALID_LOCATIONS) super().__init__(*args, **kwargs) self._set_color_choices() @@ -76,20 +77,19 @@ def edge_color(self): if self.show_edge: return to_rgba(self.text_color, self.alpha) else: - return 'none' + return None @property def draggable(self): - return self.loc_and_drag.endswith('(draggable)') + return self.location == 'draggable' @property - def location(self): - if self.loc_and_drag.endswith('(draggable)'): - return self.loc_and_drag[:-12] + def mpl_location(self): + if self.location == 'draggable': + return 'best' else: return self.loc_and_drag - def update_axes_settings_from(self, state): self.visible = state.show_legend self.loc_and_drag = state.loc_and_drag @@ -100,6 +100,7 @@ def update_axes_settings_from(self, state): 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 @@ -131,7 +132,6 @@ 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') - def __init__(self, *args, **kwargs): self._axes_aspect_ratio = None @@ -148,7 +148,6 @@ def __init__(self, *args, **kwargs): self.add_callback('y_min', self._adjust_limits_aspect_y, priority=10000) self.add_callback('y_max', self._adjust_limits_aspect_y, priority=10000) - def _set_axes_aspect_ratio(self, value): """ Set the aspect ratio of the axes in which the visualization is shown. diff --git a/glue/viewers/matplotlib/viewer.py b/glue/viewers/matplotlib/viewer.py index 8ac480778..b068ec30d 100644 --- a/glue/viewers/matplotlib/viewer.py +++ b/glue/viewers/matplotlib/viewer.py @@ -3,7 +3,6 @@ import numpy as np from matplotlib.patches import Rectangle -from matplotlib.colors import to_rgba from matplotlib.artist import setp as msetp from glue.viewers.matplotlib.mpl_axes import update_appearance_from_settings @@ -120,7 +119,7 @@ def setup_callbacks(self): self.state.add_callback('y_ticklabel_size', self.update_y_ticklabel) self.state.legend.add_callback('visible', self.draw_legend) - self.state.legend.add_callback('loc_and_drag', 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) @@ -193,14 +192,13 @@ def update_legend(self, *args): def draw_legend(self, *args): if self.state.legend.visible: handles, labels, handler_map = self.get_handles_legend() - kwargs = dict(loc=self.state.legend.location, + 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: kwargs["handler_map"] = handler_map - legend = self.axes.legend( - handles, labels, **kwargs) + legend = self.axes.legend(handles, labels, **kwargs) self._update_legend_visual(legend) legend.set_draggable(self.state.legend.draggable) else: @@ -318,7 +316,7 @@ def _script_footer(self): def _script_legend(self): state_dict = self.state.legend.as_dict() - state_dict['location'] = self.state.legend.location + 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.legend.visible: From 55567d0baff5a9285a1187cec4db1273c61cb770 Mon Sep 17 00:00:00 2001 From: Fabien Georget Date: Mon, 25 May 2020 21:46:54 +0200 Subject: [PATCH 5/6] add tests and bugfix for MatplotlibLegendState --- glue/viewers/matplotlib/state.py | 2 +- glue/viewers/matplotlib/tests/test_state.py | 27 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100755 glue/viewers/matplotlib/tests/test_state.py diff --git a/glue/viewers/matplotlib/state.py b/glue/viewers/matplotlib/state.py index 04328d6ad..7905e5410 100644 --- a/glue/viewers/matplotlib/state.py +++ b/glue/viewers/matplotlib/state.py @@ -88,7 +88,7 @@ def mpl_location(self): if self.location == 'draggable': return 'best' else: - return self.loc_and_drag + return self.location def update_axes_settings_from(self, state): self.visible = state.show_legend 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) From 836be790029924c07a2b49a79c4c949e2c7fc9fd Mon Sep 17 00:00:00 2001 From: Fabien Georget Date: Mon, 25 May 2020 22:01:17 +0200 Subject: [PATCH 6/6] update ui --- glue/viewers/matplotlib/qt/legend_editor.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue/viewers/matplotlib/qt/legend_editor.ui b/glue/viewers/matplotlib/qt/legend_editor.ui index 993c40265..e991e2865 100644 --- a/glue/viewers/matplotlib/qt/legend_editor.ui +++ b/glue/viewers/matplotlib/qt/legend_editor.ui @@ -66,7 +66,7 @@ - +