From 392f73d0e3bf41d3e3fbbf9aa2254903f6b29321 Mon Sep 17 00:00:00 2001 From: sayalaruano Date: Wed, 10 Sep 2025 21:10:59 +0200 Subject: [PATCH 1/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor(vuecore/engin?= =?UTF-8?q?es/plotly/theming.py):=20create=20helper=20functions=20for=20co?= =?UTF-8?q?mmon=20theming=20to=20simplify=20functions=20and=20avoid=20redu?= =?UTF-8?q?ndant=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vuecore/engines/plotly/theming.py | 174 ++++++++++++++------------ 1 file changed, 91 insertions(+), 83 deletions(-) diff --git a/src/vuecore/engines/plotly/theming.py b/src/vuecore/engines/plotly/theming.py index 6243eb2..07b3214 100644 --- a/src/vuecore/engines/plotly/theming.py +++ b/src/vuecore/engines/plotly/theming.py @@ -6,6 +6,88 @@ from vuecore.schemas.basic.box import BoxConfig +def _get_axis_title(config, axis: str) -> str: + """ + Helper function to get axis title from configuration with appropriate fallbacks. + + This function attempts to retrieve an axis title using the following priority: + 1. Explicit axis title if provided in configuration + 2. Label mapping from configuration if available + 3. Title-cased column name as fallback + + Parameters + ---------- + config : Any + The configuration object containing styling and layout information. + axis : str + The axis identifier ('x' or 'y'). + + Returns + ------- + str + The appropriate title for the specified axis. + """ + axis_title_attr = f"{axis}_title" + axis_value_attr = axis + + # Use explicit title if provided + if getattr(config, axis_title_attr): + return getattr(config, axis_title_attr) + + # Use label mapping if available + if config.labels and getattr(config, axis_value_attr): + axis_value = getattr(config, axis_value_attr) + if axis_value in config.labels: + return config.labels[axis_value] + + # Fall back to title-cased column name + if getattr(config, axis_value_attr): + return getattr(config, axis_value_attr).title() + + return "" + + +def _apply_common_layout(fig: go.Figure, config) -> go.Figure: + """ + Applies common layout settings to a Plotly figure. + + This function handles the layout adjustments that are common across + different plot types, such as titles, dimensions, templates, and axis + properties. + + Parameters + ---------- + fig : go.Figure + The Plotly figure object to be styled. + config : Any + The configuration object containing all styling and layout information. + + Returns + ------- + go.Figure + The Plotly figure with common layout settings applied. + """ + x_title = _get_axis_title(config, "x") + y_title = _get_axis_title(config, "y") + + layout_updates = { + "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, + } + + fig.update_layout(**{k: v for k, v in layout_updates.items() if v is not None}) + return fig + + def apply_scatter_theme(fig: go.Figure, config: ScatterConfig) -> go.Figure: """ Applies a consistent layout and theme to a Plotly scatter plot. @@ -35,28 +117,9 @@ def apply_scatter_theme(fig: go.Figure, config: ScatterConfig) -> go.Figure: selector=dict(mode="markers"), ) - # 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.labels else None or config.x.title() - ) - y_title = config.y_title or ( - config.labels.get(config.y) if config.labels else None or config.y.title() - ) + # Apply common layout + fig = _apply_common_layout(fig, config) - # Apply layout updates for scatter 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, - ) return fig @@ -94,20 +157,9 @@ def apply_line_theme(fig: go.Figure, config: LineConfig) -> go.Figure: config.labels.get(config.y) if config.labels else None or config.y.title() ) - # Apply layout updates for line 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, - ) + # Apply common layout + fig = _apply_common_layout(fig, config) + return fig @@ -134,29 +186,9 @@ def apply_bar_theme(fig: go.Figure, config: BarConfig) -> go.Figure: # Apply trace-specific updates for bar plots fig.update_traces(opacity=config.opacity, selector=dict(type="bar")) - # 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.labels else None or config.x.title() - ) - y_title = config.y_title or ( - config.labels.get(config.y) if config.labels else None or config.y.title() - ) + # Apply common layout + fig = _apply_common_layout(fig, config) - # Apply layout updates for bar 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, - barmode=config.barmode, - ) return fig @@ -185,31 +217,7 @@ def apply_box_theme(fig: go.Figure, config: BoxConfig) -> go.Figure: 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 common layout + fig = _apply_common_layout(fig, config) - # 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 From 3f0630b790e79469d47133f40fc88d9d417c789d Mon Sep 17 00:00:00 2001 From: sayalaruano Date: Wed, 10 Sep 2025 22:06:30 +0200 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8=20Feat(vuecore/engines/plotly/plo?= =?UTF-8?q?t=5Fbuilder.py):=20Add=20build=5Fplot=20factory=20function=20to?= =?UTF-8?q?=20encapsulate=20common=20code=20to=20build=20all=20plots?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vuecore/engines/plotly/__init__.py | 3 + src/vuecore/engines/plotly/plot_builder.py | 66 ++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 src/vuecore/engines/plotly/plot_builder.py diff --git a/src/vuecore/engines/plotly/__init__.py b/src/vuecore/engines/plotly/__init__.py index eb8088e..e6ef41b 100644 --- a/src/vuecore/engines/plotly/__init__.py +++ b/src/vuecore/engines/plotly/__init__.py @@ -7,6 +7,9 @@ from .box import build as build_box from .saver import save +# Import build_utils to ensure it's available +from . import plot_builder # noqa: F401 + # Register the functions with the central dispatcher register_builder( plot_type=PlotType.SCATTER, engine=EngineType.PLOTLY, func=build_scatter diff --git a/src/vuecore/engines/plotly/plot_builder.py b/src/vuecore/engines/plotly/plot_builder.py new file mode 100644 index 0000000..e049e37 --- /dev/null +++ b/src/vuecore/engines/plotly/plot_builder.py @@ -0,0 +1,66 @@ +# vuecore/engines/plotly/plot_builder.py +from typing import Dict, Any, List, Callable +import pandas as pd +import plotly.graph_objects as go +import plotly.express as px + + +def build_plot( + data: pd.DataFrame, + config: Any, + px_function: Callable, + theming_function: Callable, + theming_params: List[str], + preprocess: Callable = None, +) -> go.Figure: + """ + Base function to build Plotly figures with common patterns. + + The function follows these steps: + 1. Get all parameters from the config model + 2. Create the dictionary of arguments for the plot function + 3. Apply preprocessing + 4. Create the base figure + 5. Apply theme and additional styling + + Parameters + ---------- + data : pd.DataFrame + The DataFrame containing the plot data. + config : Any + The Pydantic model with all plot configurations. + px_function : Callable + The Plotly Express function to use (e.g., px.bar, px.scatter, etc). + theming_function : Callable + The theming function to apply to the figure. + theming_params : List[str] + List of parameter names handled by the theming function. + preprocess : Callable, Optional + Optional preprocessing function for special features. + + Returns + ------- + go.Figure + A styled Plotly figure object. + """ + # Get all parameters from the config model + all_config_params = config.model_dump() + + # Create the dictionary of arguments for the plot function + plot_args = { + k: v + for k, v in all_config_params.items() + if k not in theming_params and v is not None + } + + # Apply preprocessing if provided + if preprocess: + data, plot_args = preprocess(data, plot_args, config) + + # Create the base figure + fig = px_function(data, **plot_args) + + # Apply theme and additional styling + fig = theming_function(fig, config) + + return fig From ed60d7e4d2caa0161b4ceebfbb226d959f388309 Mon Sep 17 00:00:00 2001 From: sayalaruano Date: Wed, 10 Sep 2025 22:38:21 +0200 Subject: [PATCH 3/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20build=20plo?= =?UTF-8?q?tly=20plot=20functions=20for=20bar,=20box,=20line,=20and=20scat?= =?UTF-8?q?ter=20plots=20by=20using=20the=20plot=5Fbuilder=20factory=20fun?= =?UTF-8?q?ction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vuecore/engines/plotly/bar.py | 59 ++++++------- src/vuecore/engines/plotly/box.py | 61 ++++++------- src/vuecore/engines/plotly/line.py | 59 ++++++------- src/vuecore/engines/plotly/scatter.py | 120 ++++++++++++++++---------- 4 files changed, 149 insertions(+), 150 deletions(-) diff --git a/src/vuecore/engines/plotly/bar.py b/src/vuecore/engines/plotly/bar.py index 5a7df31..e54b3fe 100644 --- a/src/vuecore/engines/plotly/bar.py +++ b/src/vuecore/engines/plotly/bar.py @@ -6,6 +6,24 @@ from vuecore.schemas.basic.bar import BarConfig from .theming import apply_bar_theme +from .plot_builder import build_plot + +# Define parameters handled by the theme script +THEMING_PARAMS = [ + "opacity", + "barmode", + "log_x", + "log_y", + "range_x", + "range_y", + "title", + "x_title", + "y_title", + "subtitle", + "template", + "width", + "height", +] def build(data: pd.DataFrame, config: BarConfig) -> go.Figure: @@ -31,37 +49,10 @@ def build(data: pd.DataFrame, config: BarConfig) -> go.Figure: go.Figure A `plotly.graph_objects.Figure` object representing the bar 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", - "barmode", - "log_x", - "log_y", - "range_x", - "range_y", - "title", - "x_title", - "y_title", - "subtitle", - "template", - "width", - "height", - ] - - # Create the dictionary of arguments for px.bar - 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.bar - fig = px.bar(data, **plot_args) - - # Apply theme and additional styling to the generated figure. - fig = apply_bar_theme(fig, config) - - return fig + return build_plot( + data=data, + config=config, + px_function=px.bar, + theming_function=apply_bar_theme, + theming_params=THEMING_PARAMS, + ) diff --git a/src/vuecore/engines/plotly/box.py b/src/vuecore/engines/plotly/box.py index c7b2ffe..8390213 100644 --- a/src/vuecore/engines/plotly/box.py +++ b/src/vuecore/engines/plotly/box.py @@ -6,6 +6,25 @@ from vuecore.schemas.basic.box import BoxConfig from .theming import apply_box_theme +from .plot_builder import build_plot + +# 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", +] def build(data: pd.DataFrame, config: BoxConfig) -> go.Figure: @@ -31,38 +50,10 @@ def build(data: pd.DataFrame, config: BoxConfig) -> go.Figure: 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 + return build_plot( + data=data, + config=config, + px_function=px.box, + theming_function=apply_box_theme, + theming_params=THEMING_PARAMS, + ) diff --git a/src/vuecore/engines/plotly/line.py b/src/vuecore/engines/plotly/line.py index 029d263..71e1db7 100644 --- a/src/vuecore/engines/plotly/line.py +++ b/src/vuecore/engines/plotly/line.py @@ -6,6 +6,24 @@ from vuecore.schemas.basic.line import LineConfig from .theming import apply_line_theme +from .plot_builder import build_plot + +# Define parameters handled by the theme script +THEMING_PARAMS = [ + "markers", + "log_x", + "log_y", + "range_x", + "range_y", + "line_shape", + "title", + "x_title", + "y_title", + "subtitle", + "template", + "width", + "height", +] def build(data: pd.DataFrame, config: LineConfig) -> go.Figure: @@ -31,37 +49,10 @@ def build(data: pd.DataFrame, config: LineConfig) -> go.Figure: go.Figure A `plotly.graph_objects.Figure` object representing the line 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 = [ - "markers", - "log_x", - "log_y", - "range_x", - "range_y", - "line_shape", - "title", - "x_title", - "y_title", - "subtitle", - "template", - "width", - "height", - ] - - # Create the dictionary of arguments for px.line - 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 for px.line - fig = px.line(data, **plot_args) - - # Apply theme and additional styling - fig = apply_line_theme(fig, config) - - return fig + return build_plot( + data=data, + config=config, + px_function=px.line, + theming_function=apply_line_theme, + theming_params=THEMING_PARAMS, + ) diff --git a/src/vuecore/engines/plotly/scatter.py b/src/vuecore/engines/plotly/scatter.py index 9af6bdb..ab94811 100644 --- a/src/vuecore/engines/plotly/scatter.py +++ b/src/vuecore/engines/plotly/scatter.py @@ -4,63 +4,62 @@ from vuecore.schemas.basic.scatter import ScatterConfig from vuecore.utils.statistics import get_density - from .theming import apply_scatter_theme +from .plot_builder import build_plot +# Define parameters handled by the theme script +THEMING_PARAMS = [ + "opacity", + "log_x", + "log_y", + "range_x", + "range_y", + "title", + "subtitle", + "x_title", + "y_title", + "template", + "width", + "height", + "marker_line_width", + "marker_line_color", + "color_by_density", +] -def build(data: pd.DataFrame, config: ScatterConfig) -> go.Figure: + +def scatter_preprocess(data, plot_args, config): """ - Creates a Plotly scatter plot from a DataFrame and a Pydantic configuration. + Preprocess data and arguments for scatter plots with density coloring. - This function acts as a bridge between the abstract plot definition and the - 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). + This function handles special preprocessing for scatter plots, particularly + for density-based coloring. Parameters ---------- data : pd.DataFrame The DataFrame containing the plot data. + plot_args : dict + Dictionary of arguments to be passed to the Plotly Express scatter function. config : ScatterConfig - The validated Pydantic model object with all plot configurations. + The validated Pydantic model with all scatter 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", - "log_x", - "log_y", - "range_x", - "range_y", - "title", - "subtitle", - "x_title", - "y_title", - "template", - "width", - "height", - "marker_line_width", - "marker_line_color", - "color_by_density", - ] + tuple + A tuple containing: + - data : pd.DataFrame + The original DataFrame (unchanged). + - plot_args : dict + The modified plot arguments dictionary with color settings adjusted + based on the configuration. - # Create the dictionary of arguments for px.scatter - plot_args = { - 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 + Notes + ----- + When density coloring is enabled, this function calculates density values + for the data points and uses them for color mapping, removing any discrete + color mapping that might conflict with continuous coloring. + """ + # Handle density coloring 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) @@ -73,10 +72,37 @@ def build(data: pd.DataFrame, config: ScatterConfig) -> go.Figure: # Use standard group-based coloring plot_args["color"] = config.color - # Create the base figure using only the arguments for px.scatter - fig = px.scatter(data, **plot_args) + return data, plot_args + + +def build(data: pd.DataFrame, config: ScatterConfig) -> go.Figure: + """ + 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 `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). - # Apply theme and additional styling - fig = apply_scatter_theme(fig, config) + Parameters + ---------- + data : pd.DataFrame + The DataFrame containing the plot data. + config : ScatterConfig + The validated Pydantic model object with all plot configurations. - return fig + Returns + ------- + go.Figure + A `plotly.graph_objects.Figure` object representing the scatter plot. + """ + return build_plot( + data=data, + config=config, + px_function=px.scatter, + theming_function=apply_scatter_theme, + theming_params=THEMING_PARAMS, + preprocess=scatter_preprocess, + ) From 88ca551b5d68ac7240ad80fec98e78d2a52b566a Mon Sep 17 00:00:00 2001 From: sayalaruano Date: Thu, 11 Sep 2025 09:38:15 +0200 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=90=9B=20Remove=20unused=20import=20a?= =?UTF-8?q?nd=20variables,=20suggested=20by=20ruff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vuecore/engines/plotly/plot_builder.py | 3 +-- src/vuecore/engines/plotly/theming.py | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/vuecore/engines/plotly/plot_builder.py b/src/vuecore/engines/plotly/plot_builder.py index e049e37..52e2f71 100644 --- a/src/vuecore/engines/plotly/plot_builder.py +++ b/src/vuecore/engines/plotly/plot_builder.py @@ -1,8 +1,7 @@ # vuecore/engines/plotly/plot_builder.py -from typing import Dict, Any, List, Callable +from typing import Any, List, Callable import pandas as pd import plotly.graph_objects as go -import plotly.express as px def build_plot( diff --git a/src/vuecore/engines/plotly/theming.py b/src/vuecore/engines/plotly/theming.py index 07b3214..562f88c 100644 --- a/src/vuecore/engines/plotly/theming.py +++ b/src/vuecore/engines/plotly/theming.py @@ -149,14 +149,6 @@ def apply_line_theme(fig: go.Figure, config: LineConfig) -> go.Figure: line_shape=config.line_shape, ) - # 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.labels else None or config.x.title() - ) - y_title = config.y_title or ( - config.labels.get(config.y) if config.labels else None or config.y.title() - ) - # Apply common layout fig = _apply_common_layout(fig, config) From 3e2b4938d5344b7d944576a150e0e472c36e394d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ayala=20Ruano?= <52267585+sayalaruano@users.noreply.github.com> Date: Fri, 12 Sep 2025 08:06:40 +0200 Subject: [PATCH 5/6] Style(vuecore/engines/plotly/plot_builder.py): Add Optional for preprocess variable Co-authored-by: Henry Webel --- src/vuecore/engines/plotly/plot_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vuecore/engines/plotly/plot_builder.py b/src/vuecore/engines/plotly/plot_builder.py index 52e2f71..252e01c 100644 --- a/src/vuecore/engines/plotly/plot_builder.py +++ b/src/vuecore/engines/plotly/plot_builder.py @@ -10,7 +10,7 @@ def build_plot( px_function: Callable, theming_function: Callable, theming_params: List[str], - preprocess: Callable = None, + preprocess: Optional[Callable] = None, ) -> go.Figure: """ Base function to build Plotly figures with common patterns. From 7e173c308c6e52c00eb6b7d24e2adf4e1b76febd Mon Sep 17 00:00:00 2001 From: sayalaruano Date: Fri, 12 Sep 2025 10:43:33 +0200 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=90=9B=20Fix(src/vuecore/engines/plot?= =?UTF-8?q?ly/plot=5Fbuilder.py):=20add=20Optional=20missing=20import=20an?= =?UTF-8?q?d=20add=20validation=20of=20preprocess=20to=20be=20callable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vuecore/engines/plotly/plot_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vuecore/engines/plotly/plot_builder.py b/src/vuecore/engines/plotly/plot_builder.py index 252e01c..8af074d 100644 --- a/src/vuecore/engines/plotly/plot_builder.py +++ b/src/vuecore/engines/plotly/plot_builder.py @@ -1,5 +1,5 @@ # vuecore/engines/plotly/plot_builder.py -from typing import Any, List, Callable +from typing import Any, Optional, List, Callable import pandas as pd import plotly.graph_objects as go @@ -53,7 +53,7 @@ def build_plot( } # Apply preprocessing if provided - if preprocess: + if preprocess and callable(preprocess): data, plot_args = preprocess(data, plot_args, config) # Create the base figure