Skip to content

Commit 7161b42

Browse files
feat(pygal): implement span-basic (#5614)
## Implementation: `span-basic` - python/pygal Implements the **python/pygal** version of `span-basic`. **File:** `plots/span-basic/implementations/python/pygal.py` **Parent Issue:** #980 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25161205000)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 6cfbc51 commit 7161b42

2 files changed

Lines changed: 296 additions & 80 deletions

File tree

Lines changed: 60 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,96 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
span-basic: Basic Span Plot (Highlighted Region)
3-
Library: pygal 3.1.0 | Python 3.13.11
4-
Quality: 94/100 | Created: 2025-12-17
3+
Library: pygal 3.1.0 | Python 3.13.13
4+
Quality: 86/100 | Created: 2026-04-30
55
"""
66

7+
import os
8+
import sys
9+
710
import numpy as np
8-
import pygal
9-
from pygal.style import Style
1011

1112

12-
# Data - Stock prices with highlighted recession period
13+
# Pop script dir so this file (pygal.py) doesn't shadow the installed pygal package
14+
_script_dir = sys.path.pop(0)
15+
import pygal # noqa: E402
16+
from pygal.style import Style # noqa: E402
17+
18+
19+
sys.path.insert(0, _script_dir)
20+
21+
# Theme tokens
22+
THEME = os.getenv("ANYPLOT_THEME", "light")
23+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
24+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
25+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
26+
27+
# Boost span opacity on dark backgrounds so fills stay visible as "highlight regions"
28+
SPAN_OPACITY = ".45" if THEME == "dark" else ".25"
29+
30+
OKABE_ITO = ("#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442")
31+
32+
# Data - Stock prices with highlighted events
1333
np.random.seed(42)
14-
dates = np.arange(2006, 2016, 0.1) # 10 years of data
15-
# Simulate stock price with trend and volatility
34+
dates = np.arange(2006, 2016, 0.1)
1635
price = 100 + np.cumsum(np.random.randn(len(dates)) * 2)
17-
# Add a dip during recession period (2008-2009)
1836
recession_mask = (dates >= 2008) & (dates < 2010)
1937
price[recession_mask] -= np.linspace(0, 30, recession_mask.sum())
2038
price[dates >= 2010] -= 30
21-
price = price + np.abs(price.min()) + 50 # Keep positive
39+
price = price + np.abs(price.min()) + 50
2240

23-
# Calculate range for spans
24-
y_min = 40
25-
y_max = price.max() + 20
41+
y_min = 40.0
42+
y_max = float(price.max()) + 20
2643

27-
# Custom style with scaled sizes for 4800x2700 output
44+
# Style
2845
custom_style = Style(
29-
background="white",
30-
plot_background="white",
31-
foreground="#333333",
32-
foreground_strong="#333333",
33-
foreground_subtle="#666666",
34-
colors=("#FFD43B", "#D62728", "#306998"), # Yellow span, Red span, Blue line
35-
opacity=".25", # Semi-transparent for span regions
36-
opacity_hover=".4",
37-
title_font_size=60,
38-
label_font_size=36,
39-
major_label_font_size=36,
40-
legend_font_size=36,
41-
value_font_size=32,
42-
stroke_width="4",
46+
background=PAGE_BG,
47+
plot_background=PAGE_BG,
48+
foreground=INK,
49+
foreground_strong=INK,
50+
foreground_subtle=INK_MUTED,
51+
colors=OKABE_ITO,
52+
opacity=SPAN_OPACITY,
53+
opacity_hover=".5",
54+
title_font_size=32,
55+
label_font_size=22,
56+
major_label_font_size=18,
57+
legend_font_size=18,
58+
value_font_size=16,
59+
stroke_width=3,
4360
)
4461

45-
# Create XY chart with fill enabled for span regions
62+
# Plot — x-guides disabled for a cleaner grid; human_readable for polished tooltips
4663
chart = pygal.XY(
4764
style=custom_style,
4865
width=4800,
4966
height=2700,
50-
title="span-basic · pygal · pyplots.ai",
67+
title="span-basic · pygal · anyplot.ai",
5168
x_title="Year",
5269
y_title="Price ($)",
5370
show_dots=False,
54-
show_x_guides=True,
71+
show_x_guides=False,
5572
show_y_guides=True,
5673
range=(y_min, y_max),
5774
xrange=(2005.5, 2016.5),
58-
fill=True, # Enable fill for closed polygons
75+
fill=True,
5976
stroke=True,
6077
dots_size=0,
78+
human_readable=True,
6179
)
6280

63-
# Vertical span: Recession Period (2008-2009) - full height rectangle
64-
# Create closed polygon: bottom-left -> top-left -> top-right -> bottom-right -> close
65-
recession_span = [
66-
(2008, y_min),
67-
(2008, y_max),
68-
(2009, y_max),
69-
(2009, y_min),
70-
(2008, y_min), # Close the polygon
71-
]
81+
# Vertical span: Recession Period (2008-2009) — closed polygon
82+
recession_span = [(2008, y_min), (2008, y_max), (2009, y_max), (2009, y_min), (2008, y_min)]
7283
chart.add("Recession Period", recession_span)
7384

74-
# Horizontal span: Risk Zone (60-80) - full width rectangle
75-
risk_span = [
76-
(2005.5, 60),
77-
(2005.5, 80),
78-
(2016.5, 80),
79-
(2016.5, 60),
80-
(2005.5, 60), # Close the polygon
81-
]
85+
# Horizontal span: Risk Zone (price 60–80) — closed polygon
86+
risk_span = [(2005.5, 60), (2005.5, 80), (2016.5, 80), (2016.5, 60), (2005.5, 60)]
8287
chart.add("Risk Zone", risk_span)
8388

84-
# Main line data (no fill, just stroke)
85-
main_data = [(float(x), float(y)) for x, y in zip(dates, price, strict=True)]
89+
# Main line — dict format enables per-point custom tooltip labels (pygal interactive feature)
90+
main_data = [{"value": (float(x), float(y)), "label": f"${y:.0f}"} for x, y in zip(dates, price, strict=True)]
8691
chart.add("Stock Price", main_data, fill=False, stroke=True)
8792

88-
# Save outputs
89-
chart.render_to_png("plot.png")
90-
chart.render_to_file("plot.html")
93+
# Save
94+
chart.render_to_png(f"plot-{THEME}.png")
95+
with open(f"plot-{THEME}.html", "wb") as f:
96+
f.write(chart.render())

0 commit comments

Comments
 (0)