python-3.xplotlypy-shiny

Animated Plotly Graph in PyShiny (Express)


I was trying to generate an animated 3D scatter plot using plotly and display it on a dashboard I created using Shiny (Python) Express. When checking the code for an animated plotly graph on a jupyter notebook - it works perfectly, but I am not able to get the animation running on Shiny.

The graph is interactive - that is the tool tip shows the data but there's no animation when clicking on Play.

import io
from pathlib import Path
from shiny import reactive
from shiny.express import input, ui, render
from shiny.types import FileInfo
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
from shinywidgets import render_widget
import asyncio

ui.page_opts(
    title="Visualisation",
    fillable=True,
    id="page",
    )

with ui.card(full_screen=True):
    @render_widget
    def coordinate_space():
        try:
            with ui.Progress(min=1, max=5) as p:
                data = {
                    'X': [1, 2, 3, 4, 5],
                    'Y': [5, 4, 3, 2, 1],
                    'Z': [2, 3, 4, 5, 6],
                    'FZ_new': [10, 20, 30, 40, 50],
                    'FZ': [5, 10, 15, 20, 25]
                }

                scaled_coordinate_system = pd.DataFrame(data)

                p.set(2, message="Data loaded, preparing plot...")


                fig = px.scatter_3d(scaled_coordinate_system, x='X', y='Y', z='Z',
                    size='FZ_new', color='FZ_new', opacity=0.7)

                fig1 = go.Figure(fig)
                p.set(3, message="Adding traces to the plot...")

                    
                frames = [
                    go.Frame(data=[go.Scatter3d(
                        x=scaled_coordinate_system['X'], 
                        y=scaled_coordinate_system['Y'], 
                        z=scaled_coordinate_system['Z'], 
                        mode='markers', 
                        marker=dict(size=scaled_coordinate_system['FZ_new'] * 10, color="Red"),
                        opacity=0.9,
                        name='Frame 1',
                        
                    )]),
                    go.Frame(data=[go.Scatter3d(
                        x=scaled_coordinate_system['X'], 
                        y=scaled_coordinate_system['Y'], 
                        z=scaled_coordinate_system['Z'], 
                        mode='markers', 
                        marker=dict(size=scaled_coordinate_system['FZ'], color=scaled_coordinate_system['FZ']),
                        opacity=0.7,
                        name='Frame 2'
                    )])
                ]

                print("Frames")

                fig1.frames = frames

                p.set(4, message="Updating layout...")

                fig1.update_layout(
                    scene=dict(
                        xaxis_title='X AXIS',
                        yaxis_title='Y AXIS',
                        zaxis_title='Z AXIS'
                    ),
                    updatemenus=[dict(
                        type='buttons', 
                        direction = "down",
                        showactive=True, 
                        buttons=[
                            dict(
                                label='Play',
                                method='animate', 
                                args=[None, dict(
                                    frame=dict(duration=500, redraw=True),
                                    fromcurrent=True, 
                                    transition=dict(duration=0)
                                )]
                            ),
                            dict(
                                label='Pause',
                                method='animate',
                                args=[[None], dict(
                                    frame=dict(duration=0, redraw=False),
                                    mode='immediate'
                                )]
                            )
                        ]
                    )]
                )

                fig1.update_layout(margin=dict(l=0, r=0, b=0, t=0))
                camera = dict(
                    eye=dict(x=2, y=2, z=0.5)
                )

                fig1.update_layout(scene_camera=camera)
                p.set(5, message="Rendering plot...")
                return fig1

        except Exception as e:
            ui.notification_show(f"{e}", duration=20, type="error")
            return None

Above is my code for the function to render a plot. I was thinking if the animation rendering issue has something to do with async functions so I changed this to sync. I'm not sure what to do.

Edit: My animation just affects the size of the markers in the scatter plot.

Could someone help me out?


Solution

  • To render the plots, simply change these two lines as follows:
    (1) @render_widget -> @render.ui
    (2) return fig1 -> return ui.HTML(fig1.to_html())

    It will work. In case you're wondering why it doesn't work with @render_widget, that's because it implicitly transforms go.Figure() into a go.FigureWidget() and go.FigureWidget() doesn't support frames. So, the workaround is to render inside a HTML tag.