import simpy
from vidigi.logging import EventLogger
from vidigi.animation import animate_activity_log
from examples.feat_gauge_only_animations.simple_triage_assess_treat_model import Model
from vidigi.utils import EventPosition, create_event_position_df
import plotly.io as pio
pio.renderers.default = "notebook"Feature Breakdown: Comparing gauge, hybrid and gaugeless animations for simulations with high entity volumes
Vidigi was originally designed to work with models where relatively small numbers of entities were being visualised at any time.
However, many models work at a larger scale, with hundreds or thousands of entities being tracked.
Due to the way Plotly animations work, there are some limitations to what works well when larger numbers of entities are involved - but we do have some options.
Let’s start by looking at a simple three-step model where patients are triaged, assessed and treated. We’ve used the vidigi EventLogger class and only done 1 run for demonstration purposes.
model = Model(run_number=1)
logs = model.run()We can use the EventLogger .summary() function to see a quick overview of our patients.
logs.summary(){'total_events': 12652,
'event_types': {'queue': 8879, 'arrival_departure': 3773},
'time_range': (0.0, 363.92791552910586),
'unique_entities': 3645}
Let’s take a quick look at a sample of the dataframe.
logs_df = logs.to_dataframe()
logs_df| entity_id | event_type | event | time | run_number | |
|---|---|---|---|---|---|
| 0 | 1 | arrival_departure | arrival | 0.000000 | 1 |
| 1 | 1 | queue | queue_initial_review | 0.000000 | 1 |
| 2 | 1 | queue | start_initial_review | 0.000000 | 1 |
| 3 | 1 | queue | end_initial_review | 0.013129 | 1 |
| 4 | 1 | queue | queue_assessment | 0.013129 | 1 |
| ... | ... | ... | ... | ... | ... |
| 12647 | 3643 | queue | queue_initial_review | 363.627072 | 1 |
| 12648 | 3644 | arrival_departure | arrival | 363.834890 | 1 |
| 12649 | 3644 | queue | queue_initial_review | 363.834890 | 1 |
| 12650 | 3645 | arrival_departure | arrival | 363.927916 | 1 |
| 12651 | 3645 | queue | queue_initial_review | 363.927916 | 1 |
12652 rows × 5 columns
And let’s also just see what steps we have.
logs_df.event.unique()array(['arrival', 'queue_initial_review', 'start_initial_review',
'end_initial_review', 'queue_assessment', 'start_assessment',
'end_assessment', 'queue_treatment', 'start_treatment',
'end_treatment', 'depart'], dtype=object)
We’ll choose a subset of these steps to represent in our animation, and create our event positioning dataframe to stagger them.
event_position_df = create_event_position_df(
[EventPosition(event='queue_initial_review', x=200, y=600, label="Waiting for <br> Initial Review"),
EventPosition(event='queue_assessment', x=350, y=400, label="Waiting for <br> Assessment"),
EventPosition(event='queue_treatment', x=500, y=200, label="Waiting for <br> Treatment")]
)
event_position_df| event | x | y | label | resource | |
|---|---|---|---|---|---|
| 0 | queue_initial_review | 200 | 600 | Waiting for <br> Initial Review | None |
| 1 | queue_assessment | 350 | 400 | Waiting for <br> Assessment | None |
| 2 | queue_treatment | 500 | 200 | Waiting for <br> Treatment | None |
Now let’s create an animation where the step_snapshot_max is set to 0 (meaning all patients are treated as ‘excess’ queue and not visualised as individual entities), and turn on gauges to replace the ‘+ x more’ text so we have a visual indicator of queue lengths across our steps.
animate_activity_log(
event_log=logs_df,
event_position_df=event_position_df,
simulation_time_unit="days",
every_x_time_units=7,
step_snapshot_limit_gauges=True,
step_snapshot_max=0,
limit_duration=365,
override_x_max=600,
override_y_max=700,
plotly_width=1600,
time_display_units="day_clock",
)Why would we bother with this over a standard animated bar chart or some other form of representation?
In a more complex model with a large number of steps, some of which may represent different paths certain patients take, being able to arrange them in a logical order - particularly showing the potential parallel pathways.
We could also pair this with a background image that helps to represent elements of the pathway, such as the ways in which patients can branch off or circle back to different points.
Alternative representations
For comparison, let’s explore including some icons for individuals to try to demonstrate flow between steps.
However, as many of the individuals moving between the steps are not shown in the queue in the frame before they move, we observe a confusing ‘flying in’ effect from the top left of the animation, which is a plotly limitation/default which has not yet been overcome in the package.
However, one benefit is that we can use the hover features to see the wait duration of different individuals in the queue, allowing us to drill down into entity experience at different points in the model in a way that we couldn’t otherwise.
animate_activity_log(
event_log=logs_df,
event_position_df=event_position_df,
simulation_time_unit="days",
every_x_time_units=7,
step_snapshot_limit_gauges=True,
step_snapshot_max=10,
limit_duration=365,
override_x_max=600,
override_y_max=700,
wrap_queues_at=10,
custom_entity_icon_list=["⚫"],
frame_duration=1000,
plotly_width=1600,
entity_icon_size=18,
time_display_units="day_clock",
)Changing to a daily level still doesn’t really represent the rate of flow between steps.
animate_activity_log(
event_log=logs_df,
event_position_df=event_position_df,
simulation_time_unit="days",
every_x_time_units=1,
step_snapshot_limit_gauges=True,
step_snapshot_max=10,
limit_duration=365,
override_x_max=600,
override_y_max=700,
wrap_queues_at=10,
custom_entity_icon_list=["⚫"],
frame_duration=400,
frame_transition_duration=600,
plotly_width=1600,
entity_icon_size=18,
time_display_units="day_clock",
)Setting the transition duration to 0 is one way to keep the option to have the hover for the individuals in the queue while removing odd animation effects. We could remove the custom icons to make the changing of patients more obvious.
animate_activity_log(
event_log=logs_df,
event_position_df=event_position_df,
simulation_time_unit="days",
every_x_time_units=1,
step_snapshot_limit_gauges=True,
step_snapshot_max=10,
limit_duration=365,
override_x_max=600,
override_y_max=700,
wrap_queues_at=10,
frame_duration=400,
frame_transition_duration=0,
plotly_width=1600,
entity_icon_size=18,
time_display_units="day_clock",
)Finally, let’s try including all of our individuals in the plot - we’ll just make them very small and adjust our event position dataframe slightly to provide more space.
event_position_df = create_event_position_df(
[EventPosition(event='queue_initial_review', x=200, y=900, label="Waiting for <br> Initial Review"),
EventPosition(event='queue_assessment', x=350, y=500, label="Waiting for <br> Assessment"),
EventPosition(event='queue_treatment', x=500, y=200, label="Waiting for <br> Treatment")]
)
event_position_df| event | x | y | label | resource | |
|---|---|---|---|---|---|
| 0 | queue_initial_review | 200 | 900 | Waiting for <br> Initial Review | None |
| 1 | queue_assessment | 350 | 500 | Waiting for <br> Assessment | None |
| 2 | queue_treatment | 500 | 200 | Waiting for <br> Treatment | None |
You may notice that the animation starts to struggle significantly, with slowdowns occurring, despite it generating very quickly. This is due to the sheer amount of data per frame that needs to be stored. In extreme cases, this can cause browser windows to freeze.
However, this does give us more of a sense of flow between steps, which can complement the gauge animation.
The gauge version of the animation can be utilised to better track the scale of queues, while the non-gauge animation may better serve as a demonstration of how the underlying model works in terms of potential paths for people to move between steps.
fig = animate_activity_log(
event_log=logs_df,
event_position_df=event_position_df,
simulation_time_unit="days",
every_x_time_units=7,
step_snapshot_limit_gauges=True,
step_snapshot_max=9999,
debug_mode=True,
limit_duration=365,
override_x_max=600,
override_y_max=1800,
wrap_queues_at=75,
custom_entity_icon_list=["⚫"],
entity_icon_size=6,
gap_between_entities=2,
gap_between_queue_rows=15,
frame_duration=1000,
frame_transition_duration=2000,
plotly_width=1600,
time_display_units="day_clock",
)
figAnimation function called at 10:19:10
Iteration through time-unit-by-time-unit logs complete 10:19:11
Snapshot df concatenation complete at 10:19:11
Reshaped animation dataframe finished construction at 10:19:11
Placement dataframe finished construction at 10:19:11
C:\simviz\vidigi\vidigi\animation.py:1191: UserWarning:
`step_snapshot_max` is not a multiple of `wrap_queues_at`.The animation will display better if this is resolved.
Output animation generation complete at 10:19:44
Total Time Elapsed: 33.25 seconds