This example has not yet been updated to reflect all of the new features and recommendations from vidigi 2.0.0, which have further simplified the process of adding vidigi to your model and accessing and modifying your animation.
For now, all of the code below will still work, but check out the getting started page for a full guide to the recommended way to use vidigi 2.0.0.
The figure returned by generate_animation / animate_activity_log is an ordinary animated Plotly figure, so in principle you can add anything Plotly can draw to it. In practice, doing it by hand is fiddly: vidigi splits the figure’s traces into animated per-entity traces (rewritten on every frame) and static traces for the stage labels and resource icons (left untouched as the animation plays). Add your own trace naively and it either only shows up on the first frame, flickers, or overwrites the stage labels.
Three helpers in vidigi.animation handle this:
Helper
What it’s for
add_subplot_panels
turn the single-axis animation into a stacked subplot grid, so there’s room for an extra chart below it
add_synchronised_trace_from_dataframe
the common case: build a trace per frame from a long-form DataFrame with a time column
add_synchronised_trace
the lower-level primitive: you supply a callable that returns the trace(s) for each frame
A model to animate
A small walk-in clinic: patients arrive, wait for one of two nurses, are seen, and leave. Nothing here is specific to the synchronised-trace helpers - it is just something to put an extra chart next to.
import randomimport plotly.graph_objects as goimport plotly.io as pioimport simpyfrom vidigi.animation import ( add_subplot_panels, add_synchronised_trace, add_synchronised_trace_from_dataframe, generate_animation,)from vidigi.logging import EventLoggerfrom vidigi.prep import generate_animation_df, reshape_for_animationsfrom vidigi.resources import VidigiStorefrom vidigi.utils import EventPosition, create_event_position_df# "iframe" keeps the executed notebook small (the heavy Plotly HTML is written to a# gitignored iframe_figures/ folder). Use "notebook" when running interactively.pio.renderers.default ="iframe"RUN_LENGTH =120N_NURSES =2random.seed(42)env = simpy.Environment()logger = EventLogger(env=env, run_number=1)def patient(env, entity_id, nurses): logger.log_arrival(entity_id=entity_id) logger.log_queue(entity_id=entity_id, event="wait_for_nurse")# `as req: yield req` is required - without it the patient never actually waits# for or holds a nurse, and later frames show them jumping straight to the exit.with nurses.request( entity_id=entity_id, start_event="treatment_begins", end_event="treatment_ends", ) as req:yield reqyield env.timeout(random.uniform(10, 20)) logger.log_departure(entity_id=entity_id)def arrivals(env, nurses): entity_id =0whileTrue:yield env.timeout(random.expovariate(1/5)) entity_id +=1 env.process(patient(env, entity_id, nurses))nurses = VidigiStore(env, num_resources=N_NURSES, label="nurse", logger=logger)env.process(arrivals(env, nurses))env.run(until=RUN_LENGTH)event_log = logger.to_dataframe()event_log.head(20)
add_subplot_panels rebuilds the figure as a stacked subplot grid and drops the animation into the top row. row_heights is one entry per row, top to bottom - the first is the animation. The new panels’ axes are blanked by default (hide_new_panel_axes=True); here we keep them so the values can be read.
Call this before adding any synchronised traces - the panel axes have to exist before a trace can be placed on them.
Step 2 - a per-frame bar: add_synchronised_trace_from_dataframe
We want one bar per frame showing how many patients are waiting at that moment. First build a DataFrame with one row per animation snapshot - reusing the snapshot_time values generate_animation_df already produced guarantees there is exactly one row per frame.
add_synchronised_trace_from_dataframe calls make_trace once per frame with that frame’s slice of the data, and wires the returned trace into every frame.
match="index" pairs the i-th distinct time with frame i, so it does not matter that the frames are labelled differently from the raw snapshot_time values. It raises if the row count and frame count disagree.
accumulate=False (the default) gives make_trace only the current step’s rows - a snapshot. xaxis/yaxis on the trace send it to the second panel.
Step 3 - a cumulative line with a fixed reference: accumulate=True + static_traces
For the third panel we want a line that grows as the animation plays, plus a dashed line marking the final total that never moves.
accumulate=True gives make_trace every row up to and including the current frame, so the line lengthens frame by frame.
static_traces= is drawn once and shown identically on every frame - vidigi never lists its trace index in a frame, so it is left alone exactly like the stage labels are.
Step 4 - anything that isn’t a DataFrame: add_synchronised_trace
add_synchronised_trace_from_dataframe is a thin wrapper over add_synchronised_trace, which takes a frame_traces(frame_name, frame_index) callable directly. Use it when the per-frame content isn’t naturally a slice of a DataFrame - here, a caption on the main plot that reports the current wait.
The callable must return the same number of traces every call (return an empty trace for frames with nothing to show). Prefer frame_index for lookups - frame_name is the formatted time from the slider and may not match your data.
Press play, or drag the slider: the two panels and the caption all track the animation, and the stage labels and nurse icons stay put throughout.
Notes
Order matters.add_subplot_panels first (it creates the axes), then the synchronised traces. Multiple add_synchronised_trace* calls on the same figure are fine - each appends its own traces.
match="index" vs match="value"."index" is the robust default: it aligns by position and is immune to time_display_units relabelling frames. Use "value" only when some frames have no data and you want to align on the frame name string.
Redraw. A bar or a secondary-axis trace needs redraw=True on the play button to animate; the helpers detect this and set it automatically. Pass redraw= explicitly to override.
Doing it by hand.example_13 walks through the same thing without the helpers, if you need to understand or customise the mechanism. example_15 uses the helpers on a larger model.