Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
b540bfa
histogram rebase to dev
ying39purdue Apr 15, 2025
21779cd
add facet plots on histogram group by
ying39purdue Apr 2, 2025
09a9012
add unittest for facet output
ying39purdue Apr 2, 2025
b1693aa
formatting of facet plot, and unittest
ying39purdue Apr 2, 2025
a9af695
formatting of facet plot, and unittest
ying39purdue Apr 2, 2025
b5b5699
remove axis rotation and formatting
ying39purdue Apr 8, 2025
ff759aa
Unittest addition of element numbers, title, and axis labels check
ying39purdue Apr 11, 2025
7019cae
unittest rebase with dev
ying39purdue Apr 15, 2025
44b3566
correct facet function return
ying39purdue Apr 16, 2025
a0f3fcc
feat(histogram): enhance faceted layout customization
zhan4329 Mar 24, 2026
bc2e95e
fix(histogram): enforce shared bins across facets
zhan4329 Mar 25, 2026
674b84e
fix(histogram): convert numpy array to list for seaborn bins compatib…
zhan4329 Mar 25, 2026
814998f
docs(histogram): add TODO comments for binning logic review
zhan4329 Mar 25, 2026
6b69eb5
fix(histogram): close unused internal figure
zhan4329 Mar 26, 2026
60eb850
refactor(histogram): extract bin edge computation into _compute_globa…
zhan4329 Mar 30, 2026
248fb45
fix(histogram): revert group titles for facet plots without feature s…
zhan4329 Mar 30, 2026
16e3dc0
refactor(histogram): modularize facet layout logic
zhan4329 Apr 1, 2026
a2112c2
feat(histogram): wire facet layout from template
zhan4329 Apr 3, 2026
3c0050b
feat(histogram): add element control and style validation
zhan4329 Apr 3, 2026
b29ef58
fix(histogram): tighten facet validation and labels
zhan4329 Apr 7, 2026
6991def
chore(gitignore): ignore workspace directory
zhan4329 Apr 14, 2026
85c570a
fix(histogram): normalize bins defaults to Rice-rule estimator
zhan4329 Apr 14, 2026
e5e9082
refactor(histogram): simplify default bins fallback logic
zhan4329 Apr 14, 2026
a3228f8
feat(histogram): enhance facet validation and parameter naming
zhan4329 Apr 14, 2026
b990b8c
chore(gitignore): remove workspace entry
zhan4329 Apr 16, 2026
ff67a19
feat(histogram): improve ax parameter handling and validation
zhan4329 Apr 17, 2026
0fc8b32
test(histogram): update facet label validation assertions
zhan4329 Apr 17, 2026
ac5490e
test(histogram): refactor and expand facet plot test coverage
zhan4329 Apr 17, 2026
eba316b
fix(histogram): preserve facet xlabel and apply rotation
zhan4329 Apr 17, 2026
1e738a2
test(histogram): add facet mode parameters to template test
zhan4329 Apr 17, 2026
685d4f6
test(histogram): add facet validation and categorical tests
zhan4329 Apr 18, 2026
dd4bd72
refactor(histogram): relocate histogram helpers into function scope
zhan4329 Apr 18, 2026
827d654
docs(histogram): clarify facet figure size layout hints in template
zhan4329 Apr 18, 2026
02dd9ba
refactor(histogram): improve code organization of facet geometry dete…
zhan4329 Apr 18, 2026
b8e3358
test(histogram): expand facet coverage with comprehensive test suite
zhan4329 Apr 18, 2026
083315b
feat(utils): add normalize_positive_number helper function
zhan4329 Apr 19, 2026
6914244
refactor(facet): refactor facet geometry using normalize_positive_number
zhan4329 Apr 19, 2026
6af0820
fix(histogram-template): correct rotated tick label handling in facets
zhan4329 Apr 20, 2026
5c93459
fix(histogram): prune facet layout hints validation
zhan4329 Apr 20, 2026
5143007
refactor(utils): remove normalize_positive_number helper
zhan4329 Apr 20, 2026
367a922
fix(histogram_template): allow auto figure size in facet mode
zhan4329 Apr 20, 2026
c5bb404
feat(histogram): add long-label facet geometry adjustment
zhan4329 Apr 20, 2026
886fba2
fix(histogram): add max_groups guardrail
zhan4329 Apr 21, 2026
4062402
fix(histogram-template): fix figure label overlapping by scaling face…
zhan4329 Apr 21, 2026
ef31f50
refactor(histogram): simplify optional hint parsing
zhan4329 Apr 21, 2026
44ce09f
fix(histogram): ignore multiple outside overlays
zhan4329 Apr 21, 2026
0198eec
test(histogram): improve facet unittests
zhan4329 Apr 21, 2026
860c7a1
fix(histogram): finalize grouped shared-bin handling
zhan4329 Apr 21, 2026
6e6c14a
fix(histogram): ignore facet hints outside facets
zhan4329 Apr 22, 2026
4b6bd12
style(histogram): clean whitespace in histogram diff
zhan4329 Apr 22, 2026
c02fa6a
style: whitespace fix in multiple files
zhan4329 Apr 22, 2026
7e16c59
fix(histogram): gate facet hints by facet mode
zhan4329 Apr 23, 2026
88228b6
fix(histogram): ignore max_groups off grouped path
zhan4329 Apr 23, 2026
268ab36
docs(histogram): standardize documentations
zhan4329 Apr 23, 2026
0c73cfe
chore(import): remove unused imports
zhan4329 Apr 23, 2026
a422bbe
fix(histogram): gate multiple to overlay mode
zhan4329 Apr 23, 2026
7a6c048
fix(histogram): widen facet spacing in template layout
zhan4329 Apr 28, 2026
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
.vscode/launch.json
test/__pychache__
build/
*.egg-info/
*.egg-info/
156 changes: 142 additions & 14 deletions src/spac/templates/histogram_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,23 +96,27 @@ def run_from_json(
feature = text_to_value(params.get("Feature", "None"))
annotation = text_to_value(params.get("Annotation", "None"))
layer = params.get("Table_", "Original")
group_by = params.get("Group_by", "None")
group_by = text_to_value(params.get("Group_by", "None"))
max_groups = params.get("Max_Groups", 20)
together = params.get("Together", True)
fig_width = params.get("Figure_Width", 8)
fig_height = params.get("Figure_Height", 6)
fig_width = params.get("Figure_Width", "auto")
fig_height = params.get("Figure_Height", "auto")
font_size = params.get("Font_Size", 12)
fig_dpi = params.get("Figure_DPI", 300)
legend_location = params.get("Legend_Location", "best")
legend_in_figure = params.get("Legend_in_Figure", False)
take_X_log = params.get("Take_X_Log", False)
take_Y_log = params.get("Take_Y_log", False)
multiple = params.get("Multiple", "dodge")
element = params.get("Element", "bars")
shrink = params.get("Shrink_Number", 1)
bins = params.get("Bins", "auto")
alpha = params.get("Bin_Transparency", 0.75)
stat = params.get("Stat", "count")
x_rotate = params.get("X_Axis_Label_Rotation", 0)
histplot_by = params.get("Plot_By", "Annotation")
facet = params.get("Facet", False)
facet_ncol = params.get("Facet_Ncol", "auto")

# Close all existing figures to prevent extra plots
plt.close('all')
Expand Down Expand Up @@ -151,7 +155,9 @@ def run_from_json(
'No features available in adata.var_names to plot.'
)

# Validate and set bins
# Bins use a strict template contract in feature mode:
# "auto" or a positive integer. Loose null-like values are intentionally
# not treated as aliases here.
if feature is not None:
bins = text_to_value(
bins,
Expand Down Expand Up @@ -180,42 +186,138 @@ def run_from_json(
"Setting bin number calculation to auto."
)


if group_by and together:
multiple = str(multiple).strip().lower()
element = str(element).strip().lower()
stat = str(stat).strip().lower()

# Figure_Width and Figure_Height use "auto" for template defaults.
# In facet mode, it is forwarded as None to derive layout geometry.
# In non-facet mode, it falls back to 8x6 inches.
fig_width = text_to_value(
fig_width,
default_none_text="auto",
value_to_convert_to=None if facet else 8,
to_float=True,
param_name="Figure_Width"
)
fig_height = text_to_value(
fig_height,
default_none_text="auto",
value_to_convert_to=None if facet else 6,
to_float=True,
param_name="Figure_Height"
)
if fig_width is not None and fig_height is not None:
if fig_width <= 0 or fig_height <= 0:
raise ValueError(
f'Figure_Width/Height should be a positive number.'
f'Received "{fig_width}"/"{fig_height}".'
)
if fig_dpi <= 0:
raise ValueError(
f'Figure_DPI should be a positive number. Received "{fig_dpi}".'
)

# Validate x-axis label rotation
if (x_rotate < 0) or (x_rotate > 360):
raise ValueError(
f'The X label rotation should fall within 0 to 360 degree. '
f'Received "{x_rotate}".'
)

# Max_Groups applies only when Group_by is set.
# It accepts a positive integer or "unlimited".
# Missing values default to 20.
if group_by:
parsed_max_groups = max_groups
if parsed_max_groups != "unlimited":
parsed_max_groups = text_to_value(
parsed_max_groups,
value_to_convert_to=20,
to_int=True,
param_name="Max_Groups",
)
if parsed_max_groups <= 0:
raise ValueError(
f'Max_Groups should be a positive integer or "unlimited". '
f'Received "{parsed_max_groups}".'
)

# Facet requires Group_by and forbids Together=True.
# Facet_Ncol accepts "auto" or a positive integer.
if facet:
if group_by is None:
raise ValueError(
'Facet is True but Group_by is not specified. '
'Please specify Group_by when using Facet.'
)
if together:
raise ValueError(
'Together and Facet cannot both be True. Please set one to False.'
)
facet_ncol = text_to_value(
facet_ncol,
default_none_text="auto",
to_int=True,
param_name="Facet_Ncol"
)
if facet_ncol is not None:
if facet_ncol <= 0:
raise ValueError(
f'Facet_Ncol must be a positive integer or "auto". '
f'Received "{facet_ncol}".'
)

# Initialize the x-variable before the loop
if histplot_by == "Annotation":
x_var = annotation
else:
x_var = feature

# Assemble validated histogram kwargs right before the plotting call.
hist_kwargs = dict(
element=element,
shrink=shrink,
bins=bins,
alpha=alpha,
stat=stat,
)
if group_by and together:
hist_kwargs["multiple"] = multiple
if group_by:
hist_kwargs["max_groups"] = parsed_max_groups
if facet:
hist_kwargs["facet_ncol"] = facet_ncol
hist_kwargs["facet_fig_width"] = fig_width
hist_kwargs["facet_fig_height"] = fig_height
hist_kwargs["facet_tick_rotation"] = x_rotate

result = histogram(
adata=adata,
feature=feature,
annotation=annotation,
layer=text_to_value(layer, "Original"),
group_by=text_to_value(group_by),
group_by=group_by,
together=together,
ax=None,
x_log_scale=take_X_log,
y_log_scale=take_Y_log,
multiple=multiple,
shrink=shrink,
bins=bins,
alpha=alpha,
stat=stat
facet=facet,
**hist_kwargs,
)

fig = result["fig"]
axs = result["axs"]
df_counts = result["df"]

# Set figure size and dpi
fig.set_size_inches(fig_width, fig_height)
if fig_width is not None and fig_height is not None:
fig.set_size_inches(fig_width, fig_height)
logger.info(f"Set figure size to {fig_width}x{fig_height} inches.")
fig.set_dpi(fig_dpi)
logger.info(f"Set figure DPI to {fig_dpi}.")

# Ensure axes is a list
if isinstance(axs, list):
Expand Down Expand Up @@ -249,8 +351,12 @@ def run_from_json(

# Rotate x labels
ax.tick_params(axis='x', rotation=x_rotate)
if x_rotate:
for label in ax.get_xticklabels():
label.set_rotation_mode('anchor')
label.set_horizontalalignment('right')

# Set titles based on group_by
# Set titles based on group_by and facet
if text_to_value(group_by):
if together:
for ax in axes:
Expand All @@ -267,15 +373,37 @@ def run_from_json(
"Number of axes does not match number of "
"groups. Titles may not correspond correctly."
)
if facet:
fig.suptitle(
f'Histogram of "{x_var}" faceted by "{group_by}"'
)
ax_title_prefix = f'Group'
else:
ax_title_prefix = f'Histogram of "{x_var}" for group'
for ax, grp in zip(axes, unique_groups):
ax.set_title(
f'Histogram of "{x_var}" for group: "{grp}"'
f'{ax_title_prefix}: "{grp}"'
)
else:
for ax in axes:
ax.set_title(f'Count plot of "{x_var}"')

plt.tight_layout()
# Adjust layout to prevent title overlap
if facet:
rows = len({round(ax.get_position().y0, 3) for ax in axes})
fig.tight_layout(
rect=[
min(0.030, 0.02 + 0.0025 * rows),
max(0.022, 0.036 - 0.003 * rows),
min(0.992, 0.98 + 0.0025 * rows),
max(0.969, 0.975 - 0.001 * rows),
],
pad=max(0.35, 0.6 - 0.05 * rows),
h_pad=max(0.2, 0.43 - 0.04 * rows) * 6,
w_pad=max(0.2, 0.43 - 0.04 * rows) * 6,
)
else:
fig.tight_layout()

logger.info("Displaying top 10 rows of histogram dataframe:")
print(df_counts.head(10))
Expand Down
14 changes: 7 additions & 7 deletions src/spac/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1196,20 +1196,20 @@ def compute_metrics(data):

# compute summary statistics for the specified columns
def compute_summary_qc_stats(
df: pd.DataFrame,
df: pd.DataFrame,
n_mad: int = 5,
upper_quantile: float = 0.95,
lower_quantile: float = 0.05,
stat_columns_list: List[str] = ['nFeature', 'nCount', 'percent.mt']
) -> pd.DataFrame:

"""
Compute summary quality control statistics for specified columns in a dataset.

For each column in stat_columns_list, this function calculates:
- Mean
- Median
- Upper and lower thresholds based on median ± n_mad * MAD
- Upper and lower thresholds based on median ± n_mad * MAD
(median absolute deviation)
- Upper and lower quantiles

Expand All @@ -1230,7 +1230,7 @@ def compute_summary_qc_stats(
-------
pd.DataFrame
DataFrame with summary statistics for each specified column.
Columns: ["metric_name", "mean", "median", "upper_mad", "lower_mad",
Columns: ["metric_name", "mean", "median", "upper_mad", "lower_mad",
"upper_quantile", "lower_quantile"]

Raises
Expand Down Expand Up @@ -1269,8 +1269,8 @@ def compute_summary_qc_stats(
return pd.DataFrame(
stat_vals,
columns=[
"metric_name", "mean", "median",
"upper_mad", "lower_mad",
"metric_name", "mean", "median",
"upper_mad", "lower_mad",
"upper_quantile", "lower_quantile"
]
)
)
Loading