Victor Xiong
Victor Xiong

Reputation: 11

Python animation timing not working correctly

So i recently strated using Python from scratch. I am trying to build a pictogram-made bar graph, where within each bar, each row appears one after another; and then each bar appears one after another. I wrote some code which i m 100%sure is quite amateur, but the animation does not work correctly I think - instead of having constant time intervals between rows and bars, the interval gets longer as the animation progresses

Oh and in addition, the year labels do not appear.. Here is my code:

    import matplotlib

    matplotlib.use('TkAgg')
    import matplotlib.pyplot as plt
    import matplotlib.animation as animation
    from matplotlib.offsetbox import OffsetImage, AnnotationBbox
    import numpy as np

    # ========== CONFIGURATION ==========
    ICON_PATH = 'dot.png'  # Replace with your PNG
    ZOOM = 0.4  # Increased zoom for visibility
    BAR_SPACING = 0.15  # Wider spacing between bars
    ICON_SPACING_X = 0.02  # More horizontal space between icons
    ICON_SPACING_Y = 0.02  # More vertical space between rows
    LABEL_Y_OFFSET = 0 # Labels placed lower

    BARS = [
        {"total": 5 * 10, "label": "2000"},
        {"total": 5 * 25, "label": "2005"},
        {"total": 5 * 36 + 4, "label": "2010"},
        {"total": 5 * 48 + 2, "label": "2015"},
        {"total": 5 * 58 + 2, "label": "2020"},
        {"total": 5 * 60, "label": "2024"},
    ]
    
COLUMNS_PER_BAR = 5
ROW_INTERVAL_MS = 50  # Affects all bars now
BAR_DELAY_MS = 200  # Delay between bars (now respects ROW_INTERVAL_MS)
# ===================================

# Load icon (ensure it's a high-resolution PNG/SVG)
icon = plt.imread(ICON_PATH)

# Precompute bar data
max_rows = 0
for idx, bar in enumerate(BARS):
    full_rows, remainder = divmod(bar["total"], COLUMNS_PER_BAR)
    bar["full_rows"] = full_rows
    bar["remainder"] = remainder
    bar["total_rows"] = full_rows + (1 if remainder else 0)
    bar["x_pos"] = 0.1+idx * BAR_SPACING
    max_rows = max(max_rows, bar["total_rows"])

# Set up figure with expanded limits
fig, ax = plt.subplots(figsize=((len(BARS)) * BAR_SPACING+3, 12))  # Larger figure size
ax.set_xlim(-0.5, (len(BARS)) * BAR_SPACING)
ax.set_ylim(LABEL_Y_OFFSET - 2, max_rows * ICON_SPACING_Y + 2)
ax.axis('off')


def add_icon(x, y, ax):
    img = OffsetImage(icon, zoom=ZOOM)  # Increased zoom
    ab = AnnotationBbox(img, (x, y), frameon=False)
    ax.add_artist(ab)
    return ab


def animate(frame_data):
    ax.clear()
    ax.axis('off')
    current_bar_idx, current_row = frame_data

    for bar_idx, bar in enumerate(BARS[:current_bar_idx + 1]):
        # Draw up to current_row for the current bar
        if bar_idx < current_bar_idx:
            rows_to_draw = bar["total_rows"]
        else:
            rows_to_draw = current_row + 1

        for row in range(rows_to_draw):
            cols = COLUMNS_PER_BAR if row < bar["full_rows"] else bar["remainder"]
            for col in range(cols):
                x = bar["x_pos"] + col * ICON_SPACING_X
                y = 0.1+row * ICON_SPACING_Y
                add_icon(x, y, ax)

        # Add label
        ax.text(
            bar["x_pos"] + (COLUMNS_PER_BAR * ICON_SPACING_X) / 2,
            LABEL_Y_OFFSET,
            bar["label"],
            ha='center',
            va='top',
            fontsize=14,
            weight='bold'
        )

    return ax.artists


# Generate frames with correct timing for all bars
frames = []
for bar_idx, bar in enumerate(BARS):
    # Add frames for each row in this bar
    for row in range(bar["total_rows"]):
        frames.append((bar_idx, row))
    # Add delay between bars using empty frames
    if bar_idx != len(BARS) - 1:
        delay_frames = int(BAR_DELAY_MS / ROW_INTERVAL_MS)
        for _ in range(delay_frames):
            frames.append((bar_idx, bar["total_rows"] - 1))  # Hold last frame

# Create animation
ani = animation.FuncAnimation(
    fig,
    animate,
    frames=frames,
    interval=ROW_INTERVAL_MS,
    blit=True
)

plt.show()
# ani.save('animation.gif', writer='pillow', fps=1000/ROW_INTERVAL_MS)

Upvotes: 1

Views: 35

Answers (2)

furas
furas

Reputation: 143097

I tested your code and I tried some modifications and I found two problems:

  1. you clear() every frame and draw all dots again - and every frame has more dots to draw so it needs more time to do it.

  2. adding many img (AnnotationBbox) slows down code even if I reduce first problem.
    It works correctly if I use Circle() (or Ellipse()) instead of images.


Full working code - with Eclipse() and with adding only new row in every frame.

I added time to check how long takes every frame and how long takes to draw first 9 row in every column (to compare if all columns draw with similar speed)

import matplotlib

matplotlib.use('TkAgg')
#matplotlib.use('QtAgg')

import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import numpy as np

import time

