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
159 changes: 96 additions & 63 deletions plots/scatter-connected-temporal/implementations/python/pygal.py
Original file line number Diff line number Diff line change
@@ -1,109 +1,124 @@
""" pyplots.ai
""" anyplot.ai
scatter-connected-temporal: Connected Scatter Plot with Temporal Path
Library: pygal 3.1.0 | Python 3.14.3
Quality: 87/100 | Created: 2026-03-13
Library: pygal 3.1.0 | Python 3.13.13
Quality: 84/100 | Updated: 2026-06-09
"""

import os
import re
import sys


# Remove script's own directory from sys.path so the real pygal package is found first
_here = os.path.dirname(os.path.abspath(__file__))
if _here in sys.path:
sys.path.remove(_here)

import cairosvg
import numpy as np
import pygal
from pygal.style import Style


# Data — Life expectancy vs GDP per capita for a fictional country (1990-2023)
# 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"

# Imprint palette — amber semantic anchor for key events
ANYPLOT_AMBER = "#DDCC77"

# Data — Life expectancy vs GDP per capita for a developing country (1990–2023)
np.random.seed(42)
years = list(range(1990, 2024))
n_years = len(years)

# GDP per capita starts ~8000, grows with occasional dips (recessions)
gdp_base = 8000
gdp_growth = np.cumsum(np.random.normal(450, 300, n_years))
gdp_growth[8:10] -= 1500 # 1998-1999 recession dip
gdp_growth[18:20] -= 2000 # 2008-2009 financial crisis
gdp_growth[30:32] -= 800 # 2020-2021 pandemic dip
gdp_growth[8:10] -= 1500 # 19981999 recession
gdp_growth[18:20] -= 2000 # 20082009 financial crisis
gdp_growth[30:32] -= 800 # 20202021 pandemic
gdp_per_capita = gdp_base + gdp_growth
gdp_per_capita = np.maximum(gdp_per_capita, 5000)

# Life expectancy starts ~68, rises gradually with dips during crises
le_base = 68.0
le_growth = np.cumsum(np.random.normal(0.25, 0.12, n_years))
le_growth[18:20] -= 0.4 # Financial crisis stress
le_growth[30:32] -= 1.2 # Pandemic drop
le_growth[18:20] -= 0.4
le_growth[30:32] -= 1.2
life_expectancy = le_base + le_growth
life_expectancy = np.clip(life_expectancy, 64, 82)

# Define temporal eras for color gradient segments (light → dark blue)
# Darkened lightest shades for better contrast against light background
# Imprint imprint_seq gradient (#009E73 → #4467A3) for temporal progression
eras = [
("1990-1997", 0, 8, "#4da6c9"),
("1998-2003", 8, 14, "#3d8fb5"),
("2004-2009", 14, 20, "#2e78a0"),
("2010-2015", 20, 26, "#22618a"),
("2016-2019", 26, 30, "#1a4d70"),
("2020-2023", 30, 34, "#0e2f44"),
("19901997", 0, 8, "#009E73"),
("19982003", 8, 14, "#0E937D"),
("20042009", 14, 20, "#1B8886"),
("20102015", 20, 26, "#297D90"),
("20162019", 26, 30, "#367299"),
("20202023", 30, 34, "#4467A3"),
]

# Select key years to annotate
annotate_years = {1998, 2005, 2008, 2015, 2020}

# Style — refined with subtle grid and cohesive blue palette
# Title scaled for 81-char length: round(66 × 67/81) = 55
title = "Life Expectancy vs GDP · scatter-connected-temporal · python · pygal · anyplot.ai"
title_font_size = max(44, round(66 * 67 / len(title)))

font = "DejaVu Sans, Helvetica, Arial, sans-serif"
custom_style = Style(
background="white",
plot_background="#f4f7fa",
foreground="#3a3a3a",
foreground_strong="#2a2a2a",
foreground_subtle="#d8d8d8",
guide_stroke_color="#e0e0e0",
background=PAGE_BG,
plot_background=PAGE_BG,
foreground=INK,
foreground_strong=INK,
foreground_subtle=INK_MUTED,
guide_stroke_color=INK_MUTED,
guide_stroke_dasharray="3,5",
colors=("#4da6c9", "#3d8fb5", "#2e78a0", "#22618a", "#1a4d70", "#0e2f44", "#e8a838", "#c0392b", "#1a3a5c"),
colors=("#009E73", "#0E937D", "#1B8886", "#297D90", "#367299", "#4467A3", ANYPLOT_AMBER, "#AE3030", "#4467A3"),
font_family=font,
title_font_family=font,
title_font_size=52,
label_font_size=40,
major_label_font_size=36,
legend_font_size=34,
legend_font_family=font,
value_font_size=32,
value_label_font_size=36,
tooltip_font_size=26,
title_font_size=title_font_size,
label_font_size=56,
major_label_font_size=44,
legend_font_size=44,
value_font_size=36,
value_label_font_size=44,
tooltip_font_size=36,
tooltip_font_family=font,
opacity=0.92,
opacity_hover=1.0,
stroke_opacity=0.9,
stroke_opacity_hover=1.0,
)

# Axis ranges
x_min = float(np.floor(gdp_per_capita.min() / 1000) * 1000)
x_max = float(np.ceil(gdp_per_capita.max() / 1000) * 1000)
y_min = float(np.floor(life_expectancy.min()))
y_max = float(np.ceil(life_expectancy.max()) + 1)

# Create XY chart with native print_labels for year annotations
chart = pygal.XY(
width=4800,
height=2700,
width=3200,
height=1800,
style=custom_style,
title="Life Expectancy vs GDP · scatter-connected-temporal · pygal · pyplots.ai",
title=title,
x_title="GDP per Capita (USD)",
y_title="Life Expectancy (years)",
show_legend=True,
legend_at_bottom=True,
legend_at_bottom_columns=3,
legend_box_size=24,
legend_box_size=28,
stroke=True,
dots_size=10,
dots_size=12,
show_x_guides=True,
show_y_guides=True,
x_value_formatter=lambda x: f"${x:,.0f}",
x_value_formatter=lambda x: f"${x / 1000:.0f}k",
value_formatter=lambda y: f"{y:.1f} yrs",
print_labels=True,
print_values=False,
margin_bottom=120,
margin_left=70,
margin_right=70,
margin_top=55,
margin_bottom=130,
margin_left=80,
margin_right=80,
margin_top=60,
range=(y_min, y_max),
xrange=(x_min, x_max),
x_labels_major_count=7,
Expand All @@ -113,8 +128,7 @@
show_y_labels=True,
)

# Add temporal path as gradient-colored era segments
# Each segment overlaps by 1 point for visual continuity
# Temporal path as Imprint imprint_seq gradient era segments
for era_name, start, end, color in eras:
end_idx = min(end + 1, n_years)
segment_points = [
Expand All @@ -125,34 +139,53 @@
segment_points,
stroke=True,
show_dots=True,
dots_size=10,
stroke_style={"width": 6, "linecap": "round", "linejoin": "round"},
dots_size=12,
stroke_style={"width": 5, "linecap": "round", "linejoin": "round"},
)

# Highlight annotated key years with larger amber dots and native year labels
# Key years highlighted with amber dots and year labels
annotated_points = []
for yr in sorted(annotate_years):
i = yr - 1990
annotated_points.append(
{"value": (float(gdp_per_capita[i]), float(life_expectancy[i])), "label": str(yr), "color": "#e8a838"}
{"value": (float(gdp_per_capita[i]), float(life_expectancy[i])), "label": str(yr), "color": ANYPLOT_AMBER}
)
chart.add("Key years", annotated_points, stroke=False, dots_size=16)
chart.add("Key years", annotated_points, stroke=False, dots_size=20)

# Highlight start and end with distinct large markers
# Start and end markers
chart.add(
f"Start ({years[0]})",
[{"value": (float(gdp_per_capita[0]), float(life_expectancy[0])), "label": "\u25b6 1990", "color": "#c0392b"}],
[{"value": (float(gdp_per_capita[0]), float(life_expectancy[0])), "label": " 1990", "color": "#AE3030"}],
stroke=False,
dots_size=22,
dots_size=26,
)
chart.add(
f"End ({years[-1]})",
[{"value": (float(gdp_per_capita[-1]), float(life_expectancy[-1])), "label": "\u25cf 2023", "color": "#1a3a5c"}],
[{"value": (float(gdp_per_capita[-1]), float(life_expectancy[-1])), "label": " 2023", "color": "#4467A3"}],
stroke=False,
dots_size=22,
dots_size=26,
)

# Save PNG via cairosvg and HTML
# Patch label text colors for dark-theme legibility before PNG conversion
# pygal's print_labels text color does not adapt to the dark background via the foreground Style token
_label_texts = {str(yr) for yr in sorted(annotate_years)} | {"▶ 1990", "● 2023"}


def _patch_label_colors(svg_str, labels, fill_color):
def _fix(m):
tag_attrs, content = m.group(1), m.group(2)
if not any(lbl in content for lbl in labels):
return m.group(0)
if "fill=" in tag_attrs:
tag_attrs = re.sub(r'\bfill="[^"]*"', f'fill="{fill_color}"', tag_attrs)
else:
tag_attrs += f' fill="{fill_color}"'
return f"<text{tag_attrs}>{content}</text>"

return re.sub(r"<text([^>]*)>(.*?)</text>", _fix, svg_str, flags=re.DOTALL)


svg_data = chart.render()
cairosvg.svg2png(bytestring=svg_data, write_to="plot.png")
chart.render_to_file("plot.html")
svg_str = _patch_label_colors(svg_data.decode("utf-8"), _label_texts, INK)
cairosvg.svg2png(bytestring=svg_str.encode("utf-8"), write_to=f"plot-{THEME}.png")
chart.render_to_file(f"plot-{THEME}.html")
Loading
Loading