Skip to content

Commit b0bc886

Browse files
feat(plotnine): implement icicle-basic (#2850)
## Implementation: `icicle-basic` - plotnine Implements the **plotnine** version of `icicle-basic`. **File:** `plots/icicle-basic/implementations/plotnine.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20606631356)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 81472cd commit b0bc886

2 files changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
""" pyplots.ai
2+
icicle-basic: Basic Icicle Chart
3+
Library: plotnine 0.15.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-30
5+
"""
6+
7+
import pandas as pd
8+
from plotnine import aes, element_text, geom_rect, geom_text, ggplot, labs, scale_fill_manual, theme, theme_void
9+
10+
11+
# Data: File system hierarchy with sizes (MB)
12+
# Increased small node values to ensure visibility
13+
data = [
14+
{"name": "root", "parent": "", "value": 0},
15+
{"name": "Documents", "parent": "root", "value": 0},
16+
{"name": "Photos", "parent": "root", "value": 0},
17+
{"name": "Projects", "parent": "root", "value": 0},
18+
{"name": "Reports", "parent": "Documents", "value": 450},
19+
{"name": "Invoices", "parent": "Documents", "value": 280},
20+
{"name": "Notes", "parent": "Documents", "value": 180},
21+
{"name": "Vacation", "parent": "Photos", "value": 680},
22+
{"name": "Family", "parent": "Photos", "value": 520},
23+
{"name": "Events", "parent": "Photos", "value": 340},
24+
{"name": "WebApp", "parent": "Projects", "value": 0},
25+
{"name": "DataSci", "parent": "Projects", "value": 0},
26+
{"name": "Mobile", "parent": "Projects", "value": 380},
27+
{"name": "Frontend", "parent": "WebApp", "value": 320},
28+
{"name": "Backend", "parent": "WebApp", "value": 420},
29+
{"name": "Config", "parent": "WebApp", "value": 200},
30+
{"name": "Models", "parent": "DataSci", "value": 520},
31+
{"name": "Scripts", "parent": "DataSci", "value": 300},
32+
]
33+
34+
df = pd.DataFrame(data)
35+
36+
# Build lookup tables
37+
name_to_idx = {row["name"]: idx for idx, row in df.iterrows()}
38+
children_map = {name: df[df["parent"] == name]["name"].tolist() for name in df["name"]}
39+
40+
# Calculate values for non-leaf nodes (bottom-up aggregation)
41+
# Process nodes from leaves up using iterative approach
42+
processed = set()
43+
while len(processed) < len(df):
44+
for _, row in df.iterrows():
45+
name = row["name"]
46+
if name in processed:
47+
continue
48+
kids = children_map[name]
49+
if len(kids) == 0:
50+
processed.add(name)
51+
elif all(k in processed for k in kids):
52+
total = sum(df.loc[name_to_idx[k], "value"] for k in kids)
53+
df.loc[name_to_idx[name], "value"] = total
54+
processed.add(name)
55+
56+
# Calculate depths (distance from root)
57+
depths = {"root": 0}
58+
queue = ["root"]
59+
while queue:
60+
current = queue.pop(0)
61+
for child in children_map[current]:
62+
depths[child] = depths[current] + 1
63+
queue.append(child)
64+
65+
max_depth = max(depths.values())
66+
67+
# Build icicle rectangles using iterative BFS
68+
rects = []
69+
# Queue: (name, x_start, x_end)
70+
layout_queue = [("root", 0.0, 1.0)]
71+
72+
while layout_queue:
73+
name, x_start, x_end = layout_queue.pop(0)
74+
depth = depths[name]
75+
y_top = max_depth - depth + 1
76+
y_bottom = max_depth - depth
77+
value = df.loc[name_to_idx[name], "value"]
78+
79+
rects.append(
80+
{"name": name, "xmin": x_start, "xmax": x_end, "ymin": y_bottom, "ymax": y_top, "depth": depth, "value": value}
81+
)
82+
83+
# Queue children proportionally
84+
kids = children_map[name]
85+
if kids:
86+
kid_values = [(k, df.loc[name_to_idx[k], "value"]) for k in kids]
87+
kid_values.sort(key=lambda x: -x[1]) # Sort by value descending
88+
total_value = sum(v for _, v in kid_values)
89+
if total_value > 0:
90+
curr_x = x_start
91+
for kid, val in kid_values:
92+
width = (val / total_value) * (x_end - x_start)
93+
layout_queue.append((kid, curr_x, curr_x + width))
94+
curr_x += width
95+
96+
rect_df = pd.DataFrame(rects)
97+
98+
# Color palette by depth - using distinct colors (fixed yellow similarity issue)
99+
colors = {
100+
0: "#306998", # Python Blue - root
101+
1: "#4B8BBE", # Lighter blue - level 1
102+
2: "#FFD43B", # Python Yellow - level 2
103+
3: "#8B4513", # SaddleBrown - level 3 (distinct from yellow)
104+
4: "#90B4CE", # Light steel blue - level 4
105+
}
106+
rect_df["fill_color"] = rect_df["depth"].map(colors)
107+
108+
# Calculate label positions and widths
109+
rect_df["width"] = rect_df["xmax"] - rect_df["xmin"]
110+
rect_df["x_center"] = (rect_df["xmin"] + rect_df["xmax"]) / 2
111+
rect_df["y_center"] = (rect_df["ymin"] + rect_df["ymax"]) / 2
112+
113+
# Labels: show name + value for wide rectangles, name only for medium, hide for very narrow
114+
# Lowered threshold to ensure more labels show value (fix for truncated labels)
115+
rect_df["label"] = rect_df.apply(
116+
lambda r: f"{r['name']}\n({int(r['value'])} MB)" if r["width"] > 0.05 else (r["name"] if r["width"] > 0.02 else ""),
117+
axis=1,
118+
)
119+
120+
# Convert depth to categorical with proper labels for legend
121+
level_labels = {0: "Level 0 (Root)", 1: "Level 1", 2: "Level 2", 3: "Level 3", 4: "Level 4 (Leaf)"}
122+
rect_df["depth_label"] = pd.Categorical(
123+
rect_df["depth"].map(level_labels), categories=list(level_labels.values()), ordered=True
124+
)
125+
126+
# Also update dark_bg and light_bg with labels
127+
dark_bg = rect_df[rect_df["depth"].isin([0, 1, 3])]
128+
light_bg = rect_df[rect_df["depth"].isin([2, 4])]
129+
130+
# Create plot using plotnine grammar of graphics
131+
plot = (
132+
ggplot(rect_df)
133+
+ geom_rect(aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax", fill="depth_label"), color="white", size=1.5)
134+
+ geom_text(aes(x="x_center", y="y_center", label="label"), data=dark_bg, size=11, color="white", fontweight="bold")
135+
+ geom_text(
136+
aes(x="x_center", y="y_center", label="label"), data=light_bg, size=11, color="black", fontweight="bold"
137+
)
138+
+ scale_fill_manual(
139+
values={
140+
"Level 0 (Root)": "#306998",
141+
"Level 1": "#4B8BBE",
142+
"Level 2": "#FFD43B",
143+
"Level 3": "#8B4513",
144+
"Level 4 (Leaf)": "#90B4CE",
145+
},
146+
name="Hierarchy Level",
147+
)
148+
+ labs(title="icicle-basic · plotnine · pyplots.ai")
149+
+ theme_void()
150+
+ theme(
151+
figure_size=(16, 9),
152+
plot_title=element_text(size=28, ha="center", weight="bold"),
153+
legend_position="right",
154+
legend_title=element_text(size=18),
155+
legend_text=element_text(size=14),
156+
)
157+
)
158+
159+
# Save
160+
plot.save("plot.png", dpi=300, verbose=False)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
library: plotnine
2+
specification_id: icicle-basic
3+
created: '2025-12-30T21:52:31Z'
4+
updated: '2025-12-30T22:06:37Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20606631356
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 0.15.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/icicle-basic/plotnine/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/icicle-basic/plotnine/plot_thumb.png
12+
preview_html: null
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent hierarchical visualization with clear parent-child relationships visible
17+
through spatial adjacency
18+
- 'Smart label handling: shows name+value for wide rectangles, name-only for medium,
19+
hidden for narrow'
20+
- Good color contrast between hierarchy levels with appropriate text colors (white
21+
on dark, black on light)
22+
- Proper bottom-up value aggregation for non-leaf nodes
23+
- Clean theme_void usage appropriate for this chart type
24+
- Well-implemented BFS algorithm for layout calculation
25+
weaknesses:
26+
- Legend shows Hierarchy Level with just numbers 0-3 instead of the more descriptive
27+
labels defined in the code

0 commit comments

Comments
 (0)