Skip to content

Commit 820906b

Browse files
feat(pygal): implement dumbbell-basic (#5422)
## Implementation: `dumbbell-basic` - python/pygal Implements the **python/pygal** version of `dumbbell-basic`. **File:** `plots/dumbbell-basic/implementations/python/pygal.py` **Parent Issue:** #945 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24945459219)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent f6d3ac5 commit 820906b

2 files changed

Lines changed: 329 additions & 82 deletions

File tree

Lines changed: 96 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,122 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
dumbbell-basic: Basic Dumbbell Chart
3-
Library: pygal 3.1.0 | Python 3.13.11
4-
Quality: 58/100 | Created: 2025-12-23
3+
Library: pygal 3.1.0 | Python 3.14.4
4+
Quality: 87/100 | Updated: 2026-04-26
55
"""
66

7-
import pygal
8-
from pygal.style import Style
7+
import os
8+
import sys
9+
from pathlib import Path
910

1011

11-
# Data - Employee satisfaction scores before and after policy changes
12-
# Includes positive changes, no change, and slight decrease to demonstrate full capability
13-
categories = ["Engineering", "Marketing", "Sales", "HR", "Finance", "Operations", "Customer Support", "Product"]
14-
before = [65, 58, 72, 45, 71, 52, 70, 70]
15-
after = [82, 75, 78, 72, 65, 71, 70, 85] # Finance decreased (-6), Customer Support unchanged (0)
12+
# Remove script directory from path to avoid name collision with the pygal package
13+
_script_dir = str(Path(__file__).parent)
14+
sys.path = [p for p in sys.path if p != _script_dir]
1615

17-
# Sort by difference (improvement) for better pattern visibility
18-
differences = [a - b for a, b in zip(after, before, strict=True)]
19-
sorted_data = sorted(zip(categories, before, after, differences, strict=True), key=lambda x: x[3], reverse=True)
20-
categories = [item[0] for item in sorted_data]
21-
before = [item[1] for item in sorted_data]
22-
after = [item[2] for item in sorted_data]
16+
import pygal # noqa: E402
17+
from pygal.style import Style # noqa: E402
2318

24-
# Number of categories
19+
20+
# Theme-adaptive chrome tokens
21+
THEME = os.getenv("ANYPLOT_THEME", "light")
22+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
23+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
24+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
25+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
26+
27+
# Okabe-Ito data colors (theme-independent)
28+
BEFORE = "#009E73" # position 1 — brand
29+
AFTER = "#D55E00" # position 2
30+
CONNECTOR = INK_SOFT # neutral chrome that adapts to theme
31+
32+
# Data — Employee satisfaction scores before and after policy changes.
33+
# Hand-picked values include one regression (Legal) to exercise full data range.
34+
categories = [
35+
"Engineering",
36+
"Sales",
37+
"Marketing",
38+
"Customer Support",
39+
"Finance",
40+
"Human Resources",
41+
"Operations",
42+
"Product",
43+
"Legal",
44+
]
45+
before = [62, 71, 58, 45, 68, 52, 64, 73, 70]
46+
after = [78, 82, 75, 69, 74, 71, 79, 85, 67]
47+
48+
# Sort by improvement (largest at top)
49+
data = sorted(zip(categories, before, after, strict=True), key=lambda x: x[2] - x[1], reverse=True)
50+
categories = [d[0] for d in data]
51+
before = [d[1] for d in data]
52+
after = [d[2] for d in data]
2553
n = len(categories)
2654

27-
# Custom style for 4800x2700 canvas
28-
# Colors: gray for connecting lines (8 series), then blue for before, yellow for after
29-
connector_colors = tuple(["#888888"] * n) # Gray for each connector line
55+
# Y positions: top row = biggest improvement (first sorted item)
56+
y_positions = list(range(n, 0, -1))
57+
58+
# Series colors map 1:1 to the order series are added below:
59+
# n connector series (drawn first, underneath) then 2 dot series.
60+
colors_tuple = (CONNECTOR,) * n + (BEFORE, AFTER)
61+
3062
custom_style = Style(
31-
background="white",
32-
plot_background="white",
33-
foreground="#333333",
34-
foreground_strong="#333333",
35-
foreground_subtle="#AAAAAA",
36-
guide_stroke_color="rgba(200, 200, 200, 0.3)", # Subtle grid with low opacity
37-
guide_stroke_dasharray="5,5", # Dashed grid for subtlety
38-
colors=connector_colors + ("#306998", "#FFD43B"), # Gray connectors, Blue before, Yellow after
39-
title_font_size=72,
40-
label_font_size=48,
41-
major_label_font_size=42,
42-
legend_font_size=56,
43-
value_font_size=36,
44-
value_label_font_size=36,
45-
stroke_width=5, # Default stroke width for connecting lines
63+
background=PAGE_BG,
64+
plot_background=PAGE_BG,
65+
foreground=INK,
66+
foreground_strong=INK,
67+
foreground_subtle=INK_MUTED,
68+
colors=colors_tuple,
69+
title_font_size=32,
70+
label_font_size=22,
71+
major_label_font_size=20,
72+
legend_font_size=20,
73+
value_font_size=16,
74+
stroke_width=4,
75+
opacity=1.0,
76+
opacity_hover=0.85,
4677
)
4778

48-
# Create XY chart for dumbbell visualization
4979
chart = pygal.XY(
5080
width=4800,
5181
height=2700,
52-
title="dumbbell-basic · pygal · pyplots.ai",
53-
x_title="Satisfaction Score (%)",
5482
style=custom_style,
83+
title="Employee Satisfaction · dumbbell-basic · pygal · anyplot.ai",
84+
x_title="Satisfaction Score (out of 100)",
85+
y_title="Department",
5586
show_legend=True,
5687
legend_at_bottom=True,
57-
legend_box_size=28,
58-
dots_size=20,
59-
stroke=True, # Enable stroke globally
60-
show_y_guides=True,
61-
show_x_guides=True,
88+
legend_at_bottom_columns=2,
89+
legend_box_size=36,
6290
margin=80,
63-
margin_bottom=120,
64-
xrange=(30, 100),
91+
show_x_guides=True,
92+
show_y_guides=False,
93+
xrange=(35, 95),
6594
range=(0, n + 1),
66-
y_labels=[{"label": cat, "value": n - i} for i, cat in enumerate(categories)],
95+
y_labels=[{"label": cat, "value": pos} for cat, pos in zip(categories, y_positions, strict=True)],
96+
truncate_legend=-1,
97+
truncate_label=-1,
98+
dots_size=22,
99+
stroke=False,
67100
)
68101

69-
# Add connecting lines (gray) - each dumbbell gets its own series with stroke enabled
70-
for i, (_cat, b, a) in enumerate(zip(categories, before, after, strict=True)):
71-
y_pos = n - i
72-
# Use explicit stroke and minimal dot size for connecting lines
73-
chart.add(None, [(b, y_pos), (a, y_pos)], stroke=True, show_dots=False)
102+
# Connector lines first so they sit underneath the dots.
103+
# title=None suppresses the legend entry while still rendering the series.
104+
for b, a, pos in zip(before, after, y_positions, strict=True):
105+
chart.add(None, [(b, pos), (a, pos)], stroke=True, show_dots=False, stroke_style={"width": 5, "linecap": "round"})
74106

75-
# Add "Before" dots (Python Blue) - circles without connecting stroke
76-
before_points = [(b, n - i) for i, b in enumerate(before)]
77-
chart.add("Before Policy Change", before_points, dots_size=25, stroke=False)
107+
# Before dots — Okabe-Ito green
108+
before_points = [
109+
{"value": (b, pos), "label": f"{cat}: {b}"} for cat, b, pos in zip(categories, before, y_positions, strict=True)
110+
]
111+
chart.add("Before policy change", before_points, stroke=False, dots_size=24)
78112

79-
# Add "After" dots (Python Yellow) - circles without connecting stroke
80-
after_points = [(a, n - i) for i, a in enumerate(after)]
81-
chart.add("After Policy Change", after_points, dots_size=25, stroke=False)
113+
# After dots — Okabe-Ito vermillion
114+
after_points = [
115+
{"value": (a, pos), "label": f"{cat}: {a}"} for cat, a, pos in zip(categories, after, y_positions, strict=True)
116+
]
117+
chart.add("After policy change", after_points, stroke=False, dots_size=24)
82118

83119
# Save outputs
84-
chart.render_to_png("plot.png")
85-
chart.render_to_file("plot.html")
120+
chart.render_to_png(f"plot-{THEME}.png")
121+
with open(f"plot-{THEME}.html", "wb") as f:
122+
f.write(chart.render())

0 commit comments

Comments
 (0)