diff --git a/glue/app/qt/layer_tree_widget.py b/glue/app/qt/layer_tree_widget.py index f170434ac..35130c75d 100644 --- a/glue/app/qt/layer_tree_widget.py +++ b/glue/app/qt/layer_tree_widget.py @@ -16,7 +16,7 @@ from glue.icons.qt import get_icon from glue.dialogs.component_arithmetic.qt import ArithmeticEditorWidget from glue.dialogs.component_manager.qt import ComponentManagerWidget -from glue.dialogs.subset_facet.qt import SubsetFacetDialog +from glue.dialogs.subset_facet.qt import SubsetFacetDialog, SubsetCategoricalsDialog from glue.dialogs.data_wizard.qt import data_wizard from glue.utils import nonpartial from glue.utils.decorators import avoid_circular @@ -144,6 +144,25 @@ def _do_action(self): parent=self._layer_tree, default=default) +class SubsetCategoriesAction(LayerAction): + + """Add a sequence of subsets from all the categories in a ComponentID""" + _title = "Create subsets from categories" + _tooltip = "Create subsets from categories" + + def _can_trigger(self): + return len(self._layer_tree.data_collection) > 0 + + def _do_action(self): + layers = self.selected_layers() + try: + default = layers[0].data + except (AttributeError, TypeError, IndexError): + default = None + SubsetCategoricalsDialog.facet(self._layer_tree.data_collection, + parent=self._layer_tree, default=default) + + class MetadataAction(LayerAction): _title = "View metadata/header" @@ -599,6 +618,7 @@ def _create_actions(self): self._actions['clear'] = ClearAction(self) self._actions['delete'] = DeleteAction(self) self._actions['facet'] = FacetAction(self) + self._actions['subsetcategories'] = SubsetCategoriesAction(self) self._actions['metadata'] = MetadataAction(self) self._actions['merge'] = MergeAction(self) self._actions['maskify'] = MaskifySubsetAction(self) diff --git a/glue/config.py b/glue/config.py index 98024eaa9..adf843a7b 100644 --- a/glue/config.py +++ b/glue/config.py @@ -418,6 +418,8 @@ def default_members(self): members.append(['Red-Yellow-Blue', cm.RdYlBu]) members.append(['Purple-Orange', cm.PuOr]) members.append(['Purple-Green', cm.PRGn]) + members.append(['Tab20', cm.tab20]) + return members def add(self, label, cmap): diff --git a/glue/core/util.py b/glue/core/util.py index c88b012f4..da09ed32f 100644 --- a/glue/core/util.py +++ b/glue/core/util.py @@ -12,7 +12,7 @@ import matplotlib.ticker as mticker __all__ = ["ThetaRadianFormatter", "relim", "split_component_view", "join_component_view", - "facet_subsets", "colorize_subsets", "disambiguate", + "subset_categorical", "facet_subsets", "colorize_subsets", "disambiguate", 'small_view', 'small_view_array', 'visible_limits', 'tick_linker', 'update_ticks'] @@ -191,6 +191,39 @@ def join_component_view(component, view): return tuple(result) +def subset_categorical(data_collection, data, cid, prefix=''): + """ + Create a series of subsets based on the categories present in + a particular attribute. + """ + TOO_MANY_CATEGORIES = 20 + + labels = [] + states = [] + component = data.get_kind(cid) + if component == 'categorical': + category_names = data[cid].categories + elif component == 'numerical': + category_names = np.unique(data[cid]) + + print(component) + if len(category_names) > TOO_MANY_CATEGORIES: + print("There are {0} categories in this component. Continuing will create a lot of subsets." + "Are you sure you want to continue?".format(len(category_names))) + for catname in category_names: + subsetstate = data.id[cid] == catname + states.append(subsetstate) + label = prefix + '{0}={1}'.format(cid, catname) + labels.append(label) + + result = [] + for lbl, s in zip(labels, states): + sg = data_collection.new_subset_group(label=lbl, subset_state=s) + result.append(sg) + + return result + + def facet_subsets(data_collection, cid, lo=None, hi=None, steps=5, prefix='', log=False): """ diff --git a/glue/dialogs/subset_facet/qt/subset_categorical.ui b/glue/dialogs/subset_facet/qt/subset_categorical.ui new file mode 100644 index 000000000..0b4f16ae5 --- /dev/null +++ b/glue/dialogs/subset_facet/qt/subset_categorical.ui @@ -0,0 +1,132 @@ + + + SubsetFacet + + + + 0 + 0 + 359 + 411 + + + + Subset Facet + + + + 4 + + + 4 + + + 4 + + + 4 + + + 4 + + + + + Datasets + + + Qt::AlignCenter + + + + + + + QComboBox::AdjustToMinimumContentsLength + + + + + + + Qt::LeftToRight + + + Attributes + + + Qt::AlignCenter + + + + + + + The attributes associated with this data set + + + + + + + + + Color Scale + + + + + + + QComboBox::AdjustToMinimumContentsLength + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + OK + + + true + + + + + + + + + + QColormapCombo + QComboBox +
glue.utils.qt.colors
+
+
+ + +
diff --git a/glue/dialogs/subset_facet/qt/subset_facet.py b/glue/dialogs/subset_facet/qt/subset_facet.py index 9d894563d..7880ad90a 100644 --- a/glue/dialogs/subset_facet/qt/subset_facet.py +++ b/glue/dialogs/subset_facet/qt/subset_facet.py @@ -3,7 +3,7 @@ from matplotlib import cm from qtpy import QtWidgets -from glue.core.util import colorize_subsets, facet_subsets +from glue.core.util import colorize_subsets, facet_subsets, subset_categorical from glue.core.state_objects import State from echo import CallbackProperty, SelectionCallbackProperty from glue.utils.qt import load_ui @@ -11,7 +11,7 @@ from echo.qt import autoconnect_callbacks_to_qt from glue.core.state_objects import StateAttributeLimitsHelper -__all__ = ['SubsetFacetDialog'] +__all__ = ['SubsetFacetDialog', 'SubsetCategoricalsDialog'] class SubsetFacetState(State): @@ -90,7 +90,79 @@ def _apply(self): @classmethod def facet(cls, collect, default=None, parent=None): """ - Class method to create facted subsets. + Class method to create faceted subsets. + + The arguments are the same as __init__. + """ + self = cls(collect, parent=parent, default=default) + value = self.exec_() + + if value == QtWidgets.QDialog.Accepted: + self._apply() + + +class SubsetCategoricalsState(State): + + data = SelectionCallbackProperty() + att = SelectionCallbackProperty() + cmap = CallbackProperty() + + def __init__(self, data_collection): + + super(SubsetCategoricalsState, self).__init__() + + self.data_helper = DataCollectionComboHelper(self, 'data', data_collection) + # Some categoricals may be encoded as integers, so we allow that as an option + self.att_helper = ComponentIDComboHelper(self, 'att', numeric=True, categorical=True) + + self.add_callback('data', self._on_data_change) + self._on_data_change() + + def _on_data_change(self, *args, **kwargs): + self.att_helper.set_multiple_data([] if self.data is None else [self.data]) + + +class SubsetCategoricalsDialog(QtWidgets.QDialog): + """ + Create a new dialog to create subsets on a categorical component + + Parameters + ---------- + collect : :class:`~glue.core.data_collection.DataCollection` + The data collection to use + default : :class:`~glue.core.data.Data`, optional + The default dataset in the collection (optional) + """ + + def __init__(self, collect, default=None, parent=None): + + super(SubsetCategoricalsDialog, self).__init__(parent=parent) + + self.state = SubsetCategoricalsState(collect) + + self.ui = load_ui('subset_categorical.ui', self, + directory=os.path.dirname(__file__)) + self._connections = autoconnect_callbacks_to_qt(self.state, self.ui) + + self._collect = collect + + if default is not None: + self.state.data = default + + self.state.cmap = cm.tab20 + + self.ui.button_ok.clicked.connect(self.accept) + self.ui.button_cancel.clicked.connect(self.reject) + + def _apply(self): + + subsets = subset_categorical(self._collect, self.state.data, self.state.att) + # colorize_subsets(subsets, self.state.cmap) + + @classmethod + def facet(cls, collect, default=None, parent=None): + """ + Class method to create faceted subsets. The arguments are the same as __init__. """