Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fc43949
♻️ Add initial version of project refactoring, using scatter plot as …
sayalaruano Jun 13, 2025
9b54fc8
♻️ Refactor code to support various engines and handle this logic wit…
sayalaruano Jun 16, 2025
7d27bc8
🎨 Add enums for plot and engine types
sayalaruano Jun 16, 2025
dd49d7b
✅ Add tests for scatter plot functions
sayalaruano Jun 16, 2025
4b5b43d
📝 Add docstrings and enums in the plotly/__init__.py file
sayalaruano Jun 16, 2025
1ec04f2
🐛 Fix ruff errors during CI
sayalaruano Jun 16, 2025
d2adca7
Merge branch 'main' into refactor-scatter
sayalaruano Jun 23, 2025
58e96db
📝 Add notebook to show scatter plot examples, and remove validation t…
sayalaruano Jun 23, 2025
155c76e
🐛 Fix circular import bug, add try-except block to install chrmore to…
sayalaruano Jun 24, 2025
fffa849
➖ Remove unused dependency
sayalaruano Jun 24, 2025
4486119
✏️ Add all expected report formats on the value-error message
sayalaruano Jun 24, 2025
95bb54e
💚 Add readthedocs dependencies to download chrome, which is required …
sayalaruano Jun 25, 2025
f68a35a
✏️ Correct identation on apt dependencies for readthedocs.yaml
sayalaruano Jun 25, 2025
03eab2d
📝 Add api_example/scatter into docs, update notebook, and create pyth…
sayalaruano Jun 25, 2025
c96d9a9
🐛 Fix indentation fro apt packages in readthedocs.yaml and imporitng …
sayalaruano Jun 25, 2025
390551a
🐛 Fix import bug in scatter python script
sayalaruano Jun 25, 2025
16d6827
🐛 Fix bug on scatter python script
sayalaruano Jun 25, 2025
23e7252
📝 Update scatter notebook header
sayalaruano Jun 25, 2025
bd7e1ad
📝 Modify header of scatter notebook and add title to basic plot
sayalaruano Jun 25, 2025
275db81
📝 Show df on the scatter notebook
sayalaruano Jun 25, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# Tests
test_results/
3 changes: 3 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ reference
# builds
_build
jupyter_execute

# VsCode
*.DS_Store
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies = [
"webweb",
"acore",
"dash-cytoscape",
"pydantic",
]

[project.optional-dependencies]
Expand Down
4 changes: 4 additions & 0 deletions src/vuecore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
import numpy as np
import pandas as pd

from .constants import PlotType, EngineType

__all__ = ["PlotType", "EngineType"]

plt.rcParams["figure.figsize"] = [4.0, 3.0]
plt.rcParams["pdf.fonttype"] = 42
plt.rcParams["ps.fonttype"] = 42
Expand Down
17 changes: 17 additions & 0 deletions src/vuecore/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from enum import auto

try:
from enum import StrEnum
except ImportError:
from strenum import StrEnum


class PlotType(StrEnum):
SCATTER = auto()
LINE = auto()
# Add other plot types as needed


class EngineType(StrEnum):
PLOTLY = auto()
# Add other engines as needed
113 changes: 113 additions & 0 deletions src/vuecore/engines/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from typing import Callable
from vuecore import PlotType, EngineType

# Import the engine modules to trigger their registration
from . import plotly # noqa: F401, E402

# from . import matplotlib # This is where you'd add a new engine

# Registries to hold the functions from each backend
PLOT_BUILDERS = {}
PLOT_SAVERS = {}


def register_builder(plot_type: PlotType, engine: EngineType, func: Callable):
"""
Registers a plot builder function for a given plot type and engine.

This allows dynamic dispatch of plotting functions depending on the desired
plot type (e.g., scatter, histogram) and backend engine (e.g., Plotly, Matplotlib).

Parameters
----------
plot_type : PlotType
The type of plot (e.g., SCATTER).
engine : EngineType
The rendering engine (e.g., PLOTLY).
func : Callable
The plotting function to register for this type and engine.

Returns
-------
None
"""
if engine not in PLOT_BUILDERS:
PLOT_BUILDERS[engine] = {}
PLOT_BUILDERS[engine][plot_type] = func


def register_saver(engine: EngineType, func: Callable):
"""
Registers a save function for a given engine.

This allows saving plots using engine-specific logic (e.g., Plotly's `write_image`,
Matplotlib's `savefig`, etc.).

Parameters
----------
engine : EngineType
The rendering engine for which to register the saver function.
func : Callable
The saving function to use for this engine.

Returns
-------
None
"""
PLOT_SAVERS[engine] = func


def get_builder(plot_type: PlotType, engine: EngineType) -> Callable:
"""
Retrieves a plot builder function from the registry.

Looks up the plotting function based on the specified plot type and engine.

Parameters
----------
plot_type : PlotType
The type of plot to retrieve.
engine : EngineType
The engine used to render the plot.

Returns
-------
Callable
The registered plotting function.

Raises
------
ValueError
If no function is found for the given plot type and engine.
"""
try:
return PLOT_BUILDERS[engine][plot_type]
except KeyError:
raise ValueError(f"No '{plot_type}' builder found for engine '{engine}'")


