Beautiful animated timeline graphs tutorial

Hello, I am happy to share tutorial for animated graphs in Plotly, where I used a full potential of this amazing library.
Creation of such graphs was an amazing coding experience. Please let me know, if you like them :blush:

1 Like

@TomDuricek
What an amazing article. Thank you so much for writing it and sharing with us.

The code below (taken from Tom’s repo) will generate two charts. This is one of them.

Here’s the full .py code in case people are interested:

import imageio
import numpy as np
import pandas as pd

from io import BytesIO
import plotly.express as px
import plotly.graph_objects as go



df = pd.read_csv('https://raw.githubusercontent.com/tomasduricek/animated-graphs/main/retail_data.csv')
df['date'] = pd.to_datetime(df['date']
                            .astype(str)
                            .str[:7],
                            format="%Y-%m")  # Control ChatGPT date format output
df = df.sort_values(['date', 'retail_group'], ignore_index=True)

RETAIL_GROUP_COLORS = ['#1F4068', '#527A82', '#DE8918', '#BF3200']
FIRST_DAY_OF_ALL_YEARS = df[df['date'].dt.month == 1]['date'].unique()
N_UNIQUE_RETAIL_GROUPS = df['retail_group'].nunique()

df_indexed = pd.DataFrame()
for index in np.arange(start=0,
                       stop=len(df)+1,
                       step=N_UNIQUE_RETAIL_GROUPS):
    df_slicing = df.iloc[:index].copy()
    df_slicing['frame'] = (index//N_UNIQUE_RETAIL_GROUPS)
    df_indexed = pd.concat([df_indexed, df_slicing])

# Scatter Plot
scatter_plot = px.scatter(
    df_indexed,
    x='date',
    y='average_monthly_income',
    color='retail_group',
    animation_frame='frame',
    color_discrete_sequence=RETAIL_GROUP_COLORS
)

for frame in scatter_plot.frames:
    for data in frame.data:
        data.update(mode='markers',
                    showlegend=True,
                    opacity=1)
        data['x'] = np.take(data['x'], [-1])
        data['y'] = np.take(data['y'], [-1])

# Line Plot
line_plot = px.line(
    df_indexed,
    x='date',
    y='average_monthly_income',
    color='retail_group',
    animation_frame='frame',
    color_discrete_sequence=RETAIL_GROUP_COLORS,
    width=1000,
    height=500,
    line_shape='spline' # make a line graph curvy
)
line_plot.update_traces(showlegend=False)  # legend will be from line graph
for frame in line_plot.frames:
    for data in frame.data:
        data.update(mode='lines', opacity=0.8, showlegend=False)

# Stationary combined plot
combined_plot = go.Figure(
    data=line_plot.data + scatter_plot.data,
    frames=[
        go.Frame(data=line_plot.data + scatter_plot.data, name=scatter_plot.name)
        for line_plot, scatter_plot in zip(line_plot.frames, scatter_plot.frames)
    ],
    layout=line_plot.layout
)

combined_plot.update_yaxes(
    gridcolor='#7a98cf',
    griddash='dot',
    gridwidth=.5,
    linewidth=2,
    tickwidth=2
)

combined_plot.update_xaxes(
    title_font=dict(size=16),
    linewidth=2,
    tickwidth=2
)

combined_plot.update_traces(
    line=dict(width=5),
    marker=dict(size=25))

combined_plot.update_layout(
    font=dict(size=18),
    yaxis=dict(tickfont=dict(size=16)),
    xaxis=dict(tickfont=dict(size=16)),
    showlegend=True,
    legend=dict(
        title='Retail group'),
    template='simple_white',
    title="<b>Monthly Retail Prices During Covid-19</b>",
    yaxis_title="<b>Average price (k)</b>",
    xaxis_title="<b>Date</b>",
    yaxis_showgrid=True,
    xaxis_range=[df_indexed['date'].min() + pd.DateOffset(months=3),
                 df_indexed['date'].max() + pd.DateOffset(months=3)],
    yaxis_range=[df_indexed['average_monthly_income'].min() * 0.75,
                 df_indexed['average_monthly_income'].max() * 1.25],
    plot_bgcolor='#fffcf7',
    paper_bgcolor='#fffcf7',
    title_x=0.5
)

# adjust speed of animation
combined_plot['layout'].pop("sliders")
combined_plot.layout.updatemenus[0].buttons[0]['args'][1]['frame']['duration'] = 120
combined_plot.layout.updatemenus[0].buttons[0]['args'][1]['transition']['duration'] = 50
combined_plot.layout.updatemenus[0].buttons[0]['args'][1]['transition']['redraw'] = False
combined_plot.show()


# Autoscaled Combined Plot
combined_plot = go.Figure(
    data=line_plot.data + scatter_plot.data,
    frames=[
        frame.update(
            layout={
                'xaxis': {'range': [min(frame.data[0].x),
                                    max(frame.data[0].x + pd.DateOffset(months=2))],
                          'tickformat': '%Y',
                          'tickangle': 0,
                          'tickvals': FIRST_DAY_OF_ALL_YEARS},
                'yaxis': {'range': [min(frame.data[0].y) * 0.3,
                                    max(frame.data[0].y) * 1.5],
                          'nticks': 6},
            }
        )
        for frame in combined_plot.frames
    ],
    layout=line_plot.layout
)

combined_plot.update_yaxes(
    gridcolor='#7a98cf',
    griddash='dot',
    gridwidth=.5,
    linewidth=2,
    tickwidth=2
)

combined_plot.update_xaxes(
    title_font=dict(size=16),
    linewidth=2,
    tickwidth=2
)

combined_plot.update_traces(
    line=dict(width=5),
    marker=dict(size=25))

combined_plot.update_layout(
    font=dict(size=18),
    yaxis=dict(tickfont=dict(size=16)),
    xaxis=dict(tickfont=dict(size=16)),
    showlegend=True,
    legend=dict(
        title='Retail group'),
    template='simple_white',
    title="<b>Monthly Retail Prices During Covid-19</b>",
    yaxis_title="<b>Average price (k)</b>",
    xaxis_title="<b>Date",
    yaxis_showgrid=True,
    xaxis_range=[df_indexed['date'].min() + pd.DateOffset(months=3),
                 df_indexed['date'].max() + pd.DateOffset(months=3)],
    yaxis_range=[df_indexed['average_monthly_income'].min() * 0.75,
                 df_indexed['average_monthly_income'].max() * 1.25],
    plot_bgcolor='#fffcf7',
    paper_bgcolor='#fffcf7',
    title_x=0.5
)

# adjust speed of animation
combined_plot['layout'].pop("sliders")
combined_plot.layout.updatemenus[0].buttons[0]['args'][1]['frame']['duration'] = 150
combined_plot.layout.updatemenus[0].buttons[0]['args'][1]['transition']['duration'] = 120
combined_plot.layout.updatemenus[0].buttons[0]['args'][1]['transition']['redraw'] = False
combined_plot.show()
1 Like