|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
7 | | -import pygal |
8 | | -from pygal.style import Style |
| 7 | +import os |
| 8 | +import sys |
| 9 | +from pathlib import Path |
9 | 10 |
|
10 | 11 |
|
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] |
16 | 15 |
|
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 |
23 | 18 |
|
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] |
25 | 53 | n = len(categories) |
26 | 54 |
|
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 | + |
30 | 62 | 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, |
46 | 77 | ) |
47 | 78 |
|
48 | | -# Create XY chart for dumbbell visualization |
49 | 79 | chart = pygal.XY( |
50 | 80 | width=4800, |
51 | 81 | height=2700, |
52 | | - title="dumbbell-basic · pygal · pyplots.ai", |
53 | | - x_title="Satisfaction Score (%)", |
54 | 82 | style=custom_style, |
| 83 | + title="Employee Satisfaction · dumbbell-basic · pygal · anyplot.ai", |
| 84 | + x_title="Satisfaction Score (out of 100)", |
| 85 | + y_title="Department", |
55 | 86 | show_legend=True, |
56 | 87 | 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, |
62 | 90 | margin=80, |
63 | | - margin_bottom=120, |
64 | | - xrange=(30, 100), |
| 91 | + show_x_guides=True, |
| 92 | + show_y_guides=False, |
| 93 | + xrange=(35, 95), |
65 | 94 | 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, |
67 | 100 | ) |
68 | 101 |
|
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"}) |
74 | 106 |
|
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) |
78 | 112 |
|
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) |
82 | 118 |
|
83 | 119 | # 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