Skip to content

Commit 40a72d9

Browse files
feat(matplotlib): implement lollipop-basic (#9595)
## Implementation: `lollipop-basic` - python/matplotlib Implements the **python/matplotlib** version of `lollipop-basic`. **File:** `plots/lollipop-basic/implementations/python/matplotlib.py` **Parent Issue:** #934 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/28496285913)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent b71b932 commit 40a72d9

2 files changed

Lines changed: 147 additions & 103 deletions

File tree

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
""" anyplot.ai
22
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
55
"""
66

77
import os
88

99
import matplotlib.pyplot as plt
10+
import matplotlib.ticker as mticker
1011
import numpy as np
1112

1213

@@ -15,10 +16,12 @@
1516
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
1617
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
1718
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
19+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
1820

21+
# Imprint palette — first series always brand green
1922
BRAND_GREEN = "#009E73"
2023

21-
# Data: Product sales by category
24+
# Data: retail category sales (thousands)
2225
categories = [
2326
"Electronics",
2427
"Clothing",
@@ -33,27 +36,58 @@
3336
]
3437
values = [87, 72, 65, 58, 52, 45, 41, 38, 32, 25]
3538

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])
4043

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)
4247
ax.set_facecolor(PAGE_BG)
4348

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)
4782

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)
5186

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)
5588

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}"))
5791

5892
ax.spines["top"].set_visible(False)
5993
ax.spines["right"].set_visible(False)
@@ -63,5 +97,6 @@
6397
ax.yaxis.grid(True, alpha=0.15, color=INK, linewidth=0.8)
6498
ax.set_axisbelow(True)
6599

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

Comments
 (0)