# Every cell is used for the marginal boxes; the scatter shows a random subsample per
# species, since 112,000 overplotted markers read as two solid blobs (and bloat the page).
POINTS_PER_SPECIES = 5000
rng = np.random.default_rng(0)
def box_stats(values):
"""Quartiles and Tukey fences for a Plotly box trace built from precomputed statistics."""
q1, median, q3 = np.percentile(values, [25, 50, 75])
iqr = q3 - q1
return dict(
q1=[q1],
median=[median],
q3=[q3],
lowerfence=[max(values.min(), q1 - 1.5 * iqr)],
upperfence=[min(values.max(), q3 + 1.5 * iqr)],
mean=[values.mean()],
)
fig = make_subplots(
rows=2,
cols=2,
column_widths=[0.85, 0.15],
row_heights=[0.15, 0.85],
shared_xaxes=True,
shared_yaxes=True,
horizontal_spacing=0.02,
vertical_spacing=0.02,
)
for species in groups:
cells = analysis_df[analysis_df[GROUP_COL] == species]
color = COLORS[species]
shown = cells.iloc[
rng.choice(len(cells), size=min(POINTS_PER_SPECIES, len(cells)), replace=False)
]
fig.add_trace(
go.Scattergl(
x=shown[AREA_COL],
y=shown["eccentricity"],
mode="markers",
showlegend=False,
marker=dict(color=color, size=3, opacity=0.4, line=dict(width=0)),
hovertemplate=f"{species}<br>area %{{x:.1f}} μm²<br>ecc. %{{y:.2f}}<extra></extra>",
),
row=2,
col=1,
)
# A legend-only proxy, so the key shows a full-size, full-opacity marker.
fig.add_trace(
go.Scatter(
x=[None],
y=[None],
mode="markers",
name=f"{DISPLAY[species]} (<i>n</i> = {len(cells):,})",
marker=dict(color=color, size=9),
),
row=2,
col=1,
)
box_kwargs = dict(
showlegend=False,
fillcolor=color,
line=dict(color=apc.charcoal.hex_code, width=1),
width=0.45,
hoverinfo="skip",
)
fig.add_trace(
go.Box(**box_stats(cells[AREA_COL].to_numpy()), y=[species], orientation="h", **box_kwargs),
row=1,
col=1,
)
fig.add_trace(
go.Box(**box_stats(cells["eccentricity"].to_numpy()), x=[species], **box_kwargs),
row=2,
col=2,
)
fig.update_xaxes(title_text=AREA_LABEL.capitalize(), range=[0, MAX_AREA_UM2], row=2, col=1)
fig.update_yaxes(title_text="Eccentricity", range=[0, 1.02], row=2, col=1)
for row, col in ((1, 1), (2, 2)): # the marginals borrow their scale from the scatter
fig.update_xaxes(showticklabels=False, ticks="", showline=False, row=row, col=col)
fig.update_yaxes(showticklabels=False, ticks="", showline=False, row=row, col=col)
fig.update_layout(
width=720,
height=560,
legend=dict(
x=0.99, y=0.03, xanchor="right", yanchor="bottom", bgcolor="rgba(255,255,255,0.75)"
),
)
apc.plotly.style_plot(fig, monospaced_axes="xy", row=2, col=1)
fig.show()