Commit 6fd5c886 authored by nfontrod's avatar nfontrod
Browse files

src/gc_content/stat_annot.py: major pep8 modifications

parent 0117f4ca
#!/usr/bin/env python3
# -*- coding;: utf-8 -*-
# -*- coding: UTF-8 -*-
"""
Description:
......@@ -9,6 +9,7 @@ Description:
github.com/webermarcolivier/statannot/blob/master/statannot/statannot.py.
"""
import matplotlib.pyplot as plt
from matplotlib import lines
import matplotlib.transforms as mtransforms
......@@ -19,196 +20,203 @@ from seaborn.utils import remove_na
def add_stat_annotation(ax, data=None, x=None, y=None, hue=None, order=None,
hue_order=None, boxPairList=None, loc='inside',
useFixedOffset=False, lineYOffsetToBoxAxesCoord=None,
lineYOffsetAxesCoord=None, lineHeightAxesCoord=0.02,
textYOffsetPoints=1, color='0.2', linewidth=1.5,
fontsize='medium', verbose=1):
hue_order=None, box_pair_list=None, loc='inside',
use_fixed_offset=False,
liney_offset2box_axes_coord=None,
liney_offset_axes_coord=None,
line_height_axes_coord=0.02,
text_y_offset_points=1, color='0.2', linewidth=1.5,
fontsize='medium', verbose=1, min_pval = 1e-16):
"""
User should use the same argument for the data, x, y, hue, order, \
hue_order as the seaborn boxplot function.
boxPairList can be of either form:
box_pair_list can be of either form:
boxplot: [(cat1, cat2, pval), (cat3, cat4, pval)]
"""
def find_x_position_box(boxPlotter, boxName):
def find_x_position_box(box_plotter, box_name):
"""
boxName can be either a name "cat" or a tuple ("cat", "hue")
box_name can be either a name "cat" or a tuple ("cat", "hue")
"""
if boxPlotter.plot_hues is None:
cat = boxName
hueOffset = 0
if box_plotter.plot_hues is None:
cat = box_name
hue_offset = 0
else:
cat = boxName[0]
hue = boxName[1]
hueOffset = boxPlotter.hue_offsets[
boxPlotter.hue_names.index(hue)]
cat = box_name[0]
chue = box_name[1]
hue_offset = box_plotter.hue_offsets[
box_plotter.hue_names.index(chue)]
groupPos = boxPlotter.group_names.index(cat)
boxPos = groupPos + hueOffset
return boxPos
group_pos = box_plotter.group_names.index(cat)
box_pos = group_pos + hue_offset
return box_pos
def get_box_data(boxPlotter, boxName):
def get_box_data(cbox_plotter, box_name):
"""
boxName can be either a name "cat" or a tuple ("cat", "hue")
box_name can be either a name "cat" or a tuple ("cat", "hue")
Here we really have to duplicate seaborn code, because there is not
direct access to the
box_data in the BoxPlotter class.
"""
cat = boxName
if boxPlotter.plot_hues is None:
cat = box_name
if cbox_plotter.plot_hues is None:
box_max_l = []
for cat in boxPlotter.group_names:
i = boxPlotter.group_names.index(cat)
group_data = boxPlotter.plot_data[i]
for cat in cbox_plotter.group_names:
i = cbox_plotter.group_names.index(cat)
group_data = cbox_plotter.plot_data[i]
box_data = remove_na(group_data)
box_max_l.append(np.max(box_data))
box_max_l.append(np.mean(box_data))
return max(box_max_l)
else:
i = boxPlotter.group_names.index(cat[0])
group_data = boxPlotter.plot_data[i]
i = cbox_plotter.group_names.index(cat[0])
group_data = cbox_plotter.plot_data[i]
box_data = []
for hue in boxPlotter.hue_names:
hue_mask = boxPlotter.plot_hues[i] == hue
for chue in cbox_plotter.hue_names:
hue_mask = cbox_plotter.plot_hues[i] == chue
box_data.append(np.max(remove_na(group_data[hue_mask])))
return max(box_data)
fig = plt.gcf()
validList = ['inside', 'outside']
if loc not in validList:
valid_list = ['inside', 'outside']
if loc not in valid_list:
raise ValueError(f"loc value should be one of the following: "
f"{', '.join(validList)}.")
f"{', '.join(valid_list)}.")
# Create the same BoxPlotter object as seaborn's boxplot
boxPlotter = sns.categorical._BoxPlotter(x, y, hue, data, order, hue_order,
orient=None, width=.8, color=None,
palette=None, saturation=.75,
dodge=True, fliersize=5,
linewidth=None)
box_plotter = sns.categorical._BoxPlotter(x, y, hue, data, order,
hue_order, orient=None, width=.8,
color=None, palette=None,
saturation=.75, dodge=True,
fliersize=5, linewidth=None)
ylim = ax.get_ylim()
yRange = ylim[1] - ylim[0]
y_range = ylim[1] - ylim[0]
if lineYOffsetAxesCoord is None:
if liney_offset_axes_coord is None:
if loc == 'inside':
lineYOffsetAxesCoord = 0.005
if lineYOffsetToBoxAxesCoord is None:
lineYOffsetToBoxAxesCoord = 0.1
liney_offset_axes_coord = 0.005
if liney_offset2box_axes_coord is None:
liney_offset2box_axes_coord = 0.1
elif loc == 'outside':
lineYOffsetAxesCoord = 0.03
lineYOffsetToBoxAxesCoord = lineYOffsetAxesCoord
liney_offset_axes_coord = 0.03
liney_offset2box_axes_coord = liney_offset_axes_coord
else:
if loc == 'inside':
if lineYOffsetToBoxAxesCoord is None:
lineYOffsetToBoxAxesCoord = 0.06
if liney_offset2box_axes_coord is None:
liney_offset2box_axes_coord = 0.06
elif loc == 'outside':
lineYOffsetToBoxAxesCoord = lineYOffsetAxesCoord
yOffset = lineYOffsetAxesCoord * yRange
yOffsetToBox = lineYOffsetToBoxAxesCoord * yRange
liney_offset2box_axes_coord = liney_offset_axes_coord
y_offset = liney_offset_axes_coord * y_range
y_offset_to_box = liney_offset2box_axes_coord * y_range
yStack = []
annList = []
for box1, box2, pval in boxPairList:
y_stack = []
ann_list = []
max_val = []
for box1, box2, pval in box_pair_list:
groupNames = boxPlotter.group_names
group_names = box_plotter.group_names
cat1 = box1
cat2 = box2
if isinstance(cat1, tuple):
hue_names = boxPlotter.hue_names
valid = cat1[0] in groupNames and cat2[0] in groupNames and \
cat1[1] in hue_names and cat2[1] in hue_names
hue_names = box_plotter.hue_names
valid = cat1[0] in group_names and cat2[0] in group_names and \
cat1[1] in hue_names and cat2[1] in hue_names
else:
valid = cat1 in groupNames and cat2 in groupNames
valid = cat1 in group_names and cat2 in group_names
if valid:
# Get position of boxes
x1 = find_x_position_box(boxPlotter, box1)
x2 = find_x_position_box(boxPlotter, box2)
box_data1 = get_box_data(boxPlotter, box1)
box_data2 = get_box_data(boxPlotter, box2)
x1 = find_x_position_box(box_plotter, box1)
x2 = find_x_position_box(box_plotter, box2)
box_data1 = get_box_data(box_plotter, box1)
box_data2 = get_box_data(box_plotter, box2)
ymax1 = box_data1
ymax2 = box_data2
if pval > 1e-16:
text = "p = {:.2e}".format(pval)
else:
text = "p < 1e-16"
text = f"p < {min_pval:.0e}"
if loc == 'inside':
yRef = max(ymax1, ymax2)
y_ref = max(ymax1, ymax2)
else:
yRef = ylim[1]
if len(yStack) > 0 and isinstance(box1, str):
yRef2 = max(yRef, max(yStack))
y_ref = ylim[1]
if len(y_stack) > 0:
y_ref2 = max(y_ref, max(y_stack))
else:
yRef2 = yRef
y_ref2 = y_ref
if len(yStack) == 0 or not isinstance(box1, str):
y = yRef2 + yOffsetToBox
if len(y_stack) == 0:
y = y_ref2 + y_offset_to_box
else:
y = yRef2 + yOffset
h = lineHeightAxesCoord * yRange
lineX, lineY = [x1, x1, x2, x2], [y, y + h, y + h, y]
y = y_ref2 + y_offset
h = line_height_axes_coord * y_range
line_x, line_y = [x1, x1, x2, x2], [y, y + h, y + h, y]
if loc == 'inside':
ax.plot(lineX, lineY, lw=linewidth, c=color)
ax.plot(line_x, line_y, lw=linewidth, c=color)
elif loc == 'outside':
line = lines.Line2D(lineX, lineY, lw=linewidth, c=color,
line = lines.Line2D(line_x, line_y, lw=linewidth, c=color,
transform=ax.transData)
line.set_clip_on(False)
ax.add_line(line)
if text is not None:
ann = ax.annotate(text, xy=(np.mean([x1, x2]), y + h),
xytext=(0, textYOffsetPoints),
xytext=(0, text_y_offset_points),
textcoords='offset points',
xycoords='data', ha='center', va='bottom',
fontsize=fontsize,
clip_on=False, annotation_clip=False)
annList.append(ann)
fontsize=fontsize, clip_on=False,
annotation_clip=False)
ann_list.append(ann)
new_max_ylim = 1.1 * (y + h)
new_max_ylim = 1.1*(y + h)
if new_max_ylim > ylim[1]:
ax.set_ylim((ylim[0], 1.1 * (y + h)))
ax.set_ylim((ylim[0], 1.1*(y + h)))
if text is not None:
plt.draw()
yTopAnnot = None
gotMatplotlibError = False
if not useFixedOffset:
y_top_annot = None
got_matplotlib_error = False
if not use_fixed_offset:
try:
bbox = ann.get_window_extent()
bbox_data = bbox.transformed(ax.transData.inverted())
yTopAnnot = bbox_data.ymax
y_top_annot = bbox_data.ymax
except RuntimeError:
gotMatplotlibError = True
got_matplotlib_error = True
if useFixedOffset or gotMatplotlibError:
if use_fixed_offset or got_matplotlib_error:
if verbose >= 1:
print("Warning: cannot get the text bounding box. "
"Falling back to a fixed y offset. Layout may "
"be not optimal.")
# We will apply a fixed offset in points, based on the font size of the annotation.
fontsizePoints = FontProperties(
size='medium').get_size_in_points()
offsetTrans = mtransforms.offset_copy(ax.transData,
fig=fig,
x=0,
y=1.0 * fontsizePoints + textYOffsetPoints,
units='points')
yTopDisplay = offsetTrans.transform((0, y + h))
yTopAnnot = ax.transData.inverted().transform(yTopDisplay)[
1]
# We will apply a fixed offset in points,
# based on the font size of the annotation.
fontsize_points = FontProperties(size='medium'
).get_size_in_points()
offset_trans = mtransforms.offset_copy(
ax.transData, fig=fig, x=0,
y=1.0 * fontsize_points + text_y_offset_points,
units='points')
y_top_display = offset_trans.transform((0, y + h))
y_top_annot = ax.transData.inverted().\
transform(y_top_display)[1]
else:
yTopAnnot = y + h
y_top_annot = y + h
yStack.append(yTopAnnot)
y_stack.append(y_top_annot)
max_val.append(max(y_stack))
if hue is not None and len(y_stack) == len(box_plotter.hue_names):
y_stack = []
else:
raise ValueError("boxPairList contains an unvalid box pair.")
raise ValueError("box_pair_list contains an unvalid box pair.")
yStackMax = max(yStack)
y_stack_max = max(max_val + y_stack)
if loc == 'inside':
if ylim[1] < 1.03 * yStackMax:
ax.set_ylim((ylim[0], 1.03 * yStackMax))
if ylim[1] < 1.03 * y_stack_max:
ax.set_ylim((ylim[0], 1.03 * y_stack_max))
elif loc == 'outside':
ax.set_ylim((ylim[0], ylim[1]))
return ax
return ax
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment