Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE/new_plot.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ Please complete the following sections when you submit your pull request. Note t
-->
### Description

Adds new [PlotName] plot to VueCore.
Add the [PlotName] plot to VueCore.

### Tasks Checklist

- [ ] Create **Pydantic schema** in the `vuecore/schemas` folder. It should be aligned with the [plotly API](https://plotly.com/python-api-reference/index.html)
- [ ] Create a script with a **build function** in the `vuecore/engines/plotly` folder
- [ ] Update `theming.py` script in the `vuecore/engines/plotly` folder
- [ ] Register the new **builder** in the _`_init__.py` script of the `vuecore/engines/plotly` folder
- [ ] 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
- [ ] Create a script with the **user-facing function** in the `vuecore/plots` folder. It gathers the Pydantic schema, builder function, and saves the plot
- [ ] Create an **api example jupyter notebook** in the `docs/api_examples folder`
- [ ] Use **jupytext** to sync the Jupyter notebook with a Python script
Expand Down
366 changes: 366 additions & 0 deletions docs/api_examples/box_plot.ipynb

Large diffs are not rendered by default.

178 changes: 178 additions & 0 deletions docs/api_examples/box_plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# ---
# jupyter:
# jupytext:
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.17.2
# kernelspec:
# display_name: vuecore-dev
# language: python
# name: python3
# ---

# %% [markdown]
# # Box Plot
#
# ![VueCore logo][vuecore_logo]
#
# [![Open In Colab][colab_badge]][colab_link]
#
# [VueCore][vuecore_repo] is a Python package for creating interactive and static visualizations of multi-omics data.
# 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.
#
# 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.
#
# ## Notebook structure
#
# 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.
#
# 0. [Work environment setup](#0-work-environment-setup)
# 1. [Basic box plot](#1-basic-box-plot)
# 2. [Advanced box plot](#2-advanced-box-plot)
#
# ## Credits and Contributors
# - 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].
# - You can find more details about the project in this [GitHub repository][vuecore_repo].
#
# [colab_badge]: https://colab.research.google.com/assets/colab-badge.svg
# [colab_link]: https://colab.research.google.com/github/Multiomics-Analytics-Group/vuecore/blob/main/docs/api_examples/box_plot.ipynb
# [vuecore_logo]: https://raw.githubusercontent.com/Multiomics-Analytics-Group/vuecore/main/docs/images/logo/vuecore_logo.svg
# [Mona]: https://multiomics-analytics-group.github.io/
# [Biosustain]: https://www.biosustain.dtu.dk/
# [vuecore_repo]: https://github.com/Multiomics-Analytics-Group/vuecore
# [vuegen_repo]: https://github.com/Multiomics-Analytics-Group/vuegen
# [acore_repo]: https://github.com/Multiomics-Analytics-Group/acore

# %% [markdown]
# ## 0. Work environment setup

# %% [markdown]
# ### 0.1. Installing libraries and creating global variables for platform and working directory
#
# 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.

# %% tags=["hide-output"]
# VueCore library
# %pip install vuecore

# %% tags=["hide-cell"]
import os

IN_COLAB = "COLAB_GPU" in os.environ

# %% tags=["hide-cell"]
# Create a directory for outputs
output_dir = "./outputs"
os.makedirs(output_dir, exist_ok=True)

# %% [markdown]
# ### 0.2. Importing libraries

# %%
# Imports
import pandas as pd
import numpy as np
from pathlib import Path

from vuecore.plots.basic.box import create_box_plot

# %% [markdown]
# ### 0.3. Create sample data
# 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.

# %%
# Set a random seed for reproducibility of the synthetic data
np.random.seed(42)

# Parameters
num_samples = 200
sample_groups = ["Patient A", "Patient B", "Patient C", "Patient D"]
treatments = ["Control", "Treated"]

# Sample metadata
sample_ids = np.random.choice(sample_groups, size=num_samples)
treatment_assignments = np.random.choice(treatments, size=num_samples)
gene_ids = [f"Gene_{g}" for g in np.random.randint(1, 1500, size=num_samples)]

# Base expression values
base_expr = np.random.normal(loc=100, scale=35, size=num_samples)

# Treatment effect simulation
treatment_effect = np.where(
treatment_assignments == "Treated",
np.random.normal(loc=50, scale=30, size=num_samples),
0,
)

# Small random per-gene offset for extra variability
gene_offset = np.random.normal(loc=0, scale=20, size=num_samples)

# Final expression
expr = base_expr + treatment_effect + gene_offset

# Construct DataFrame
gene_exp_df = pd.DataFrame(
{
"Sample_ID": sample_ids,
"Treatment": treatment_assignments,
"Gene_ID": gene_ids,
"Expression": expr,
}
)

# %% [markdown]
# ## 1. Basic Box Plot
# A basic box plot can be created by simply providing the `x` and `y` columns from the DataFrame, along with style options like `title`.

# %%
# Define output file path for the PNG plot
file_path_basic_png = Path(output_dir) / "box_plot_basic.png"

# Generate the basic box plot
box_plot_basic = create_box_plot(
data=gene_exp_df,
x="Treatment",
y="Expression",
title="Gene Expression Levels by Treatment",
file_path=file_path_basic_png,
)

box_plot_basic.show()

# %% [markdown]
# ## 2. Advanced Box Plot
# 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`.

# %%
# Define the output file path for the advanced HTML plot
file_path_adv_html = Path(output_dir) / "box_plot_advanced.html"

# Generate the advanced box plot
box_plot_adv = create_box_plot(
data=gene_exp_df,
x="Treatment",
y="Expression",
color="Sample_ID",
boxmode="group",
notched=True,
title="Gene Expression Levels with Control and Treatment Condition",
subtitle="Distribution of gene expression across different treatments and patient samples",
labels={
"Treatment": "Treatment",
"Expression": "Gene Expression",
"Sample_ID": "Patient Sample ID",
},
color_discrete_map={
"Patient A": "#508AA8",
"Patient B": "#A8505E",
"Patient C": "#86BF84",
"Patient D": "#A776AF",
},
category_orders={"Sample_ID": ["Patient A", "Patient B", "Patient C", "Patient D"]},
hover_data=["Gene_ID"],
file_path=file_path_adv_html,
)

box_plot_adv.show()
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
api_examples/scatter_plot
api_examples/line_plot
api_examples/bar_plot
api_examples/box_plot
```

```{toctree}
Expand Down
1 change: 1 addition & 0 deletions src/vuecore/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class PlotType(StrEnum):
SCATTER = auto()
LINE = auto()
BAR = auto()
BOX = auto()


class EngineType(StrEnum):
Expand Down
2 changes: 2 additions & 0 deletions src/vuecore/engines/plotly/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .scatter import build as build_scatter
from .line import build as build_line
from .bar import build as build_bar
from .box import build as build_box
from .saver import save

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

register_saver(engine=EngineType.PLOTLY, func=save)
68 changes: 68 additions & 0 deletions src/vuecore/engines/plotly/box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# vuecore/engines/plotly/box.py

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go

from vuecore.schemas.basic.box import BoxConfig
from .theming import apply_box_theme


def build(data: pd.DataFrame, config: BoxConfig) -> go.Figure:
"""
Creates a Plotly box plot figure from a DataFrame and a Pydantic configuration.

This function acts as a bridge between the abstract plot definition and the
Plotly Express implementation. It translates the validated `BoxConfig`
into the arguments for `plotly.express.box` and also forwards any
additional, unvalidated keyword arguments from Plotly. The resulting figure
is then customized with layout and theme settings using `plotly.graph_objects`.
(https://plotly.com/python-api-reference/generated/plotly.express.box.html).

Parameters
----------
data : pd.DataFrame
The DataFrame containing the plot data.
config : BoxConfig
The validated Pydantic model with all plot configurations.

Returns
-------
go.Figure
A `plotly.graph_objects.Figure` object representing the box plot.
"""
# Get all parameters from the config model, including extras
all_config_params = config.model_dump()

# Define parameters handled by the theme script
theming_params = [
"boxmode",
"log_x",
"log_y",
"range_x",
"range_y",
"notched",
"points",
"title",
"x_title",
"y_title",
"subtitle",
"template",
"width",
"height",
]

# Create the dictionary of arguments for px.box
plot_args = {
k: v
for k, v in all_config_params.items()
if k not in theming_params and v is not None
}

# Create the base figure using only the arguments relevant to px.box
fig = px.box(data, **plot_args)

# Apply theme and additional styling to the generated figure.
fig = apply_box_theme(fig, config)

return fig
56 changes: 56 additions & 0 deletions src/vuecore/engines/plotly/theming.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from vuecore.schemas.basic.scatter import ScatterConfig
from vuecore.schemas.basic.line import LineConfig
from vuecore.schemas.basic.bar import BarConfig
from vuecore.schemas.basic.box import BoxConfig


def apply_scatter_theme(fig: go.Figure, config: ScatterConfig) -> go.Figure:
Expand Down Expand Up @@ -157,3 +158,58 @@ def apply_bar_theme(fig: go.Figure, config: BarConfig) -> go.Figure:
barmode=config.barmode,
)
return fig


