Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pull request from ElevenTX #29

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c26f33a
added automatic cache of the logicle of common values on a grid for 1…
omerwe-eleventx May 12, 2021
f966322
added support for .fcs files" with a smaller than expected number of …
omerwe-eleventx May 12, 2021
7968421
fixed bug in handling of log transform
omerwe-eleventx May 12, 2021
245e65d
Fixed bug in handling of log transform, and better handling of legend…
omerwe-eleventx May 12, 2021
001bb7a
added legend_kwargs for threshold gate
omerwe-eleventx May 20, 2021
9bfb00a
fixed several bugs related to manual ellipse gating
omerwe-eleventx May 20, 2021
43db121
Allow opening FCS files through file handlers
omerwe-eleventx Jun 6, 2021
e161540
Add plot keyword to preview function
omerwe-eleventx Jun 29, 2021
b92e16c
Added legend_kwargs handling to threshold1d
omerwe-eleventx Jun 30, 2021
119c9a3
Print population sizes
omerwe-eleventx Jul 1, 2021
70200c6
Changed order of threshold2d legends
omerwe-eleventx Jul 1, 2021
29c9881
Added handling for transformation of empty vectors (often occurring w…
omerwe-eleventx Jul 1, 2021
d3b516a
Updated cell count text to always be a single line
omerwe-eleventx Jul 2, 2021
d95e104
Fixed a bug in experiment.delete_all_populations()
omerwe-eleventx Jul 4, 2021
147d948
Updated bugfix for experiment.delete_all_populations()
omerwe-eleventx Jul 4, 2021
624d2f1
legend for threshold now prints %cells in a separate line
omerwe-eleventx Jul 4, 2021
1625a53
Changed plotting cells code. We add a new line for threshold gates, b…
omerwe-eleventx Jul 4, 2021
a6f326c
Separate logicle_cache into a new file
omerwe-eleventx Jul 19, 2021
602f3d1
Merge branch 'master' into 2.0.1
omerwe-eleventx Jul 19, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions cytopy/data/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import os
import re
import gc
import io

__author__ = "Ross Burton"
__copyright__ = "Copyright 2020, cytopy"
Expand Down Expand Up @@ -659,9 +660,9 @@ def delete_all_populations(self,
-------
None
"""
for f in self.fcs_files:
for f in self.fcs_files:
if sample_id == 'all' or f.primary_id == sample_id:
f.populations = [p for p in f.populations if p.population_name == "root"]
f.delete_populations('all')
f.save()

def sample_exists(self, sample_id: str) -> bool:
Expand Down Expand Up @@ -942,7 +943,7 @@ def add_fcs_files(self,
if self.sample_exists(sample_id):
raise DuplicateSampleError(f'A file group with id {sample_id} already exists')
feedback("Creating new FileGroup...")
if isinstance(primary, str):
if isinstance(primary, str) or isinstance(primary, io.IOBase):
fcs_file = FCSFile(filepath=primary, comp_matrix=comp_matrix)
else:
fcs_file = primary
Expand All @@ -964,7 +965,7 @@ def add_fcs_files(self,
data_directory=self.data_directory)
for ctrl_id, path in controls.items():
feedback(f"Adding control file {ctrl_id}...")
if isinstance(path, str):
if isinstance(path, str) or isinstance(primary, io.IOBase):
fcs_file = FCSFile(filepath=path, comp_matrix=comp_matrix)
else:
fcs_file = path
Expand Down
6 changes: 5 additions & 1 deletion cytopy/data/gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1507,10 +1507,11 @@ def _manual(self) -> ShapelyPoly:
raise TypeError("Centroid should be a list of two float values")
if not all(isinstance(x, float) for x in [width, height, angle]):
raise TypeError("Width, height, and angle should be of type float")
return ellipse_to_polygon(centroid=centroid,
polygon_obj = ellipse_to_polygon(centroid=centroid,
width=width,
height=height,
angle=angle)
return [polygon_obj]

def _fit(self,
data: pd.DataFrame) -> List[ShapelyPoly]:
Expand All @@ -1527,6 +1528,9 @@ def _fit(self,
list
List of Shapely polygon's
"""
if self.method == "manual":
return self._manual()

params = {k: v for k, v in self.method_kwargs.items() if k != "conf"}
self.model = globals()[self.method](**params)
if not self.method_kwargs.get("probabilistic_ellipse", True):
Expand Down
7 changes: 6 additions & 1 deletion cytopy/data/gating_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ def get_gate(self,
def preview_gate(self,
gate: str or ThresholdGate or PolygonGate or EllipseGate,
create_plot_kwargs: typing.Union[dict, None] = None,
plot_gate_kwargs: typing.Union[dict, None] = None):
plot_gate_kwargs: typing.Union[dict, None] = None,
plot: bool = True
):
"""
Preview the results of some given Gate

Expand Down Expand Up @@ -240,6 +242,9 @@ def preview_gate(self,
data, ctrl_parent_data = self._load_gate_dataframes(gate=gate, fda_norm=False)
plot_data = data
gate.fit(data=data, ctrl_data=ctrl_parent_data)

if not plot: return

create_plot_kwargs["transform_x"] = create_plot_kwargs.get("transform_x", None) or gate.transform_x
create_plot_kwargs["transform_y"] = create_plot_kwargs.get("transform_y", None) or gate.transform_y
create_plot_kwargs["transform_x_kwargs"] = create_plot_kwargs.get("transform_x_kwargs",
Expand Down
1 change: 1 addition & 0 deletions cytopy/data/read_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ def _get_spill_matrix(matrix_string: str) -> pd.DataFrame:
"""
matrix_list = matrix_string.split(',')
n = int(matrix_list[0])
if len(matrix_list) <= n+1: return None #Omer: Added by me
header = matrix_list[1:(n+1)]
header = [i.strip().replace('\n', '') for i in header]
values = [i.strip().replace('\n', '') for i in matrix_list[n+1:]]
Expand Down
160 changes: 115 additions & 45 deletions cytopy/flow/plotting/flow_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,11 @@ def _hist2d_axis_limits(self,
X limit, y limit
"""
if self.transform_x == "log":
xlim = transform.safe_range(data, "x")
xlim = transform.safe_range(data, x)
else:
xlim = [data[x].min(), data[x].max()]
if self.transform_y == "log":
ylim = transform.safe_range(data, "y")
ylim = transform.safe_range(data, y)
else:
ylim = [data[y].min(), data[y].max()]
xlim = pd.DataFrame({"Min": [xlim[0]], "Max": [xlim[1]]})
Expand Down Expand Up @@ -468,13 +468,16 @@ def plot_gate_children(self,
if isinstance(gate, ThresholdGate):
return self._plot_threshold(definitions={c.definition: c.name for c in gate.children},
geoms=[c.geom for c in gate.children],
lw=lw)
lw=lw,
legend_kwargs=legend_kwargs
)
# Otherwise, we assume polygon shape
return self._plot_polygon(geoms=[c.geom for c in gate.children],
labels=[c.name for c in gate.children],
colours=gate_colours,
lw=lw,
legend_kwargs=legend_kwargs)
legend_kwargs=legend_kwargs
)

def plot_population_geoms(self,
parent: pd.DataFrame,
Expand All @@ -484,7 +487,9 @@ def plot_population_geoms(self,
transform_x: str or None = None,
transform_y: str or None = None,
plot_kwargs: dict or None = None,
legend_kwargs: dict or None = None):
legend_kwargs: dict or None = None,
plot_stats:bool = True
):
"""
This will plot the geometric shapes from the list of child populations generated from a single Gate,
overlaid on the parent population upon which the Gate has been applied. The parent data should be provided
Expand Down Expand Up @@ -552,20 +557,42 @@ def plot_population_geoms(self,
**plot_kwargs)
# If threshold, add threshold lines to plot and return axes
if isinstance(children[0].geom, ThresholdGeom):
return self._plot_threshold(definitions={c.definition: c.population_name for c in children},

#definitions={c.definition: c.population_name for c in children}
definitions = {}
for c in children:
label = c.population_name
if plot_stats: label += '\n%0.0f%% of cells'%(c.n/parent.shape[0]*100)

definitions[c.definition] = label

return self._plot_threshold(definitions=definitions,
geoms=[c.geom for c in children],
lw=lw)
lw=lw,
legend_kwargs=legend_kwargs,
)
# Otherwise, we assume polygon shape

#labels = [c.population_name for c in children]
labels = []
for c in children:
label = c.population_name
if plot_stats: label += ' (%0.0f%% of cells)'%(c.n/parent.shape[0]*100)
labels.append(label)

return self._plot_polygon(geoms=[c.geom for c in children],
labels=[c.population_name for c in children],
labels=labels,
colours=gate_colours,
lw=lw,
legend_kwargs=legend_kwargs)
legend_kwargs=legend_kwargs,
)

def _plot_threshold(self,
definitions: Dict[str, str],
geoms: List[ThresholdGeom],
lw: float):
lw: float,
legend_kwargs: dict or None = None,
):
"""
Plot Child populations from ThresholdGate

Expand All @@ -587,15 +614,18 @@ def _plot_threshold(self,
self._add_threshold(x=x,
y=y,
labels=definitions,
lw=lw)
lw=lw,
legend_kwargs=legend_kwargs,
)
return self._ax

def _plot_polygon(self,
geoms: List[PolygonGeom],
labels: List[str],
colours: list or Generator,
lw: float,
legend_kwargs: dict):
legend_kwargs: dict,
):
"""
Plot the Child populations of a Polygon or Elliptical gate.

Expand Down Expand Up @@ -656,7 +686,8 @@ def _add_polygon(self,
-------
None
"""
self._ax.plot(x_values, y_values, '-k', c=colour, label=label, lw=lw)
#self._ax.plot(x_values, y_values, '-k', c=colour, label=label, lw=lw)
self._ax.plot(x_values, y_values, c=colour, label=label, lw=lw) #Omer: I removed '-k' to suppress a warning

def _add_ellipse(self,
center: (float, float),
Expand Down Expand Up @@ -700,7 +731,10 @@ def _add_ellipse(self,
label=label)
self._ax.add_patch(ellipse)

def _2dthreshold_annotations(self, labels: dict or None = None):
def _2dthreshold_annotations(self,
labels: dict or None = None,
legend_kwargs: dict or None = None,
):
"""
Annotate a 2D threshold plot

Expand All @@ -721,30 +755,38 @@ def _2dthreshold_annotations(self, labels: dict or None = None):
legend_labels = {}
for definition, label in labels.items():
for d in definition.split(","):
if "-+" == d:
legend_labels["A"] = label
elif "++" == d:
legend_labels["B"] = label
elif "--" == d:
legend_labels["C"] = label
elif "+-" == d:
legend_labels["D"] = label
else:
raise KeyError(f"Definition {d} is invalid for a 2D threshold gate.")
if "-+" == d: legend_labels["A"] = label
elif "++" == d: legend_labels["B"] = label
elif "--" == d: legend_labels["C"] = label
elif "+-" == d: legend_labels["D"] = label
else: raise KeyError(f"Definition {d} is invalid for a 2D threshold gate.")
self._threshold_annotation(0.05, 0.95, "A")
self._threshold_annotation(0.95, 0.95, "B")
self._threshold_annotation(0.05, 0.05, "C")
self._threshold_annotation(0.95, 0.05, "D")
self._ax.text(1.15, 0.95, f"A: {legend_labels.get('A')}", transform=self._ax.transAxes)
self._ax.text(1.15, 0.85, f"B: {legend_labels.get('B')}", transform=self._ax.transAxes)
self._ax.text(1.15, 0.75, f"C: {legend_labels.get('C')}", transform=self._ax.transAxes)
self._ax.text(1.15, 0.65, f"D: {legend_labels.get('D')}", transform=self._ax.transAxes)
#Omer: Change this to be in a legend
# self._ax.text(1.15, 0.95, f"A: {legend_labels.get('A')}", transform=self._ax.transAxes)
# self._ax.text(1.15, 0.85, f"B: {legend_labels.get('B')}", transform=self._ax.transAxes)
# self._ax.text(1.15, 0.75, f"C: {legend_labels.get('C')}", transform=self._ax.transAxes)
# self._ax.text(1.15, 0.65, f"D: {legend_labels.get('D')}", transform=self._ax.transAxes)
patchmp = patches.Patch(edgecolor=None, facecolor=None, label=f"A: {legend_labels.get('A')}")
patchpp = patches.Patch(edgecolor=None, facecolor=None, label=f"B: {legend_labels.get('B')}")
patchmm = patches.Patch(edgecolor=None, facecolor=None, label=f"C: {legend_labels.get('C')}")
patchpm = patches.Patch(edgecolor=None, facecolor=None, label=f"D: {legend_labels.get('D')}")

legend_kwargs = dict(legend_kwargs) if legend_kwargs is not None else {}
legend_kwargs['ncol'] = 2
list_patches = [patchmp, patchmm, patchpp, patchpm]
self._ax.legend(handles=list_patches, handlelength=0, **legend_kwargs)

def _threshold_annotation(self, x: float, y: float, text: str):
self._ax.text(x, y, text, ha='center', va='center', transform=self._ax.transAxes,
backgroundcolor="white", bbox=dict(facecolor='white', edgecolor='black', pad=5.0))

def _1dthreshold_annotations(self, labels: dict or None = None):
def _1dthreshold_annotations(self,
labels: dict or None = None,
legend_kwargs: dict or None = None,
):
"""
Annotate a 1D threshold plot

Expand All @@ -765,16 +807,28 @@ def _1dthreshold_annotations(self, labels: dict or None = None):
legend_labels = {"A": labels["-"], "B": labels["+"]}
except KeyError:
raise KeyError(f"Definitions for 1D threshold gate must be either '-' or '+', not: {labels.keys()}")

self._threshold_annotation(0.05, 0.95, "A")
self._threshold_annotation(0.95, 0.95, "B")
self._ax.text(1.15, 0.95, f"A: {legend_labels.get('A')}", transform=self._ax.transAxes)
self._ax.text(1.15, 0.85, f"B: {legend_labels.get('B')}", transform=self._ax.transAxes)
#self._ax.text(1.15, 0.95, f"A: {legend_labels.get('A')}", transform=self._ax.transAxes)
#self._ax.text(1.15, 0.85, f"B: {legend_labels.get('B')}", transform=self._ax.transAxes)

patchm = patches.Patch(edgecolor=None, facecolor=None, label=f"A: {legend_labels.get('A')}")
patchp = patches.Patch(edgecolor=None, facecolor=None, label=f"B: {legend_labels.get('B')}")
legend_kwargs = dict(legend_kwargs) if legend_kwargs is not None else {}
list_patches = [patchm, patchp]
self._ax.legend(handles=list_patches, handlelength=0, **legend_kwargs)




def _add_threshold(self,
x: float,
y: float or None,
lw: float,
labels: dict or None = None):
labels: dict or None = None,
legend_kwargs: dict or None = None,
):
"""
Add a 1D or 2D threshold (red horizontal axis line and optionally a red vertical axis line)

Expand All @@ -794,13 +848,17 @@ def _add_threshold(self,
None
"""
self._ax.axvline(x, lw=lw, c="#c92c2c")
if y is not None:
if y is not None and not np.isnan(y):
self._ax.axhline(y, lw=lw, c="#c92c2c")
# Label regions for two axis
self._2dthreshold_annotations(labels=labels)
self._2dthreshold_annotations(labels=labels,
legend_kwargs=legend_kwargs,
)
else:
# Label regions for one axis
self._1dthreshold_annotations(labels=labels)
self._1dthreshold_annotations(labels=labels,
legend_kwargs=legend_kwargs,
)

def backgate(self,
parent: pd.DataFrame,
Expand Down Expand Up @@ -1037,13 +1095,25 @@ def _set_legend(self,
-------
None
"""
anchor = kwargs.get("bbox_to_anchor", (1.1, 0.95))
loc = kwargs.get("loc", 2)
ncol = kwargs.get("ncol", 3)
fancy = kwargs.get("fancybox", True)
shadow = kwargs.get("shadow", False)
self._ax.legend(loc=loc,
bbox_to_anchor=anchor,
ncol=ncol,
fancybox=fancy,
shadow=shadow)
# anchor = kwargs.get("bbox_to_anchor", (1.1, 0.95))
# loc = kwargs.get("loc", 2)
# ncol = kwargs.get("ncol", 3)
# fancy = kwargs.get("fancybox", True)
# shadow = kwargs.get("shadow", False)
# self._ax.legend(loc=loc,
# bbox_to_anchor=anchor,
# ncol=ncol,
# fancybox=fancy,
# shadow=shadow)

#Omer: More general code
legend_kwargs = dict(**kwargs)
for (key, value) in [
('bbox_to_anchor', (1.1, 0.95)),
('loc', 2),
('ncol', 1),
('fancybox', True),
('shadow', False),
]:
if key not in legend_kwargs: legend_kwargs[key] = value
self._ax.legend(**legend_kwargs)
Loading