Skip to content

Commit 993aa82

Browse files
feat(letsplot): implement icicle-basic (#2541)
## Implementation: `icicle-basic` - letsplot Implements the **letsplot** version of `icicle-basic`. **File:** `plots/icicle-basic/implementations/letsplot.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20585402234)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 181ae2f commit 993aa82

2 files changed

Lines changed: 204 additions & 0 deletions

File tree

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
""" pyplots.ai
2+
icicle-basic: Basic Icicle Chart
3+
Library: letsplot 4.8.2 | Python 3.13.11
4+
Quality: 90/100 | Created: 2025-12-30
5+
"""
6+
7+
import pandas as pd
8+
from lets_plot import (
9+
LetsPlot,
10+
aes,
11+
element_blank,
12+
element_text,
13+
geom_rect,
14+
geom_text,
15+
ggplot,
16+
ggsave,
17+
ggsize,
18+
labs,
19+
scale_fill_manual,
20+
scale_size_identity,
21+
theme,
22+
xlim,
23+
ylim,
24+
)
25+
26+
27+
LetsPlot.setup_html()
28+
29+
# Hierarchical data: File system example
30+
# Structure: Root -> Folders -> Subfolders/Files
31+
hierarchy = [
32+
# Level 0: Root
33+
{"name": "root", "parent": "", "value": 1000},
34+
# Level 1: Main folders
35+
{"name": "Documents", "parent": "root", "value": 350},
36+
{"name": "Media", "parent": "root", "value": 400},
37+
{"name": "Projects", "parent": "root", "value": 250},
38+
# Level 2: Subfolders
39+
{"name": "Work", "parent": "Documents", "value": 200},
40+
{"name": "Personal", "parent": "Documents", "value": 150},
41+
{"name": "Photos", "parent": "Media", "value": 220},
42+
{"name": "Videos", "parent": "Media", "value": 180},
43+
{"name": "Python", "parent": "Projects", "value": 120},
44+
{"name": "Web", "parent": "Projects", "value": 130},
45+
# Level 3: Files/items
46+
{"name": "Reports", "parent": "Work", "value": 120},
47+
{"name": "Contracts", "parent": "Work", "value": 80},
48+
{"name": "Letters", "parent": "Personal", "value": 90},
49+
{"name": "Receipts", "parent": "Personal", "value": 60},
50+
{"name": "2024", "parent": "Photos", "value": 130},
51+
{"name": "2023", "parent": "Photos", "value": 90},
52+
{"name": "Movies", "parent": "Videos", "value": 100},
53+
{"name": "Clips", "parent": "Videos", "value": 80},
54+
{"name": "DataViz", "parent": "Python", "value": 70},
55+
{"name": "ML", "parent": "Python", "value": 50},
56+
{"name": "Frontend", "parent": "Web", "value": 75},
57+
{"name": "Backend", "parent": "Web", "value": 55},
58+
]
59+
60+
# Build tree structure
61+
name_to_node = {row["name"]: row for row in hierarchy}
62+
children = {}
63+
for row in hierarchy:
64+
parent = row["parent"]
65+
if parent not in children:
66+
children[parent] = []
67+
if parent:
68+
children[parent].append(row["name"])
69+
70+
# Calculate level for each node (using iteration instead of function)
71+
levels = {}
72+
for row in hierarchy:
73+
level = 0
74+
current = row["name"]
75+
while name_to_node[current]["parent"]:
76+
level += 1
77+
current = name_to_node[current]["parent"]
78+
levels[row["name"]] = level
79+
80+
max_level = max(levels.values())
81+
82+
# Calculate rectangle positions (horizontal icicle: root at top)
83+
# Using stack-based traversal instead of recursion
84+
rects = []
85+
stack = [("root", 0.0, 1.0)]
86+
87+
while stack:
88+
name, x_start, x_end = stack.pop()
89+
level = levels[name]
90+
91+
# Add rectangle for this node
92+
rects.append(
93+
{
94+
"name": name,
95+
"xmin": x_start,
96+
"xmax": x_end,
97+
"ymin": max_level - level,
98+
"ymax": max_level - level + 1,
99+
"level": level,
100+
}
101+
)
102+
103+
# Process children (add in reverse order so first child is processed first)
104+
if name in children and children[name]:
105+
child_names = children[name]
106+
total_value = sum(name_to_node[c]["value"] for c in child_names)
107+
current_x = x_start
108+
109+
for child_name in reversed(child_names):
110+
child_value = name_to_node[child_name]["value"]
111+
child_width = (x_end - x_start) * (child_value / total_value)
112+
# Calculate position for this child
113+
child_x_start = x_end - child_width
114+
stack.append((child_name, child_x_start, x_end))
115+
x_end = child_x_start
116+
117+
# Create dataframe for rectangles
118+
rect_df = pd.DataFrame(rects)
119+
rect_df["level_str"] = rect_df["level"].astype(str)
120+
121+
# Calculate center positions for labels
122+
rect_df["x_center"] = (rect_df["xmin"] + rect_df["xmax"]) / 2
123+
rect_df["y_center"] = (rect_df["ymin"] + rect_df["ymax"]) / 2
124+
rect_df["width"] = rect_df["xmax"] - rect_df["xmin"]
125+
126+
# Only show labels for rectangles wide enough (threshold based on label length)
127+
# Balanced threshold to show labels while avoiding overlap
128+
rect_df["label_len"] = rect_df["name"].str.len()
129+
rect_df["show_label"] = rect_df["width"] > (rect_df["label_len"] * 0.007 + 0.01)
130+
label_df = rect_df[rect_df["show_label"]].copy()
131+
132+
# Adjust font size based on level for better fit (smaller at deeper levels to prevent overlap)
133+
label_df["font_size"] = label_df["level"].map({0: 14, 1: 12, 2: 8, 3: 7})
134+
135+
# Color palette by level (Python colors + complementary)
136+
colors = {
137+
"0": "#306998", # Python Blue - root
138+
"1": "#FFD43B", # Python Yellow - level 1
139+
"2": "#4B8BBE", # Light blue - level 2
140+
"3": "#646464", # Gray - level 3
141+
}
142+
143+
# Create plot
144+
plot = (
145+
ggplot()
146+
+ geom_rect(
147+
aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax", fill="level_str"),
148+
data=rect_df,
149+
color="white",
150+
size=1.5,
151+
alpha=0.9,
152+
)
153+
+ geom_text(
154+
aes(x="x_center", y="y_center", label="name", size="font_size"), data=label_df, color="black", fontface="bold"
155+
)
156+
+ scale_fill_manual(values=colors, name="Level")
157+
+ scale_size_identity()
158+
+ xlim(-0.02, 1.02)
159+
+ ylim(-0.1, max_level + 1.1)
160+
+ labs(title="icicle-basic · letsplot · pyplots.ai")
161+
+ theme(
162+
axis_title=element_blank(),
163+
axis_text=element_blank(),
164+
axis_ticks=element_blank(),
165+
axis_line=element_blank(),
166+
panel_grid=element_blank(),
167+
plot_title=element_text(size=24, face="bold"),
168+
legend_text=element_text(size=16),
169+
legend_title=element_text(size=18),
170+
)
171+
+ ggsize(1600, 900)
172+
)
173+
174+
# Save as PNG (scale 3x for 4800x2700) and HTML in current directory
175+
ggsave(plot, "plot.png", path=".", scale=3)
176+
ggsave(plot, "plot.html", path=".")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
library: letsplot
2+
specification_id: icicle-basic
3+
created: '2025-12-30T00:07:18Z'
4+
updated: '2025-12-30T00:29:53Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20585402234
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 4.8.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/icicle-basic/letsplot/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/icicle-basic/letsplot/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/icicle-basic/letsplot/plot.html
13+
quality_score: 90
14+
review:
15+
strengths:
16+
- Clean hierarchical visualization with clear parent-child relationships through
17+
spatial adjacency
18+
- Intelligent label visibility logic that hides labels for narrow rectangles to
19+
prevent overlap
20+
- Good color scheme with Python-themed colors (blue/yellow) that are colorblind-safe
21+
- Proper horizontal orientation with root at top as specified
22+
- Well-structured code using stack-based traversal instead of recursion
23+
weaknesses:
24+
- Legend could be more descriptive (e.g., "Hierarchy Level" instead of just "Level")
25+
- Some empty space at the bottom of the canvas could be better utilized
26+
- Several level 3 labels are hidden (Reports, Contracts, Letters, Receipts, 2024,
27+
2023, Movies, DataViz, Frontend, Backend) - while this prevents overlap, it reduces
28+
information density

0 commit comments

Comments
 (0)