|
1 | 1 | """ anyplot.ai |
2 | 2 | lollipop-basic: Basic Lollipop Chart |
3 | | -Library: matplotlib 3.10.9 | Python 3.14.4 |
4 | | -Quality: 88/100 | Updated: 2026-04-26 |
| 3 | +Library: matplotlib 3.11.0 | Python 3.13.14 |
| 4 | +Quality: 87/100 | Updated: 2026-07-01 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import os |
8 | 8 |
|
9 | 9 | import matplotlib.pyplot as plt |
| 10 | +import matplotlib.ticker as mticker |
10 | 11 | import numpy as np |
11 | 12 |
|
12 | 13 |
|
|
15 | 16 | ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
16 | 17 | INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
17 | 18 | INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 19 | +INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" |
18 | 20 |
|
| 21 | +# Imprint palette — first series always brand green |
19 | 22 | BRAND_GREEN = "#009E73" |
20 | 23 |
|
21 | | -# Data: Product sales by category |
| 24 | +# Data: retail category sales (thousands) |
22 | 25 | categories = [ |
23 | 26 | "Electronics", |
24 | 27 | "Clothing", |
|
33 | 36 | ] |
34 | 37 | values = [87, 72, 65, 58, 52, 45, 41, 38, 32, 25] |
35 | 38 |
|
36 | | -# Sort by value descending for clear ranking |
37 | | -sorted_indices = np.argsort(values)[::-1] |
38 | | -categories = [categories[i] for i in sorted_indices] |
39 | | -values = [values[i] for i in sorted_indices] |
| 39 | +# Sort descending for clear ranking story |
| 40 | +order = np.argsort(values)[::-1] |
| 41 | +categories = [categories[i] for i in order] |
| 42 | +values = np.array([values[i] for i in order]) |
40 | 43 |
|
41 | | -fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG) |
| 44 | +avg_val = values.mean() |
| 45 | + |
| 46 | +fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG) |
42 | 47 | ax.set_facecolor(PAGE_BG) |
43 | 48 |
|
44 | | -x_positions = np.arange(len(categories)) |
45 | | -ax.vlines(x_positions, ymin=0, ymax=values, color=BRAND_GREEN, linewidth=2.5) |
46 | | -ax.scatter(x_positions, values, color=BRAND_GREEN, s=300, zorder=3, edgecolors=PAGE_BG, linewidths=1.5) |
| 49 | +x = np.arange(len(categories)) |
| 50 | + |
| 51 | +# Stems |
| 52 | +ax.vlines(x, ymin=0, ymax=values, color=BRAND_GREEN, linewidth=2.0, zorder=2) |
| 53 | + |
| 54 | +# Markers — slightly larger for the top performer to create focal point |
| 55 | +marker_sizes = np.where(x == 0, 120, 80) |
| 56 | +ax.scatter(x, values, color=BRAND_GREEN, s=marker_sizes, zorder=3, edgecolors=PAGE_BG, linewidths=1.0) |
| 57 | + |
| 58 | +# Value labels above each marker |
| 59 | +for xi, v in zip(x, values, strict=False): |
| 60 | + ax.text(xi, v + 2.5, f"{v}K", ha="center", va="bottom", fontsize=8, color=INK_SOFT, fontweight="medium") |
| 61 | + |
| 62 | +# Average reference line — structural anchor for context |
| 63 | +ax.axhline(avg_val, color=INK_MUTED, linewidth=0.9, linestyle="--", zorder=1) |
| 64 | +ax.text(len(x) - 0.5, avg_val + 1.5, f"avg {avg_val:.0f}K", ha="right", va="bottom", fontsize=7.5, color=INK_MUTED) |
| 65 | + |
| 66 | +# Callout annotation on the top performer using matplotlib's annotation API |
| 67 | +ax.annotate( |
| 68 | + "Top performer", |
| 69 | + xy=(x[0], values[0]), |
| 70 | + xytext=(x[0] + 0.9, values[0] + 13), |
| 71 | + fontsize=6.5, |
| 72 | + color=INK, |
| 73 | + ha="center", |
| 74 | + arrowprops={"arrowstyle": "->", "color": INK_MUTED, "lw": 0.8}, |
| 75 | + bbox={"facecolor": ELEVATED_BG, "edgecolor": INK_SOFT, "boxstyle": "round,pad=0.3", "linewidth": 0.6}, |
| 76 | +) |
| 77 | + |
| 78 | +title = "lollipop-basic · python · matplotlib · anyplot.ai" |
| 79 | +ax.set_title(title, fontsize=12, fontweight="medium", color=INK, pad=8) |
| 80 | +ax.set_xlabel("Product Category", fontsize=10, color=INK) |
| 81 | +ax.set_ylabel("Sales (thousands)", fontsize=10, color=INK) |
47 | 82 |
|
48 | | -ax.set_xlabel("Product Category", fontsize=20, color=INK) |
49 | | -ax.set_ylabel("Sales (thousands)", fontsize=20, color=INK) |
50 | | -ax.set_title("lollipop-basic · matplotlib · anyplot.ai", fontsize=24, fontweight="medium", color=INK) |
| 83 | +ax.set_xticks(x) |
| 84 | +ax.set_xticklabels(categories, rotation=45, ha="right", fontsize=8) |
| 85 | +ax.tick_params(axis="both", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT) |
51 | 86 |
|
52 | | -ax.set_xticks(x_positions) |
53 | | -ax.set_xticklabels(categories, rotation=45, ha="right", fontsize=16) |
54 | | -ax.tick_params(axis="both", labelsize=16, colors=INK_SOFT, labelcolor=INK_SOFT) |
| 87 | +ax.set_ylim(0, max(values) * 1.38) |
55 | 88 |
|
56 | | -ax.set_ylim(0, max(values) * 1.1) |
| 89 | +# Tidy y-axis numeric labels via FuncFormatter |
| 90 | +ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:.0f}")) |
57 | 91 |
|
58 | 92 | ax.spines["top"].set_visible(False) |
59 | 93 | ax.spines["right"].set_visible(False) |
|
63 | 97 | ax.yaxis.grid(True, alpha=0.15, color=INK, linewidth=0.8) |
64 | 98 | ax.set_axisbelow(True) |
65 | 99 |
|
66 | | -plt.tight_layout() |
67 | | -plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG) |
| 100 | +# Manual margins — bbox_inches must stay None (default) to preserve exact canvas size |
| 101 | +fig.subplots_adjust(left=0.10, right=0.97, top=0.91, bottom=0.26) |
| 102 | +plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG) |
0 commit comments