Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 86 additions & 120 deletions plots/bar-race-animated/implementations/python/pygal.py
Original file line number Diff line number Diff line change
@@ -1,96 +1,76 @@
""" pyplots.ai
""" anyplot.ai
bar-race-animated: Animated Bar Chart Race
Library: pygal 3.1.0 | Python 3.13.11
Quality: 87/100 | Created: 2026-01-11
Library: pygal 3.1.0 | Python 3.13.13
Quality: 80/100 | Updated: 2026-05-19
"""

import os
import sys
from io import BytesIO


# Remove the script's own directory from sys.path so "pygal.py" doesn't shadow the installed package.
sys.path.pop(0)

import cairosvg
import numpy as np
import pygal
from PIL import Image, ImageDraw, ImageFont
from pygal.style import Style


# Data - Technology companies market cap evolution (2019-2024)
np.random.seed(42)
# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

companies = ["Apple", "Microsoft", "Alphabet", "Amazon", "Meta", "Tesla", "NVIDIA", "Samsung"]
years = [2019, 2020, 2021, 2022, 2023, 2024]
# Okabe-Ito palette — consistent entity colors across all frames
OKABE_ITO = ("#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442")

# Generate realistic market cap data (in billions USD)
base_values = {
"Apple": 1200,
"Microsoft": 900,
"Alphabet": 800,
"Amazon": 700,
"Meta": 400,
"Tesla": 100,
"NVIDIA": 150,
"Samsung": 300,
}
# Data — Country GDP rankings (approximate, trillion USD)
countries = ["USA", "China", "Japan", "Germany", "UK", "India", "France"]
years = [2000, 2005, 2010, 2015, 2020, 2023]

growth_rates = {
"Apple": [1.0, 1.3, 1.8, 1.5, 2.0, 2.8],
"Microsoft": [1.0, 1.4, 2.0, 1.6, 2.2, 2.9],
"Alphabet": [1.0, 1.2, 1.6, 1.2, 1.5, 2.0],
"Amazon": [1.0, 1.5, 1.6, 1.0, 1.3, 1.8],
"Meta": [1.0, 1.5, 1.8, 0.6, 1.2, 1.8],
"Tesla": [1.0, 5.0, 8.0, 4.0, 6.0, 7.0],
"NVIDIA": [1.0, 2.0, 4.0, 2.5, 6.0, 14.0],
"Samsung": [1.0, 1.1, 1.3, 0.9, 1.1, 1.4],
gdp_data = {
"USA": [10.2, 13.0, 14.9, 18.2, 20.9, 26.9],
"China": [1.2, 2.3, 6.1, 11.1, 14.7, 17.5],
"Japan": [4.9, 4.8, 5.7, 4.4, 5.0, 4.2],
"Germany": [1.9, 2.9, 3.4, 3.4, 3.9, 4.5],
"UK": [1.6, 2.5, 2.4, 2.9, 2.7, 3.1],
"India": [0.5, 0.8, 1.7, 2.1, 2.7, 3.7],
"France": [1.4, 2.2, 2.7, 2.4, 2.6, 2.9],
}

# Calculate market cap for each year
data = {}
for company in companies:
data[company] = [int(base_values[company] * growth_rates[company][i]) for i in range(len(years))]

# Colors for consistent entity tracking - distinct hues to avoid confusion
company_colors = {
"Apple": "#306998", # Python Blue (primary)
"Microsoft": "#FFD43B", # Python Yellow (primary)
"Alphabet": "#34A853", # Google Green (distinct from blues)
"Amazon": "#FF9900", # Amazon Orange
"Meta": "#E040FB", # Purple (distinct from blue tones)
"Tesla": "#CC0000", # Tesla Red
"NVIDIA": "#76B900", # NVIDIA Green
"Samsung": "#795548", # Brown (distinct from all other colors)
}
country_colors = {country: OKABE_ITO[i] for i, country in enumerate(countries)}

# Create individual charts for each year
# Create individual pygal charts for each snapshot year
charts = []
for year_idx, year in enumerate(years):
# Get values for this year and sort by value (descending)
year_data = [(company, data[company][year_idx]) for company in companies]
year_data.sort(key=lambda x: x[1], reverse=True)

# Create style for this chart
year_style = Style(
background="white",
plot_background="white",
foreground="#333333",
foreground_strong="#333333",
foreground_subtle="#666666",
year_data = sorted([(c, gdp_data[c][year_idx]) for c in countries], key=lambda x: x[1], reverse=True)

snap_style = Style(
background=PAGE_BG,
plot_background=PAGE_BG,
foreground=INK,
foreground_strong=INK,
foreground_subtle=INK_MUTED,
title_font_size=52,
label_font_size=36,
major_label_font_size=32,
legend_font_size=32,
value_font_size=32,
tooltip_font_size=28,
value_font_size=28,
)

chart = pygal.HorizontalBar(
width=1500,
height=950,
style=year_style,
show_legend=False, # Disable legend on individual charts - use global legend only
style=snap_style,
show_legend=False,
title=str(year),
x_title="Market Cap ($B)",
x_title="GDP (Trillion USD)",
print_values=True,
print_values_position="middle",
value_formatter=lambda x: f"${x:,.0f}B",
value_formatter=lambda x: f"${x:.1f}T" if x is not None else "",
margin=40,
spacing=12,
truncate_label=-1,
Expand All @@ -99,28 +79,24 @@
show_minor_x_labels=False,
)

# Set x_labels for company names (shown on y-axis for horizontal bar)
chart.x_labels = [company for company, _ in year_data]

# Add each company as a separate series with its value at the correct position
# Use None placeholders for other positions to avoid stacking
num_companies = len(year_data)
for idx, (company, value) in enumerate(year_data):
values = [None] * num_companies
values[idx] = {"value": value, "color": company_colors[company]}
chart.add(company, values)
chart.x_labels = [c for c, _ in year_data]
num_countries = len(year_data)
for idx, (country, value) in enumerate(year_data):
slot = [None] * num_countries
slot[idx] = {"value": value, "color": country_colors[country]}
chart.add(country, slot)

charts.append(chart)

# Render each chart to PNG and combine into grid
# Render each chart to PNG
chart_images = []
for chart in charts:
svg_data = chart.render()
png_data = cairosvg.svg2png(bytestring=svg_data, output_width=1500, output_height=950)
img = Image.open(BytesIO(png_data))
chart_images.append(img)

# Create 3x2 grid layout (4800 x 2700 final size)
# Composite 3×2 grid at 4800 × 2700
grid_width = 4800
grid_height = 2700
title_height = 160
Expand All @@ -129,86 +105,76 @@
cell_width = grid_width // 3
cell_height = content_height // 2

combined = Image.new("RGB", (grid_width, grid_height), "white")
combined = Image.new("RGB", (grid_width, grid_height), PAGE_BG)
draw = ImageDraw.Draw(combined)

# Load fonts
try:
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 72)
legend_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 36)
except OSError:
title_font = ImageFont.load_default()
legend_font = ImageFont.load_default()

# Add main title
title_text = "bar-race-animated · pygal · pyplots.ai"
title_text = "GDP Country Rankings · bar-race-animated · python · pygal · anyplot.ai"
bbox = draw.textbbox((0, 0), title_text, font=title_font)
title_width = bbox[2] - bbox[0]
draw.text(((grid_width - title_width) // 2, 40), title_text, fill="#333333", font=title_font)

# Paste charts into grid
positions = [
(0, 0),
(1, 0),
(2, 0), # Top row: 2019, 2020, 2021
(0, 1),
(1, 1),
(2, 1), # Bottom row: 2022, 2023, 2024
]
draw.text(((grid_width - title_width) // 2, 40), title_text, fill=INK, font=title_font)

positions = [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1)]
for idx, (col, row) in enumerate(positions):
if idx < len(chart_images):
img = chart_images[idx].resize((cell_width, cell_height), Image.Resampling.LANCZOS)
x = col * cell_width
y = title_height + row * cell_height
combined.paste(img, (x, y))

# Add legend at bottom - larger boxes and centered layout
legend_y = grid_height - legend_height + 20
# Legend bar at bottom
legend_y = grid_height - legend_height + 25
box_size = 40
spacing_between = grid_width // len(companies)
legend_x_start = spacing_between // 2 - 80 # Center the legend items
spacing_between = grid_width // len(countries)
legend_x_start = spacing_between // 2 - 80

for i, company in enumerate(companies):
for i, country in enumerate(countries):
x_pos = legend_x_start + i * spacing_between
# Draw color box
draw.rectangle([x_pos, legend_y, x_pos + box_size, legend_y + box_size], fill=company_colors[company])
# Draw company name
draw.text((x_pos + box_size + 12, legend_y - 2), company, fill="#333333", font=legend_font)
draw.rectangle([x_pos, legend_y, x_pos + box_size, legend_y + box_size], fill=country_colors[country])
draw.text((x_pos + box_size + 12, legend_y - 2), country, fill=INK, font=legend_font)

# Save as PNG
combined.save("plot.png", dpi=(300, 300))
combined.save(f"plot-{THEME}.png", dpi=(300, 300))

# Save as HTML (interactive SVG version showing 2024 final state)
# HTML: interactive 2023 snapshot with pygal tooltips
html_style = Style(
background="white",
plot_background="white",
foreground="#333333",
foreground_strong="#333333",
foreground_subtle="#666666",
title_font_size=36,
label_font_size=20,
major_label_font_size=18,
legend_font_size=18,
value_font_size=16,
tooltip_font_size=16,
background=PAGE_BG,
plot_background=PAGE_BG,
foreground=INK,
foreground_strong=INK,
foreground_subtle=INK_MUTED,
colors=OKABE_ITO,
title_font_size=28,
label_font_size=18,
major_label_font_size=16,
legend_font_size=16,
value_font_size=14,
)

html_chart = pygal.HorizontalBar(
width=1200,
height=800,
style=html_style,
show_legend=True,
title="Tech Company Market Cap 2024 · bar-race-animated · pygal · pyplots.ai",
x_title="Market Cap (Billion USD)",
title="GDP Country Rankings 2023 · bar-race-animated · python · pygal · anyplot.ai",
x_title="GDP (Trillion USD)",
print_values=True,
value_formatter=lambda x: f"${x:,.0f}B",
value_formatter=lambda x: f"${x:.1f}T" if x is not None else "",
)

final_data = [(company, data[company][-1]) for company in companies]
final_data.sort(key=lambda x: x[1], reverse=True)
final_data = sorted([(c, gdp_data[c][-1]) for c in countries], key=lambda x: x[1], reverse=True)

for company, value in final_data:
html_chart.add(company, [{"value": value, "color": company_colors[company]}])
html_chart.x_labels = [c for c, _ in final_data]
num_final = len(final_data)
for idx, (country, value) in enumerate(final_data):
slot = [None] * num_final
slot[idx] = {"value": value, "color": country_colors[country]}
html_chart.add(country, slot)

html_chart.render_to_file("plot.html")
with open(f"plot-{THEME}.html", "wb") as f:
f.write(html_chart.render())
Loading
Loading