From 12ac8305fb3b153bda39332867b65163b581daf0 Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Fri, 7 Jul 2023 16:13:14 -0400 Subject: [PATCH 1/7] Add lasso/polygon tool to glupyter viewers --- glue_jupyter/bqplot/common/tools.py | 76 +++++++++++++++- glue_jupyter/bqplot/image/viewer.py | 2 +- glue_jupyter/bqplot/scatter/viewer.py | 2 +- glue_jupyter/icons/glue_lasso.svg | 125 ++++++++++++++++++++++++++ 4 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 glue_jupyter/icons/glue_lasso.svg diff --git a/glue_jupyter/bqplot/common/tools.py b/glue_jupyter/bqplot/common/tools.py index 3db30951..e74baab8 100644 --- a/glue_jupyter/bqplot/common/tools.py +++ b/glue_jupyter/bqplot/common/tools.py @@ -2,9 +2,9 @@ from contextlib import nullcontext import numpy as np -from bqplot import PanZoom +from bqplot import PanZoom, Lines from bqplot.interacts import BrushSelector, BrushIntervalSelector -from bqplot_image_gl.interacts import BrushEllipseSelector +from bqplot_image_gl.interacts import BrushEllipseSelector, MouseInteraction from glue import __version__ as glue_version from glue.core.roi import RectangularROI, RangeROI, CircularROI, EllipticalROI, PolygonalROI from glue.core.subset import RoiSubsetState @@ -178,6 +178,78 @@ def activate(self): super().activate() +@viewer_tool +class BqplotPolygonMode(BqplotSelectionTool): + + icon = os.path.join(ICONS_DIR, 'glue_lasso') + tool_id = 'bqplot:polygon' + action_text = 'Polygonal ROI' + tool_tip = ('Lasso a region of interest\n') + + def __init__(self, viewer, roi=None, finalize_callback=None, **kwargs): + + super().__init__(viewer, **kwargs) + + self.interact = MouseInteraction(x_scale=self.viewer.scale_x, + y_scale=self.viewer.scale_y, + move_throttle=70) + self.patch = Lines(x=[[]], y=[[]], fill_colors=['yellow'], colors=['yellow'], + fill='inside', close_path=True, + scales={'x': self.viewer.scale_x, 'y': self.viewer.scale_y}) + if roi is not None: + self.update_from_roi(roi) + self.finalize_callback = finalize_callback + + def update_from_roi(self, roi): + """ + TOTO: This should update self.xlist and self.ylist from the existing ROI + """ + with self.viewer._output_widget or nullcontext(): + pass + + def activate(self): + super().activate() + + self.viewer.add_event_callback(self.on_msg, events=['dragstart', 'dragmove', 'dragend']) + + def deactivate(self): + self.viewer.remove_event_callback(self.on_msg) + super().deactivate() + + def on_msg(self, event): + name = event['event'] + domain = event['domain'] + x, y = domain['x'], domain['y'] + if name == 'dragstart': + self.original_marks = list(self.viewer.figure.marks) + self.viewer.figure.marks = self.original_marks + [self.patch] + self.xlist = [x] + self.ylist = [y] + elif name == 'dragmove': + self.xlist.append(x) + self.ylist.append(y) + self.patch.x = self.xlist + self.patch.y = self.ylist + elif name == 'dragend': + if self.xlist is not None and self.ylist is not None: + roi = PolygonalROI(vx=self.xlist, vy=self.ylist) + self.viewer.apply_roi(roi) + + new_marks = [] + for mark in self.viewer.figure.marks: + if mark == self.patch: + pass + else: + new_marks.append(mark) + self.viewer.figure.marks = new_marks + self.xlist = [] + self.ylist = [] + self.patch.x = [[]] + self.patch.y = [[]] + if self.finalize_callback is not None: + self.finalize_callback() + + @viewer_tool class BqplotCircleMode(BqplotSelectionTool): diff --git a/glue_jupyter/bqplot/image/viewer.py b/glue_jupyter/bqplot/image/viewer.py index d0434b96..4fa6afc1 100644 --- a/glue_jupyter/bqplot/image/viewer.py +++ b/glue_jupyter/bqplot/image/viewer.py @@ -30,7 +30,7 @@ class BqplotImageView(BqplotBaseView): _state_cls = BqplotImageViewerState _options_cls = ImageViewerStateWidget - tools = ['bqplot:home', 'bqplot:panzoom', 'bqplot:rectangle', 'bqplot:circle'] + tools = ['bqplot:home', 'bqplot:panzoom', 'bqplot:rectangle', 'bqplot:circle', 'bqplot:polygon'] def __init__(self, session): diff --git a/glue_jupyter/bqplot/scatter/viewer.py b/glue_jupyter/bqplot/scatter/viewer.py index ebd6ca21..84b3e05b 100644 --- a/glue_jupyter/bqplot/scatter/viewer.py +++ b/glue_jupyter/bqplot/scatter/viewer.py @@ -26,7 +26,7 @@ class BqplotScatterView(BqplotBaseView): _layer_style_widget_cls = ScatterLayerStateWidget tools = ['bqplot:home', 'bqplot:panzoom', 'bqplot:rectangle', 'bqplot:circle', - 'bqplot:xrange', 'bqplot:yrange'] + 'bqplot:xrange', 'bqplot:yrange', 'bqplot:polygon'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/glue_jupyter/icons/glue_lasso.svg b/glue_jupyter/icons/glue_lasso.svg new file mode 100644 index 00000000..ed819f2e --- /dev/null +++ b/glue_jupyter/icons/glue_lasso.svg @@ -0,0 +1,125 @@ + + + +image/svg+xml \ No newline at end of file From 2d6b0403b9aceab17c9531177cb09cd8c68b705e Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Fri, 7 Jul 2023 16:21:10 -0400 Subject: [PATCH 2/7] Just keep track of vertices in the bqplot Lines object --- glue_jupyter/bqplot/common/tools.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/glue_jupyter/bqplot/common/tools.py b/glue_jupyter/bqplot/common/tools.py index e74baab8..3168afd8 100644 --- a/glue_jupyter/bqplot/common/tools.py +++ b/glue_jupyter/bqplot/common/tools.py @@ -223,17 +223,14 @@ def on_msg(self, event): if name == 'dragstart': self.original_marks = list(self.viewer.figure.marks) self.viewer.figure.marks = self.original_marks + [self.patch] - self.xlist = [x] - self.ylist = [y] + self.patch.x = [x] + self.patch.y = [y] elif name == 'dragmove': - self.xlist.append(x) - self.ylist.append(y) - self.patch.x = self.xlist - self.patch.y = self.ylist + self.patch.x = np.append(self.patch.x, x) + self.patch.y = np.append(self.patch.y, y) elif name == 'dragend': - if self.xlist is not None and self.ylist is not None: - roi = PolygonalROI(vx=self.xlist, vy=self.ylist) - self.viewer.apply_roi(roi) + roi = PolygonalROI(vx=self.patch.x, vy=self.patch.y) + self.viewer.apply_roi(roi) new_marks = [] for mark in self.viewer.figure.marks: @@ -242,8 +239,6 @@ def on_msg(self, event): else: new_marks.append(mark) self.viewer.figure.marks = new_marks - self.xlist = [] - self.ylist = [] self.patch.x = [[]] self.patch.y = [[]] if self.finalize_callback is not None: From 47b17802d2ea9a96327eceadc8b269eae130bbe4 Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Tue, 11 Jul 2023 10:08:37 -0400 Subject: [PATCH 3/7] Simplify icon specification --- glue_jupyter/bqplot/common/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue_jupyter/bqplot/common/tools.py b/glue_jupyter/bqplot/common/tools.py index 3168afd8..aeb92786 100644 --- a/glue_jupyter/bqplot/common/tools.py +++ b/glue_jupyter/bqplot/common/tools.py @@ -181,7 +181,7 @@ def activate(self): @viewer_tool class BqplotPolygonMode(BqplotSelectionTool): - icon = os.path.join(ICONS_DIR, 'glue_lasso') + icon = 'glue_lasso' tool_id = 'bqplot:polygon' action_text = 'Polygonal ROI' tool_tip = ('Lasso a region of interest\n') From bdd5508d6a19f846c363482bc62cafbaa389d963 Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Tue, 11 Jul 2023 10:45:18 -0400 Subject: [PATCH 4/7] Change color and opacity to match other selection tools --- glue_jupyter/bqplot/common/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue_jupyter/bqplot/common/tools.py b/glue_jupyter/bqplot/common/tools.py index aeb92786..e5a026c1 100644 --- a/glue_jupyter/bqplot/common/tools.py +++ b/glue_jupyter/bqplot/common/tools.py @@ -193,7 +193,7 @@ def __init__(self, viewer, roi=None, finalize_callback=None, **kwargs): self.interact = MouseInteraction(x_scale=self.viewer.scale_x, y_scale=self.viewer.scale_y, move_throttle=70) - self.patch = Lines(x=[[]], y=[[]], fill_colors=['yellow'], colors=['yellow'], + self.patch = Lines(x=[[]], y=[[]], fill_colors=[INTERACT_COLOR], colors=[INTERACT_COLOR], opacities=[0.6], fill='inside', close_path=True, scales={'x': self.viewer.scale_x, 'y': self.viewer.scale_y}) if roi is not None: From 50d476521e5bfeda6aeebe0f57a5619689d8829f Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Tue, 11 Jul 2023 10:53:02 -0400 Subject: [PATCH 5/7] Fix codestyle --- glue_jupyter/bqplot/common/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/glue_jupyter/bqplot/common/tools.py b/glue_jupyter/bqplot/common/tools.py index e5a026c1..d4d17bbb 100644 --- a/glue_jupyter/bqplot/common/tools.py +++ b/glue_jupyter/bqplot/common/tools.py @@ -193,8 +193,8 @@ def __init__(self, viewer, roi=None, finalize_callback=None, **kwargs): self.interact = MouseInteraction(x_scale=self.viewer.scale_x, y_scale=self.viewer.scale_y, move_throttle=70) - self.patch = Lines(x=[[]], y=[[]], fill_colors=[INTERACT_COLOR], colors=[INTERACT_COLOR], opacities=[0.6], - fill='inside', close_path=True, + self.patch = Lines(x=[[]], y=[[]], fill_colors=[INTERACT_COLOR], colors=[INTERACT_COLOR], + opacities=[0.6], fill='inside', close_path=True, scales={'x': self.viewer.scale_x, 'y': self.viewer.scale_y}) if roi is not None: self.update_from_roi(roi) From 098747f5251fda9e73b38feec6206d49f2fda3b5 Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Thu, 13 Jul 2023 16:57:08 -0400 Subject: [PATCH 6/7] Remove redundant MouseInteraction that was causing this to fail in classic Notebook --- glue_jupyter/bqplot/common/tools.py | 35 +++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/glue_jupyter/bqplot/common/tools.py b/glue_jupyter/bqplot/common/tools.py index d4d17bbb..7003a7bd 100644 --- a/glue_jupyter/bqplot/common/tools.py +++ b/glue_jupyter/bqplot/common/tools.py @@ -4,7 +4,7 @@ import numpy as np from bqplot import PanZoom, Lines from bqplot.interacts import BrushSelector, BrushIntervalSelector -from bqplot_image_gl.interacts import BrushEllipseSelector, MouseInteraction +from bqplot_image_gl.interacts import BrushEllipseSelector from glue import __version__ as glue_version from glue.core.roi import RectangularROI, RangeROI, CircularROI, EllipticalROI, PolygonalROI from glue.core.subset import RoiSubsetState @@ -180,7 +180,12 @@ def activate(self): @viewer_tool class BqplotPolygonMode(BqplotSelectionTool): - + """ + Since Bqplot LassoSelector does not allow us to get the coordinates of the + selection (see https://github.com/bqplot/bqplot/pull/674), we simply use + a callback on the default viewer MouseInteraction and a patch to + display the selection. + """ icon = 'glue_lasso' tool_id = 'bqplot:polygon' action_text = 'Polygonal ROI' @@ -190,9 +195,6 @@ def __init__(self, viewer, roi=None, finalize_callback=None, **kwargs): super().__init__(viewer, **kwargs) - self.interact = MouseInteraction(x_scale=self.viewer.scale_x, - y_scale=self.viewer.scale_y, - move_throttle=70) self.patch = Lines(x=[[]], y=[[]], fill_colors=[INTERACT_COLOR], colors=[INTERACT_COLOR], opacities=[0.6], fill='inside', close_path=True, scales={'x': self.viewer.scale_x, 'y': self.viewer.scale_y}) @@ -208,12 +210,31 @@ def update_from_roi(self, roi): pass def activate(self): - super().activate() + """ + We do not call super().activate() because we don't have a separate interact, + instead we just add a callback to the default viewer MouseInteraction. + """ + + # We need to make sure any existing callbacks associated with this + # viewer are cleared. This can happen if the user switches between + # different viewers without deactivating the tool. + try: + self.viewer.remove_event_callback(self.on_msg) + except KeyError: + pass + # Disable any active tool in other viewers + if self.viewer.session.application.get_setting('single_global_active_tool'): + for viewer in self.viewer.session.application.viewers: + if viewer is not self.viewer: + viewer.toolbar.active_tool = None self.viewer.add_event_callback(self.on_msg, events=['dragstart', 'dragmove', 'dragend']) def deactivate(self): - self.viewer.remove_event_callback(self.on_msg) + try: + self.viewer.remove_event_callback(self.on_msg) + except KeyError: + pass super().deactivate() def on_msg(self, event): From b12d66a59041b7b09a0861bc84a7c9be0381351e Mon Sep 17 00:00:00 2001 From: Jonathan Foster Date: Thu, 13 Jul 2023 17:04:12 -0400 Subject: [PATCH 7/7] Document why we don't use update_from_roi --- glue_jupyter/bqplot/common/tools.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/glue_jupyter/bqplot/common/tools.py b/glue_jupyter/bqplot/common/tools.py index 7003a7bd..8f7c21d5 100644 --- a/glue_jupyter/bqplot/common/tools.py +++ b/glue_jupyter/bqplot/common/tools.py @@ -204,10 +204,11 @@ def __init__(self, viewer, roi=None, finalize_callback=None, **kwargs): def update_from_roi(self, roi): """ - TOTO: This should update self.xlist and self.ylist from the existing ROI + While other tools allow the user to click and drag to reposition a selection, + this probably does not make sense for a polygonal selection, so we do not do + not support this. """ - with self.viewer._output_widget or nullcontext(): - pass + pass def activate(self): """