def get_saver(engine: EngineType) -> Callable:
"""
Retrieves a save function from the registry.

Returns the function used to save plots for the specified engine.

Parameters
----------
engine : EngineType
The engine for which the saving function should be retrieved.

Returns
-------
Callable
The registered saving function.

Raises
------
ValueError
If no saver function is registered for the given engine.
"""
try:
return PLOT_SAVERS[engine]
except KeyError:
raise ValueError(f"No saver found for engine '{engine}'")
11 changes: 11 additions & 0 deletions src/vuecore/engines/plotly/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from vuecore.engines import register_builder, register_saver
from vuecore import PlotType, EngineType

from .scatter import build as build_scatter
from .saver import save

# Register the functions with the central dispatcher
register_builder(
plot_type=PlotType.SCATTER, engine=EngineType.PLOTLY, func=build_scatter
)
register_saver(engine=EngineType.PLOTLY, func=save)
56 changes: 56 additions & 0 deletions src/vuecore/engines/plotly/saver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import plotly.graph_objects as go
from pathlib import Path


def save(fig: go.Figure, filepath: str) -> None:
"""
Saves a Plotly figure to a file, inferring the format from the extension.

This utility provides a single interface for exporting a figure to various
static and interactive formats.

Parameters
----------
fig : go.Figure
The Plotly figure object to save.
filepath : str
The destination path for the file (e.g., 'my_plot.png', 'figure.html').
The format is determined by the file extension.

Returns
-------
None

Raises
------
ValueError
If the file extension is not one of the supported formats.
ImportError
If required libraries for image export (e.g., kaleido) are not installed.

Examples
--------
>>> import plotly.express as px
>>> fig = px.scatter(x=[1, 2, 3], y=[1, 2, 3])
>>> # Save as an interactive HTML file
>>> save(fig, 'scatter.html')
Plot saved to scatter.html
>>> # Save as a static PNG image
>>> save(fig, 'scatter.png')
Plot saved to scatter.png
"""
path = Path(filepath)
suffix = path.suffix.lower()

if suffix in [".png", ".jpg", ".jpeg", ".webp", ".svg", ".pdf"]:
fig.write_image(filepath)
elif suffix == ".html":
fig.write_html(filepath, include_plotlyjs="cdn")
elif suffix == ".json":
fig.write_json(filepath)
else:
raise ValueError(
f"Unsupported file format: '{suffix}'. Supported formats: .png, .svg, .pdf, .html, .json"
)

print(f"Plot saved to {filepath}")
53 changes: 53 additions & 0 deletions src/vuecore/engines/plotly/scatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from vuecore.schemas.distribution.scatter import ScatterConfig
from vuecore.utils.statistics import get_density
from .theming import apply_scatter_theme


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

This function acts as a bridge between the abstract plot definition and the
Plotly Express implementation. It translates the validated configuration
into the arguments for `plotly.express.scatter`.

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

Returns
-------
go.Figure
A `plotly.graph_objects.Figure` object representing the scatter plot.
"""
plot_args = {
"x": config.x,
"y": config.y,
"size": config.size,
"symbol": config.symbol,
"text": config.text,
"hover_data": config.hover_cols,
"trendline": config.trendline,
}

if config.color_by_density:
# Calculate density and pass it to the 'color' argument
density_values = get_density(data[config.x].values, data[config.y].values)
plot_args["color"] = density_values
else:
# Use standard group-based coloring
plot_args["color"] = config.group
plot_args["color_discrete_map"] = config.colors

fig = px.scatter(data, **plot_args)

# Apply theme
fig = apply_scatter_theme(fig, config)

return fig
43 changes: 43 additions & 0 deletions src/vuecore/engines/plotly/theming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import plotly.graph_objects as go
from vuecore.schemas.distribution.scatter import ScatterConfig


def apply_scatter_theme(fig: go.Figure, config: ScatterConfig) -> go.Figure:
"""
Applies a consistent layout and theme to a Plotly figure.

This function separates styling from plot creation, allowing for a consistent
look and feel across different plot types. It updates traces and layout
properties based on the provided configuration.

Parameters
----------
fig : go.Figure
The Plotly figure object to be styled.
config : ScatterConfig
The configuration object containing styling info like titles and dimensions.

Returns
-------
go.Figure
The styled Plotly figure object.
"""
fig.update_traces(
marker=dict(
opacity=config.marker_opacity,
line=dict(width=config.marker_line_width, color=config.marker_line_color),
),
selector=dict(mode="markers"),
)

fig.update_layout(
title_text=config.title,
xaxis_title=config.x_title or config.x.title(),
yaxis_title=config.y_title or config.y.title(),
height=config.height,
width=config.width,
template="plotly_white",
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
hovermode="closest",
)
return fig
Loading