Skip to content

Commit e702489

Browse files
authored
✨ Feat: Add scripts, test, and example for box plot (#30)
* ✨ Feat(vuecore/schemas/basic/box.py): Create Pydantic schema of the box plot * ✨ Feat(vuecore/engines/plotly/box.py): Create script with build function for box plot * ✨ Feat(vuecore/engines/plotly/theming.py): Add apply_box_theme function to the script * ✨ Feat(vuecore/engines/plotly/__init__.py): Register box plot builder and add it to the PlotType StrEnum * ✨ Feat(vuecore/plots/box.py): Create script with the user-facing function for the box plot * ✨ Feat(docs/api_examples/box_plot.ipynb): Create notebook api example for box plot and sync it with a python script * 📝 Docs: update index.md to add box plot example * 📝 Docs: update code to generate synthetic data on box plot exmaple * ✨ Feat(tests/test_boxplot.py): Create box plot test * 🧑‍💻 Update PR template description
1 parent 58096d8 commit e702489

File tree

11 files changed

+1012
-2
lines changed

11 files changed

+1012
-2
lines changed

.github/PULL_REQUEST_TEMPLATE/new_plot.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ Please complete the following sections when you submit your pull request. Note t
33
-->
44
### Description
55

6-
Adds new [PlotName] plot to VueCore.
6+
Add the [PlotName] plot to VueCore.
77

88
### Tasks Checklist
99

1010
- [ ] Create **Pydantic schema** in the `vuecore/schemas` folder. It should be aligned with the [plotly API](https://plotly.com/python-api-reference/index.html)
1111
- [ ] Create a script with a **build function** in the `vuecore/engines/plotly` folder
1212
- [ ] Update `theming.py` script in the `vuecore/engines/plotly` folder
13-
- [ ] Register the new **builder** in the _`_init__.py` script of the `vuecore/engines/plotly` folder
13+
- [ ] Add the new plot in the **PlotType StrEnum** of the `vuecore/constants.py` script, and register the new **builder** in the `__init__.py` script of the `vuecore/engines/plotly` folder
1414
- [ ] Create a script with the **user-facing function** in the `vuecore/plots` folder. It gathers the Pydantic schema, builder function, and saves the plot
1515
- [ ] Create an **api example jupyter notebook** in the `docs/api_examples folder`
1616
- [ ] Use **jupytext** to sync the Jupyter notebook with a Python script

docs/api_examples/box_plot.ipynb

Lines changed: 366 additions & 0 deletions
Large diffs are not rendered by default.

docs/api_examples/box_plot.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# ---
2+
# jupyter:
3+
# jupytext:
4+
# text_representation:
5+
# extension: .py
6+
# format_name: percent
7+
# format_version: '1.3'
8+
# jupytext_version: 1.17.2
9+
# kernelspec:
10+
# display_name: vuecore-dev
11+
# language: python
12+
# name: python3
13+
# ---
14+
15+
# %% [markdown]
16+
# # Box Plot
17+
#
18+
# ![VueCore logo][vuecore_logo]
19+
#
20+
# [![Open In Colab][colab_badge]][colab_link]
21+
#
22+
# [VueCore][vuecore_repo] is a Python package for creating interactive and static visualizations of multi-omics data.
23+
# It is part of a broader ecosystem of tools—including [ACore][acore_repo] for data processing and [VueGen][vuegen_repo] for automated reporting—that together enable end-to-end workflows for omics analysis.
24+
#
25+
# This notebook demonstrates how to generate box plots using plotting functions from VueCore. We showcase basic and advanced plot configurations, highlighting key customization options such as grouping, color mapping, text annotations, and export to multiple file formats.
26+
#
27+
# ## Notebook structure
28+
#
29+
# First, we will set up the work environment by installing the necessary packages and importing the required libraries. Next, we will create basic and advanced box plots.
30+
#
31+
# 0. [Work environment setup](#0-work-environment-setup)
32+
# 1. [Basic box plot](#1-basic-box-plot)
33+
# 2. [Advanced box plot](#2-advanced-box-plot)
34+
#
35+
# ## Credits and Contributors
36+
# - This notebook was created by Sebastián Ayala-Ruano under the supervision of Henry Webel and Alberto Santos, head of the [Multiomics Network Analytics Group (MoNA)][Mona] at the [Novo Nordisk Foundation Center for Biosustainability (DTU Biosustain)][Biosustain].
37+
# - You can find more details about the project in this [GitHub repository][vuecore_repo].
38+
#
39+
# [colab_badge]: https://colab.research.google.com/assets/colab-badge.svg
40+
# [colab_link]: https://colab.research.google.com/github/Multiomics-Analytics-Group/vuecore/blob/main/docs/api_examples/box_plot.ipynb
41+
# [vuecore_logo]: https://raw.githubusercontent.com/Multiomics-Analytics-Group/vuecore/main/docs/images/logo/vuecore_logo.svg
42+
# [Mona]: https://multiomics-analytics-group.github.io/
43+
# [Biosustain]: https://www.biosustain.dtu.dk/
44+
# [vuecore_repo]: https://github.com/Multiomics-Analytics-Group/vuecore
45+
# [vuegen_repo]: https://github.com/Multiomics-Analytics-Group/vuegen
46+
# [acore_repo]: https://github.com/Multiomics-Analytics-Group/acore
47+
48+
# %% [markdown]
49+
# ## 0. Work environment setup
50+
51+
# %% [markdown]
52+
# ### 0.1. Installing libraries and creating global variables for platform and working directory
53+
#
54+
# To run this notebook locally, you should create a virtual environment with the required libraries. If you are running this notebook on Google Colab, everything should be set.
55+
56+
# %% tags=["hide-output"]
57+
# VueCore library
58+
# %pip install vuecore
59+
60+
# %% tags=["hide-cell"]
61+
import os
62+
63+
IN_COLAB = "COLAB_GPU" in os.environ
64+
65+
# %% tags=["hide-cell"]
66+
# Create a directory for outputs
67+
output_dir = "./outputs"
68+
os.makedirs(output_dir, exist_ok=True)
69+
70+
# %% [markdown]
71+
# ### 0.2. Importing libraries
72+
73+
# %%
74+
# Imports
75+
import pandas as pd
76+
import numpy as np
77+
from pathlib import Path
78+
79+
from vuecore.plots.basic.box import create_box_plot
80+
81+
# %% [markdown]
82+
# ### 0.3. Create sample data
83+
# We create a synthetic dataset simulating gene expression levels across different patient samples and treatment conditions, with each data point representing a unique gene's expression level under a specific treatment for a particular patient.
84+
85+
# %%
86+
# Set a random seed for reproducibility of the synthetic data
87+
np.random.seed(42)
88+
89+
# Parameters
90+
num_samples = 200
91+
sample_groups = ["Patient A", "Patient B", "Patient C", "Patient D"]
92+
treatments = ["Control", "Treated"]
93+
94+
# Sample metadata
95+
sample_ids = np.random.choice(sample_groups, size=num_samples)
96+
treatment_assignments = np.random.choice(treatments, size=num_samples)
97+
gene_ids = [f"Gene_{g}" for g in np.random.randint(1, 1500, size=num_samples)]
98+
99+
# Base expression values
100+
base_expr = np.random.normal(loc=100, scale=35, size=num_samples)
101+
102+
# Treatment effect simulation
103+
treatment_effect = np.where(
104+
treatment_assignments == "Treated",
105+
np.random.normal(loc=50, scale=30, size=num_samples),
106+
0,
107+
)
108+
109+
# Small random per-gene offset for extra variability
110+
gene_offset = np.random.normal(loc=0, scale=20, size=num_samples)
111+
112+
# Final expression
113+
expr = base_expr + treatment_effect + gene_offset
114+
115+
# Construct DataFrame
116+
gene_exp_df = pd.DataFrame(
117+
{
118+
"Sample_ID": sample_ids,
119+
"Treatment": treatment_assignments,
120+
"Gene_ID": gene_ids,
121+
"Expression": expr,
122+
}
123+
)
124+
125+
# %% [markdown]
126+
# ## 1. Basic Box Plot
127+
# A basic box plot can be created by simply providing the `x` and `y` columns from the DataFrame, along with style options like `title`.
128+
129+
# %%
130+
# Define output file path for the PNG plot
131+
file_path_basic_png = Path(output_dir) / "box_plot_basic.png"
132+
133+
# Generate the basic box plot
134+
box_plot_basic = create_box_plot(
135+
data=gene_exp_df,
136+
x="Treatment",
137+
y="Expression",
138+
title="Gene Expression Levels by Treatment",
139+
file_path=file_path_basic_png,
140+
)
141+
142+
box_plot_basic.show()
143+
144+
# %% [markdown]
145+
# ## 2. Advanced Box Plot
146+
# Here is an example of an advanced box plot with more descriptive parameters, including `color and box grouping`, `text annotations`, `hover tooltips`, and export to `HTML`.
147+
148+
# %%
149+
# Define the output file path for the advanced HTML plot
150+
file_path_adv_html = Path(output_dir) / "box_plot_advanced.html"
151+
152+
# Generate the advanced box plot
153+
box_plot_adv = create_box_plot(
154+
data=gene_exp_df,
155+
x="Treatment",
156+
y="Expression",
157+
color="Sample_ID",
158+
boxmode="group",
159+
notched=True,
160+
title="Gene Expression Levels with Control and Treatment Condition",
161+
subtitle="Distribution of gene expression across different treatments and patient samples",
162+
labels={
163+
"Treatment": "Treatment",
164+
"Expression": "Gene Expression",
165+
"Sample_ID": "Patient Sample ID",
166+
},
167+
color_discrete_map={
168+
"Patient A": "#508AA8",
169+
"Patient B": "#A8505E",
170+
"Patient C": "#86BF84",
171+
"Patient D": "#A776AF",
172+
},
173+
category_orders={"Sample_ID": ["Patient A", "Patient B", "Patient C", "Patient D"]},
174+
hover_data=["Gene_ID"],
175+
file_path=file_path_adv_html,
176+
)
177+
178+
box_plot_adv.show()

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
api_examples/scatter_plot
1515
api_examples/line_plot
1616
api_examples/bar_plot
17+
api_examples/box_plot
1718
```
1819

1920
```{toctree}

src/vuecore/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class PlotType(StrEnum):
1313
SCATTER = auto()
1414
LINE = auto()
1515
BAR = auto()
16+
BOX = auto()
1617

1718

1819
class EngineType(StrEnum):

src/vuecore/engines/plotly/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .scatter import build as build_scatter
55
from .line import build as build_line
66
from .bar import build as build_bar
7+
from .box import build as build_box
78
from .saver import save
89

910
# Register the functions with the central dispatcher
@@ -12,5 +13,6 @@
1213
)
1314
register_builder(plot_type=PlotType.LINE, engine=EngineType.PLOTLY, func=build_line)
1415
register_builder(plot_type=PlotType.BAR, engine=EngineType.PLOTLY, func=build_bar)
16+
register_builder(plot_type=PlotType.BOX, engine=EngineType.PLOTLY, func=build_box)
1517

1618
register_saver(engine=EngineType.PLOTLY, func=save)

src/vuecore/engines/plotly/box.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# vuecore/engines/plotly/box.py
2+
3+
import pandas as pd
4+
import plotly.express as px
5+
import plotly.graph_objects as go
6+
7+
from vuecore.schemas.basic.box import BoxConfig
8+
from .theming import apply_box_theme
9+
10+
11+
def build(data: pd.DataFrame, config: BoxConfig) -> go.Figure:
12+
"""
13+
Creates a Plotly box plot figure from a DataFrame and a Pydantic configuration.
14+
15+
This function acts as a bridge between the abstract plot definition and the
16+
Plotly Express implementation. It translates the validated `BoxConfig`
17+
into the arguments for `plotly.express.box` and also forwards any
18+
additional, unvalidated keyword arguments from Plotly. The resulting figure
19+
is then customized with layout and theme settings using `plotly.graph_objects`.
20+
(https://plotly.com/python-api-reference/generated/plotly.express.box.html).
21+
22+
Parameters
23+
----------
24+
data : pd.DataFrame
25+
The DataFrame containing the plot data.
26+
config : BoxConfig
27+
The validated Pydantic model with all plot configurations.
28+
29+
Returns
30+
-------
31+
go.Figure
32+
A `plotly.graph_objects.Figure` object representing the box plot.
33+
"""
34+
# Get all parameters from the config model, including extras
35+
all_config_params = config.model_dump()
36+
37+
# Define parameters handled by the theme script
38+
theming_params = [
39+
"boxmode",
40+
"log_x",
41+
"log_y",
42+
"range_x",
43+
"range_y",
44+
"notched",
45+
"points",
46+
"title",
47+
"x_title",
48+
"y_title",
49+
"subtitle",
50+
"template",
51+
"width",
52+
"height",
53+
]
54+
55+
# Create the dictionary of arguments for px.box
56+
plot_args = {
57+
k: v
58+
for k, v in all_config_params.items()
59+
if k not in theming_params and v is not None
60+
}
61+
62+
# Create the base figure using only the arguments relevant to px.box
63+
fig = px.box(data, **plot_args)
64+
65+
# Apply theme and additional styling to the generated figure.
66+
fig = apply_box_theme(fig, config)
67+
68+
return fig

src/vuecore/engines/plotly/theming.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from vuecore.schemas.basic.scatter import ScatterConfig
44
from vuecore.schemas.basic.line import LineConfig
55
from vuecore.schemas.basic.bar import BarConfig
6+
from vuecore.schemas.basic.box import BoxConfig
67

78

89
def apply_scatter_theme(fig: go.Figure, config: ScatterConfig) -> go.Figure:
@@ -157,3 +158,58 @@ def apply_bar_theme(fig: go.Figure, config: BarConfig) -> go.Figure:
157158
barmode=config.barmode,
158159
)
159160
return fig
161+
162+
163+
def apply_box_theme(fig: go.Figure, config: BoxConfig) -> go.Figure:
164+
"""
165+
Applies a consistent layout and theme to a Plotly box plot.
166+
167+
This function handles all styling and layout adjustments, such as titles,
168+
dimensions, templates, and trace properties, separating these concerns
169+
from the initial data mapping.
170+
171+
Parameters
172+
----------
173+
fig : go.Figure
174+
The Plotly figure object to be styled.
175+
config : BoxConfig
176+
The configuration object containing all styling and layout info.
177+
178+
Returns
179+
-------
180+
go.Figure
181+
The styled Plotly figure object.
182+
"""
183+
# Apply trace-specific updates for box plots
184+
fig.update_traces(
185+
boxpoints=config.points, notched=config.notched, selector=dict(type="box")
186+
)
187+
188+
# Use the labels dictionary to set axis titles, falling back to defaults
189+
x_title = config.x_title or (
190+
config.labels.get(config.x)
191+
if config.x and config.labels
192+
else None or (config.x.title() if config.x else None)
193+
)
194+
y_title = config.y_title or (
195+
config.labels.get(config.y)
196+
if config.y and config.labels
197+
else None or (config.y.title() if config.y else None)
198+
)
199+
200+
# Apply layout updates for box plot
201+
fig.update_layout(
202+
title_text=config.title,
203+
title_subtitle_text=config.subtitle,
204+
xaxis_title=x_title,
205+
yaxis_title=y_title,
206+
height=config.height,
207+
width=config.width,
208+
template=config.template,
209+
xaxis_type="log" if config.log_x else None,
210+
yaxis_type="log" if config.log_y else None,
211+
xaxis_range=config.range_x,
212+
yaxis_range=config.range_y,
213+
boxmode=config.boxmode,
214+
)
215+
return fig

0 commit comments

Comments
 (0)