#!/usr/bin/env python3.8
# coding: latin-1
# (c) Massachusetts Institute of Technology 2015-2018
# (c) Brian Teague 2018-2022
#
# 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
---------------------------------
Base classes for `IOperation` default views:
`OpView` -- a view that has an operation, `OpView.op`, as an attribute.
`Op1DView` -- an `OpView` that has a `Op1DView.channel` attribute
and its attendant `Op1DView.scale`. This class overrides `Base1DView`
to delegate those attributes to `OpView.op`.
`Op2DView` -- an `OpView` that has `Op2DView.xchannel` (and
`Op2DView.xscale`) and `Op2DView.ychannel` (and `Op2DView.yscale`).
This class overrides `Base2DView` to delegate those attributes to `OpView.op`.
`ByView` -- an `OpView` that can plot various plots depending on what is
passed to `ByView.plot`'s ``plot_name`` parameter.
`AnnotatingView` -- An `IView` that plots an underlying data plot, then
plots some annotations on top of it.
'''
from warnings import warn
import collections
from natsort import natsorted
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 `IOperation` that this view is associated with. If you
created the view using `default_view`, this is already set.
"""
op = Instance(IOperation)
[docs]@provides(IView)
class Op1DView(OpView, Base1DView):
"""
Attributes
----------
channel : Str
The channel this view is viewing. If you created the view using
`default_view`, this is already set.
scale : {'linear', 'log', 'logicle'}
The way to scale the x axes. If you created the view using
`default_view`, this may be already set.
"""
channel = DelegatesTo('op')
scale = DelegatesTo('op')
[docs]@provides(IView)
class Op2DView(OpView, Base2DView):
"""
Attributes
----------
xchannel : Str
The channels to use for this view's X axis. If you created the
view using `default_view`, this is already set.
ychannel : Str
The channels to use for this view's Y axis. If you created the
view using `default_view`, this is already set.
xscale : {'linear', 'log', 'logicle'}
The way to scale the x axis. If you created the view using
`default_view`, this may be already set.
yscale : {'linear', 'log', 'logicle'}
The way to scale the y axis. If you created the view using
`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 `plot`.
Attributes
----------
facets : List(Str)
A read-only list of the conditions used to facet this view.
by : List(Str)
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 natsorted([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 `plot`.
Parameters
----------
experiment : `Experiment`
The `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 = [x for x in self.by if x not in 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 `IView` can make multiple plots, ``plot_name`` is
the name of the plot to make. Must be one of the values retrieved
from `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 = [x for x in self.by if x not in 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',
"Don't set view.plot_name if you don't also set operation.by"
.format(plot_name))
groupby = experiment.data.groupby(by)
if plot_name not in groupby.groups.keys():
raise util.CytoflowViewError('plot_name',
"Plot {} must be one of the values "
"returned by enum_plots(). "
"Possible plot names: {} "
"(DEBUG: groupby keys: {}"
.format(plot_name,
[x for x in self.enum_plots(experiment)],
groupby.groups.keys()))
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 `IView` that doesn't actually do any plotting.
"""
def _grid_plot(self, experiment, grid, **kwargs):
return {}
[docs]@provides(IView)
class AnnotatingView(BaseDataView):
"""
A `IView` that plots an underlying data plot, then plots some
annotations on top of it. See `gaussian.GaussianMixture1DView` for an
example. By default, it assumes that the annotations are to be plotted
in the same color as the view's `huefacet`, and sets `huefacet`
accordingly if the annotation isn't already set to a different facet.
.. note::
The ``annotation_facet`` and ``annotation_plot`` parameters that the
`plot` method consumes are only for internal use, which is why
they're not documented in the `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