|
| 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=".") |
0 commit comments