Reputation: 11
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
Reputation: 143097
I tested your code and I tried some modifications and I found two problems:
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.
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:
Upvotes: 1
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