{"spec_id":"upset-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nupset-basic: UpSet Plot for Multi-Set Intersection Analysis\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 90/100 | Created: 2026-05-13\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_bar,\n    geom_point,\n    geom_rect,\n    geom_segment,\n    ggbunch,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\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\"\nBRAND = \"#009E73\"\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\"]\n\n# Data: user acquisition channels (marketing segments)\nnp.random.seed(42)\nchannels = [\"Email\", \"Social\", \"Search\", \"Referral\", \"Direct\"]\nn_users = 1000\nprobs = [0.42, 0.55, 0.38, 0.25, 0.32]\n\nraw = {ch: (np.random.random(n_users) < p) for ch, p in zip(channels, probs, strict=False)}\ndf = pd.DataFrame(raw)\ndf = df[df.any(axis=1)].reset_index(drop=True)\n\n# Compute intersections sorted by size\ndf[\"key\"] = df.apply(lambda r: frozenset(ch for ch in channels if r[ch]), axis=1)\ninter = df.groupby(\"key\").size().reset_index(name=\"count\")\ninter = inter.sort_values(\"count\", ascending=False).head(12).reset_index(drop=True)\ninter[\"idx\"] = range(len(inter))\ninter[\"degree\"] = inter[\"key\"].apply(len)\ninter[\"degree_str\"] = inter[\"degree\"].astype(str)\n\n# Set ordering: smallest size at bottom (y=0), largest at top\nset_sizes_raw = df[channels].sum().sort_values(ascending=True)\nordered_sets = list(set_sizes_raw.index)\nset_y = {s: i for i, s in enumerate(ordered_sets)}\n\n# Dot matrix data\ndot_rows = []\nfor _, row in inter.iterrows():\n    for ch in channels:\n        dot_rows.append({\"x\": row[\"idx\"], \"y\": set_y[ch], \"active\": ch in row[\"key\"]})\ndot_df = pd.DataFrame(dot_rows)\n\n# Vertical connecting lines for each intersection column\nline_rows = []\nfor _, row in inter.iterrows():\n    ys = [set_y[ch] for ch in channels if ch in row[\"key\"]]\n    if len(ys) > 1:\n        line_rows.append({\"x\": row[\"idx\"], \"ymin\": min(ys), \"ymax\": max(ys)})\nline_df = pd.DataFrame(line_rows)\n\n# Set size bars data (using geom_rect for precise horizontal bars)\nbar_h = 0.38\nset_df = pd.DataFrame(\n    {\"set\": ordered_sets, \"y\": [set_y[s] for s in ordered_sets], \"size\": [int(set_sizes_raw[s]) for s in ordered_sets]}\n)\nset_df[\"xmin\"] = 0\nset_df[\"xmax\"] = set_df[\"size\"]\nset_df[\"ymin\"] = set_df[\"y\"] - bar_h\nset_df[\"ymax\"] = set_df[\"y\"] + bar_h\n\n# Shared axis limits\ny_lim = [-0.6, len(channels) - 0.4]\ny_breaks = list(range(len(channels)))\nx_lim = [-0.7, len(inter) - 0.3]\nx_breaks = list(range(len(inter)))\nmax_set_size = int(set_df[\"size\"].max())\n\n# Shared base theme\nbase_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major=element_line(color=INK_SOFT, size=0.15),\n    panel_grid_minor=element_blank(),\n    axis_title=element_text(color=INK, size=20),\n    axis_text=element_text(color=INK_SOFT, size=16),\n    axis_line=element_line(color=INK_SOFT),\n    plot_title=element_text(color=INK, size=24, face=\"bold\"),\n    legend_background=element_rect(fill=ELEVATED_BG),\n    legend_text=element_text(color=INK_SOFT, size=14),\n    legend_title=element_text(color=INK, size=16),\n)\n\n# Intersection size bars (top panel)\ndegree_palette = {str(d): IMPRINT[d - 1] for d in range(1, 6)}\n\np_top = (\n    ggplot(inter, aes(x=\"idx\", y=\"count\", fill=\"degree_str\"))\n    + geom_bar(stat=\"identity\", width=0.65)\n    + scale_fill_manual(values=degree_palette, name=\"Degree\")\n    + scale_x_continuous(limits=x_lim, breaks=x_breaks)\n    + scale_y_continuous(expand=[0.02, 0])\n    + labs(x=\"\", y=\"Intersection Size\", title=\"upset-basic · letsplot · anyplot.ai\")\n    + base_theme\n    + theme(\n        axis_text_x=element_blank(),\n        axis_ticks_x=element_blank(),\n        panel_grid_major_x=element_blank(),\n        legend_position=\"right\",\n    )\n)\n\n# Dot matrix (bottom-right panel)\ninactive_color = \"#C0BDB5\" if THEME == \"light\" else \"#404038\"\n\np_matrix = (\n    ggplot()\n    + geom_point(\n        data=dot_df[~dot_df[\"active\"]].reset_index(drop=True),\n        mapping=aes(x=\"x\", y=\"y\"),\n        color=inactive_color,\n        size=3.5,\n        alpha=0.5,\n    )\n    + geom_segment(data=line_df, mapping=aes(x=\"x\", xend=\"x\", y=\"ymin\", yend=\"ymax\"), color=BRAND, size=3.5, alpha=0.9)\n    + geom_point(data=dot_df[dot_df[\"active\"]].reset_index(drop=True), mapping=aes(x=\"x\", y=\"y\"), color=BRAND, size=5.5)\n    + scale_x_continuous(limits=x_lim, breaks=x_breaks)\n    + scale_y_continuous(limits=y_lim, breaks=y_breaks, labels=ordered_sets)\n    + labs(x=\"Intersection\", y=\"\")\n    + base_theme\n    + theme(axis_ticks=element_blank(), panel_grid_major=element_blank())\n)\n\n# Set size bars (bottom-left panel, horizontal via geom_rect)\np_sets = (\n    ggplot(set_df, aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"))\n    + geom_rect(fill=BRAND, color=None)\n    + scale_x_continuous(limits=[0, max_set_size * 1.12], expand=[0, 0])\n    + scale_y_continuous(limits=y_lim, breaks=y_breaks, labels=ordered_sets)\n    + labs(x=\"Set Size\", y=\"\")\n    + base_theme\n    + theme(axis_ticks_y=element_blank(), panel_grid_major_y=element_blank())\n)\n\n# Combine with ggbunch (1600x900 base; scale=3 → 4800x2700 px)\n# Regions: (x, y, width, height) in relative [0,1] coordinates\ntop_h = 520 / 900\nbot_h = 380 / 900\nleft_w = 380 / 1600\nright_w = 1220 / 1600\n\nfig = ggbunch(\n    [p_top, p_sets, p_matrix], [(0, 0, 1.0, top_h), (0, top_h, left_w, bot_h), (left_w, top_h, right_w, bot_h)]\n) + ggsize(1600, 900)\n\n# Save\nggsave(fig, f\"plot-{THEME}.png\", path=\".\", scale=3)\nggsave(fig, f\"plot-{THEME}.html\", path=\".\")\n"}