Hi,
I have two 3d scatterplots side-by-side. The plots show similar data and I’d like to be able to rotate either one of the figures and have the other follow suit so that the two are always viewed from the same angle.
Current behaviour: Rotating graph1 also rotates graph2 such that both remain synchronised (as intended). If you then rotate graph2, graph1 does not follow suit; the two graphs behave independently. If you then rotate graph1 again, graph2 no longer updates either. The same behaviour occurs the other way around (i.e .if you start by rotating graph2).
Intended behaviour: You should be able to switch between rotating either of the graphs and the other should follow suit.
Below is a minimal working example. I’m currently trying to do this with dash, but if there’s a better way with just plotly, that should work great for my use case.
Thanks for you help!
# plotly.__version__ == 5.16.0
import plotly.graph_objects as go
import plotly.express as px
# dash.__version__ == 2.12.0
from dash import Dash, dcc, html, callback_context
from dash.dependencies import Input, Output
# to render in notebook
import plotly.io as pio
pio.renderers.default = "notebook_connected"
# Create data for scatter plots
df = px.data.iris()
fig1 = px.scatter_3d(df, x='sepal_length', y='sepal_width', z='petal_width', color='species')
fig2 = px.scatter_3d(df, x='sepal_length', y='sepal_width', z='petal_width', color='species')
# Define app layout
app = Dash(__name__)
app.layout = html.Div([
html.Div([
dcc.Graph(id='graph1', figure=fig1),
dcc.Graph(id='graph2', figure=fig2)
], style={'display': 'flex'})
])
# Define callback function to update camera position of one subplot when the other subplot is rotated
@app.callback(Output('graph1', 'figure'), Output('graph2', 'figure'), Input('graph1', 'relayoutData'), Input('graph2', 'relayoutData'))
def update_camera(relayout_data1, relayout_data2):
# get the id of the component that triggered the callback--used to determine which graph to update
ctx = callback_context
caller = None if not ctx.triggered else ctx.triggered[0]['prop_id'].split(".")[0] # in {'graph1', 'graph2', None}
# initialization, no relayoutData
if caller is None:
return (fig1, fig2)
# graph1 was interacted with: update graph2
elif (caller == 'graph1') and relayout_data1 and ('scene.camera' in relayout_data1):
print("1 updated 2")
fig2.layout.scene.camera = relayout_data1['scene.camera']
return (None, fig2)
# graph2 was interacted with: update graph1
elif (caller == 'graph2') and relayout_data2 and ('scene.camera' in relayout_data2):
print("2 updated 1")
fig1.layout.scene.camera = relayout_data2['scene.camera']
return (fig1, None)
# we didn't modify the camera, so no updates
else:
return (fig1, fig2)
if __name__ == '__main__':
port = 8050
app.run_server(debug=True, port=port)