{"spec_id":"upset-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nupset-basic: UpSet Plot for Multi-Set Intersection Analysis\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 84/100 | Created: 2026-05-13\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove script directory from sys.path to avoid importing local altair.py\nscript_dir = os.path.dirname(os.path.abspath(__file__))\nif script_dir in sys.path:\n    sys.path.remove(script_dir)\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\n\n\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nBRAND = \"#009E73\"\nC2 = \"#C475FD\"\nC3 = \"#4467A3\"\nC4 = \"#BD8233\"\nC5 = \"#AE3030\"\n\n# Data — gene sets from 5 genomic experiments\nnp.random.seed(42)\n\nset_names = [\"Exp A\", \"Exp B\", \"Exp C\", \"Exp D\", \"Exp E\"]\nn_sets = len(set_names)\n\n# Generate membership with realistic overlap structure\nn_genes = 600\nmemberships = {s: set() for s in set_names}\n\n# Seed unique members per set\nbase_sizes = [220, 180, 160, 140, 120]\ngene_id = 0\nfor s, sz in zip(set_names, base_sizes, strict=True):\n    for _ in range(sz):\n        memberships[s].add(f\"g{gene_id}\")\n        gene_id += 1\n\n# Add overlapping genes\noverlap_groups = [\n    ([\"Exp A\", \"Exp B\"], 60),\n    ([\"Exp A\", \"Exp C\"], 40),\n    ([\"Exp B\", \"Exp C\"], 35),\n    ([\"Exp A\", \"Exp B\", \"Exp C\"], 25),\n    ([\"Exp B\", \"Exp D\"], 30),\n    ([\"Exp C\", \"Exp D\"], 20),\n    ([\"Exp A\", \"Exp D\"], 18),\n    ([\"Exp D\", \"Exp E\"], 22),\n    ([\"Exp A\", \"Exp B\", \"Exp D\"], 15),\n    ([\"Exp A\", \"Exp B\", \"Exp C\", \"Exp D\"], 10),\n    ([\"Exp A\", \"Exp E\"], 12),\n    ([\"Exp B\", \"Exp C\", \"Exp E\"], 8),\n    ([\"Exp A\", \"Exp B\", \"Exp C\", \"Exp D\", \"Exp E\"], 5),\n]\n\nfor sets_in_group, count in overlap_groups:\n    for _ in range(count):\n        gid = f\"g{gene_id}\"\n        gene_id += 1\n        for s in sets_in_group:\n            memberships[s].add(gid)\n\n# Build element→sets mapping\nall_genes = set()\nfor s in set_names:\n    all_genes |= memberships[s]\n\nrecords = []\nfor g in all_genes:\n    belongs = tuple(sorted(s for s in set_names if g in memberships[s]))\n    records.append({\"gene\": g, \"combo\": belongs})\n\ndf_elements = pd.DataFrame(records)\n\n# Compute intersection sizes\ncombo_counts = df_elements.groupby(\"combo\").size().reset_index(name=\"size\")\ncombo_counts = combo_counts.sort_values(\"size\", ascending=False).reset_index(drop=True)\ncombo_counts[\"col_idx\"] = range(len(combo_counts))\n\n# Keep top 14 intersections for readability\ntop_n = 14\ncombo_counts = combo_counts.head(top_n).reset_index(drop=True)\ncombo_counts[\"col_idx\"] = range(len(combo_counts))\n\n# Compute set sizes\nset_sizes = {s: len(memberships[s]) for s in set_names}\nset_df = pd.DataFrame([{\"set\": s, \"size\": set_sizes[s]} for s in set_names])\nset_df[\"row_idx\"] = range(n_sets)\n\n# Build dot matrix data\ndot_rows = []\nfor _, row in combo_counts.iterrows():\n    sets_in = set(row[\"combo\"])\n    for s_idx, s in enumerate(set_names):\n        dot_rows.append(\n            {\"col_idx\": row[\"col_idx\"], \"row_idx\": s_idx, \"set\": s, \"active\": s in sets_in, \"degree\": len(sets_in)}\n        )\n\ndot_df = pd.DataFrame(dot_rows)\n\n# Build connector (vertical line) data for each intersection column\nconn_rows = []\nfor _, row in combo_counts.iterrows():\n    sets_in = sorted([set_names.index(s) for s in row[\"combo\"]])\n    if len(sets_in) >= 2:\n        conn_rows.append({\"col_idx\": row[\"col_idx\"], \"y_min\": sets_in[0], \"y_max\": sets_in[-1]})\n\nconn_df = pd.DataFrame(conn_rows)\n\n# Add intersection labels for bar chart\ncombo_counts[\"label\"] = combo_counts[\"combo\"].apply(lambda c: \" ∩ \".join(c) if len(c) <= 2 else f\"{len(c)}-way\")\n\n# --- Chart dimensions ---\nW = 1600\nH = 900\nbar_top_h = 320\nmatrix_h = 260\nbar_left_w = 200\nmain_w = W - bar_left_w - 20\n\ncol_step = main_w // top_n\nrow_step = matrix_h // n_sets\n\n# Altair uses data-space coordinates, so we work with col_idx / row_idx directly\n# and set explicit step sizes via scale\n\n# 1. Top intersection bar chart\nbar_top = (\n    alt.Chart(combo_counts)\n    .mark_bar(color=BRAND, stroke=PAGE_BG, strokeWidth=1)\n    .encode(\n        x=alt.X(\"col_idx:O\", axis=None, scale=alt.Scale(paddingInner=0.25)),\n        y=alt.Y(\n            \"size:Q\",\n            title=\"Intersection Size\",\n            axis=alt.Axis(\n                titleFontSize=22,\n                labelFontSize=18,\n                titleColor=INK,\n                labelColor=INK_SOFT,\n                domainColor=INK_SOFT,\n                tickColor=INK_SOFT,\n                gridColor=INK,\n                gridOpacity=0.10,\n            ),\n        ),\n        tooltip=[alt.Tooltip(\"label:N\", title=\"Intersection\"), alt.Tooltip(\"size:Q\", title=\"Genes\")],\n    )\n    .properties(width=main_w, height=bar_top_h)\n)\n\n# Count labels on top of each bar to highlight key intersections\nbar_labels = (\n    alt.Chart(combo_counts)\n    .mark_text(dy=-10, fontSize=16, fontWeight=\"bold\")\n    .encode(\n        x=alt.X(\"col_idx:O\", axis=None, scale=alt.Scale(paddingInner=0.25)),\n        y=alt.Y(\"size:Q\"),\n        text=alt.Text(\"size:Q\", format=\"d\"),\n        color=alt.value(INK_SOFT),\n    )\n    .properties(width=main_w, height=bar_top_h)\n)\n\nbar_top_chart = alt.layer(bar_top, bar_labels)\n\n# 2. Dot matrix — inactive dots\ndot_inactive = (\n    alt.Chart(dot_df[dot_df[\"active\"] == False])\n    .mark_circle(size=120, opacity=0.18)\n    .encode(\n        x=alt.X(\"col_idx:O\", axis=None, scale=alt.Scale(paddingInner=0.25)),\n        y=alt.Y(\"row_idx:O\", scale=alt.Scale(reverse=False), axis=None),\n        color=alt.value(INK_SOFT),\n    )\n    .properties(width=main_w, height=matrix_h)\n)\n\n# Active dots colored by degree\ndegree_colors = {1: BRAND, 2: C2, 3: C3, 4: C4, 5: C5}\ndot_df[\"dot_color\"] = dot_df[\"degree\"].map(degree_colors)\n\ndot_active = (\n    alt.Chart(dot_df[dot_df[\"active\"] == True])\n    .mark_circle(size=200)\n    .encode(\n        x=alt.X(\"col_idx:O\", axis=None, scale=alt.Scale(paddingInner=0.25)),\n        y=alt.Y(\"row_idx:O\", scale=alt.Scale(reverse=False), axis=None),\n        color=alt.Color(\n            \"degree:O\",\n            scale=alt.Scale(domain=[1, 2, 3, 4, 5], range=[BRAND, C2, C3, C4, C5]),\n            legend=alt.Legend(\n                title=\"Degree\",\n                titleColor=INK,\n                labelColor=INK_SOFT,\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n                labelFontSize=16,\n                titleFontSize=16,\n                orient=\"bottom-right\",\n            ),\n        ),\n        tooltip=[alt.Tooltip(\"set:N\", title=\"Set\"), alt.Tooltip(\"degree:Q\", title=\"Degree\")],\n    )\n    .properties(width=main_w, height=matrix_h)\n)\n\n# Connector lines — thicker for visibility at canvas scale\nconn_chart = (\n    alt.Chart(conn_df)\n    .mark_rule(strokeWidth=5)\n    .encode(\n        x=alt.X(\"col_idx:O\", axis=None, scale=alt.Scale(paddingInner=0.25)),\n        y=alt.Y(\"y_min:Q\", axis=None, scale=alt.Scale(domain=[-0.5, n_sets - 0.5], reverse=False)),\n        y2=alt.Y2(\"y_max:Q\"),\n        color=alt.value(INK_SOFT),\n    )\n    .properties(width=main_w, height=matrix_h)\n)\n\nmatrix_layer = alt.layer(dot_inactive, conn_chart, dot_active)\n\n# 3. Set size bar (horizontal, left side) — BRAND green to visually link to intersection bars\nset_bar = (\n    alt.Chart(set_df)\n    .mark_bar(color=BRAND, opacity=0.75)\n    .encode(\n        y=alt.Y(\n            \"row_idx:O\",\n            scale=alt.Scale(reverse=False),\n            axis=alt.Axis(\n                labels=True,\n                labelExpr=\"datum.value == 0 ? 'Exp A' : datum.value == 1 ? 'Exp B' : datum.value == 2 ? 'Exp C' : datum.value == 3 ? 'Exp D' : 'Exp E'\",\n                labelFontSize=18,\n                labelColor=INK_SOFT,\n                domainColor=INK_SOFT,\n                tickColor=INK_SOFT,\n                titleColor=INK,\n                title=None,\n            ),\n        ),\n        x=alt.X(\n            \"size:Q\",\n            title=\"Set Size\",\n            sort=\"descending\",\n            axis=alt.Axis(\n                titleFontSize=22,\n                labelFontSize=18,\n                titleColor=INK,\n                labelColor=INK_SOFT,\n                domainColor=INK_SOFT,\n                tickColor=INK_SOFT,\n                gridColor=INK,\n                gridOpacity=0.10,\n            ),\n            scale=alt.Scale(reverse=True),\n        ),\n        tooltip=[alt.Tooltip(\"set:N\", title=\"Set\"), alt.Tooltip(\"size:Q\", title=\"Genes\")],\n    )\n    .properties(width=bar_left_w, height=matrix_h)\n)\n\n# Set name labels on the right of the set bar (in the matrix row axis)\nset_label = (\n    alt.Chart(set_df)\n    .mark_text(align=\"left\", dx=4, fontSize=18)\n    .encode(y=alt.Y(\"row_idx:O\", axis=None), text=alt.Text(\"set:N\"), color=alt.value(INK_SOFT))\n    .properties(width=20, height=matrix_h)\n)\n\n# Spacer chart for top-left corner alignment\nspacer_top = (\n    alt.Chart(pd.DataFrame({\"x\": [0]}))\n    .mark_point(opacity=0)\n    .encode(x=alt.X(\"x:Q\", axis=None))\n    .properties(width=bar_left_w, height=bar_top_h)\n)\n\n# Compose layout: [spacer | bar_top] / [set_bar | matrix]\ntop_row = alt.hconcat(spacer_top, bar_top_chart, spacing=4)\nbottom_row = alt.hconcat(set_bar, matrix_layer, spacing=4)\n\nchart = (\n    alt.vconcat(top_row, bottom_row, spacing=8)\n    .properties(\n        title=alt.Title(\"upset-basic · altair · anyplot.ai\", fontSize=28, color=INK, anchor=\"start\"), background=PAGE_BG\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT)\n    .configure_concat(spacing=4)\n)\n\n# Save\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}