From 55ae9e88817daabea770ee9ea534731e56693187 Mon Sep 17 00:00:00 2001 From: Alex Fernandez Luces Date: Mon, 27 Oct 2025 16:11:56 +0100 Subject: [PATCH 1/5] feat(plotly): Add plotly dash as backend --- .../backends/plotly/plotly_dash.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/ansys/tools/visualization_interface/backends/plotly/plotly_dash.py diff --git a/src/ansys/tools/visualization_interface/backends/plotly/plotly_dash.py b/src/ansys/tools/visualization_interface/backends/plotly/plotly_dash.py new file mode 100644 index 00000000..18b7433b --- /dev/null +++ b/src/ansys/tools/visualization_interface/backends/plotly/plotly_dash.py @@ -0,0 +1,47 @@ +# Copyright (C) 2024 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Module for dash plotly.""" +from dash import Dash, dcc, html + +from ansys.tools.visualization_interface.backends.plotly.plotly_interface import PlotlyBackend + + +class PlotlyDashBackend(PlotlyBackend): + """Plotly Dash interface for visualization.""" + + def __init__(self, app: Dash = None) -> None: + """Initialize the Plotly Dash backend. + + Parameters + ---------- + app : Dash + The Dash application instance. + """ + super().__init__() + self._app = app or Dash(__name__) + + def show(self) -> None: + """Render the Plotly figure in a Dash application.""" + self._app.layout = html.Div([ + dcc.Graph(figure=self._fig) + ]) + self._app.run() From bb687e74ec449ef8aa2a2528816913d0be860fb2 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:19:29 +0000 Subject: [PATCH 2/5] chore: adding changelog file 411.miscellaneous.md [dependabot-skip] --- doc/changelog.d/411.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/411.miscellaneous.md diff --git a/doc/changelog.d/411.miscellaneous.md b/doc/changelog.d/411.miscellaneous.md new file mode 100644 index 00000000..9ecee722 --- /dev/null +++ b/doc/changelog.d/411.miscellaneous.md @@ -0,0 +1 @@ +Feat(plotly): Add plotly dash as backend From 918ff356800da4f08c64cb6e05191a6c6a943676 Mon Sep 17 00:00:00 2001 From: Alex Fernandez Luces Date: Mon, 27 Oct 2025 16:19:53 +0100 Subject: [PATCH 3/5] fix: Show method --- .../backends/plotly/plotly_dash.py | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/src/ansys/tools/visualization_interface/backends/plotly/plotly_dash.py b/src/ansys/tools/visualization_interface/backends/plotly/plotly_dash.py index 18b7433b..d68a811c 100644 --- a/src/ansys/tools/visualization_interface/backends/plotly/plotly_dash.py +++ b/src/ansys/tools/visualization_interface/backends/plotly/plotly_dash.py @@ -20,10 +20,15 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for dash plotly.""" +from typing import TYPE_CHECKING, Union + from dash import Dash, dcc, html from ansys.tools.visualization_interface.backends.plotly.plotly_interface import PlotlyBackend +if TYPE_CHECKING: + import plotly.graph_objects as go + class PlotlyDashBackend(PlotlyBackend): """Plotly Dash interface for visualization.""" @@ -39,9 +44,46 @@ def __init__(self, app: Dash = None) -> None: super().__init__() self._app = app or Dash(__name__) - def show(self) -> None: - """Render the Plotly figure in a Dash application.""" - self._app.layout = html.Div([ - dcc.Graph(figure=self._fig) - ]) - self._app.run() + def show(self, + plottable_object=None, + screenshot: str = None, + name_filter=None, + **kwargs) -> Union["go.Figure", None]: + """Render the Plotly scene. + + Parameters + ---------- + plottable_object : Any, optional + Object to show, by default None. + screenshot : str, optional + Path to save a screenshot, by default None. + name_filter : bool, optional + Flag to filter the object, by default None. + kwargs : dict + Additional options the selected backend accepts. + + Returns + ------- + Union[go.Figure, None] + The figure of the plot if in doc building environment. Else, None. + """ + import os + if os.environ.get("PYANSYS_VISUALIZER_DOC_MODE"): + return self._fig + + if plottable_object is not None: + self.plot(plottable_object) + + # Only show in browser if no screenshot is being taken + if not screenshot: + self._app.layout = html.Div([ + dcc.Graph(figure=self._fig) + ]) + self._app.run() + + else: + screenshot_str = str(screenshot) + if screenshot_str.endswith('.html'): + self._fig.write_html(screenshot_str) + else: + self._fig.write_image(screenshot_str) From bd6b01de73effbeac82ff4b96391fe33492d1118 Mon Sep 17 00:00:00 2001 From: Alex Fernandez Luces Date: Tue, 28 Oct 2025 13:27:21 +0100 Subject: [PATCH 4/5] feat: Add mesh hiding dropdown --- .../01-basic-plotly-examples/dash-usage.py | 57 +++++++ .../backends/plotly/plotly_dash.py | 144 +++++++++++++++++- .../plotly/widgets/dropdown_manager.py | 89 +++++++++++ 3 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 examples/01-basic-plotly-examples/dash-usage.py create mode 100644 src/ansys/tools/visualization_interface/backends/plotly/widgets/dropdown_manager.py diff --git a/examples/01-basic-plotly-examples/dash-usage.py b/examples/01-basic-plotly-examples/dash-usage.py new file mode 100644 index 00000000..f5d1ea41 --- /dev/null +++ b/examples/01-basic-plotly-examples/dash-usage.py @@ -0,0 +1,57 @@ +# Copyright (C) 2024 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +""" +.. _ref_plain_usage_dash: + +====================================== +Plain usage of the plotly dash backend +====================================== + +This example shows the plain usage of the Plotly Dash backend in the Visualization Interface Tool to plot different objects, +including PyVista meshes, custom objects, and Plotly-specific objects. +""" + +from ansys.tools.visualization_interface.backends.plotly.plotly_dash import PlotlyDashBackend +from ansys.tools.visualization_interface.types.mesh_object_plot import MeshObjectPlot +from ansys.tools.visualization_interface import Plotter +import pyvista as pv +from plotly.graph_objects import Mesh3d + + +# Create a plotter with the Plotly backend +pl = Plotter(backend=PlotlyDashBackend()) + +# Create a PyVista mesh +mesh = pv.Sphere() +mesh2 = pv.Cube(center=(2,0,0)) +# Plot the mesh +pl.plot(mesh, name="Sphere") +pl.plot(mesh2, name="Cube") + +# ---------------------------------- +# Start the server and show the plot +# ---------------------------------- +# +# .. code-block:: python +# +# pl.show() + diff --git a/src/ansys/tools/visualization_interface/backends/plotly/plotly_dash.py b/src/ansys/tools/visualization_interface/backends/plotly/plotly_dash.py index d68a811c..922a1d50 100644 --- a/src/ansys/tools/visualization_interface/backends/plotly/plotly_dash.py +++ b/src/ansys/tools/visualization_interface/backends/plotly/plotly_dash.py @@ -22,9 +22,10 @@ """Module for dash plotly.""" from typing import TYPE_CHECKING, Union -from dash import Dash, dcc, html +from dash import Dash, Input, Output, dcc, html from ansys.tools.visualization_interface.backends.plotly.plotly_interface import PlotlyBackend +from ansys.tools.visualization_interface.backends.plotly.widgets.dropdown_manager import DashDropdownManager if TYPE_CHECKING: import plotly.graph_objects as go @@ -43,6 +44,142 @@ def __init__(self, app: Dash = None) -> None: """ super().__init__() self._app = app or Dash(__name__) + self._dropdown_manager = DashDropdownManager(self._fig) + self._setup_callbacks() + + @property + def dropdown_manager(self) -> DashDropdownManager: + """Get the dropdown manager for this backend. + + Returns + ------- + DashDropdownManager + The dropdown manager instance. + """ + return self._dropdown_manager + + def plot(self, plottable_object, name: str = None, **plotting_options) -> None: + """Plot a single object using Plotly and track mesh names for dropdown. + + Parameters + ---------- + plottable_object : Any + The object to plot. + name : str, optional + Name of the mesh for labeling in Plotly. + plotting_options : dict + Additional plotting options. + """ + # Call parent plot method + super().plot(plottable_object, name=name, **plotting_options) + + # Track mesh names for dropdown functionality + if name: + self._dropdown_manager.add_mesh_name(name) + else: + # Try to get name from the latest trace added + if self._fig.data: + latest_trace = self._fig.data[-1] + trace_name = getattr(latest_trace, 'name', None) + if trace_name: + self._dropdown_manager.add_mesh_name(trace_name) + + def _setup_callbacks(self) -> None: + """Setup Dash callbacks for mesh visibility control.""" + # Store reference to self for use in callback + backend_instance = self + + @self._app.callback( + Output('mesh-graph', 'figure'), + Input('mesh-visibility-dropdown', 'value'), + prevent_initial_call=True + ) + def update_mesh_visibility(hidden_meshes): + """Update mesh visibility based on dropdown selection. + + Parameters + ---------- + hidden_meshes : List[str] + List of mesh names to hide. + + Returns + ------- + go.Figure + Updated figure with modified mesh visibility. + """ + if hidden_meshes is None: + hidden_meshes = [] + + # Get all mesh names + all_mesh_names = backend_instance.dropdown_manager.get_mesh_names() + visible_mesh_names = [name for name in all_mesh_names if name not in hidden_meshes] + + # Create a copy of the figure to avoid modifying the original + import plotly.graph_objects as go + updated_fig = go.Figure(backend_instance._fig) + + # Update visibility for each trace + for i, trace in enumerate(updated_fig.data): + trace_name = getattr(trace, 'name', None) + is_visible = trace_name in visible_mesh_names + updated_fig.data[i].visible = is_visible + + return updated_fig + + def create_dash_layout(self) -> html.Div: + """Create the Dash layout with optional dropdown for mesh visibility. + + Returns + ------- + html.Div + The Dash layout component. + """ + components = [] + + if self.dropdown_manager.get_mesh_names(): + # Add dropdown for mesh visibility control + mesh_names = self.dropdown_manager.get_mesh_names() + components.append( + dcc.Dropdown( + id='mesh-visibility-dropdown', + options=[{'label': name, 'value': name} for name in mesh_names], + multi=True, + placeholder="Select meshes to hide", + searchable=True, + style={ + 'width': '280px', + 'fontSize': '14px' + } + ) + + ) + + # Add the main graph + components.append(dcc.Graph( + id='mesh-graph', + figure=self._fig, + style={ + 'height': '100vh', + 'width': '100%', + 'margin': '0', + 'padding': '0' + }, + config={ + 'responsive': True, + 'displayModeBar': True, + 'displaylogo': False + } + )) + + return html.Div(components, style={ + 'fontFamily': '"Open Sans", verdana, arial, sans-serif', + 'backgroundColor': '#ffffff', + 'minHeight': '100vh', + 'width': '100%', + 'margin': '0', + 'padding': '0', + 'position': 'relative' + }) def show(self, plottable_object=None, @@ -76,9 +213,8 @@ def show(self, # Only show in browser if no screenshot is being taken if not screenshot: - self._app.layout = html.Div([ - dcc.Graph(figure=self._fig) - ]) + # Always use the create_dash_layout method to ensure dropdown is included when enabled + self._app.layout = self.create_dash_layout() self._app.run() else: diff --git a/src/ansys/tools/visualization_interface/backends/plotly/widgets/dropdown_manager.py b/src/ansys/tools/visualization_interface/backends/plotly/widgets/dropdown_manager.py new file mode 100644 index 00000000..808dac4f --- /dev/null +++ b/src/ansys/tools/visualization_interface/backends/plotly/widgets/dropdown_manager.py @@ -0,0 +1,89 @@ +# Copyright (C) 2024 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Module for dropdown management in Plotly figures.""" +from typing import Any, Dict, List + +import plotly.graph_objects as go + + +class DashDropdownManager: + """Class to manage dropdown menus in a Plotly figure. + + This class allows adding dropdown menus to a Plotly figure for controlling + mesh visibility and other properties. + + Parameters + ---------- + fig : go.Figure + The Plotly figure to which dropdowns will be added. + """ + + def __init__(self, fig: go.Figure): + """Initialize DropdownManager.""" + self._fig = fig + self._mesh_names = [] + + def add_mesh_name(self, name: str) -> None: + """Add a mesh name to track for dropdown functionality. + + Parameters + ---------- + name : str + The name of the mesh to track. + """ + if name and name not in self._mesh_names: + self._mesh_names.append(name) + + def get_mesh_names(self) -> List[str]: + """Get the list of tracked mesh names. + + Returns + ------- + List[str] + List of mesh names. + """ + return self._mesh_names.copy() + + def get_visibility_args_for_meshes(self, visible_mesh_names: List[str]) -> Dict[str, Any]: + """Get visibility arguments for showing only specified meshes. + + Parameters + ---------- + visible_mesh_names : List[str] + List of mesh names that should be visible. + + Returns + ------- + Dict[str, Any] + Arguments for restyle method to set mesh visibility. + """ + visibility = [] + for trace in self._fig.data: + trace_name = getattr(trace, 'name', None) + is_visible = trace_name in visible_mesh_names + visibility.append(is_visible) + + return {"visible": visibility} + + def clear(self) -> None: + """Clear all tracked mesh names.""" + self._mesh_names.clear() From 0b1ee293187f93d06b3e218a43c24da9ebc241b7 Mon Sep 17 00:00:00 2001 From: Alex Fernandez Luces Date: Wed, 29 Oct 2025 10:44:42 +0100 Subject: [PATCH 5/5] fix: Update dependencies --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index a41173f0..9ba8fbf6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,11 +42,16 @@ plotly = [ "kaleido >= 1.1.0,<2", ] +dash = [ + "dash >= 3.2.0,<4" +] + all = [ "pyside6 >= 6.8.0,<7", "pyvistaqt >= 0.11.1,<1", "plotly >= 6.3.1,<7", "kaleido >= 1.1.0,<2", + "dash >= 3.2.0,<4" ] tests = [