#!/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.views.base_views
-------------------------
'''
from traits.api import HasStrictTraits, Str, Tuple, List, Dict, provides
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
from natsort import natsorted
from warnings import warn
import cytoflow
import cytoflow.utility as util
from .i_view import IView
[docs]class BaseView(HasStrictTraits):
"""
The base class for facetted plotting.
Attributes
----------
xfacet, yfacet : String
Set to one of the :attr:`~.Experiment.conditions` in the :class:`.Experiment`, and
a new row or column of subplots will be added for every unique value
of that condition.
huefacet : String
Set to one of the :attr:`~.Experiment.conditions` in the in the :class:`.Experiment`,
and a new color will be added to the plot for every unique value of
that condition.
huescale : {'linear', 'log', 'logicle'}
How should the color scale for :attr:`huefacet` be scaled?
"""
xfacet = Str
yfacet = Str
huefacet = Str
huescale = util.ScaleEnum
[docs] def plot(self, experiment, data, **kwargs):
"""
Base function for facetted plotting
Parameters
----------
experiment: Experiment
The :class:`.Experiment` to plot using this view.
title : str
Set the plot title
xlabel, ylabel : str
Set the X and Y axis labels
huelabel : str
Set the label for the hue facet (in the legend)
legend : bool
Plot a legend for the color or hue facet? Defaults to `True`.
sharex, sharey : bool
If there are multiple subplots, should they share axes? Defaults
to `True`.
row_order, col_order, hue_order : list
Override the row/column/hue facet value order with the given list.
If a value is not given in the ordering, it is not plotted.
Defaults to a "natural ordering" of all the values.
height : float
The height of *each row* in inches. Default = 3.0
aspect : float
The aspect ratio *of each subplot*. Default = 1.5
col_wrap : int
If `xfacet` is set and `yfacet` is not set, you can "wrap" the
subplots around so that they form a multi-row grid by setting
`col_wrap` to the number of columns you want.
sns_style : {"darkgrid", "whitegrid", "dark", "white", "ticks"}
Which `seaborn` style to apply to the plot? Default is `whitegrid`.
sns_context : {"paper", "notebook", "talk", "poster"}
Which `seaborn` context to use? Controls the scaling of plot
elements such as tick labels and the legend. Default is `talk`.
despine : Bool
Remove the top and right axes from the plot? Default is `True`.
Other Parameters
----------------
cmap : matplotlib colormap
If plotting a huefacet with many values, use this color map instead
of the default.
norm : matplotlib.colors.Normalize
If plotting a huefacet with many values, use this object for color
scale normalization.
"""
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
col_wrap = kwargs.pop('col_wrap', None)
if col_wrap is not None and self.yfacet:
raise util.CytoflowViewError('yfacet',
"Can't set yfacet and col_wrap at the same time.")
if col_wrap is not None and not self.xfacet:
raise util.CytoflowViewError('xfacet',
"Must set xfacet to use col_wrap.")
if col_wrap is not None and col_wrap < 2:
raise util.CytoflowViewError(None,
"col_wrap must be None or > 1")
title = kwargs.pop("title", None)
xlabel = kwargs.pop("xlabel", None)
ylabel = kwargs.pop("ylabel", None)
huelabel = kwargs.pop("huelabel", self.huefacet)
sharex = kwargs.pop("sharex", True)
sharey = kwargs.pop("sharey", True)
height = kwargs.pop("height", 3)
aspect = kwargs.pop("aspect", 1.5)
legend = kwargs.pop('legend', True)
despine = kwargs.pop('despine', False)
if cytoflow.RUNNING_IN_GUI:
sns_style = kwargs.pop('sns_style', 'whitegrid')
sns_context = kwargs.pop('sns_context', 'talk')
sns.set_style(sns_style, rc = {"xtick.bottom": True, "ytick.left": True})
sns.set_context(sns_context)
else:
if 'sns_style' in kwargs:
kwargs.pop('sns_style')
warn("'sns_style' is ignored when not running in the GUI",
util.CytoflowViewWarning)
if 'sns_context' in kwargs:
kwargs.pop('sns_context')
warn("'sns_context' is ignored when not running in the GUI",
util.CytoflowViewWarning)
col_order = kwargs.pop("col_order", (natsorted(data[self.xfacet].unique()) if self.xfacet else None))
row_order = kwargs.pop("row_order", (natsorted(data[self.yfacet].unique()) if self.yfacet else None))
hue_order = kwargs.pop("hue_order", (natsorted(data[self.huefacet].unique()) if self.huefacet else None))
g = sns.FacetGrid(data,
height = height,
aspect = aspect,
col = (self.xfacet if self.xfacet else None),
row = (self.yfacet if self.yfacet else None),
hue = (self.huefacet if self.huefacet else None),
col_order = col_order,
row_order = row_order,
hue_order = hue_order,
col_wrap = col_wrap,
legend_out = False,
sharex = sharex,
sharey = sharey)
plot_ret = self._grid_plot(experiment = experiment, grid = g, **kwargs)
kwargs.update(plot_ret)
xscale = kwargs.pop("xscale", None)
yscale = kwargs.pop("yscale", None)
xlim = kwargs.pop("xlim", None)
ylim = kwargs.pop("ylim", None)
for ax in g.axes.flatten():
if xscale:
ax.set_xscale(xscale.name, **xscale.get_mpl_params(ax.get_xaxis()))
if yscale:
ax.set_yscale(yscale.name, **yscale.get_mpl_params(ax.get_yaxis()))
if xlim:
ax.set_xlim(xlim)
if ylim:
ax.set_ylim(ylim)
# if we are sharing x axes, make sure the x limits are the same for each
if sharex:
fig = plt.gcf()
fig_x_min = float("inf")
fig_x_max = float("-inf")
for ax in fig.get_axes():
ax_x_min, ax_x_max = ax.get_xlim()
if ax_x_min < fig_x_min:
fig_x_min = ax_x_min
if ax_x_max > fig_x_max:
fig_x_max = ax_x_max
for ax in fig.get_axes():
ax.set_xlim(fig_x_min, fig_x_max)
# if we are sharing y axes, make sure the y limits are the same for each
if sharey:
fig = plt.gcf()
fig_y_max = float("-inf")
for ax in fig.get_axes():
_, ax_y_max = ax.get_ylim()
if ax_y_max > fig_y_max:
fig_y_max = ax_y_max
for ax in fig.get_axes():
ax.set_ylim(None, fig_y_max)
# if we have a hue facet and a lot of hues, make a color bar instead
# of a super-long legend.
cmap = kwargs.pop('cmap', None)
norm = kwargs.pop('norm', None)
legend_data = kwargs.pop('legend_data', None)
if legend:
if cmap and norm:
plot_ax = plt.gca()
cax, _ = mpl.colorbar.make_axes(plt.gcf().get_axes())
mpl.colorbar.ColorbarBase(cax,
cmap = cmap,
norm = norm)
plt.sca(plot_ax)
elif self.huefacet:
current_palette = mpl.rcParams['axes.prop_cycle']
if util.is_numeric(data[self.huefacet]) and \
len(g.hue_names) > len(current_palette):
cmap = mpl.colors.ListedColormap(sns.color_palette("husl",
n_colors = len(g.hue_names)))
hue_scale = util.scale_factory(self.huescale,
experiment,
data = data[self.huefacet].values)
plot_ax = plt.gca()
cax, _ = mpl.colorbar.make_axes(plt.gcf().get_axes())
mpl.colorbar.ColorbarBase(cax,
cmap = cmap,
norm = hue_scale.norm(),
label = huelabel)
plt.sca(plot_ax)
else:
g.add_legend(title = huelabel, legend_data = legend_data)
ax = g.axes.flat[0]
legend = ax.legend_
self._update_legend(legend)
if title:
plt.title(title)
if xlabel == "":
xlabel = None
if ylabel == "":
ylabel = None
g.set_axis_labels(xlabel, ylabel)
sns.despine(top = despine,
right = despine,
bottom = False,
left = False)
def _grid_plot(self, experiment, grid, xlim, ylim, xscale, yscale, **kwargs):
raise NotImplementedError("You must override _grid_plot in a derived class")
def _update_legend(self, legend):
pass # no-op
[docs]class BaseDataView(BaseView):
"""
The base class for data views (as opposed to statistics views).
"""
subset = Str
[docs] def plot(self, experiment, **kwargs):
"""
Plot some data from an experiment. This function takes care of
checking for facet name validity and subsetting, then passes the
underlying dataframe to `BaseView.plot`
Parameters
----------
min_quantile : float (>0.0 and <1.0, default = 0.001)
Clip data that is less than this quantile.
max_quantile : float (>0.0 and <1.0, default = 1.00)
Clip data that is greater than this quantile.
Other Parameters
----------------
lim : Dict(Str : (float, float))
Set the range of each channel's axis. If unspecified, assume
that the limits are the minimum and maximum of the clipped data.
Required.
scale : Dict(Str : IScale)
Scale the data on each axis. Required.
"""
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
if self.xfacet and self.xfacet not in experiment.conditions:
raise util.CytoflowViewError('xfacet',
"X facet {0} not in the experiment"
.format(self.xfacet))
if self.yfacet and self.yfacet not in experiment.conditions:
raise util.CytoflowViewError('yfacet',
"Y facet {0} not in the experiment"
.format(self.yfacet))
if self.huefacet and self.huefacet not in experiment.conditions:
raise util.CytoflowViewError('huefacet',
"Hue facet {0} not in the experiment"
.format(self.huefacet))
# adjust the limits to clip extreme values
min_quantile = kwargs.pop("min_quantile", 0.001)
max_quantile = kwargs.pop("max_quantile", 1.0)
if min_quantile < 0.0 or min_quantile > 1:
raise util.CytoflowViewError('min_quantile',
"min_quantile must be between 0 and 1")
if max_quantile < 0.0 or max_quantile > 1:
raise util.CytoflowViewError('max_quantile',
"max_quantile must be between 0 and 1")
if min_quantile >= max_quantile:
raise util.CytoflowViewError('min_quantile',
"min_quantile must be less than max_quantile")
lim = kwargs.get('lim')
scale = kwargs.get('scale')
for c in lim.keys():
if lim[c] is None:
lim[c] = (experiment[c].quantile(min_quantile),
experiment[c].quantile(max_quantile))
elif isinstance(lim[c], list) or isinstance(lim[c], tuple):
if len(lim[c]) != 2:
raise util.CytoflowError('lim',
'Length of lim\[{}\] must be 2'
.format(c))
if lim[c][0] is None:
lim[c] = (experiment[c].quantile(min_quantile),
lim[c][1])
if lim[c][1] is None:
lim[c] = (lim[c][0],
experiment[c].quantile(max_quantile))
else:
raise util.CytoflowError('lim',
"lim\[{}\] is an unknown data type"
.format(c))
lim[c] = [scale[c].clip(x) for x in lim[c]]
facets = [x for x in [self.xfacet, self.yfacet, self.huefacet] if x]
if len(facets) != len(set(facets)):
raise util.CytoflowViewError(None,
"Can't reuse facets")
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))
super().plot(experiment,
experiment.data,
**kwargs)
[docs]class Base1DView(BaseDataView):
"""
A data view that plots data from a single channel.
Attributes
----------
channel : Str
The channel to view
scale : {'linear', 'log', 'logicle'}
The scale applied to the data before plotting it.
"""
channel = Str
scale = util.ScaleEnum
[docs] def plot(self, experiment, **kwargs):
"""
Parameters
----------
lim : (float, float)
Set the range of the plot's data axis.
orientation : {'vertical', 'horizontal'}
"""
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
if not self.channel:
raise util.CytoflowViewError('channel',
"Must specify a channel")
if self.channel not in experiment.data:
raise util.CytoflowViewError('channel',
"Channel {0} not in the experiment"
.format(self.channel))
# get the scale
scale = kwargs.pop('scale', None)
if scale is None:
scale = util.scale_factory(self.scale, experiment, channel = self.channel)
lim = kwargs.pop("lim", None)
super().plot(experiment,
lim = {self.channel : lim},
scale = {self.channel : scale},
**kwargs)
[docs]class Base2DView(BaseDataView):
"""
A data view that plots data from two channels.
Attributes
----------
xchannel, ychannel : Str
The channels to view
xscale, yscale : {'linear', 'log', 'logicle'} (default = 'linear')
The scales applied to the data before plotting it.
xlim, ylim : (float, float)
Set the min and max limits of the plots' x and y axes.
"""
xchannel = Str
xscale = util.ScaleEnum
ychannel = Str
yscale = util.ScaleEnum
[docs] def plot(self, experiment, **kwargs):
"""
Parameters
----------
xlim, ylim : (float, float)
Set the range of the plot's axis.
"""
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
if not self.xchannel:
raise util.CytoflowViewError('xchannel',
"Must specify an xchannel")
if self.xchannel not in experiment.data:
raise util.CytoflowViewError('xchannel',
"Channel {} not in the experiment"
.format(self.xchannel))
if not self.ychannel:
raise util.CytoflowViewError('ychannel',
"Must specify a ychannel")
if self.ychannel not in experiment.data:
raise util.CytoflowViewError('ychannel',
"Channel {} not in the experiment"
.format(self.ychannel))
# get the scale
xscale = kwargs.pop('xscale', None)
if xscale is None:
xscale = util.scale_factory(self.xscale, experiment, channel = self.xchannel)
yscale = kwargs.pop('yscale', None)
if yscale is None:
yscale = util.scale_factory(self.yscale, experiment, channel = self.ychannel)
xlim = kwargs.pop('xlim', None)
ylim = kwargs.pop('ylim', None)
super().plot(experiment,
lim = {self.xchannel : xlim,
self.ychannel : ylim},
scale = {self.xchannel : xscale,
self.ychannel : yscale},
**kwargs)
[docs]class BaseNDView(BaseDataView):
"""
A data view that plots data from one or more channels.
Attributes
----------
channels : List(Str)
The channels to view
scale : Dict(Str : {"linear", "logicle", "log"})
Re-scale the data in the specified channels before plotting. If a
channel isn't specified, assume that the scale is linear.
"""
channels = List(Str)
scale = Dict(Str, util.ScaleEnum)
[docs] def plot(self, experiment, **kwargs):
"""
Parameters
----------
lim : Dict(Str : (float, float))
Set the range of each channel's axis. If unspecified, assume
that the limits are the minimum and maximum of the clipped data
"""
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
if len(self.channels) == 0:
raise util.CytoflowOpError('channels',
"Must set at least one channel")
for c in self.channels:
if c not in experiment.data:
raise util.CytoflowOpError('channels',
"Channel {0} not found in the experiment"
.format(c))
for c in self.scale:
if c not in self.channels:
raise util.CytoflowOpError('scale',
"Scale set for channel {0}, but it isn't "
"in 'channels'"
.format(c))
# get the scale
scale = {}
for c in self.channels:
if c in self.scale:
scale[c] = util.scale_factory(self.scale[c], experiment, channel = c)
else:
scale[c] = util.scale_factory(util.get_default_scale(), experiment, channel = c)
lim = kwargs.pop("lim", {})
for c in self.channels:
if c not in lim:
lim[c] = None
super().plot(experiment,
lim = lim,
scale = scale,
**kwargs)
[docs]@provides(IView)
class BaseStatisticsView(BaseView):
"""
The base class for statisticxs views (as opposed to data views).
Attributes
----------
variable : str
The condition that varies when plotting this statistic: used for the
x axis of line plots, the bar groups in bar plots, etc.
subset : str
An expression that specifies the subset of the statistic to plot.
"""
# deprecated or removed attributes give warnings & errors, respectively
by = util.Deprecated(new = 'variable', err_string = "'by' is deprecated, please use 'variable'")
variable = Str
subset = Str
[docs] def enum_plots(self, experiment, data):
"""
Enumerate the named plots we can make from this set of statistics.
"""
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
if not self.variable:
raise util.CytoflowViewError('variable',
"variable not set")
if self.variable not in experiment.conditions:
raise util.CytoflowViewError('variable',
"variable {0} not in the experiment"
.format(self.variable))
data, facets, names = self._subset_data(data)
by = list(set(names) - set(facets))
class plot_enum(object):
def __init__(self, data, by):
self.by = by
self._iter = None
self._returned = False
if by:
self._iter = 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(data.reset_index(), by)
[docs] def plot(self, experiment, data, plot_name = None, **kwargs):
"""
Plot some data from a statistic.
This function takes care of checking for facet name validity and
subsetting, then passes the dataframe to `BaseView.plot`
"""
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
if not self.variable:
raise util.CytoflowViewError('variable',
"variable not set")
if self.variable not in experiment.conditions:
raise util.CytoflowViewError('variable',
"variable {0} not in the experiment"
.format(self.variable))
data, facets, names = self._subset_data(data)
unused_names = list(set(names) - set(facets))
if plot_name is not None and not unused_names:
raise util.CytoflowViewError('plot_name',
"You specified a plot name, but all "
"the facets are already used")
if unused_names:
groupby = data.groupby(unused_names)
if plot_name is None:
raise util.CytoflowViewError('plot_name',
"You must use facets {} in either the "
"plot variables or the plot name. "
"Possible plot names: {}"
.format(unused_names, list(groupby.groups.keys())))
if plot_name not in set(groupby.groups.keys()):
raise util.CytoflowViewError('plot_name',
"Plot {} not from plot_enum; must "
"be one of {}"
.format(plot_name, list(groupby.groups.keys())))
data = groupby.get_group(plot_name)
# FacetGrid needs a "long" data set
data.reset_index(inplace = True)
super().plot(experiment, data, **kwargs)
def _subset_data(self, data):
if self.subset:
try:
# TODO - either sanitize column names, or check to see that
# all conditions are valid Python variables
data = data.query(self.subset)
except Exception as e:
raise util.CytoflowViewError('subset',
"Subset string '{0}' isn't valid"
.format(self.subset)) from e
if len(data) == 0:
raise util.CytoflowViewError('subset',
"Subset string '{0}' returned no values"
.format(self.subset))
names = list(data.index.names)
for name in names:
unique_values = data.index.get_level_values(name).unique()
if len(unique_values) == 1:
warn("Only one value for level {}; dropping it.".format(name),
util.CytoflowViewWarning)
try:
data.index = data.index.droplevel(name)
except AttributeError as e:
raise util.CytoflowViewError(None,
"Must have more than one "
"value to plot.") from e
names = list(data.index.names)
if self.xfacet and self.xfacet not in data.index.names:
raise util.CytoflowViewError('xfacet',
"X facet {} not in statistics; must be one of {}"
.format(self.xfacet, data.index.names))
if self.yfacet and self.yfacet not in data.index.names:
raise util.CytoflowViewError('yfacet',
"Y facet {} not in statistics; must be one of {}"
.format(self.yfacet, data.index.names))
if self.huefacet and self.huefacet not in data.index.names:
raise util.CytoflowViewError('huefacet',
"Hue facet {} not in statistics; must be one of {}"
.format(self.huefacet, data.index.names))
facets = [x for x in [self.variable, self.xfacet, self.yfacet, self.huefacet] if x]
if len(facets) != len(set(facets)):
raise util.CytoflowViewError(None, "Can't reuse facets")
return data, facets, names
[docs]class Base1DStatisticsView(BaseStatisticsView):
"""
The base class for 1-dimensional statistic views -- ie, the :attr:`variable`
attribute is on the x axis, and the statistic value is on the y axis.
Attributes
----------
statistic : (str, str)
The name of the statistic to plot. Must be a key in the
:attr:`~Experiment.statistics` attribute of the :class:`~.Experiment`
being plotted.
error_statistic : (str, str)
The name of the statistic used to plot error bars. Must be a key in the
:attr:`~Experiment.statistics` attribute of the :class:`~.Experiment`
being plotted.
scale : {'linear', 'log', 'logicle'}
The scale applied to the data before plotting it.
"""
REMOVED_ERROR = "Statistics changed dramatically in 0.5; please see the documentation"
by = util.Removed(err_string = REMOVED_ERROR)
yfunction = util.Removed(err_string = REMOVED_ERROR)
ychannel = util.Removed(err_string = REMOVED_ERROR)
channel = util.Removed(err_string = REMOVED_ERROR)
function = util.Removed(err_string = REMOVED_ERROR)
error_bars = util.Removed(err_string = REMOVED_ERROR)
xvariable = util.Deprecated(new = "variable")
statistic = Tuple(Str, Str)
error_statistic = Tuple(Str, Str)
scale = util.ScaleEnum
[docs] def enum_plots(self, experiment):
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
data = self._make_data(experiment)
return super().enum_plots(experiment, data)
[docs] def plot(self, experiment, plot_name = None, **kwargs):
"""
Parameters
----------
orientation : {'vertical', 'horizontal'}
lim : (float, float)
Set the range of the plot's axis.
"""
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
data = self._make_data(experiment)
if not self.variable:
raise util.CytoflowViewError('variable',
"variable not set")
if self.variable not in experiment.conditions:
raise util.CytoflowViewError('variable',
"variable {0} not in the experiment"
.format(self.variable))
scale = util.scale_factory(self.scale,
experiment,
statistic = self.statistic,
error_statistic = self.error_statistic)
super().plot(experiment,
data,
plot_name = plot_name,
scale = scale,
**kwargs)
def _make_data(self, experiment):
if experiment is None:
raise util.CytoflowViewError('experiment', "No experiment specified")
if not self.statistic:
raise util.CytoflowViewError('statistic', "Statistic not set")
if self.statistic not in experiment.statistics:
raise util.CytoflowViewError('statistic',
"Can't find the statistic {} in the experiment"
.format(self.statistic))
else:
stat = experiment.statistics[self.statistic]
if not util.is_numeric(stat):
raise util.CytoflowViewError('statistic',
"Statistic must be numeric")
if self.error_statistic[0]:
if self.error_statistic not in experiment.statistics:
raise util.CytoflowViewError('error_statistic',
"Can't find the error statistic in the experiment")
else:
error_stat = experiment.statistics[self.error_statistic]
else:
error_stat = None
if error_stat is not None:
if set(stat.index.names) != set(error_stat.index.names):
raise util.CytoflowViewError('error_statistic',
"Data statistic and error statistic "
"don't have the same index.")
try:
error_stat.index = error_stat.index.reorder_levels(stat.index.names)
error_stat.sort_index(inplace = True)
except AttributeError:
pass
if not stat.index.equals(error_stat.index):
raise util.CytoflowViewError('error_statistic',
"Data statistic and error statistic "
" don't have the same index.")
if stat.name == error_stat.name:
raise util.CytoflowViewError('error_statistic',
"Data statistic and error statistic can "
"not have the same name.")
data = pd.DataFrame(index = stat.index)
data[stat.name] = stat
if error_stat is not None:
data[error_stat.name] = error_stat
return data
[docs]class Base2DStatisticsView(BaseStatisticsView):
"""
The base class for 2-dimensional statistic views -- ie, the :attr:`variable`
attribute varies independently, and the corresponding values from the x and
y statistics are plotted on the x and y axes.
Attributes
----------
xstatistic, ystatistic : (str, str)
The name of the statistics to plot. Must be a keys in the
:attr:`~Experiment.statistics` attribute of the :class:`~.Experiment`
being plotted.
x_error_statistic, y_error_statistic : (str, str)
The name of the statistics used to plot error bars. Must be keys in the
:attr:`~Experiment.statistics` attribute of the :class:`~.Experiment`
being plotted.
xscale, yscale : {'linear', 'log', 'logicle'}
The scales applied to the data before plotting it.
"""
STATS_REMOVED = "{} has been removed. Statistics changed dramatically in 0.5; please see the documentation."
xchannel = util.Removed(err_string = STATS_REMOVED)
xfunction = util.Removed(err_string = STATS_REMOVED)
ychannel = util.Removed(err_string = STATS_REMOVED)
yfunction = util.Removed(err_string = STATS_REMOVED)
xstatistic = Tuple(Str, Str)
ystatistic = Tuple(Str, Str)
x_error_statistic = Tuple(Str, Str)
y_error_statistic = Tuple(Str, Str)
xscale = util.ScaleEnum
yscale = util.ScaleEnum
[docs] def enum_plots(self, experiment):
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
data = self._make_data(experiment)
return super().enum_plots(experiment, data)
[docs] def plot(self, experiment, plot_name = None, **kwargs):
"""
Parameters
----------
xlim, ylim : (float, float)
Set the range of the plot's axis.
"""
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
data = self._make_data(experiment)
xscale = util.scale_factory(self.xscale,
experiment,
statistic = self.xstatistic,
error_statistic = self.x_error_statistic)
yscale = util.scale_factory(self.yscale,
experiment,
statistic = self.ystatistic,
error_statistic = self.y_error_statistic)
super().plot(experiment,
data,
plot_name,
xscale = xscale,
yscale = yscale,
**kwargs)
def _make_data(self, experiment):
if experiment is None:
raise util.CytoflowViewError('experiment',
"No experiment specified")
if not self.xstatistic:
raise util.CytoflowViewError('xstatistic',
"X Statistic not set")
if self.xstatistic not in experiment.statistics:
raise util.CytoflowViewError('xstatistic',
"Can't find the statistic {} in the experiment"
.format(self.xstatistic))
else:
xstat = experiment.statistics[self.xstatistic]
if not util.is_numeric(xstat):
raise util.CytoflowViewError('xstatistic',
"X statistic must be numeric")
if self.x_error_statistic[0]:
if self.x_error_statistic not in experiment.statistics:
raise util.CytoflowViewError('x_error_statistic',
"Can't find the X error statistic in the experiment")
else:
x_error_stat = experiment.statistics[self.x_error_statistic]
else:
x_error_stat = None
if x_error_stat is not None:
if set(xstat.index.names) != set(x_error_stat.index.names):
raise util.CytoflowViewError('x_error_statistic',
"X data statistic and error statistic "
"don't have the same index.")
try:
x_error_stat.index = x_error_stat.index.reorder_levels(xstat.index.names)
x_error_stat.sort_index(inplace = True)
except AttributeError:
pass
if not xstat.index.equals(x_error_stat.index):
raise util.CytoflowViewError('x_error_statistic',
"X data statistic and error statistic "
" don't have the same index.")
if xstat.name == x_error_stat.name:
raise util.CytoflowViewError('x_error_statistic',
"X data statistic and error statistic can "
"not have the same name.")
if not self.ystatistic:
raise util.CytoflowViewError('ystatistic',
"Y statistic not set")
if self.ystatistic not in experiment.statistics:
raise util.CytoflowViewError('ystatistic',
"Can't find the Y statistic {} in the experiment"
.format(self.ystatistic))
else:
ystat = experiment.statistics[self.ystatistic]
if not util.is_numeric(ystat):
raise util.CytoflowViewError('ystatistic',
"Y statistic must be numeric")
if self.y_error_statistic[0]:
if self.y_error_statistic not in experiment.statistics:
raise util.CytoflowViewError('y_error_statistic',
"Can't find the Y error statistic in the experiment")
else:
y_error_stat = experiment.statistics[self.y_error_statistic]
else:
y_error_stat = None
if y_error_stat is not None:
if set(ystat.index.names) != set(y_error_stat.index.names):
raise util.CytoflowViewError('y_error_statistic',
"Y data statistic and error statistic "
"don't have the same index.")
try:
y_error_stat.index = y_error_stat.index.reorder_levels(ystat.index.names)
y_error_stat.sort_index(inplace = True)
except AttributeError:
pass
if not ystat.index.equals(y_error_stat.index):
raise util.CytoflowViewError('y_error_statistic',
"Y data statistic and error statistic "
" don't have the same index.")
if ystat.name == y_error_stat.name:
raise util.CytoflowViewError('y_error_statistic',
"Data statistic and error statistic can "
"not have the same name.")
if xstat.name == ystat.name:
raise util.CytoflowViewError('ystatistic',
"X and Y statistics can "
"not have the same name.")
if set(xstat.index.names) != set(ystat.index.names):
raise util.CytoflowViewError('ystatistic',
"X and Y data statistics "
"don't have the same index.")
try:
ystat.index = ystat.index.reorder_levels(xstat.index.names)
ystat.sort_index(inplace = True)
except AttributeError:
pass
intersect_idx = xstat.index.intersection(ystat.index)
xstat = xstat.reindex(intersect_idx)
xstat.sort_index(inplace = True)
ystat = ystat.reindex(intersect_idx)
ystat.sort_index(inplace = True)
data = pd.DataFrame(index = xstat.index)
data[xstat.name] = xstat
data[ystat.name] = ystat
if x_error_stat is not None:
data[x_error_stat.name] = x_error_stat
if y_error_stat is not None:
data[y_error_stat.name] = y_error_stat
return data