|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | slope-basic: Basic Slope Chart (Slopegraph) |
3 | | -Library: matplotlib 3.10.8 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: matplotlib 3.10.9 | Python 3.13.13 |
| 4 | +Quality: 90/100 | Updated: 2026-04-30 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | import matplotlib.pyplot as plt |
| 10 | +import matplotlib.ticker as mticker |
8 | 11 | from matplotlib.lines import Line2D |
9 | 12 |
|
10 | 13 |
|
11 | | -# Data: Sales figures (in millions $) for products comparing Q1 vs Q4 |
12 | | -# Deterministic data with well-spaced values to avoid label overlap |
| 14 | +# Theme |
| 15 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 16 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 17 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 18 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 19 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 20 | + |
| 21 | +COLOR_INC = "#009E73" # Okabe-Ito pos 1 — increase (brand green) |
| 22 | +COLOR_DEC = "#D55E00" # Okabe-Ito pos 2 — decrease (vermillion) |
| 23 | + |
| 24 | +# Data |
13 | 25 | products = ["Product A", "Product B", "Product C", "Product D", "Product E", "Product F", "Product G", "Product H"] |
14 | 26 | q1_sales = [3.0, 6.5, 10.0, 13.5, 17.0, 20.5, 24.0, 27.5] |
15 | 27 | q4_sales = [5.5, 4.0, 14.0, 11.0, 20.0, 17.5, 28.0, 25.0] |
16 | | - |
17 | | -# Calculate changes to determine colors |
18 | 28 | changes = [q4 - q1 for q1, q4 in zip(q1_sales, q4_sales, strict=True)] |
19 | 29 |
|
20 | | -# Create figure |
21 | | -fig, ax = plt.subplots(figsize=(16, 9)) |
22 | | - |
23 | | -# Label collision avoidance: sort and adjust positions if too close |
| 30 | +# Label collision avoidance: sort by value and nudge positions if too close |
24 | 31 | min_gap = 1.8 |
25 | 32 |
|
26 | | -# Adjust Q1 labels (left side) |
27 | 33 | q1_indexed = sorted(enumerate(q1_sales), key=lambda x: x[1]) |
28 | 34 | q1_label_pos = [0.0] * len(q1_sales) |
29 | 35 | for i, (orig_idx, val) in enumerate(q1_indexed): |
|
36 | 42 | else: |
37 | 43 | q1_label_pos[orig_idx] = val |
38 | 44 |
|
39 | | -# Adjust Q4 labels (right side) |
40 | 45 | q4_indexed = sorted(enumerate(q4_sales), key=lambda x: x[1]) |
41 | 46 | q4_label_pos = [0.0] * len(q4_sales) |
42 | 47 | for i, (orig_idx, val) in enumerate(q4_indexed): |
|
49 | 54 | else: |
50 | 55 | q4_label_pos[orig_idx] = val |
51 | 56 |
|
52 | | -# Plot each slope line |
| 57 | +# Plot |
| 58 | +fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG) |
| 59 | +ax.set_facecolor(PAGE_BG) |
| 60 | + |
53 | 61 | x_positions = [0, 1] |
| 62 | + |
| 63 | +# Vertical column lines at axis positions — structural anchors for the slopegraph |
| 64 | +for x in x_positions: |
| 65 | + ax.axvline(x, color=INK_SOFT, linewidth=1.2, alpha=0.35, zorder=0) |
| 66 | + |
54 | 67 | for i, (product, q1, q4, change) in enumerate(zip(products, q1_sales, q4_sales, changes, strict=True)): |
55 | | - # Color by direction: blue for increase, yellow/orange for decrease |
56 | | - color = "#306998" if change >= 0 else "#FFD43B" |
57 | | - ax.plot(x_positions, [q1, q4], marker="o", markersize=12, linewidth=3, color=color) |
58 | | - |
59 | | - # Labels at both endpoints with adjusted positions to avoid overlap |
60 | | - ax.text( |
61 | | - -0.05, |
62 | | - q1_label_pos[i], |
63 | | - f"{product}: ${q1:.1f}M", |
64 | | - ha="right", |
65 | | - va="center", |
66 | | - fontsize=14, |
| 68 | + color = COLOR_INC if change >= 0 else COLOR_DEC |
| 69 | + ax.plot( |
| 70 | + x_positions, |
| 71 | + [q1, q4], |
| 72 | + marker="o", |
| 73 | + markersize=12, |
| 74 | + linewidth=3, |
67 | 75 | color=color, |
68 | | - fontweight="bold", |
| 76 | + markeredgecolor=PAGE_BG, |
| 77 | + markeredgewidth=1.5, |
69 | 78 | ) |
70 | | - ax.text(1.05, q4_label_pos[i], f"${q4:.1f}M", ha="left", va="center", fontsize=14, color=color, fontweight="bold") |
71 | 79 |
|
72 | | -# Style the axes |
73 | | -ax.set_xlim(-0.6, 1.6) |
| 80 | + # Left label: product name + Q1 value; dotted connector if label was nudged |
| 81 | + lpos = q1_label_pos[i] |
| 82 | + if abs(lpos - q1) > 0.05: |
| 83 | + ax.plot([-0.03, -0.03], [q1, lpos], color=color, linewidth=0.8, alpha=0.35, linestyle=":") |
| 84 | + ax.text(-0.06, lpos, f"{product}: ${q1:.1f}M", ha="right", va="center", fontsize=16, color=color, fontweight="bold") |
| 85 | + |
| 86 | + # Right label: product name + Q4 value; dotted connector if label was nudged |
| 87 | + rpos = q4_label_pos[i] |
| 88 | + if abs(rpos - q4) > 0.05: |
| 89 | + ax.plot([1.03, 1.03], [q4, rpos], color=color, linewidth=0.8, alpha=0.35, linestyle=":") |
| 90 | + ax.text(1.06, rpos, f"{product}: ${q4:.1f}M", ha="left", va="center", fontsize=16, color=color, fontweight="bold") |
| 91 | + |
| 92 | +# Style |
| 93 | +ax.set_xlim(-0.75, 1.75) |
74 | 94 | ax.set_xticks(x_positions) |
75 | | -ax.set_xticklabels(["Q1 2024", "Q4 2024"], fontsize=20, fontweight="bold") |
76 | | -ax.set_ylabel("Sales (Millions $)", fontsize=20) |
77 | | -ax.set_title("slope-basic · matplotlib · pyplots.ai", fontsize=24) |
| 95 | +ax.set_xticklabels(["Q1 2024", "Q4 2024"], fontsize=20, fontweight="bold", color=INK) |
| 96 | +ax.set_ylabel("Sales", fontsize=20, color=INK) |
| 97 | +ax.set_title("slope-basic · matplotlib · anyplot.ai", fontsize=24, fontweight="medium", color=INK) |
| 98 | + |
| 99 | +ax.tick_params(axis="x", length=0) |
| 100 | +ax.tick_params(axis="y", labelsize=16, labelcolor=INK_SOFT, colors=INK_SOFT) |
| 101 | + |
| 102 | +# FuncFormatter for y-axis: show units inline as "$XM" for self-documenting tick labels |
| 103 | +ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda val, _: f"${val:.0f}M")) |
78 | 104 |
|
79 | | -# Remove spines and set grid |
80 | 105 | ax.spines["top"].set_visible(False) |
81 | 106 | ax.spines["right"].set_visible(False) |
82 | 107 | ax.spines["bottom"].set_visible(False) |
83 | | -ax.tick_params(axis="y", labelsize=16) |
84 | | -ax.tick_params(axis="x", length=0) |
85 | | -ax.grid(True, axis="y", alpha=0.3, linestyle="--") |
| 108 | +ax.spines["left"].set_color(INK_SOFT) |
| 109 | + |
| 110 | +ax.grid(True, axis="y", alpha=0.10, linewidth=0.8, color=INK) |
86 | 111 |
|
87 | | -# Add legend for color coding |
| 112 | +# Legend centered below title |
88 | 113 | legend_elements = [ |
89 | | - Line2D([0], [0], color="#306998", linewidth=3, label="Increase"), |
90 | | - Line2D([0], [0], color="#FFD43B", linewidth=3, label="Decrease"), |
| 114 | + Line2D( |
| 115 | + [0], [0], color=COLOR_INC, linewidth=3, marker="o", markersize=10, markeredgecolor=PAGE_BG, label="Increase" |
| 116 | + ), |
| 117 | + Line2D( |
| 118 | + [0], [0], color=COLOR_DEC, linewidth=3, marker="o", markersize=10, markeredgecolor=PAGE_BG, label="Decrease" |
| 119 | + ), |
91 | 120 | ] |
92 | | -ax.legend(handles=legend_elements, loc="upper right", fontsize=16) |
| 121 | +leg = ax.legend( |
| 122 | + handles=legend_elements, loc="upper center", bbox_to_anchor=(0.5, 0.98), fontsize=16, frameon=True, ncol=2 |
| 123 | +) |
| 124 | +leg.get_frame().set_facecolor(ELEVATED_BG) |
| 125 | +leg.get_frame().set_edgecolor(INK_SOFT) |
| 126 | +plt.setp(leg.get_texts(), color=INK_SOFT) |
93 | 127 |
|
94 | 128 | plt.tight_layout() |
95 | | -plt.savefig("plot.png", dpi=300, bbox_inches="tight") |
| 129 | +plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG) |
0 commit comments