From b50511b221bff317700a0a929425933510ebae3e Mon Sep 17 00:00:00 2001 From: sayalaruano Date: Tue, 12 Aug 2025 15:06:43 +0200 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=94=A5=20Remove=20schemas/distributio?= =?UTF-8?q?n=20folder=20-=20replaced=20with=20basic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vuecore/schemas/distribution/__init__.py | 0 tests/test_scatter.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 src/vuecore/schemas/distribution/__init__.py diff --git a/src/vuecore/schemas/distribution/__init__.py b/src/vuecore/schemas/distribution/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_scatter.py b/tests/test_scatter.py index e7a0114..f34763b 100644 --- a/tests/test_scatter.py +++ b/tests/test_scatter.py @@ -1,6 +1,6 @@ import pandas as pd import pytest -from vuecore.plots.distribution.scatter import create_scatter_plot +from vuecore.plots.basic.scatter import create_scatter_plot @pytest.fixture From 6a9c0f6b4c66bd68386a2af734c4835b5702add6 Mon Sep 17 00:00:00 2001 From: sayalaruano Date: Tue, 12 Aug 2025 15:58:40 +0200 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=8E=A8=20Format:=20Align=20Pydantic?= =?UTF-8?q?=20config=20with=20the=20Plotly=20API=20and=20vuecore=20line=20?= =?UTF-8?q?plot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vuecore/schemas/basic/scatter.py | 185 ++++++++++++++++++--------- 1 file changed, 124 insertions(+), 61 deletions(-) diff --git a/src/vuecore/schemas/basic/scatter.py b/src/vuecore/schemas/basic/scatter.py index 12d8d6b..4af9931 100644 --- a/src/vuecore/schemas/basic/scatter.py +++ b/src/vuecore/schemas/basic/scatter.py @@ -1,105 +1,168 @@ -from vuecore import PlotType from typing import Dict, List, Optional -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field, ConfigDict, model_validator class ScatterConfig(BaseModel): """ Pydantic model for validating and managing scatter plot configurations. - This model defines all the possible parameters that can be used to customize - a scatter plot, from data mapping to styling and layout. It ensures that - user-provided configurations are type-safe and adhere to the expected structure. + This model serves as a curated API for the most relevant parameters + for scatter plots, closely aligned with the `plotly.express.scatter` API + (https://plotly.com/python-api-reference/generated/plotly.express.scatter.html). + + This model includes the most relevant parameters for data mapping, styling, + and layout. It ensures that user-provided configurations are type-safe and + adhere to the expected structure. The plotting function handles parameters + defined here, and also accepts additional Plotly keyword arguments, + forwarding them to the appropriate `plotly.express.scatter` or + `plotly.graph_objects.Figure` call. Attributes --------- + -----Data Mapping----- x : str - Column name for the x-axis. + Column name for x-axis values. y : str - Column name for the y-axis. - type : Optional[PlotType] - The type of plot. Defaults to `SCATTER`. - group : Optional[str] - Column for grouping data, typically used for coloring markers. - size : Optional[str] - Column to determine marker size, enabling a third dimension of data. + Column name for y-axis values. + color : Optional[str] + Column to assign color to markers. symbol : Optional[str] - Column to determine the shape of markers. + Column to assign marker symbols. + size : Optional[str] + Column to determine marker size. + hover_name : Optional[str] + Column for bold text in hover tooltip. + hover_data : List[str] + Additional columns for hover tooltip. text : Optional[str] - Column for adding text labels directly onto markers. - hover_cols : List[str] - Additional data columns to display in the hover tooltip. + Column for text labels on markers. + facet_row : Optional[str] + Column for vertical facetting. + facet_col : Optional[str] + Column for horizontal facetting. + error_x : Optional[str] + Column for x-axis error bars. + error_y : Optional[str] + Column for y-axis error bars. + labels : Optional[Dict[str, str]] + Column name overrides for display. + color_discrete_map : Optional[Dict[str, str]] + Specific color mappings for color column values. + symbol_map : Optional[Dict[str, str]] + Specific symbol mappings for symbol column values. + -----Styling and Layout----- + opacity : float + Marker opacity (0-1). + size_max : int + Maximum marker size. + trendline : Optional[str] + Trendline type (ols/lowess/rolling/expanding/ewm). + trendline_options : Optional[Dict] + Advanced options for trendlines. + log_x : bool + Enable logarithmic x-axis. + log_y : bool + Enable logarithmic y-axis. + range_x : Optional[List[float]] + Manual x-axis range [min, max]. + range_y : Optional[List[float]] + Manual y-axis range [min, max]. title : str - The main title of the plot. - x_title : Optional[str] - Custom title for the x-axis. If None, defaults to the `x` column name. - y_title : Optional[str] - Custom title for the y-axis. If None, defaults to the `y` column name. - height : int - Height of the plot in pixels. + Main plot title. + subtitle : Optional[str] + Plot subtitle. + template : str + Plotly visual theme/template. width : int - Width of the plot in pixels. - colors : Optional[Dict[str, str]] - A dictionary mapping group names from the `group` column to specific colors. - trendline : Optional[str] - If specified, adds a trendline to the plot (e.g., 'ols', 'lowess'). + Plot width in pixels. + height : int + Plot height in pixels. + color_by_density : bool + Color points by density instead of category. + marker_line_width : float + Width of marker border lines. + marker_line_color : str + Color of marker border lines. """ + # General Configuration + # Allow extra parameters to pass through to Plotly + model_config = ConfigDict(extra="allow") + # Data mapping - x: str = Field(..., description="Column name for the x-axis.") - y: str = Field(..., description="Column name for the y-axis.") - type: Optional[PlotType] = Field( - PlotType.SCATTER, description="Type of plot, defaults to SCATTER." + x: str = Field(..., description="Column for x-axis values.") + y: str = Field(..., description="Column for y-axis values.") + color: Optional[str] = Field( + None, description="Column for color assignment (replaces 'group')." ) - group: Optional[str] = Field( - None, description="Column for grouping data, often used for color." + symbol: Optional[str] = Field( + None, description="Column for marker symbol assignment." ) size: Optional[str] = Field(None, description="Column to determine marker size.") - symbol: Optional[str] = Field( - None, description="Column to determine marker symbol." + hover_name: Optional[str] = Field( + None, description="Column for bold text in hover tooltip." + ) + hover_data: List[str] = Field( + [], description="Additional columns for hover tooltip." ) text: Optional[str] = Field(None, description="Column for text labels on markers.") - hover_cols: List[str] = Field( - [], description="Additional columns to show on hover." + facet_row: Optional[str] = Field(None, description="Column for vertical facetting.") + facet_col: Optional[str] = Field( + None, description="Column for horizontal facetting." + ) + error_x: Optional[str] = Field(None, description="Column for x-axis error bars.") + error_y: Optional[str] = Field(None, description="Column for y-axis error bars.") + labels: Optional[Dict[str, str]] = Field( + None, description="Column name overrides for display purposes." + ) + color_discrete_map: Optional[Dict[str, str]] = Field( + None, description="Specific color mappings for color column values." + ) + symbol_map: Optional[Dict[str, str]] = Field( + None, description="Specific symbol mappings for symbol column values." ) # Styling and Layout - title: str = Field("Scatter Plot", description="The main title of the plot.") - x_title: Optional[str] = Field( - None, description="Title for the x-axis. Defaults to x column name." + opacity: float = Field(0.8, ge=0, le=1, description="Overall opacity of markers.") + size_max: int = Field(20, description="Maximum size for markers.") + trendline: Optional[str] = Field( + None, description="Trendline type (ols/lowess/rolling/expanding/ewm)." + ) + trendline_options: Optional[Dict] = Field( + None, description="Advanced options for trendline configuration." ) - y_title: Optional[str] = Field( - None, description="Title for the y-axis. Defaults to y column name." + log_x: bool = Field(False, description="Enable logarithmic x-axis scale.") + log_y: bool = Field(False, description="Enable logarithmic y-axis scale.") + range_x: Optional[List[float]] = Field( + None, description="Manual x-axis range [min, max]." ) - height: int = Field(600, description="Height of the plot in pixels.") - width: int = Field(800, description="Width of the plot in pixels.") - colors: Optional[Dict[str, str]] = Field( - None, description="Mapping of group names to specific colors." + range_y: Optional[List[float]] = Field( + None, description="Manual y-axis range [min, max]." ) - marker_opacity: float = Field( - 0.8, ge=0, le=1, description="Opacity of the markers." + title: str = Field("Scatter Plot", description="Main title of the plot.") + subtitle: Optional[str] = Field( + None, description="Subtitle displayed below main title." ) + template: str = Field("plotly_white", description="Plotly visual theme/template.") + width: int = Field(800, description="Plot width in pixels.") + height: int = Field(600, description="Plot height in pixels.") marker_line_width: float = Field( - 0.5, ge=0, description="Width of the line surrounding each marker." + 0.5, ge=0, description="Width of marker border lines." ) marker_line_color: str = Field( - "DarkSlateGrey", description="Color of the line surrounding each marker." + "DarkSlateGrey", description="Color of marker border lines." ) # Special features - trendline: Optional[str] = Field( - None, description="Adds a trendline. E.g., 'ols' for Ordinary Least Squares." - ) color_by_density: bool = Field( - False, description="If True, color points by density instead of group." + False, description="Color points by density instead of category." ) @model_validator(mode="after") - def check_exclusive_coloring(self) -> "ScatterConfig": - """Ensure that coloring by group and by density are mutually exclusive.""" - if self.color_by_density and self.group: + def validate_exclusive_color_options(self) -> "ScatterConfig": + if self.color_by_density and self.color: raise ValueError( - "Cannot set 'group' when 'color_by_density' is True. " - "Coloring is mutually exclusive." + "Cannot use both 'color' and 'color_by_density'. " + "These options are mutually exclusive." ) return self From 454280b0df4ebe004ae14adcbbb8440fe007bbfb Mon Sep 17 00:00:00 2001 From: sayalaruano Date: Tue, 12 Aug 2025 17:44:28 +0200 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=8E=A8=20Format:=20update=20build,=20?= =?UTF-8?q?theming,=20and,=20create=5Fscatter=5Fplot=20to=20align=20with?= =?UTF-8?q?=20the=20Plotly=20API=20and=20the=20line=20plot=20from=20vuecor?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vuecore/engines/plotly/scatter.py | 54 ++++++++++++++++++++------- src/vuecore/engines/plotly/theming.py | 43 ++++++++++++++++++--- src/vuecore/plots/basic/scatter.py | 37 +++++------------- 3 files changed, 86 insertions(+), 48 deletions(-) diff --git a/src/vuecore/engines/plotly/scatter.py b/src/vuecore/engines/plotly/scatter.py index e90c36d..a0f4972 100644 --- a/src/vuecore/engines/plotly/scatter.py +++ b/src/vuecore/engines/plotly/scatter.py @@ -10,46 +10,72 @@ def build(data: pd.DataFrame, config: ScatterConfig) -> go.Figure: """ - Creates a Plotly scatter plot figure from a DataFrame and configuration. + Creates a Plotly scatter plot 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 configuration - into the arguments for `plotly.express.scatter`. + Plotly Express implementation. It translates the validated `ScattereConfig` + into the arguments for `plotly.express.scatter` 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.scatter.html). Parameters ---------- data : pd.DataFrame The DataFrame containing the plot data. config : ScatterConfig - The validated Pydantic model object containing all plot configurations. + The validated Pydantic model object with all plot configurations. Returns ------- go.Figure A `plotly.graph_objects.Figure` object representing the scatter 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 = [ + "opacity", + "size_max", + "log_x", + "log_y", + "range_x", + "range_y", + "title", + "subtitle", + "template", + "width", + "height", + "marker_line_width", + "marker_line_color", + "color_by_density", + ] + + # Create the dictionary of arguments for px.scatter 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, + k: v + for k, v in all_config_params.items() + if k not in theming_params and v is not None } + # Handle density coloring separately 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 + + # Remove discrete color mapping for density plots + if "color_discrete_map" in plot_args: + del plot_args["color_discrete_map"] else: # Use standard group-based coloring - plot_args["color"] = config.group - plot_args["color_discrete_map"] = config.colors + plot_args["color"] = config.color + # Create the base figure using only the arguments for px.scatter fig = px.scatter(data, **plot_args) - # Apply theme + # Apply theme and additional styling fig = apply_scatter_theme(fig, config) return fig diff --git a/src/vuecore/engines/plotly/theming.py b/src/vuecore/engines/plotly/theming.py index 86a0f8c..52cc859 100644 --- a/src/vuecore/engines/plotly/theming.py +++ b/src/vuecore/engines/plotly/theming.py @@ -5,32 +5,63 @@ def apply_scatter_theme(fig: go.Figure, config: ScatterConfig) -> go.Figure: """ - Applies a consistent layout and theme to a Plotly figure. + Applies a consistent layout and theme to a Plotly scatter plot. - 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. + 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 : ScatterConfig - The configuration object containing styling info like titles and dimensions. + The configuration object containing all styling and layout info. Returns ------- go.Figure The styled Plotly figure object. """ + # Apply trace-specific updates fig.update_traces( marker=dict( - opacity=config.marker_opacity, + opacity=config.opacity, line=dict(width=config.marker_line_width, color=config.marker_line_color), + size_max=config.size_max, ), selector=dict(mode="markers"), ) + # Use the labels dictionary to set axis titles, falling back to column names + x_title = ( + config.labels.get(config.x, config.x.title()) + if config.labels + else config.x.title() + ) + y_title = ( + config.labels.get(config.y, config.y.title()) + if config.labels + else config.y.title() + ) + + # Apply layout updates + 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 "linear", + yaxis_type="log" if config.log_y else "linear", + xaxis_range=config.range_x, + yaxis_range=config.range_y, + # legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1), + hovermode="closest", + ) + fig.update_layout( title_text=config.title, xaxis_title=config.x_title or config.x.title(), diff --git a/src/vuecore/plots/basic/scatter.py b/src/vuecore/plots/basic/scatter.py index 0c691f6..361b5ff 100644 --- a/src/vuecore/plots/basic/scatter.py +++ b/src/vuecore/plots/basic/scatter.py @@ -41,38 +41,19 @@ def create_scatter_plot( Raises ------ pydantic.ValidationError - If the provided kwargs do not match the `ScatterConfig` schema. + If the provided keyword arguments do not conform to the `ScatterConfig` schema, + e.g., a required parameter is missing or a value has an incorrect type. ValueError - If columns specified in the configuration do not exist in the DataFrame. + Raised by the plotting engine if a column specified in the configuration is not + found in the provided DataFrame. Examples -------- - >>> import pandas as pd - >>> sample_df = pd.DataFrame({ - ... 'gene_expression': [1.2, 2.5, 3.1, 4.5, 5.2, 6.8], - ... 'log_p_value': [0.5, 1.5, 2.0, 3.5, 4.0, 5.5], - ... 'regulation': ['Up', 'Up', 'None', 'Down', 'Down', 'Down'], - ... 'significance_score': [10, 20, 5, 40, 55, 80], - ... 'gene_name': ['GENE_A', 'GENE_B', 'GENE_C', 'GENE_D', 'GENE_E', 'GENE_F'] - ... }) - >>> - >>> # Create a simple scatter plot and save it to HTML - >>> fig = create_scatter_plot( - ... data=sample_df, - ... x='gene_expression', - ... y='log_p_value', - ... group='regulation', - ... size='significance_score', - ... text='gene_name', - ... title="Gene Expression vs. Significance", - ... x_title="Log2 Fold Change", - ... y_title="-Log10(P-value)", - ... colors={'Up': '#d62728', 'Down': '#1f77b4', 'None': '#7f7f7f'}, - ... file_path="my_scatter_plot.html" - ... ) - >>> - >>> # The returned `fig` object can be displayed in a notebook or further modified - >>> # fig.show() + For detailed examples and usage, please refer to the documentation: + + * **Jupyter Notebook:** `docs/api_examples/scatter_plot.ipynb` - + https://vuecore.readthedocs.io/en/latest/api_examples/scatter_plot.html + * **Python Script:** `docs/api_examples/scatter_plot.py` """ # 1. Validate configuration using Pydantic config = ScatterConfig(**kwargs) From c6929db5dd975de81ca19e882d8ebb37fbe86fea Mon Sep 17 00:00:00 2001 From: sayalaruano Date: Tue, 12 Aug 2025 18:44:05 +0200 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=8E=A8=20Format:=20update=20scatter?= =?UTF-8?q?=20plot=20epi=20example=20with=20the=20new=20changes=20on=20the?= =?UTF-8?q?=20other=20scripts,=20and=20also=20addx=5Ftitle=20and=20y=5Ftit?= =?UTF-8?q?le=20fields=20on=20he=20Pydantic=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api_examples/scatter_plot.ipynb | 21 +++++++++++---------- docs/api_examples/scatter_plot.py | 7 ++++--- src/vuecore/engines/plotly/scatter.py | 3 ++- src/vuecore/engines/plotly/theming.py | 26 +++++--------------------- src/vuecore/schemas/basic/scatter.py | 12 +++++++++--- 5 files changed, 31 insertions(+), 38 deletions(-) diff --git a/docs/api_examples/scatter_plot.ipynb b/docs/api_examples/scatter_plot.ipynb index bc8f871..345e4e0 100644 --- a/docs/api_examples/scatter_plot.ipynb +++ b/docs/api_examples/scatter_plot.ipynb @@ -483,7 +483,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "[VueCore] Plot saved to ./outputs/scatter_basic.pdf\n" + "[VueCore] Plot saved to ./outputs/scatter_basic.png\n" ] }, { @@ -4381,9 +4381,9 @@ { "data": { "text/html": [ - "