def apply_box_theme(fig: go.Figure, config: BoxConfig) -> go.Figure:
"""
Applies a consistent layout and theme to a Plotly box plot.

This function handles all styling and layout adjustments, such as titles,
dimensions, templates, and trace properties, separating these concerns
from the initial data mapping.

Parameters
----------
fig : go.Figure
The Plotly figure object to be styled.
config : BoxConfig
The configuration object containing all styling and layout info.

Returns
-------
go.Figure
The styled Plotly figure object.
"""
# Apply trace-specific updates for box plots
fig.update_traces(
boxpoints=config.points, notched=config.notched, selector=dict(type="box")
)

# Use the labels dictionary to set axis titles, falling back to defaults
x_title = config.x_title or (
config.labels.get(config.x)
if config.x and config.labels
else None or (config.x.title() if config.x else None)
)
y_title = config.y_title or (
config.labels.get(config.y)
if config.y and config.labels
else None or (config.y.title() if config.y else None)
)

# Apply layout updates for box plot
fig.update_layout(
title_text=config.title,
title_subtitle_text=config.subtitle,
xaxis_title=x_title,
yaxis_title=y_title,
height=config.height,
width=config.width,
template=config.template,
xaxis_type="log" if config.log_x else None,
yaxis_type="log" if config.log_y else None,
xaxis_range=config.range_x,
yaxis_range=config.range_y,
boxmode=config.boxmode,
)
return fig
Loading