Skip to content
Merged
1 change: 1 addition & 0 deletions doc/changelog.d/411.miscellaneous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Feat(plotly): Add plotly dash as backend
57 changes: 57 additions & 0 deletions examples/01-basic-plotly-examples/dash-usage.py
Original file line number Diff line number Diff line change
@@ -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()

5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# 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 typing import TYPE_CHECKING, Union

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


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__)
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,
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:
# 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:
screenshot_str = str(screenshot)
if screenshot_str.endswith('.html'):
self._fig.write_html(screenshot_str)
else:
self._fig.write_image(screenshot_str)
Original file line number Diff line number Diff line change
@@ -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()
Loading