sequenceDiagram participant Caller as animate_activity_log (or User) participant GA as generate_animation participant PX as plotly.express participant GO as plotly.graph_objects participant Figure Caller->>+GA: Call generate_animation(data, layout, scenario, options...) GA->>+PX: px.scatter(data, x="x_final", y="y_final", animation_frame="minute_display", animation_group="patient", text="icon", ...) PX-->>-GA: Return initial Figure object Note over GA, Figure: Figure now contains base animated scatter plot. GA->>+GO: Create Scatter trace for Stage Labels (mode="text") GO-->>-GA: Stage Label trace object GA->>Figure: fig.add_trace(Stage Label trace) alt scenario is provided GA->>GA: Calculate Resource Placeholder positions (using layout, scenario, options) GA->>+GO: Create Scatter trace for Resource Placeholders (mode="markers") GO-->>-GA: Resource Placeholder trace object GA->>Figure: fig.add_trace(Resource Placeholder trace) end alt add_background_image is provided GA->>Figure: fig.add_layout_image(...) end GA->>Figure: fig.update_traces(textfont_size=...) GA->>Figure: fig.update_xaxes(...) / fig.update_yaxes(...) GA->>Figure: fig.update_layout(...) Note over GA, Figure: Figure is now fully configured with static layers and styling. GA-->>-Caller: Return final Figure object
Chapter 6: Animation Generation (generate_animation
)
In Chapter 5: Snapshot Preparation (reshape_for_animations
& generate_animation_df
), we saw how vidigi
transforms the raw event_log
into a detailed, frame-by-frame blueprint, the full_patient_df_plus_pos
DataFrame. This blueprint tells us precisely where each entity (with its assigned icon) should be at every time snapshot (minute
). Now, we need to turn this meticulously prepared data into an actual moving picture.
This final step is handled by the generate_animation
function. Think of the previous steps as preparing the film reel, frame by frame. generate_animation
is the projector that takes this reel and displays the movie.
Motivation: Bringing the Data to Life
We have the data: entity IDs, time steps, icons, and exact X/Y coordinates. But this data is static, just a large table. The goal is to create a dynamic, visual representation where we can actually see the entities (our emojis) moving through the stages defined in our layout, forming queues, using resources, and progressing through the system over time.
generate_animation
leverages the power of the Plotly Express library, specifically its animated scatter plot capabilities, to achieve this. It takes the prepared snapshot data and maps it onto a visual canvas, adding interactive controls and static background elements to create the final animation figure.
The Core Task: Rendering the Animated Scatter Plot
The primary job of generate_animation
is to take the full_patient_df_plus_pos
DataFrame and use it to generate a Plotly Figure
object containing an animated scatter plot.
Here’s what it needs to do: 1. Plot Entities: For each time snapshot (minute
), plot each active entity’s assigned icon
at its calculated x_final
, y_final
coordinates. 2. Animate Movement: Instruct Plotly to treat points with the same patient
ID across different time snapshots as the same object, allowing Plotly to automatically generate smooth transitions (tweening) between their positions from one frame to the next. 3. Add Context: Overlay static elements like stage labels (e.g., “Waiting Area”), resource placeholders (e.g., markers for available nurse bays), and potentially a background image. 4. Configure Display: Set up the plot’s appearance, including axes, size, time display format on the slider, animation speed, and interactive controls.
Usage (How it’s Called)
While generate_animation
is the engine doing the plotting work, you’ll typically interact with it indirectly via the main Animation Facade (animate_activity_log
). The facade function prepares the data using the functions from Chapter 5 and then passes the result (full_patient_df_plus_pos
) along with layout information and customisation parameters to generate_animation
.
However, understanding the key parameters generate_animation
accepts is useful, especially if you want to customise the final look or potentially use it directly after preparing the data yourself:
# Conceptual call structure:
= generate_animation(
fig =prepared_data_df, # Output from generate_animation_df
full_patient_df_plus_pos=layout_df, # Layout definition (Chapter 3)
event_position_df=scenario_object, # For resource counts (Chapter 3 & 4)
scenario=900, # Figure height in pixels
plotly_height=None, # Figure width (None for auto)
plotly_width=True, # Show the play/pause button
include_play_button='path/to/bg.png', # Optional background image
add_background_image=True, # Show text labels for stages
display_stage_labels=24, # Size of emoji icons
icon_and_text_size=None, # Set fixed X-axis limit
override_x_max=None, # Set fixed Y-axis limit
override_y_max='dhm', # Format time on slider ('dhm', 'd', None)
time_display_units=0.8, # Opacity of resource markers
resource_opacity='🏥', # Use a custom icon for resources
custom_resource_icon=20, # Resource wrapping (match generate_animation_df)
wrap_resources_at=10, # Resource spacing (match generate_animation_df)
gap_between_resources=30, # Row spacing (match generate_animation_df)
gap_between_rows=False, # Show axes/grid for layout setup?
setup_mode=400, # Milliseconds per frame
frame_duration=600, # Milliseconds for transition tweening
frame_transition_duration=False
debug_mode
)
# fig is now a plotly.graph_objs._figure.Figure object
# fig.show() or fig.write_html(...)
The most crucial inputs are the full_patient_df_plus_pos
(the film reel) and the event_position_df
(the map/layout). The other parameters control the aesthetics and behaviour of the animation.
Under the Bonnet: How it Works
Let’s break down the steps generate_animation
takes internally to create the visualisation.
1. Plotly Express Core: The Animated Scatter Plot
The foundation of the animation is created using plotly.express.scatter
(often imported as px
). This powerful function can create animated plots directly from a DataFrame. The key lies in specifying the correct arguments:
data_frame=full_patient_df_plus_pos
: The input data containing entity, position, icon, and time.x="x_final"
,y="y_final"
: The columns containing the coordinates for each point in each frame.animation_frame="minute_display"
: This column tells Plotly which rows belong to which frame of the animation. The function prepares a user-friendlyminute_display
column based on thetime_display_units
parameter.animation_group="patient"
: This is vital. It tells Plotly that rows with the samepatient
value across differentanimation_frame
s represent the same logical entity. Plotly uses this to smoothly interpolate the position (x_final
,y_final
) between frames, creating the illusion of movement.text="icon"
: Instead of plotting a standard marker (like a dot), we tell Plotly to display the text from theicon
column (which contains our emojis) at the(x_final, y_final)
position.opacity=0
: We make the underlying scatter marker itself invisible, as we only want to see thetext
(the emoji).hover_name
,hover_data
: Configure the information shown when hovering over an entity in the interactive plot.range_x
,range_y
: Set the boundaries of the plot axes.height
,width
: Set the dimensions of the figure.
This single px.scatter
call generates the base Plotly figure (fig
) with the animated entities.
# From: vidigi/animation.py (Simplified generate_animation function)
import plotly.express as px
# ... other imports ...
def generate_animation(full_patient_df_plus_pos, event_position_df, scenario=None, #... other params ...
):# ... code to calculate x_max, y_max, prepare 'minute_display' column ...
# Define hover info based on whether scenario (for resource_id) is present
if scenario is not None:
= ["patient", "pathway", "time", "minute", "resource_id"]
hovers else:
= ["patient", "pathway", "time", "minute"]
hovers
# 1. Create the core animated scatter plot
= px.scatter(
fig 'minute'), # Ensure data is time-sorted
full_patient_df_plus_pos.sort_values(="x_final",
x="y_final",
y="minute_display", # Drive animation by time snapshots
animation_frame="patient", # Link entities across frames
animation_group="icon", # Display emoji icon as text
text="event", # Info on hover
hover_name=hovers, # More info on hover
hover_data=[0, x_max], # Set plot boundaries
range_x=[0, y_max],
range_y=plotly_height,
height=plotly_width,
width=0 # Make actual scatter marker invisible
opacity
)
# ... Code to add static layers and configure layout ...
return fig
This sets up the core animation – emojis moving around based on the prepared data.
2. Adding Static Layers
Once the base fig
object exists, generate_animation
adds static layers using Plotly’s graph_objects
module (often imported as go
). These elements don’t change from frame to frame.
Stage Labels: If
display_stage_labels=True
, it iterates through theevent_position_df
and adds ago.Scatter
trace withmode="text"
. This trace plots the text from thelabel
column ofevent_position_df
near the corresponding basex
,y
coordinates.# From: vidigi/animation.py (Simplified generate_animation function) import plotly.graph_objects as go # ... inside generate_animation, after px.scatter ... if display_stage_labels: fig.add_trace(go.Scatter(# Offset slightly from the base coordinates for better visibility =[pos + 10 for pos in event_position_df['x'].to_list()], x=event_position_df['y'].to_list(), y="text", # Display text, not markers mode="", # No legend entry name=event_position_df['label'].to_list(), # Use labels from layout df text="middle right", # Position text relative to coordinates textposition='none' # No hover interaction for labels hoverinfo ))
Resource Placeholders: If a
scenario
object is provided, it calculates the positions for each individual resource slot based on theevent_position_df
(finding rows with a non-nullresource
column), the resource count fromgetattr(scenario, resource_name)
, and layout parameters (gap_between_resources
,wrap_resources_at
,gap_between_rows
). It then adds ago.Scatter
trace withmode="markers"
(ormode="markers+text"
ifcustom_resource_icon
is used) to display these placeholders, often as light blue circles, slightly offset from where the entities using them will appear.# From: vidigi/animation.py (Simplified generate_animation function) # ... inside generate_animation ... if scenario is not None: # --- Calculate resource positions (Simplified - see Chapter 3 for details) --- = event_position_df[event_position_df['resource'].notnull()].copy() events_with_resources # Get counts from scenario object 'resource_count'] = events_with_resources['resource'].apply( events_with_resources[lambda resource_name: getattr(scenario, resource_name) )# Calculate individual resource slot positions including wrapping # (Complex calculation involving gaps and wrapping omitted for brevity - stores results in resource_pos_df) = calculate_resource_slot_positions( resource_pos_df events_with_resources, gap_between_resources, gap_between_rows, wrap_resources_at )# --- End Calculation --- # Add the trace for resource placeholders if custom_resource_icon is not None: fig.add_trace(go.Scatter(=resource_pos_df['x_final'], x=[y - 10 for y in resource_pos_df['y_final']], # Offset slightly y="markers+text", mode=custom_resource_icon, # Use custom icon text text=dict(opacity=0), # Hide underlying marker marker=resource_opacity, # Icon opacity opacity='none' hoverinfo ))else: fig.add_trace(go.Scatter(=resource_pos_df['x_final'], x=[y - 10 for y in resource_pos_df['y_final']], # Offset slightly y="markers", mode=dict(color='LightSkyBlue', size=15), # Default marker marker=resource_opacity, opacity='none' hoverinfo ))
(Note:
calculate_resource_slot_positions
is a conceptual representation of the logic embedded withingenerate_animation
that determines thex_final
,y_final
for each resource slot based on its ID, the base position, gaps, and wrapping rules.)Background Image: If
add_background_image
path is provided, it usesfig.add_layout_image
to embed the image into the plot background, stretched to fit the axes.# From: vidigi/animation.py (Simplified generate_animation function) if add_background_image is not None: fig.add_layout_image(dict( =add_background_image, # Path or URL to the image source="x domain", # Coordinates relative to x-axis domain (0 to 1) xref="y domain", # Coordinates relative to y-axis domain (0 to 1) yref=1, y=1, # Position image anchor at top-right of plot area x=1, sizey=1, # Image covers full width and height sizex="right", xanchor="top", yanchor="stretch", # Stretch image to fit sizing=0.5, # Make semi-transparent opacity="below" # Place behind data points layer ) )
3. Layout and Styling
Finally, generate_animation
applies various layout settings to polish the figure:
- Icon Size: Updates the text font size for the main scatter trace to control the emoji size (
fig.update_traces(textfont_size=icon_and_text_size)
). - Axes: Hides tick labels, grid lines, and zero lines for a cleaner appearance, unless
setup_mode=True
(which is useful for initially determining coordinates forevent_position_df
). It also disables zooming (fixedrange=True
).python # From: vidigi/animation.py (Simplified generate_animation function) if not setup_mode: fig.update_xaxes(showticklabels=False, showgrid=False, zeroline=False, fixedrange=True) fig.update_yaxes(showticklabels=False, showgrid=False, zeroline=False, fixedrange=True)
- Titles and Legend: Removes axis titles and the legend (
fig.update_layout(yaxis_title=None, xaxis_title=None, showlegend=False)
). - Animation Controls: Optionally removes the play/pause buttons (
if not include_play_button: fig["layout"].pop("updatemenus")
) and sets the frame duration and transition speed.python # From: vidigi/animation.py (Simplified generate_animation function) # Adjust speed of animation fig.layout.updatemenus[0].buttons[0].args[1]['frame']['duration'] = frame_duration fig.layout.updatemenus[0].buttons[0].args[1]['transition']['duration'] = frame_transition_duration
Conceptual Flow Diagram
This sequence shows how generate_animation
builds the final figure layer by layer, starting with the core animation and adding static contextual elements and styling.
Conclusion
The generate_animation
function is the final piece of the puzzle in the vidigi
animation workflow. It acts as the rendering engine, taking the meticulously prepared full_patient_df_plus_pos
DataFrame (the “film reel” created in Chapter 5) and projecting it onto a visual canvas using Plotly Express’s powerful animated scatter plot capabilities.
By mapping entity positions over time, adding contextual static layers like stage labels and resource placeholders derived from the Layout Configuration (event_position_df
) and scenario
object, and applying various styling options, it produces the final, interactive Plotly Figure
object. This figure visually represents the dynamics captured in your simulation’s event_log
, bringing your model to life.
This chapter concludes our walkthrough of the core components involved in generating animations with vidigi
, from the high-level facade function down to the final plotting engine. Understanding these pieces allows you to effectively use vidigi
and troubleshoot or customise the visualisations for your specific simulation models.
Generated by AI Codebase Knowledge Builder