{"spec_id":"circlepacking-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ncirclepacking-basic: Circle Packing Chart\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-11\n\"\"\"\n\nimport math\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    coord_fixed,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Okabe-Ito palette\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n# Hierarchical data - File system storage breakdown (GB)\nnp.random.seed(42)\nhierarchy = [\n    # Level 1: Main folders\n    {\"id\": \"Documents\", \"parent\": \"root\", \"value\": None, \"label\": \"Documents\"},\n    {\"id\": \"Media\", \"parent\": \"root\", \"value\": None, \"label\": \"Media\"},\n    {\"id\": \"Code\", \"parent\": \"root\", \"value\": None, \"label\": \"Code\"},\n    # Level 2: Subfolders under Documents\n    {\"id\": \"Work\", \"parent\": \"Documents\", \"value\": 25, \"label\": \"Work\"},\n    {\"id\": \"Personal\", \"parent\": \"Documents\", \"value\": 18, \"label\": \"Personal\"},\n    {\"id\": \"Archive\", \"parent\": \"Documents\", \"value\": 12, \"label\": \"Archive\"},\n    # Level 2: Subfolders under Media\n    {\"id\": \"Photos\", \"parent\": \"Media\", \"value\": 45, \"label\": \"Photos\"},\n    {\"id\": \"Videos\", \"parent\": \"Media\", \"value\": 65, \"label\": \"Videos\"},\n    {\"id\": \"Music\", \"parent\": \"Media\", \"value\": 22, \"label\": \"Music\"},\n    # Level 2: Subfolders under Code\n    {\"id\": \"Projects\", \"parent\": \"Code\", \"value\": 35, \"label\": \"Projects\"},\n    {\"id\": \"Libraries\", \"parent\": \"Code\", \"value\": 15, \"label\": \"Libraries\"},\n    {\"id\": \"Backups\", \"parent\": \"Code\", \"value\": 8, \"label\": \"Backups\"},\n]\n\n# Data preparation\ndf = pd.DataFrame(hierarchy)\nfor idx, row in df[df[\"value\"].isna()].iterrows():\n    children = df[df[\"parent\"] == row[\"id\"]]\n    df.loc[idx, \"value\"] = children[\"value\"].sum()\n\nroot_radius = 90\nroot_cx, root_cy = 0, 0\ntotal_value = df[df[\"parent\"] == \"root\"][\"value\"].sum()\n\npolygon_rows = []\nlabel_rows = []\ncircle_id = 0\n\n# Root circle\nangles = np.linspace(0, 2 * np.pi, 60, endpoint=False)\nx_root = root_cx + root_radius * np.cos(angles)\ny_root = root_cy + root_radius * np.sin(angles)\nfor x, y in zip(x_root, y_root, strict=True):\n    polygon_rows.append({\"x\": x, \"y\": y, \"circle_id\": circle_id, \"depth\": 0, \"color\": \"root\"})\ncircle_id += 1\n\n# Level 1 circles\nlevel1 = df[df[\"parent\"] == \"root\"].copy()\nlevel1[\"radius\"] = np.sqrt(level1[\"value\"] / total_value) * root_radius * 0.95\nlevel1 = level1.sort_values(\"radius\", ascending=False).reset_index(drop=True)\n\n# Pack level 1 circles with force simulation\nlevel1_radii = level1[\"radius\"].tolist()\nn = len(level1_radii)\nradii = np.array(level1_radii)\nangles_init = np.linspace(0, 2 * np.pi, n, endpoint=False)\nlevel1_x = root_cx + root_radius * 0.4 * np.cos(angles_init)\nlevel1_y = root_cy + root_radius * 0.4 * np.sin(angles_init)\n\nfor _iteration in range(800):\n    # Repulsion between pairs\n    for i in range(n):\n        for j in range(i + 1, n):\n            dx = level1_x[j] - level1_x[i]\n            dy = level1_y[j] - level1_y[i]\n            dist = math.sqrt(dx * dx + dy * dy)\n            min_dist = radii[i] + radii[j] + 1.0\n            if dist < min_dist and dist > 0.001:\n                overlap = (min_dist - dist) / 2\n                norm_x = dx / dist\n                norm_y = dy / dist\n                level1_x[i] -= norm_x * overlap * 0.5\n                level1_y[i] -= norm_y * overlap * 0.5\n                level1_x[j] += norm_x * overlap * 0.5\n                level1_y[j] += norm_y * overlap * 0.5\n    # Boundary constraint\n    for i in range(n):\n        dx = level1_x[i] - root_cx\n        dy = level1_y[i] - root_cy\n        dist = math.sqrt(dx * dx + dy * dy)\n        max_dist = root_radius - radii[i] - 1.0\n        if dist > max_dist and dist > 0.001:\n            scale = max_dist / dist\n            level1_x[i] = root_cx + dx * scale\n            level1_y[i] = root_cy + dy * scale\n    # Gentle center attraction\n    level1_x = root_cx + (level1_x - root_cx) * 0.998\n    level1_y = root_cy + (level1_y - root_cy) * 0.998\n\n# Draw level 1 circles\nlevel1_positions = {}\nfor i, (_, row) in enumerate(level1.iterrows()):\n    level1_positions[row[\"id\"]] = {\"x\": level1_x[i], \"y\": level1_y[i], \"radius\": row[\"radius\"]}\n    # Circle polygon\n    angles = np.linspace(0, 2 * np.pi, 60, endpoint=False)\n    x_circ = level1_x[i] + row[\"radius\"] * np.cos(angles)\n    y_circ = level1_y[i] + row[\"radius\"] * np.sin(angles)\n    for x, y in zip(x_circ, y_circ, strict=True):\n        polygon_rows.append({\"x\": x, \"y\": y, \"circle_id\": circle_id, \"depth\": 1, \"color\": row[\"id\"]})\n    # Label\n    label_rows.append(\n        {\"x\": level1_x[i], \"y\": level1_y[i] + row[\"radius\"] * 0.65, \"label\": row[\"label\"], \"depth\": 1, \"size\": 14}\n    )\n    circle_id += 1\n\n# Level 2 circles\nfor parent_id, pos in level1_positions.items():\n    children = df[df[\"parent\"] == parent_id].copy()\n    if children.empty:\n        continue\n    parent_value = children[\"value\"].sum()\n    children[\"radius\"] = np.sqrt(children[\"value\"] / parent_value) * pos[\"radius\"] * 0.75\n    children = children.sort_values(\"radius\", ascending=False).reset_index(drop=True)\n\n    # Pack children with force simulation\n    children_radii = children[\"radius\"].tolist()\n    n_c = len(children_radii)\n    radii_c = np.array(children_radii)\n    angles_init = np.linspace(0, 2 * np.pi, n_c, endpoint=False)\n    children_x = pos[\"x\"] + pos[\"radius\"] * 0.4 * np.cos(angles_init)\n    children_y = pos[\"y\"] + pos[\"radius\"] * 0.4 * np.sin(angles_init)\n\n    for _iteration in range(600):\n        for i in range(n_c):\n            for j in range(i + 1, n_c):\n                dx = children_x[j] - children_x[i]\n                dy = children_y[j] - children_y[i]\n                dist = math.sqrt(dx * dx + dy * dy)\n                min_dist = radii_c[i] + radii_c[j] + 1.0\n                if dist < min_dist and dist > 0.001:\n                    overlap = (min_dist - dist) / 2\n                    norm_x = dx / dist\n                    norm_y = dy / dist\n                    children_x[i] -= norm_x * overlap * 0.5\n                    children_y[i] -= norm_y * overlap * 0.5\n                    children_x[j] += norm_x * overlap * 0.5\n                    children_y[j] += norm_y * overlap * 0.5\n        for i in range(n_c):\n            dx = children_x[i] - pos[\"x\"]\n            dy = children_y[i] - pos[\"y\"]\n            dist = math.sqrt(dx * dx + dy * dy)\n            max_dist = pos[\"radius\"] * 0.92 - radii_c[i] - 1.0\n            if dist > max_dist and dist > 0.001:\n                scale = max_dist / dist\n                children_x[i] = pos[\"x\"] + dx * scale\n                children_y[i] = pos[\"y\"] + dy * scale\n        children_x = pos[\"x\"] + (children_x - pos[\"x\"]) * 0.998\n        children_y = pos[\"y\"] + (children_y - pos[\"y\"]) * 0.998\n\n    # Draw children circles\n    for i, (_, row) in enumerate(children.iterrows()):\n        angles = np.linspace(0, 2 * np.pi, 60, endpoint=False)\n        x_circ = children_x[i] + row[\"radius\"] * np.cos(angles)\n        y_circ = children_y[i] + row[\"radius\"] * np.sin(angles)\n        for x, y in zip(x_circ, y_circ, strict=True):\n            polygon_rows.append({\"x\": x, \"y\": y, \"circle_id\": circle_id, \"depth\": 2, \"color\": row[\"id\"]})\n        # Label (if circle large enough)\n        if row[\"radius\"] > 6:\n            label_rows.append({\"x\": children_x[i], \"y\": children_y[i], \"label\": row[\"label\"], \"depth\": 2, \"size\": 11})\n        circle_id += 1\n\npolygon_df = pd.DataFrame(polygon_rows)\nlabel_df = pd.DataFrame(label_rows)\n\n# Color mapping: depth-based with Okabe-Ito\ncolor_map = {\n    \"root\": INK_SOFT,\n    \"Documents\": IMPRINT[0],\n    \"Media\": IMPRINT[1],\n    \"Code\": IMPRINT[2],\n    \"Work\": IMPRINT[0],\n    \"Personal\": IMPRINT[0],\n    \"Archive\": IMPRINT[0],\n    \"Photos\": IMPRINT[1],\n    \"Videos\": IMPRINT[1],\n    \"Music\": IMPRINT[1],\n    \"Projects\": IMPRINT[2],\n    \"Libraries\": IMPRINT[2],\n    \"Backups\": IMPRINT[2],\n}\n\nunique_colors = polygon_df[\"color\"].unique()\ncolor_values = [color_map.get(c, INK_SOFT) for c in unique_colors]\n\n# Plot\nplot = (\n    ggplot(polygon_df)\n    + geom_polygon(aes(x=\"x\", y=\"y\", fill=\"color\", group=\"circle_id\"), color=\"white\", size=0.8, alpha=0.92)\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"), data=label_df[label_df[\"depth\"] == 1], size=14, color=\"white\", fontface=\"bold\"\n    )\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=label_df[label_df[\"depth\"] == 2], size=11, color=INK)\n    + scale_fill_manual(values=color_values)\n    + coord_fixed(ratio=1)\n    + scale_x_continuous(limits=(-105, 105))\n    + scale_y_continuous(limits=(-105, 105))\n    + labs(title=\"circlepacking-basic · letsplot · anyplot.ai\")\n    + ggsize(1200, 1200)\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_title=element_text(size=24, hjust=0.5, color=INK),\n        legend_position=\"none\",\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        axis_line=element_blank(),\n        panel_grid=element_blank(),\n    )\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=3)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}