From f565fc0d910543c0b231bf8a94ab94ada05fc4f9 Mon Sep 17 00:00:00 2001 From: Will Dampier Date: Wed, 1 Apr 2015 14:12:40 -0400 Subject: [PATCH 1/8] changes required to implement a CategoricalRoi --- glue/core/data.py | 11 ++++++++ glue/core/roi.py | 51 +++++++++++++++++++++++++++++++++++++ glue/core/tests/test_roi.py | 37 +++++++++++++++++++++++++-- 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/glue/core/data.py b/glue/core/data.py index 4f553cc02..7f555b662 100644 --- a/glue/core/data.py +++ b/glue/core/data.py @@ -217,6 +217,13 @@ def numeric(self): """ return np.can_cast(self.data[0], np.complex) + @property + def categorical(self): + """ + Whether or not the datatype is categorical + """ + return False + def __str__(self): return "Component with shape %s" % shape_to_string(self.shape) @@ -389,6 +396,10 @@ def __init__(self, categorical_data, categories=None, jitter=None, units=None): else: self._update_data() + @property + def categorical(self): + return True + def _update_categories(self, categories=None): """ :param categories: A sorted array of categories to find in the dataset. diff --git a/glue/core/roi.py b/glue/core/roi.py index c256e513d..f8183db62 100644 --- a/glue/core/roi.py +++ b/glue/core/roi.py @@ -1102,3 +1102,54 @@ def finalize_selection(self, event): if self._patch is not None: self._patch.set_visible(False) self._axes.figure.canvas.draw() + + +class CategoricalRoi(Roi): + + def __init__(self, orientation=None, categories=None): + self.categories = np.unique(categories) + if orientation not in ['x', 'y', None]: + raise TypeError("Orientation must be one of 'x', 'y'") + + self.orientation = orientation + + def _categorical_helper(self, indata): + """ + A helper function to do the rigamaroll of getting categorical data. + + :param indata: Any type of input data + :return: The best guess at the categorical data associated with indata + """ + + try: + if indata.categorical: + return indata._categorical_data + else: + return indata[:] + except AttributeError: + return np.asarray(indata) + + def contains(self, x, y): + + if self.orientation == 'x': + check = self._categorical_helper(x) + else: + check = self._categorical_helper(y) + + index = np.minimum(np.searchsorted(self.categories, check), + len(self.categories)-1) + return self.categories[index] == check + + def update_categories(self, categories): + + self.categories = np.unique(self._categorical_helper(categories)) + + def defined(self): + """ Returns True if the ROI is defined """ + return self.categories is not None and \ + self.orientation is not None + + def reset(self): + + self.orientation = None + self.categories = None diff --git a/glue/core/tests/test_roi.py b/glue/core/tests/test_roi.py index 4929ead62..143ceebe3 100644 --- a/glue/core/tests/test_roi.py +++ b/glue/core/tests/test_roi.py @@ -7,10 +7,10 @@ import numpy as np from numpy.testing import assert_almost_equal from matplotlib.figure import Figure +from glue.core.data import CategoricalComponent from mock import MagicMock -from ..roi import (RectangularROI, UndefinedROI, CircularROI, PolygonalROI, - PointROI, MplPickROI, +from ..roi import (RectangularROI, UndefinedROI, CircularROI, PolygonalROI, CategoricalRoi, MplCircularROI, MplRectangularROI, MplPolygonalROI, XRangeROI, MplXRangeROI, YRangeROI, MplYRangeROI) @@ -340,6 +340,39 @@ def test_str(self): """ __str__ returns a string """ assert type(str(self.roi)) == str + +class TestCategorical(object): + + def setup_method(self, method): + self.roi = CategoricalRoi() + + def test_defined(self): + + assert not self.roi.defined() + nroi = CategoricalRoi(orientation='x', + categories=['a', 'b', 'c']) + assert nroi.defined() + nroi.reset() + assert not nroi.defined() + + def test_loads_from_components(self): + + comp = CategoricalComponent(np.array(['a', 'a', 'b'])) + self.roi.update_categories(comp) + + np.testing.assert_array_equal(self.roi.categories, + np.array(['a', 'b'])) + + def test_applies_components(self): + + comp = CategoricalComponent(np.array(['a', 'b', 'c'])) + self.roi.update_categories(CategoricalComponent(np.array(['a', 'b']))) + self.roi.orientation = 'x' + contained = self.roi.contains(comp, None) + np.testing.assert_array_equal(contained, + np.array([True, True, False])) + + def test_move(self): """Move polygon""" self.define_as_square() From 6da819600f8921a6fee0e5f705078eda5496837c Mon Sep 17 00:00:00 2001 From: Will Dampier Date: Wed, 1 Apr 2015 14:45:40 -0400 Subject: [PATCH 2/8] changes required to implement a categorical subset state --- glue/core/roi.py | 17 +++-------------- glue/core/subset.py | 27 +++++++++++++++++++++++++++ glue/core/tests/test_roi.py | 5 +---- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/glue/core/roi.py b/glue/core/roi.py index f8183db62..b6dd86394 100644 --- a/glue/core/roi.py +++ b/glue/core/roi.py @@ -1106,12 +1106,8 @@ def finalize_selection(self, event): class CategoricalRoi(Roi): - def __init__(self, orientation=None, categories=None): + def __init__(self, categories=None): self.categories = np.unique(categories) - if orientation not in ['x', 'y', None]: - raise TypeError("Orientation must be one of 'x', 'y'") - - self.orientation = orientation def _categorical_helper(self, indata): """ @@ -1131,11 +1127,7 @@ def _categorical_helper(self, indata): def contains(self, x, y): - if self.orientation == 'x': - check = self._categorical_helper(x) - else: - check = self._categorical_helper(y) - + check = self._categorical_helper(x) index = np.minimum(np.searchsorted(self.categories, check), len(self.categories)-1) return self.categories[index] == check @@ -1146,10 +1138,7 @@ def update_categories(self, categories): def defined(self): """ Returns True if the ROI is defined """ - return self.categories is not None and \ - self.orientation is not None + return self.categories is not None def reset(self): - - self.orientation = None self.categories = None diff --git a/glue/core/subset.py b/glue/core/subset.py index 8853af6af..9b6b2ce27 100644 --- a/glue/core/subset.py +++ b/glue/core/subset.py @@ -446,6 +446,33 @@ def copy(self): return result +class CategoricalRoiSubsetState(SubsetState): + + def __init__(self, att=None, roi=None): + super(CategoricalRoiSubsetState, self).__init__() + self.att = att + self.roi = roi + + @property + def attributes(self): + return self.att, + + @memoize + @contract(data='isinstance(Data)', view='array_view') + def to_mask(self, data, view=None): + x = data[self.att, view] + result = self.roi.contains(x, None) + assert x.shape == result.shape + return result + + def copy(self): + result = CategoricalRoiSubsetState() + result.att = self.att + result.roi = self.roi + return result + + + class RangeSubsetState(SubsetState): def __init__(self, lo, hi, att=None): diff --git a/glue/core/tests/test_roi.py b/glue/core/tests/test_roi.py index 143ceebe3..a9fde0aa0 100644 --- a/glue/core/tests/test_roi.py +++ b/glue/core/tests/test_roi.py @@ -348,9 +348,7 @@ def setup_method(self, method): def test_defined(self): - assert not self.roi.defined() - nroi = CategoricalRoi(orientation='x', - categories=['a', 'b', 'c']) + nroi = CategoricalRoi(categories=['a', 'b', 'c']) assert nroi.defined() nroi.reset() assert not nroi.defined() @@ -367,7 +365,6 @@ def test_applies_components(self): comp = CategoricalComponent(np.array(['a', 'b', 'c'])) self.roi.update_categories(CategoricalComponent(np.array(['a', 'b']))) - self.roi.orientation = 'x' contained = self.roi.contains(comp, None) np.testing.assert_array_equal(contained, np.array([True, True, False])) From bd803efc059c98e72b26fddbce4d9d664212e065 Mon Sep 17 00:00:00 2001 From: Will Dampier Date: Fri, 3 Apr 2015 13:29:47 -0400 Subject: [PATCH 3/8] changes required to implement categorical components from ScatterClient --- glue/clients/scatter_client.py | 47 +++++++++++++++++++---- glue/clients/tests/test_scatter_client.py | 30 +++++++++++++++ glue/core/roi.py | 16 ++++++++ glue/core/subset.py | 12 +++++- glue/core/tests/test_roi.py | 8 ++++ 5 files changed, 103 insertions(+), 10 deletions(-) diff --git a/glue/clients/scatter_client.py b/glue/clients/scatter_client.py index dbbc74d4b..cc429a36f 100644 --- a/glue/clients/scatter_client.py +++ b/glue/clients/scatter_client.py @@ -6,8 +6,8 @@ from ..core.client import Client from ..core.data import Data, IncompatibleAttribute, ComponentID, CategoricalComponent -from ..core.subset import RoiSubsetState, RangeSubsetState -from ..core.roi import PolygonalROI, RangeROI +from ..core.subset import RoiSubsetState, RangeSubsetState, CategoricalRoiSubsetState, AndState +from ..core.roi import PolygonalROI, RangeROI, CategoricalRoi, RectangularROI from ..core.util import relim from ..core.edit_subset_mode import EditSubsetMode from ..core.message import ComponentReplacedMessage @@ -251,6 +251,30 @@ def _set_xydata(self, coord, attribute, snap=True): self._pull_properties() self._redraw() + def _process_categorical_roi(self, roi): + """ Returns a RoiSubsetState object. + """ + + if isinstance(roi, RectangularROI): + + lo, hi = roi.xmin, roi.xmax + xcomp = self._get_data_components('x').next() + if self._check_categorical(self.xatt): + x_subset = CategoricalRoiSubsetState.from_range(xcomp, self.xatt, lo, hi) + else: + x_subset = RangeSubsetState(lo, hi, self.xatt) + + lo, hi = roi.ymin, roi.ymax + ycomp = self._get_data_components('y').next() + if self._check_categorical(self.yatt): + y_subset = CategoricalRoiSubsetState.from_range(ycomp, self.yatt, lo, hi) + else: + y_subset = RangeSubsetState(lo, hi, self.yatt) + else: + raise AssertionError + + return AndState(x_subset, y_subset) + def apply_roi(self, roi): # every editable subset is updated # using specified ROI @@ -258,13 +282,20 @@ def apply_roi(self, roi): if isinstance(roi, RangeROI): lo, hi = roi.range() att = self.xatt if roi.ori == 'x' else self.yatt - subset_state = RangeSubsetState(lo, hi, att) + if self._check_categorical(att): + comp = self._get_data_components(roi.ori).next() + subset_state = CategoricalRoiSubsetState.from_range(comp, att, lo, hi) + else: + subset_state = RangeSubsetState(lo, hi, att) else: - subset_state = RoiSubsetState() - subset_state.xatt = self.xatt - subset_state.yatt = self.yatt - x, y = roi.to_polygon() - subset_state.roi = PolygonalROI(x, y) + if self._check_categorical(self.xatt) or self._check_categorical(self.yatt): + subset_state = self._process_categorical_roi(roi) + else: + subset_state = RoiSubsetState() + subset_state.xatt = self.xatt + subset_state.yatt = self.yatt + x, y = roi.to_polygon() + subset_state.roi = PolygonalROI(x, y) mode = EditSubsetMode() visible = [d for d in self._data if self.is_visible(d)] diff --git a/glue/clients/tests/test_scatter_client.py b/glue/clients/tests/test_scatter_client.py index f1518f5de..869b822b1 100644 --- a/glue/clients/tests/test_scatter_client.py +++ b/glue/clients/tests/test_scatter_client.py @@ -677,6 +677,36 @@ def test_high_cardinatility_timing(self): timer = timeit(timer_func, number=1) assert timer < 3 # this is set for Travis speed + def test_apply_roi(self): + data = self.add_data_and_attributes() + roi = core.roi.RectangularROI() + roi.update_limits(*self.roi_limits) + x, y = self.roi_points + self.client.apply_roi(roi) + + def test_range_rois_preserved(self): + data = self.add_data_and_attributes() + assert self.client.xatt is not self.client.yatt + + roi = core.roi.XRangeROI() + roi.set_range(1, 2) + self.client.apply_roi(roi) + assert isinstance(data.edit_subset.subset_state, + core.subset.CategoricalRoiSubsetState) + assert data.edit_subset.subset_state.att == self.client.xatt + + roi = core.roi.YRangeROI() + roi.set_range(1, 2) + self.client.apply_roi(roi) + assert isinstance(data.edit_subset.subset_state, + core.subset.RangeSubsetState) + assert data.edit_subset.subset_state.att == self.client.yatt + roi = core.roi.RectangularROI(xmin=1, xmax=2, ymin=1, ymax=2) + + self.client.apply_roi(roi) + assert isinstance(data.edit_subset.subset_state, + core.subset.AndState) + # REMOVED TESTS def test_invalid_plot(self): """ This fails because the axis ticks shouldn't reset after diff --git a/glue/core/roi.py b/glue/core/roi.py index b6dd86394..c146f040d 100644 --- a/glue/core/roi.py +++ b/glue/core/roi.py @@ -1142,3 +1142,19 @@ def defined(self): def reset(self): self.categories = None + + @staticmethod + def from_range(cat_comp, lo, hi): + """ + Utility function to help contruct the Roi from a range. + + :param cat_comp: Anything understood by ._categorical_helper ... array, list or component + :param lo: lower bound of the range + :param hi: upper bound of the range + :return: CategoricalRoi object + """ + + roi = CategoricalRoi() + cat_data = cat_comp._categories + roi.update_categories(cat_data[np.floor(lo):np.ceil(hi)]) + return roi diff --git a/glue/core/subset.py b/glue/core/subset.py index 9b6b2ce27..8e1ea24c0 100644 --- a/glue/core/subset.py +++ b/glue/core/subset.py @@ -14,6 +14,7 @@ from ..utils import view_shape from ..external.six import PY3 from .contracts import contract +from .roi import CategoricalRoi __all__ = ['Subset', 'SubsetState', 'RoiSubsetState', 'CompositeSubsetState', 'OrState', 'AndState', 'XorState', 'InvertState', @@ -460,10 +461,10 @@ def attributes(self): @memoize @contract(data='isinstance(Data)', view='array_view') def to_mask(self, data, view=None): - x = data[self.att, view] + x = data.get_component(self.att)._categorical_data[view] result = self.roi.contains(x, None) assert x.shape == result.shape - return result + return result.ravel() def copy(self): result = CategoricalRoiSubsetState() @@ -471,6 +472,13 @@ def copy(self): result.roi = self.roi return result + @staticmethod + def from_range(component, att, lo, hi): + + roi = CategoricalRoi.from_range(component, lo, hi) + subset = CategoricalRoiSubsetState(roi=roi, + att=att) + return subset class RangeSubsetState(SubsetState): diff --git a/glue/core/tests/test_roi.py b/glue/core/tests/test_roi.py index a9fde0aa0..28ab5562a 100644 --- a/glue/core/tests/test_roi.py +++ b/glue/core/tests/test_roi.py @@ -369,6 +369,14 @@ def test_applies_components(self): np.testing.assert_array_equal(contained, np.array([True, True, False])) + def test_from_range(self): + + comp = CategoricalComponent(np.array(list('abcdefghijklmnopqrstuvwxyz')*2)) + + roi = CategoricalRoi.from_range(comp, 6, 10) + np.testing.assert_array_equal(roi.categories, + np.array(list('ghij'))) + def test_move(self): """Move polygon""" From 480273bcaf1575a96e7a43ef3548fb8ab6314e87 Mon Sep 17 00:00:00 2001 From: Will Dampier Date: Fri, 3 Apr 2015 13:47:14 -0400 Subject: [PATCH 4/8] changes required to implement the new CategoricalRoi on histogram clients. --- glue/clients/histogram_client.py | 11 ++++++++--- glue/clients/tests/test_histogram_client.py | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/glue/clients/histogram_client.py b/glue/clients/histogram_client.py index 2709abf37..62c204f68 100644 --- a/glue/clients/histogram_client.py +++ b/glue/clients/histogram_client.py @@ -5,7 +5,7 @@ from ..core.client import Client from ..core import message as msg from ..core.data import Data, CategoricalComponent -from ..core.subset import RangeSubsetState +from ..core.subset import RangeSubsetState, CategoricalRoiSubsetState from ..core.exceptions import IncompatibleDataException, IncompatibleAttribute from ..core.edit_subset_mode import EditSubsetMode from .layer_artist import HistogramLayerArtist, LayerArtistContainer @@ -379,8 +379,13 @@ def apply_roi(self, roi): lo = 10 ** lo hi = 10 ** hi - state = RangeSubsetState(lo, hi) - state.att = self.component + comp = self._get_data_components('x').next() + if comp.categorical: + state = CategoricalRoiSubsetState.from_range(comp, self.component, + lo, hi) + else: + state = RangeSubsetState(lo, hi) + state.att = self.component mode = EditSubsetMode() visible = [d for d in self.data if self.is_layer_visible(d)] focus = visible[0] if len(visible) > 0 else None diff --git a/glue/clients/tests/test_histogram_client.py b/glue/clients/tests/test_histogram_client.py index 6bce22526..6e810457e 100644 --- a/glue/clients/tests/test_histogram_client.py +++ b/glue/clients/tests/test_histogram_client.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function import pytest +import numpy as np from mock import MagicMock @@ -12,7 +13,7 @@ from ...core.data_collection import DataCollection from ...core.exceptions import IncompatibleDataException from ...core.data import Data, CategoricalComponent, ComponentID -from ...core.subset import RangeSubsetState +from ...core.subset import RangeSubsetState, CategoricalRoiSubsetState from .util import renderless_figure @@ -369,6 +370,23 @@ def test_tick_labels(self): xlabels = [formatter.format_data(pos) for pos in range(6)] assert correct_labels == xlabels + def test_apply_roi(self): + self.client.add_layer(self.data) + self.client.set_component(self.data.id['x']) + # bins are 1...4 + + self.data.edit_subset = [self.data.subsets[0]] + + roi = MagicMock() + roi.to_polygon.return_value = [1.2, 2, 4], [2, 3, 4] + + self.client.apply_roi(roi) + state = self.data.subsets[0].subset_state + assert isinstance(state, CategoricalRoiSubsetState) + np.testing.assert_equal(self.data.subsets[0].subset_state.roi.categories, + np.array(['a', 'b', 'c', 'd', 'e'])) + + # REMOVED TESTS def test_xlog_axes_labels(self): """ log-scale doesn't make sense for categorical data""" From 89c40ebb55483a9364b60785bf81b6627e8b5dcf Mon Sep 17 00:00:00 2001 From: Will Dampier Date: Fri, 3 Apr 2015 13:56:05 -0400 Subject: [PATCH 5/8] Fixing for python-3 comp ... no more .next() on generators? --- glue/clients/histogram_client.py | 24 +++++++++++++----------- glue/clients/scatter_client.py | 29 +++++++++++++++++++---------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/glue/clients/histogram_client.py b/glue/clients/histogram_client.py index 62c204f68..599675599 100644 --- a/glue/clients/histogram_client.py +++ b/glue/clients/histogram_client.py @@ -379,17 +379,19 @@ def apply_roi(self, roi): lo = 10 ** lo hi = 10 ** hi - comp = self._get_data_components('x').next() - if comp.categorical: - state = CategoricalRoiSubsetState.from_range(comp, self.component, - lo, hi) - else: - state = RangeSubsetState(lo, hi) - state.att = self.component - mode = EditSubsetMode() - visible = [d for d in self.data if self.is_layer_visible(d)] - focus = visible[0] if len(visible) > 0 else None - mode.update(self.data, state, focus_data=focus) + comp = list(self._get_data_components('x')) + if comp: + comp = comp[0] + if comp.categorical: + state = CategoricalRoiSubsetState.from_range(comp, self.component, + lo, hi) + else: + state = RangeSubsetState(lo, hi) + state.att = self.component + mode = EditSubsetMode() + visible = [d for d in self.data if self.is_layer_visible(d)] + focus = visible[0] if len(visible) > 0 else None + mode.update(self.data, state, focus_data=focus) def register_to_hub(self, hub): dfilter = lambda x: x.sender.data in self._artists diff --git a/glue/clients/scatter_client.py b/glue/clients/scatter_client.py index cc429a36f..519e9b20b 100644 --- a/glue/clients/scatter_client.py +++ b/glue/clients/scatter_client.py @@ -258,18 +258,24 @@ def _process_categorical_roi(self, roi): if isinstance(roi, RectangularROI): lo, hi = roi.xmin, roi.xmax - xcomp = self._get_data_components('x').next() - if self._check_categorical(self.xatt): - x_subset = CategoricalRoiSubsetState.from_range(xcomp, self.xatt, lo, hi) + xcomp = list(self._get_data_components('x')) + if xcomp: + if self._check_categorical(self.xatt): + x_subset = CategoricalRoiSubsetState.from_range(xcomp[0], self.xatt, lo, hi) + else: + x_subset = RangeSubsetState(lo, hi, self.xatt) else: - x_subset = RangeSubsetState(lo, hi, self.xatt) + x_subset = None lo, hi = roi.ymin, roi.ymax - ycomp = self._get_data_components('y').next() - if self._check_categorical(self.yatt): - y_subset = CategoricalRoiSubsetState.from_range(ycomp, self.yatt, lo, hi) + ycomp = list(self._get_data_components('y')) + if ycomp: + if self._check_categorical(self.yatt): + y_subset = CategoricalRoiSubsetState.from_range(ycomp[0], self.yatt, lo, hi) + else: + y_subset = RangeSubsetState(lo, hi, self.yatt) else: - y_subset = RangeSubsetState(lo, hi, self.yatt) + y_subset = None else: raise AssertionError @@ -283,8 +289,11 @@ def apply_roi(self, roi): lo, hi = roi.range() att = self.xatt if roi.ori == 'x' else self.yatt if self._check_categorical(att): - comp = self._get_data_components(roi.ori).next() - subset_state = CategoricalRoiSubsetState.from_range(comp, att, lo, hi) + comp = list(self._get_data_components(roi.ori)) + if comp: + subset_state = CategoricalRoiSubsetState.from_range(comp[0], att, lo, hi) + else: + subset_state = None else: subset_state = RangeSubsetState(lo, hi, att) else: From 21924e35fb959e03a266f82311e3016d099a46f7 Mon Sep 17 00:00:00 2001 From: Will Dampier Date: Fri, 3 Apr 2015 14:11:32 -0400 Subject: [PATCH 6/8] Fixed some crazy branching logic --- glue/clients/scatter_client.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/glue/clients/scatter_client.py b/glue/clients/scatter_client.py index 519e9b20b..32583d50f 100644 --- a/glue/clients/scatter_client.py +++ b/glue/clients/scatter_client.py @@ -256,30 +256,22 @@ def _process_categorical_roi(self, roi): """ if isinstance(roi, RectangularROI): - - lo, hi = roi.xmin, roi.xmax - xcomp = list(self._get_data_components('x')) - if xcomp: - if self._check_categorical(self.xatt): - x_subset = CategoricalRoiSubsetState.from_range(xcomp[0], self.xatt, lo, hi) - else: - x_subset = RangeSubsetState(lo, hi, self.xatt) - else: - x_subset = None - - lo, hi = roi.ymin, roi.ymax - ycomp = list(self._get_data_components('y')) - if ycomp: - if self._check_categorical(self.yatt): - y_subset = CategoricalRoiSubsetState.from_range(ycomp[0], self.yatt, lo, hi) + subsets = [] + axes = [('x', roi.xmin, roi.xmax), + ('y', roi.ymin, roi.ymax)] + for coord, lo, hi in axes: + comp = list(self._get_data_components(coord)) + if comp: + if comp[0].categorical: + subset = CategoricalRoiSubsetState.from_range(comp[0], self.xatt, lo, hi) + else: + subset = RangeSubsetState(lo, hi, self.xatt) else: - y_subset = RangeSubsetState(lo, hi, self.yatt) - else: - y_subset = None + subset = None + subsets.append(subset) else: raise AssertionError - - return AndState(x_subset, y_subset) + return AndState(*subsets) def apply_roi(self, roi): # every editable subset is updated From ce4070e77038b971aa6d089825f288c65e696124 Mon Sep 17 00:00:00 2001 From: Will Date: Mon, 22 Jun 2015 10:18:13 -0400 Subject: [PATCH 7/8] fixed rebase issues and improved docstrings. --- glue/core/roi.py | 20 +++++++++++++++++++- glue/core/tests/test_roi.py | 13 +------------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/glue/core/roi.py b/glue/core/roi.py index c146f040d..7ced0d67d 100644 --- a/glue/core/roi.py +++ b/glue/core/roi.py @@ -1106,6 +1106,10 @@ def finalize_selection(self, event): class CategoricalRoi(Roi): + """ + A ROI abstraction to represent selections of categorical data. + """ + def __init__(self, categories=None): self.categories = np.unique(categories) @@ -1126,6 +1130,20 @@ def _categorical_helper(self, indata): return np.asarray(indata) def contains(self, x, y): + """ + Test whether a set categorical elements fall within + the region of interest + + :param x: Any array-like object of categories + (includes CategoricalComponenets) + :param y: Unused but required for compatibility + + *Returns* + + A list of True/False values, for whether each x value falls + within the ROI + + """ check = self._categorical_helper(x) index = np.minimum(np.searchsorted(self.categories, check), @@ -1146,7 +1164,7 @@ def reset(self): @staticmethod def from_range(cat_comp, lo, hi): """ - Utility function to help contruct the Roi from a range. + Utility function to help construct the Roi from a range. :param cat_comp: Anything understood by ._categorical_helper ... array, list or component :param lo: lower bound of the range diff --git a/glue/core/tests/test_roi.py b/glue/core/tests/test_roi.py index 28ab5562a..ecba75371 100644 --- a/glue/core/tests/test_roi.py +++ b/glue/core/tests/test_roi.py @@ -11,7 +11,7 @@ from mock import MagicMock from ..roi import (RectangularROI, UndefinedROI, CircularROI, PolygonalROI, CategoricalRoi, - MplCircularROI, MplRectangularROI, MplPolygonalROI, + MplCircularROI, MplRectangularROI, MplPolygonalROI, MplPickROI, PointROI, XRangeROI, MplXRangeROI, YRangeROI, MplYRangeROI) from .. import roi as r @@ -378,17 +378,6 @@ def test_from_range(self): np.array(list('ghij'))) - def test_move(self): - """Move polygon""" - self.define_as_square() - vx = list(self.roi.vx) - vy = list(self.roi.vy) - self.roi.move_to(1, 1) - assert type(self.roi.vx) == list - assert type(self.roi.vy) == list - assert self.roi.vx[0] - vx[0] == 1 - assert self.roi.vy[0] - vy[0] == 1 - class DummyEvent(object): def __init__(self, x, y, inaxes=True): From 11810a56442e52b1fdf4daca39db1c6eb880223b Mon Sep 17 00:00:00 2001 From: Will Date: Mon, 22 Jun 2015 12:38:39 -0400 Subject: [PATCH 8/8] fixed empty categories issue --- glue/core/roi.py | 5 ++++- glue/core/tests/test_roi.py | 21 +++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/glue/core/roi.py b/glue/core/roi.py index 7ced0d67d..b8dc9f5c8 100644 --- a/glue/core/roi.py +++ b/glue/core/roi.py @@ -1111,7 +1111,10 @@ class CategoricalRoi(Roi): """ def __init__(self, categories=None): - self.categories = np.unique(categories) + if categories is None: + self.categories = None + else: + self.update_categories(categories) def _categorical_helper(self, indata): """ diff --git a/glue/core/tests/test_roi.py b/glue/core/tests/test_roi.py index ecba75371..2404f1a6f 100644 --- a/glue/core/tests/test_roi.py +++ b/glue/core/tests/test_roi.py @@ -343,8 +343,10 @@ def test_str(self): class TestCategorical(object): - def setup_method(self, method): - self.roi = CategoricalRoi() + def test_empty(self): + + roi = CategoricalRoi() + assert not roi.defined() def test_defined(self): @@ -355,17 +357,24 @@ def test_defined(self): def test_loads_from_components(self): + roi = CategoricalRoi() comp = CategoricalComponent(np.array(['a', 'a', 'b'])) - self.roi.update_categories(comp) + roi.update_categories(comp) - np.testing.assert_array_equal(self.roi.categories, + np.testing.assert_array_equal(roi.categories, np.array(['a', 'b'])) + roi = CategoricalRoi(categories=comp) + np.testing.assert_array_equal(roi.categories, + np.array(['a', 'b'])) + + def test_applies_components(self): + roi = CategoricalRoi() comp = CategoricalComponent(np.array(['a', 'b', 'c'])) - self.roi.update_categories(CategoricalComponent(np.array(['a', 'b']))) - contained = self.roi.contains(comp, None) + roi.update_categories(CategoricalComponent(np.array(['a', 'b']))) + contained = roi.contains(comp, None) np.testing.assert_array_equal(contained, np.array([True, True, False]))