# ========== CONFIGURATION ==========
ICON_PATH = 'dot.png'  # Replace with your PNG
ZOOM = 0.4  # Increased zoom for visibility
BAR_SPACING = 0.15  # Wider spacing between bars
ICON_SPACING_X = 0.02  # More horizontal space between icons
ICON_SPACING_Y = 0.01  # More vertical space between rows
LABEL_Y_OFFSET = 0 # Labels placed lower

BARS = [
    {"total": 5 * 10, "label": "2000"},
    {"total": 5 * 25, "label": "2005"},
    {"total": 5 * 36 + 4, "label": "2010"},
    {"total": 5 * 48 + 2, "label": "2015"},
    {"total": 5 * 58 + 2, "label": "2020"},
    {"total": 5 * 60, "label": "2024"},
]
    
#COLUMNS_PER_BAR = 5
COLUMNS_PER_BAR = 5
ROW_INTERVAL_MS = 50  # Affects all bars now
BAR_DELAY_MS = 200  # Delay between bars (now respects ROW_INTERVAL_MS)

# ===================================

# Load icon (ensure it's a high-resolution PNG/SVG)
icon = plt.imread(ICON_PATH)

# Precompute bar data
max_rows = 0
for idx, bar in enumerate(BARS):
    full_rows, remainder = divmod(bar["total"], COLUMNS_PER_BAR)
    bar["full_rows"] = full_rows
    bar["remainder"] = remainder
    bar["total_rows"] = full_rows + (1 if remainder else 0)
    bar["x_pos"] = 0.1+idx * BAR_SPACING
    max_rows = max(max_rows, bar["total_rows"])

print(BARS)

# Set up figure with expanded limits
fig, ax = plt.subplots(figsize=((len(BARS)) * BAR_SPACING+3, 12))  # Larger figure size
ax.set_xlim(-0.5, (len(BARS)) * BAR_SPACING)
ax.set_ylim(LABEL_Y_OFFSET - 2, max_rows * ICON_SPACING_Y + 2)
ax.axis('off')

def add_icon(x, y, ax):
    #img = OffsetImage(icon, zoom=ZOOM)  # Increased zoom
    #ab = AnnotationBbox(img, (x, y), frameon=False)
    #ax.add_artist(ab)
    #return ab

    #ax.add_patch(plt.Circle((x, y), 0.02, color='black'))
    ax.add_patch(matplotlib.patches.Ellipse((x, y), width=0.02, height=0.01, color='black'))

def init_func():
    # run only once - at start of animation
    ax.clear()
    ax.axis('off')
    return ax.artists
    
start_column = 0  # to calculate time for 9 rows - to see if all columns runs with similar speed
    
def animate(frame_data):
    global start_column

    #ax.clear()
    #ax.axis('off')
    
    start = time.time()

    current_bar_idx, current_row = frame_data
        
    #print('--- frame ---')
    #print(f'{frame_data =}')
    
    # checking time for first 9 rows
    if current_row == 0:
        start_column = start
    elif current_row == 9:
        print(f'row: {current_row} | time: {start - start_column}')

    
    # --- draw only current row ---
    
    row = current_row + 1
    #print(f'{row = }')
    
    bar = BARS[current_bar_idx]

    #print(f'{bar = }')

    cols = COLUMNS_PER_BAR if row < bar["full_rows"] else bar["remainder"]

    #print(f'{cols = }')
    
    for col in range(cols):
        x = bar["x_pos"] + col * ICON_SPACING_X
        y = 0.1 + row * ICON_SPACING_Y
        #print(f'{x = } | {y =}')
        add_icon(x, y, ax)

    if current_row == 0:  # add text only once when first row is added
        #print(f'add text = {bar["label"]}')
        # Add label
        ax.text(
            bar["x_pos"] + (COLUMNS_PER_BAR * ICON_SPACING_X) / 2,
            LABEL_Y_OFFSET,
            bar["label"],
            ha='center',
            va='top',
            fontsize=10,
            weight='bold'
        )

    #end = time.time()
    #print(f'frame time: {end-start}')
    
    return ax.artists

# Generate frames with correct timing for all bars
frames = []
for bar_idx, bar in enumerate(BARS):
    # Add frames for each row in this bar
    for row_idx, row in enumerate(range(bar["total_rows"])):
        frames.append((bar_idx, row))

    # Add delay between bars using empty frames
    if bar_idx != len(BARS) - 1:
        delay_frames = int(BAR_DELAY_MS / ROW_INTERVAL_MS)
        for _ in range(delay_frames):
            frames.append((bar_idx, bar["total_rows"] - 1))  # Hold last frame

#print(frames)

# Create animation
ani = animation.FuncAnimation(
    fig,
    animate,
    init_func=init_func,
    frames=frames,
    interval=ROW_INTERVAL_MS,
    blit=True
)

plt.show()
#ani.save('animation.gif', writer='pillow', fps=1000/ROW_INTERVAL_MS)

Result:

enter image description here

Upvotes: 1

Coco Q.
Coco Q.

Reputation: 814

Remove blit=True in the animation.FuncAnimation() call:

ani = animation.FuncAnimation(
    fig,
    animate,
    frames=frames,
    interval=ROW_INTERVAL_MS
)

Move the label drawing outside the animation function by adding it before the animation creation:

# Add labels once before animation starts
for bar in BARS:
    ax.text(
        bar["x_pos"] + (COLUMNS_PER_BAR * ICON_SPACING_X) / 2,
        LABEL_Y_OFFSET,
        bar["label"],
        ha='center',
        va='top',
        fontsize=14,
        weight='bold'
    )

This ensures labels are drawn once and stay visible throughout the animation.

Upvotes: 2

Related Questions