Skip to content

Commit 6ae57b6

Browse files
Merge branch 'main' into implementation/lollipop-basic/altair
2 parents d5c8b8f + d63fcd0 commit 6ae57b6

10 files changed

Lines changed: 683 additions & 497 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)
Lines changed: 63 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""" anyplot.ai
22
lollipop-basic: Basic Lollipop Chart
3-
Library: plotly 6.7.0 | Python 3.14.4
4-
Quality: 87/100 | Updated: 2026-04-26
3+
Library: plotly 6.8.0 | Python 3.13.14
4+
Quality: 92/100 | Updated: 2026-07-01
55
"""
66

77
import os
@@ -15,90 +15,112 @@
1515
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
1616
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
1717
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
18-
GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)"
19-
BRAND = "#009E73" # Okabe-Ito position 1 — ALWAYS first series
18+
GRID = "rgba(26,26,23,0.15)" if THEME == "light" else "rgba(240,239,232,0.15)"
19+
BRAND = "#009E73" # Imprint palette position 1 — ALWAYS first series
20+
ACCENT = "#C475FD" # Imprint palette position 2 — focal point for top performer
2021

21-
# Data — Product sales by category (deterministic, sorted descending)
22-
categories = [
23-
"Electronics",
24-
"Clothing",
25-
"Home & Garden",
26-
"Sports",
27-
"Books",
28-
"Toys",
29-
"Beauty",
30-
"Automotive",
31-
"Food & Grocery",
32-
"Health",
22+
# Data — streaming platform monthly watch hours by genre (thousands of hours, sorted descending)
23+
genres = [
24+
"Drama",
25+
"Comedy",
26+
"Thriller",
27+
"Action",
28+
"Documentary",
29+
"Animation",
30+
"Romance",
31+
"Horror",
32+
"Reality TV",
33+
"Sci-Fi",
3334
]
34-
values = [124820, 97340, 86715, 75260, 64480, 53905, 47620, 41370, 37815, 30945]
35+
watch_hours = [3840, 2970, 2450, 2180, 1820, 1540, 1280, 990, 820, 650]
3536

3637
# Plot
3738
fig = go.Figure()
3839

39-
# Stems — one segmented Scatter trace via None separators (single trace, fewer DOM nodes)
40-
stem_x = []
41-
stem_y = []
42-
for cat, val in zip(categories, values, strict=True):
43-
stem_x.extend([cat, cat, None])
44-
stem_y.extend([0, val, None])
40+
# Accent stem first — anchors Drama as the leftmost category on the x-axis
41+
fig.add_trace(
42+
go.Scatter(
43+
x=[genres[0], genres[0], None],
44+
y=[0, watch_hours[0], None],
45+
mode="lines",
46+
line={"color": ACCENT, "width": 3},
47+
showlegend=False,
48+
hoverinfo="skip",
49+
)
50+
)
51+
52+
# Standard stems — brand color for positions 1–9 via None-separator trick
53+
stem_x, stem_y = [], []
54+
for genre, hours in zip(genres[1:], watch_hours[1:], strict=True):
55+
stem_x.extend([genre, genre, None])
56+
stem_y.extend([0, hours, None])
4557

4658
fig.add_trace(
4759
go.Scatter(x=stem_x, y=stem_y, mode="lines", line={"color": BRAND, "width": 3}, showlegend=False, hoverinfo="skip")
4860
)
4961

50-
# Markers — circular dots at the top of each stem
62+
# Markers — top performer in accent, rest in brand green
63+
marker_colors = [ACCENT] + [BRAND] * (len(genres) - 1)
5164
fig.add_trace(
5265
go.Scatter(
53-
x=categories,
54-
y=values,
66+
x=genres,
67+
y=watch_hours,
5568
mode="markers",
56-
marker={"color": BRAND, "size": 22, "line": {"color": PAGE_BG, "width": 2.5}, "symbol": "circle"},
69+
marker={"color": marker_colors, "size": 22, "line": {"color": PAGE_BG, "width": 2.5}, "symbol": "circle"},
5770
showlegend=False,
58-
hovertemplate="<b>%{x}</b><br>Sales: $%{y:,.0f}<extra></extra>",
71+
hovertemplate="<b>%{x}</b><br>%{y:,.0f}k hours/month<extra></extra>",
5972
cliponaxis=False,
6073
)
6174
)
6275

76+
# Title fontsize scaled for length (plotly default 16px, floor 11px)
77+
title_text = "Streaming Hours by Genre · lollipop-basic · python · plotly · anyplot.ai"
78+
title_fontsize = max(11, round(16 * 67 / len(title_text)))
79+
6380
# Style
6481
fig.update_layout(
82+
autosize=False,
6583
title={
66-
"text": "Product Sales by Category · lollipop-basic · plotly · anyplot.ai",
67-
"font": {"size": 28, "color": INK},
84+
"text": title_text,
85+
"font": {"size": title_fontsize, "color": INK},
6886
"x": 0.5,
6987
"xanchor": "center",
70-
"y": 0.95,
88+
"y": 0.97,
7189
},
7290
xaxis={
73-
"title": {"text": "Product Category", "font": {"size": 22, "color": INK}},
74-
"tickfont": {"size": 18, "color": INK_SOFT},
91+
"title": {"text": "Content Genre", "font": {"size": 12, "color": INK}},
92+
"tickfont": {"size": 10, "color": INK_SOFT},
7593
"tickangle": -35,
7694
"showgrid": False,
95+
"showline": True,
96+
"mirror": False,
7797
"linecolor": INK_SOFT,
7898
"ticks": "outside",
7999
"tickcolor": INK_SOFT,
80100
"ticklen": 6,
81101
},
82102
yaxis={
83-
"title": {"text": "Sales ($)", "font": {"size": 22, "color": INK}},
84-
"tickfont": {"size": 18, "color": INK_SOFT},
85-
"tickformat": "$,.0f",
103+
"title": {"text": "Watch Hours (thousands/month)", "font": {"size": 12, "color": INK}},
104+
"tickfont": {"size": 10, "color": INK_SOFT},
105+
"tickformat": ",.0f",
86106
"gridcolor": GRID,
87107
"gridwidth": 1,
88108
"zeroline": True,
89109
"zerolinecolor": INK_SOFT,
90110
"zerolinewidth": 1.5,
111+
"showline": True,
112+
"mirror": False,
91113
"linecolor": INK_SOFT,
92-
"range": [0, max(values) * 1.1],
114+
"range": [0, max(watch_hours) * 1.12],
93115
},
94116
paper_bgcolor=PAGE_BG,
95117
plot_bgcolor=PAGE_BG,
96118
font={"color": INK, "family": "Inter, system-ui, sans-serif"},
97-
margin={"l": 110, "r": 60, "t": 110, "b": 160},
119+
margin={"l": 110, "r": 60, "t": 80, "b": 140},
98120
showlegend=False,
99-
hoverlabel={"bgcolor": ELEVATED_BG, "bordercolor": INK_SOFT, "font": {"color": INK, "size": 16}},
121+
hoverlabel={"bgcolor": ELEVATED_BG, "bordercolor": INK_SOFT, "font": {"color": INK, "size": 14}},
100122
)
101123

102-
# Save
103-
fig.write_image(f"plot-{THEME}.png", width=1600, height=900, scale=3)
124+
# Save — canonical 3200×1800 landscape canvas
125+
fig.write_image(f"plot-{THEME}.png", width=800, height=450, scale=4)
104126
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

plots/lollipop-basic/implementations/python/plotnine.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
""" anyplot.ai
22
lollipop-basic: Basic Lollipop Chart
3-
Library: plotnine 0.15.3 | Python 3.14.4
4-
Quality: 85/100 | Updated: 2026-04-26
3+
Library: plotnine 0.15.7 | Python 3.13.14
4+
Quality: 84/100 | Updated: 2026-07-01
55
"""
66

77
import os
8+
import sys
9+
10+
11+
# Prevent circular import: remove this script's directory from sys.path so
12+
# "from plotnine import ..." resolves to the installed library, not this file.
13+
_here = os.path.dirname(os.path.abspath(__file__))
14+
sys.path = [p for p in sys.path if os.path.abspath(p) != _here]
815

916
import pandas as pd
1017
from plotnine import (
1118
aes,
19+
element_blank,
1220
element_line,
1321
element_rect,
1422
element_text,
1523
geom_point,
1624
geom_segment,
25+
geom_text,
1726
ggplot,
1827
ggsave,
1928
labs,
29+
scale_size_continuous,
2030
theme,
2131
theme_minimal,
2232
)
@@ -28,7 +38,7 @@
2838
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
2939
BRAND = "#009E73"
3040

31-
# Data - Product sales by category, sorted by value
41+
# Product sales by category, sorted ascending for ranking narrative
3242
data = {
3343
"category": [
3444
"Electronics",
@@ -48,28 +58,37 @@
4858
df = pd.DataFrame(data)
4959
df = df.sort_values("value", ascending=True).reset_index(drop=True)
5060
df["category"] = pd.Categorical(df["category"], categories=df["category"], ordered=True)
61+
df["label"] = df["value"].astype(str) + "k"
5162

52-
# Plot
5363
plot = (
5464
ggplot(df, aes(x="category", y="value"))
55-
+ geom_segment(aes(x="category", xend="category", y=0, yend="value"), color=BRAND, size=1.5)
56-
+ geom_point(color=BRAND, size=6, fill=BRAND)
57-
+ labs(x="Product Category", y="Sales (thousands $)", title="lollipop-basic · plotnine · anyplot.ai")
65+
+ geom_segment(aes(x="category", xend="category", y=0, yend="value"), color=BRAND, size=0.6)
66+
+ geom_point(aes(size="value"), color=BRAND, fill=BRAND, show_legend=False)
67+
+ geom_text(
68+
aes(label="label"),
69+
color=INK_SOFT,
70+
size=2.8, # geom_text size is in mm (~2.8mm ≈ 8pt at dpi=400)
71+
nudge_y=16,
72+
va="bottom",
73+
)
74+
+ scale_size_continuous(range=[2, 7])
75+
+ labs(x="Product Category", y="Sales (thousands $)", title="lollipop-basic · python · plotnine · anyplot.ai")
5876
+ theme_minimal()
5977
+ theme(
60-
figure_size=(16, 9),
78+
figure_size=(8, 4.5),
6179
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
6280
panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
63-
text=element_text(size=14, color=INK),
64-
axis_title=element_text(size=20, color=INK),
65-
axis_text=element_text(size=16, color=INK_SOFT),
81+
panel_border=element_blank(),
82+
text=element_text(size=7, color=INK),
83+
axis_title=element_text(size=10, color=INK),
84+
axis_text=element_text(size=8, color=INK_SOFT),
6685
axis_text_x=element_text(angle=45, ha="right", color=INK_SOFT),
6786
axis_line=element_line(color=INK_SOFT),
68-
plot_title=element_text(size=24, color=INK),
69-
panel_grid_minor=element_line(alpha=0),
70-
panel_grid_major_x=element_line(alpha=0),
87+
plot_title=element_text(size=12, color=INK),
88+
panel_grid_minor=element_blank(),
89+
panel_grid_major_x=element_blank(),
7190
panel_grid_major_y=element_line(color=INK, alpha=0.15, size=0.3),
7291
)
7392
)
7493

75-
ggsave(plot, filename=f"plot-{THEME}.png", dpi=300, width=16, height=9)
94+
ggsave(plot, filename=f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in")

0 commit comments

Comments
 (0)