#!/usr/bin/env python3.4
# coding: latin-1
# (c) Massachusetts Institute of Technology 2015-2018
# (c) Brian Teague 2018-2021
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
'''
cytoflow.operations.base_op_views
---------------------------------
'''
from warnings import warn
import collections
from traits.api import (provides, Instance, Property, List, DelegatesTo)
import cytoflow.utility as util
from .i_operation import IOperation
from cytoflow.views import IView
from cytoflow.views.base_views import BaseDataView, Base1DView, Base2DView
[docs]@provides(IView)
class OpView(BaseDataView):
"""
Attributes
----------
op : Instance(IOperation)
The :class:`IOperation` that this view is associated with. If you
created the view using :meth:`default_view`, this is already set.
"""
op = Instance(IOperation)
[docs]@provides(IView)
class Op1DView(OpView, Base1DView):
"""
Attributes
----------
channel : String
The channel this view is viewing. If you created the view using
:meth:`default_view`, this is already set.
scale : {'linear', 'log', 'logicle'}
The way to scale the x axes. If you created the view using
:meth:`default_view`, this may be already set.
"""
channel = DelegatesTo('op')
scale = DelegatesTo('op')
[docs]@provides(IView)
class Op2DView(OpView, Base2DView):
"""
Attributes
----------
xchannel, ychannel : String
The channels to use for this view's X and Y axes. If you created the
view using :meth:`default_view`, this is already set.
xscale, yscale : {'linear', 'log', 'logicle'}
The way to scale the x axes. If you created the view using
:meth:`default_view`, this may be already set.
"""
xchannel = DelegatesTo('op')
xscale = DelegatesTo('op')
ychannel = DelegatesTo('op')
yscale = DelegatesTo('op')
[docs]@provides(IView)
class ByView(OpView):
"""
A view that can plot various plots based on the ``plot_name`` parameter
of :meth:`plot`.
Attributes
----------
facets : List(String)
A read-only list of the conditions used to facet this view.
by : List(String)
A read-only list of the conditions used to group this view's data before
plotting.
"""
facets = Property(List)
by = Property(List)
def _get_facets(self):
return [x for x in [self.xfacet, self.yfacet, self.huefacet] if x]
def _get_by(self):
if self.op.by:
return self.op.by
else:
return []
[docs] def enum_plots(self, experiment):
"""
Returns an iterator over the possible plots that this View can
produce. The values returned can be passed to the ``plot_name``
keyword of :meth:`plot`.
Parameters
----------
experiment : Experiment
The :class:`Experiment` that will be producing the plots.
"""
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
if len(self.by) == 0 and len(self.facets) > 1:
raise util.CytoflowViewError('facets',
"You can only facet this view if you "
"specify some variables in `by`")
for facet in self.facets:
if facet not in experiment.conditions:
raise util.CytoflowViewError('facets',
"Facet {} not in the experiment"
.format(facet))
# if facet not in self.by:
# raise util.CytoflowViewError('facets',
# "Facet {} must be one of {}"
# .format(facet, self.by))
if len(self.facets) != len(set(self.facets)):
raise util.CytoflowViewError('facets',
"You can't reuse facets!")
for b in self.by:
if b not in experiment.conditions:
raise util.CytoflowOpError('by',
"Aggregation metadata {} not found, "
"must be one of {}"
.format(b, experiment.conditions))
if self.subset:
try:
experiment = experiment.query(self.subset)
except util.CytoflowError as e:
raise util.CytoflowViewError('subset', str(e)) from e
except Exception as e:
raise util.CytoflowViewError('subset',
"Subset string '{0}' isn't valid"
.format(self.subset)) from e
if len(experiment) == 0:
raise util.CytoflowViewError('subset',
"Subset string '{0}' returned no events"
.format(self.subset))
by = list(set(self.by) - set(self.facets))
class plot_enum(object):
def __init__(self, by, experiment):
self.by = by
self._iter = None
self._returned = False
if by:
self._iter = experiment.data.groupby(by).__iter__()
def __iter__(self):
return self
def __next__(self):
if self._iter:
return next(self._iter)[0]
else:
if self._returned:
raise StopIteration
else:
self._returned = True
return None
return plot_enum(by, experiment)
[docs] def plot(self, experiment, **kwargs):
"""
Make the plot.
Parameters
----------
plot_name : str
If this :class:`IView` can make multiple plots, ``plot_name`` is
the name of the plot to make. Must be one of the values retrieved
from :meth:`enum_plots`.
"""
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
if len(self.by) == 0 and len(self.facets) > 1:
raise util.CytoflowViewError('facets',
"You can only facet this view if you "
"specify some variables in `by`")
for facet in self.facets:
if facet not in experiment.conditions:
raise util.CytoflowViewError('facets',
"Facet {} not in the experiment"
.format(facet))
if facet not in self.by:
raise util.CytoflowViewError('facets',
"Facet {} must be one of {}"
.format(facet, self.by))
if len(self.facets) != len(set(self.facets)):
raise util.CytoflowViewError('facets',
"You can't reuse facets!")
for b in self.by:
if b not in experiment.conditions:
raise util.CytoflowOpError('by',
"Aggregation metadata {} not found, "
"must be one of {}"
.format(b, experiment.conditions))
# yes, this is going to happen again in BaseDataView, but we need to do
# it here to see if we're dropping any levels (via reset_index) before
# doing the groupby
if self.subset:
try:
experiment = experiment.query(self.subset)
experiment.data.reset_index(drop = True, inplace = True)
except Exception as e:
raise util.CytoflowViewError('subset',
"Subset string '{0}' isn't valid"
.format(self.subset)) from e
if len(experiment) == 0:
raise util.CytoflowViewError('subset',
"Subset string '{0}' returned no events"
.format(self.subset))
# see if we're making subplots
by = list(set(self.by) - set(self.facets))
plot_name = kwargs.get('plot_name', None)
if by and plot_name is None:
raise util.CytoflowViewError('plot_name',
"You must use facets {} in either the "
"plot facets or the plot name. "
"Possible plot names: {}"
.format(by, [x for x in self.enum_plots(experiment)]))
if plot_name is not None:
if plot_name is not None and not by:
raise util.CytoflowViewError('plot_name',
"Plot {} not from enum_plots()"
.format(plot_name))
groupby = experiment.data.groupby(by)
if plot_name not in set(groupby.groups.keys()):
raise util.CytoflowViewError('plot_name',
"Plot {} not from enum_plots()"
.format(plot_name))
experiment = experiment.clone()
experiment.data = groupby.get_group(plot_name)
experiment.data.reset_index(drop = True, inplace = True)
super().plot(experiment, **kwargs)
[docs]@provides(IView)
class By1DView(ByView, Op1DView):
pass
[docs]@provides(IView)
class By2DView(ByView, Op2DView):
pass
[docs]@provides(IView)
class NullView(BaseDataView):
"""
An :class:`IView` that doesn't actually do any plotting.
"""
def _grid_plot(self, experiment, grid, **kwargs):
return {}
[docs]@provides(IView)
class AnnotatingView(BaseDataView):
"""
A :class:`IView` that plots an underlying data plot, then plots some
annotations on top of it. See :class:`~.gaussian.GaussianMixture1DView` for an
example. By default, it assumes that the annotations are to be plotted
in the same color as the view's :attr:`huefacet`, and sets :attr:`huefacet`
accordingly if the annotation isn't already set to a different facet.
.. note::
The ``annotation_facet`` and ``annotation_plot`` parameters that the
:meth:`plot` method consumes are only for internal use, which is why
they're not documented in the :meth:`plot` docstring.
"""
[docs] def plot(self, experiment, **kwargs):
"""
Parameters
----------
color : matplotlib color
The color to plot the annotations. Overrides the default color
cycle.
"""
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
annotation_facet = kwargs.pop('annotation_facet', None)
annotation_trait = kwargs.pop('annotation_trait', None)
if annotation_facet is not None and annotation_facet in experiment.data:
if annotation_trait:
self.trait_set(**{annotation_trait : annotation_facet})
elif not self.huefacet:
warn("Setting 'huefacet' to '{}'".format(annotation_facet),
util.CytoflowViewWarning)
annotation_trait = 'huefacet'
self.trait_set(**{'huefacet' : annotation_facet})
super().plot(experiment,
annotation_facet = annotation_facet,
**kwargs)
def _grid_plot(self, experiment, grid, **kwargs):
# pop the annotation stuff off of kwargs so the underlying data plotter
# doesn't get confused
annotation_facet = kwargs.pop('annotation_facet', None)
annotations = kwargs.pop('annotations', None)
plot_name = kwargs.pop('plot_name', None)
color = kwargs.get('color', None)
# plot the underlying data plots
plot_ret = super()._grid_plot(experiment, grid, **kwargs)
kwargs.update(plot_ret)
# plot the annotations on top
for (i, j, k), _ in grid.facet_data():
ax = grid.facet_axis(i, j)
row_name = grid.row_names[i] if grid.row_names and grid._row_var is not annotation_facet else None
col_name = grid.col_names[j] if grid.col_names and grid._col_var is not annotation_facet else None
hue_name = grid.hue_names[k] if grid.hue_names and grid._hue_var is not annotation_facet else None
facets = [x for x in [row_name, col_name, hue_name] if x is not None]
if plot_name is not None:
if isinstance(plot_name, collections.Iterable) and not isinstance(plot_name, str):
plot_name = list(plot_name)
else:
plot_name = [plot_name]
annotation_name = plot_name + facets
else:
annotation_name = facets
annotation = None
for group, a in annotations.items():
if isinstance(group, collections.Iterable) and not isinstance(group, str):
g_set = set(group)
else:
g_set = set([group])
if g_set == set(annotation_name):
annotation = a
if (annotation is None
and len(annotations.keys()) == 1
and list(annotations.keys())[0] is True):
annotation = annotations[True]
if annotation is None:
continue
if annotation_facet is not None:
if annotation_facet == grid._row_var:
annotation_value = grid.row_names[i]
elif annotation_facet == grid._col_var:
annotation_value = grid.col_names[j]
elif annotation_facet == grid._hue_var:
annotation_value = grid.hue_names[k]
else:
annotation_value = None
else:
annotation_value = None
annotation_color = grid._facet_color(k, color)
self._annotation_plot(ax,
annotation,
annotation_facet,
annotation_value,
annotation_color,
**kwargs)
return plot_ret
def _strip_trait(self, val):
if val:
trait_name = self._find_trait_name(val)
if trait_name is not None:
view = self.clone_traits('all')
view.trait_set(**{trait_name : ""})
return view, trait_name
return self, None
def _find_trait_name(self, val):
traits = self.trait_get()
for n, v in traits.items():
if v == val:
return n