Skip to content

Commit d449da4

Browse files
feat(plotly): Add plotly dash as backend (#411)
Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent 975c316 commit d449da4

File tree

5 files changed

+377
-0
lines changed

5 files changed

+377
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Feat(plotly): Add plotly dash as backend
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright (C) 2024 - 2025 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
"""
23+
.. _ref_plain_usage_dash:
24+
25+
======================================
26+
Plain usage of the plotly dash backend
27+
======================================
28+
29+
This example shows the plain usage of the Plotly Dash backend in the Visualization Interface Tool to plot different objects,
30+
including PyVista meshes, custom objects, and Plotly-specific objects.
31+
"""
32+
33+
from ansys.tools.visualization_interface.backends.plotly.plotly_dash import PlotlyDashBackend
34+
from ansys.tools.visualization_interface.types.mesh_object_plot import MeshObjectPlot
35+
from ansys.tools.visualization_interface import Plotter
36+
import pyvista as pv
37+
from plotly.graph_objects import Mesh3d
38+
39+
40+
# Create a plotter with the Plotly backend
41+
pl = Plotter(backend=PlotlyDashBackend())
42+
43+
# Create a PyVista mesh
44+
mesh = pv.Sphere()
45+
mesh2 = pv.Cube(center=(2,0,0))
46+
# Plot the mesh
47+
pl.plot(mesh, name="Sphere")
48+
pl.plot(mesh2, name="Cube")
49+
50+
# ----------------------------------
51+
# Start the server and show the plot
52+
# ----------------------------------
53+
#
54+
# .. code-block:: python
55+
#
56+
# pl.show()
57+

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,16 @@ plotly = [
4242
"kaleido >= 1.1.0,<2",
4343
]
4444

45+
dash = [
46+
"dash >= 3.2.0,<4"
47+
]
48+
4549
all = [
4650
"pyside6 >= 6.8.0,<7",
4751
"pyvistaqt >= 0.11.1,<1",
4852
"plotly >= 6.3.1,<7",
4953
"kaleido >= 1.1.0,<2",
54+
"dash >= 3.2.0,<4"
5055
]
5156

5257
tests = [
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Copyright (C) 2024 - 2025 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
"""Module for dash plotly."""
23+
from typing import TYPE_CHECKING, Union
24+
25+
from dash import Dash, Input, Output, dcc, html
26+
27+
from ansys.tools.visualization_interface.backends.plotly.plotly_interface import PlotlyBackend
28+
from ansys.tools.visualization_interface.backends.plotly.widgets.dropdown_manager import DashDropdownManager
29+
30+
if TYPE_CHECKING:
31+
import plotly.graph_objects as go
32+
33+
34+
class PlotlyDashBackend(PlotlyBackend):
35+
"""Plotly Dash interface for visualization."""
36+
37+
def __init__(self, app: Dash = None) -> None:
38+
"""Initialize the Plotly Dash backend.
39+
40+
Parameters
41+
----------
42+
app : Dash
43+
The Dash application instance.
44+
"""
45+
super().__init__()
46+
self._app = app or Dash(__name__)
47+
self._dropdown_manager = DashDropdownManager(self._fig)
48+
self._setup_callbacks()
49+
50+
@property
51+
def dropdown_manager(self) -> DashDropdownManager:
52+
"""Get the dropdown manager for this backend.
53+
54+
Returns
55+
-------
56+
DashDropdownManager
57+
The dropdown manager instance.
58+
"""
59+
return self._dropdown_manager
60+
61+
def plot(self, plottable_object, name: str = None, **plotting_options) -> None:
62+
"""Plot a single object using Plotly and track mesh names for dropdown.
63+
64+
Parameters
65+
----------
66+
plottable_object : Any
67+
The object to plot.
68+
name : str, optional
69+
Name of the mesh for labeling in Plotly.
70+
plotting_options : dict
71+
Additional plotting options.
72+
"""
73+
# Call parent plot method
74+
super().plot(plottable_object, name=name, **plotting_options)
75+
76+
# Track mesh names for dropdown functionality
77+
if name:
78+
self._dropdown_manager.add_mesh_name(name)
79+
else:
80+
# Try to get name from the latest trace added
81+
if self._fig.data:
82+
latest_trace = self._fig.data[-1]
83+
trace_name = getattr(latest_trace, 'name', None)
84+
if trace_name:
85+
self._dropdown_manager.add_mesh_name(trace_name)
86+
87+
def _setup_callbacks(self) -> None:
88+
"""Setup Dash callbacks for mesh visibility control."""
89+
# Store reference to self for use in callback
90+
backend_instance = self
91+
92+
@self._app.callback(
93+
Output('mesh-graph', 'figure'),
94+
Input('mesh-visibility-dropdown', 'value'),
95+
prevent_initial_call=True
96+
)
97+
def update_mesh_visibility(hidden_meshes):
98+
"""Update mesh visibility based on dropdown selection.
99+
100+
Parameters
101+
----------
102+
hidden_meshes : List[str]
103+
List of mesh names to hide.
104+
105+
Returns
106+
-------
107+
go.Figure
108+
Updated figure with modified mesh visibility.
109+
"""
110+
if hidden_meshes is None:
111+
hidden_meshes = []
112+
113+
# Get all mesh names
114+
all_mesh_names = backend_instance.dropdown_manager.get_mesh_names()
115+
visible_mesh_names = [name for name in all_mesh_names if name not in hidden_meshes]
116+
117+
# Create a copy of the figure to avoid modifying the original
118+
import plotly.graph_objects as go
119+
updated_fig = go.Figure(backend_instance._fig)
120+
121+
# Update visibility for each trace
122+
for i, trace in enumerate(updated_fig.data):
123+
trace_name = getattr(trace, 'name', None)
124+
is_visible = trace_name in visible_mesh_names
125+
updated_fig.data[i].visible = is_visible
126+
127+
return updated_fig
128+
129+
def create_dash_layout(self) -> html.Div:
130+
"""Create the Dash layout with optional dropdown for mesh visibility.
131+
132+
Returns
133+
-------
134+
html.Div
135+
The Dash layout component.
136+
"""
137+
components = []
138+
139+
if self.dropdown_manager.get_mesh_names():
140+
# Add dropdown for mesh visibility control
141+
mesh_names = self.dropdown_manager.get_mesh_names()
142+
components.append(
143+
dcc.Dropdown(
144+
id='mesh-visibility-dropdown',
145+
options=[{'label': name, 'value': name} for name in mesh_names],
146+
multi=True,
147+
placeholder="Select meshes to hide",
148+
searchable=True,
149+
style={
150+
'width': '280px',
151+
'fontSize': '14px'
152+
}
153+
)
154+
155+
)
156+
157+
# Add the main graph
158+
components.append(dcc.Graph(
159+
id='mesh-graph',
160+
figure=self._fig,
161+
style={
162+
'height': '100vh',
163+
'width': '100%',
164+
'margin': '0',
165+
'padding': '0'
166+
},
167+
config={
168+
'responsive': True,
169+
'displayModeBar': True,
170+
'displaylogo': False
171+
}
172+
))
173+
174+
return html.Div(components, style={
175+
'fontFamily': '"Open Sans", verdana, arial, sans-serif',
176+
'backgroundColor': '#ffffff',
177+
'minHeight': '100vh',
178+
'width': '100%',
179+
'margin': '0',
180+
'padding': '0',
181+
'position': 'relative'
182+
})
183+
184+
def show(self,
185+
plottable_object=None,
186+
screenshot: str = None,
187+
name_filter=None,
188+
**kwargs) -> Union["go.Figure", None]:
189+
"""Render the Plotly scene.
190+
191+
Parameters
192+
----------
193+
plottable_object : Any, optional
194+
Object to show, by default None.
195+
screenshot : str, optional
196+
Path to save a screenshot, by default None.
197+
name_filter : bool, optional
198+
Flag to filter the object, by default None.
199+
kwargs : dict
200+
Additional options the selected backend accepts.
201+
202+
Returns
203+
-------
204+
Union[go.Figure, None]
205+
The figure of the plot if in doc building environment. Else, None.
206+
"""
207+
import os
208+
if os.environ.get("PYANSYS_VISUALIZER_DOC_MODE"):
209+
return self._fig
210+
211+
if plottable_object is not None:
212+
self.plot(plottable_object)
213+
214+
# Only show in browser if no screenshot is being taken
215+
if not screenshot:
216+
# Always use the create_dash_layout method to ensure dropdown is included when enabled
217+
self._app.layout = self.create_dash_layout()
218+
self._app.run()
219+
220+
else:
221+
screenshot_str = str(screenshot)
222+
if screenshot_str.endswith('.html'):
223+
self._fig.write_html(screenshot_str)
224+
else:
225+
self._fig.write_image(screenshot_str)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright (C) 2024 - 2025 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
"""Module for dropdown management in Plotly figures."""
23+
from typing import Any, Dict, List
24+
25+
import plotly.graph_objects as go
26+
27+
28+
class DashDropdownManager:
29+
"""Class to manage dropdown menus in a Plotly figure.
30+
31+
This class allows adding dropdown menus to a Plotly figure for controlling
32+
mesh visibility and other properties.
33+
34+
Parameters
35+
----------
36+
fig : go.Figure
37+
The Plotly figure to which dropdowns will be added.
38+
"""
39+
40+
def __init__(self, fig: go.Figure):
41+
"""Initialize DropdownManager."""
42+
self._fig = fig
43+
self._mesh_names = []
44+
45+
def add_mesh_name(self, name: str) -> None:
46+
"""Add a mesh name to track for dropdown functionality.
47+
48+
Parameters
49+
----------
50+
name : str
51+
The name of the mesh to track.
52+
"""
53+
if name and name not in self._mesh_names:
54+
self._mesh_names.append(name)
55+
56+
def get_mesh_names(self) -> List[str]:
57+
"""Get the list of tracked mesh names.
58+
59+
Returns
60+
-------
61+
List[str]
62+
List of mesh names.
63+
"""
64+
return self._mesh_names.copy()
65+
66+
def get_visibility_args_for_meshes(self, visible_mesh_names: List[str]) -> Dict[str, Any]:
67+
"""Get visibility arguments for showing only specified meshes.
68+
69+
Parameters
70+
----------
71+
visible_mesh_names : List[str]
72+
List of mesh names that should be visible.
73+
74+
Returns
75+
-------
76+
Dict[str, Any]
77+
Arguments for restyle method to set mesh visibility.
78+
"""
79+
visibility = []
80+
for trace in self._fig.data:
81+
trace_name = getattr(trace, 'name', None)
82+
is_visible = trace_name in visible_mesh_names
83+
visibility.append(is_visible)
84+
85+
return {"visible": visibility}
86+
87+
def clear(self) -> None:
88+
"""Clear all tracked mesh names."""
89+
self._mesh_names.clear()

0 commit comments

Comments
 (0)