posit-dev / py-shinywidgets

Render ipywidgets inside a PyShiny app
MIT License
47 stars 5 forks source link

Animated Plotly Graphs Not Rendering #156

Open Secret-Ambush opened 3 months ago

Secret-Ambush commented 3 months ago

I was trying to generate an animated 3D scatter plot using plotly and display it on a dashboard I created using PyShiny 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 a minimal reproducible 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.

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

Could someone help me out?

cpsievert commented 3 months ago

Thanks for the report -- this is a particularly tricky one.

In your example, fig1 is a go.Figure(). Since @render_widget is expecting an object that inherits from ipywidgets.Widget, it'll actually implicitly transform that go.Figure() into a go.FigureWidget(). And, if you try a minimal go.FigureWidget() in a Jupyter notebook with frames, you get:

ValueError: 
Frames are not supported by the plotly.graph_objs.FigureWidget class.
Note: Frames are supported by the plotly.graph_objs.Figure class

That said, if you just want the plot to render as it does in the notebook (i.e., without transforming Figure to FigureWidget), you can do that by changing two lines: (1) @render_widget -> @render.ui and (2) return fig1 -> return ui.HTML(fig1.to_html()).

Unfortunately, this is mainly a limitation of FigureWidget, but I will transfer this issue over {shinywidgets} since it should probably be throwing that same ValueError instead of silently dropping frames

Secret-Ambush commented 3 months ago

Thanks a lot, @cpsievert! Rendering inside a HTML tag works perfectly, and I'm now able to understood why it's not working when using @render_widget

Kind Regards