{"spec_id":"network-bipartite","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nnetwork-bipartite: Bipartite Network Graph\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 89/100 | Created: 2026-05-14\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\nnp.random.seed(42)\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\nCOLOR_A = \"#009E73\"  # Authors — Okabe-Ito position 1\nCOLOR_B = \"#C475FD\"  # Papers — Okabe-Ito position 2\n\n# Data — researcher-paper affiliation network (bibliometrics)\nauthor_names = [\n    \"Chen, L.\",\n    \"Smith, R.\",\n    \"Patel, A.\",\n    \"Kim, J.\",\n    \"Müller, K.\",\n    \"Davis, M.\",\n    \"López, C.\",\n    \"Nguyen, T.\",\n    \"Brown, E.\",\n    \"Ivanova, S.\",\n    \"Ahmed, F.\",\n    \"Wilson, G.\",\n]\npaper_ids = [f\"P{i + 1:02d}\" for i in range(15)]\n\n# Edges: each author co-authors 2–4 papers\nedge_list = []\nfor author in author_names:\n    n = np.random.randint(2, 5)\n    selected = np.random.choice(paper_ids, size=n, replace=False)\n    for p in selected:\n        edge_list.append({\"source\": author, \"target\": p})\nedges_df = pd.DataFrame(edge_list).drop_duplicates(subset=[\"source\", \"target\"])\n\n# Node degrees\nsrc_deg = edges_df.groupby(\"source\").size().reset_index(name=\"degree\")\ntgt_deg = edges_df.groupby(\"target\").size().reset_index(name=\"degree\")\n\n# Node positions — two columns, evenly spaced vertically\nnodes_a = pd.DataFrame(\n    {\"node\": author_names, \"x\": 0.22, \"y\": np.linspace(0.08, 0.88, len(author_names)), \"set\": \"Author\"}\n).merge(src_deg.rename(columns={\"source\": \"node\"}), on=\"node\", how=\"left\")\nnodes_a[\"degree\"] = nodes_a[\"degree\"].fillna(0).astype(int)\n\nnodes_b = pd.DataFrame(\n    {\"node\": paper_ids, \"x\": 0.78, \"y\": np.linspace(0.08, 0.88, len(paper_ids)), \"set\": \"Paper\"}\n).merge(tgt_deg.rename(columns={\"target\": \"node\"}), on=\"node\", how=\"left\")\nnodes_b[\"degree\"] = nodes_b[\"degree\"].fillna(0).astype(int)\n\nnodes_df = pd.concat([nodes_a, nodes_b], ignore_index=True)\n\n# Edge coordinates for mark_rule (x,y → x2,y2)\nnode_pos = nodes_df.set_index(\"node\")[[\"x\", \"y\"]].to_dict(\"index\")\nedge_records = []\nfor _, row in edges_df.iterrows():\n    s, t = node_pos[row[\"source\"]], node_pos[row[\"target\"]]\n    edge_records.append({\"x\": s[\"x\"], \"y\": s[\"y\"], \"x2\": t[\"x\"], \"y2\": t[\"y\"]})\nedge_data = pd.DataFrame(edge_records)\n\n# Shared scale objects\nxscale = alt.Scale(domain=[0.0, 1.0])\nyscale = alt.Scale(domain=[0.0, 1.05])\n\nTITLE = \"network-bipartite · altair · anyplot.ai\"\n\n# Edge lines\nedges_chart = (\n    alt.Chart(edge_data)\n    .mark_rule(color=INK_SOFT, opacity=0.28, strokeWidth=1.2)\n    .encode(x=alt.X(\"x:Q\", scale=xscale, axis=None), y=alt.Y(\"y:Q\", scale=yscale, axis=None), x2=\"x2:Q\", y2=\"y2:Q\")\n)\n\n# Nodes — size encodes degree, color encodes set membership\nnodes_chart = (\n    alt.Chart(nodes_df)\n    .mark_circle(stroke=PAGE_BG, strokeWidth=2)\n    .encode(\n        x=alt.X(\"x:Q\", scale=xscale, axis=None),\n        y=alt.Y(\"y:Q\", scale=yscale, axis=None),\n        color=alt.Color(\n            \"set:N\",\n            scale=alt.Scale(domain=[\"Author\", \"Paper\"], range=[COLOR_A, COLOR_B]),\n            legend=alt.Legend(title=\"Node Set\", titleFontSize=20, labelFontSize=18, orient=\"bottom-right\"),\n        ),\n        size=alt.Size(\"degree:Q\", scale=alt.Scale(range=[200, 1200]), legend=None),\n        tooltip=[\"node:N\", \"set:N\", \"degree:N\"],\n    )\n)\n\n# Node labels — authors right-aligned, papers left-aligned\nlabels_a = (\n    alt.Chart(nodes_a[[\"node\", \"x\", \"y\"]])\n    .mark_text(align=\"right\", dx=-30, fontSize=15)\n    .encode(\n        x=alt.X(\"x:Q\", scale=xscale, axis=None),\n        y=alt.Y(\"y:Q\", scale=yscale, axis=None),\n        text=\"node:N\",\n        color=alt.value(INK_SOFT),\n    )\n)\n\nlabels_b = (\n    alt.Chart(nodes_b[[\"node\", \"x\", \"y\"]])\n    .mark_text(align=\"left\", dx=30, fontSize=15)\n    .encode(\n        x=alt.X(\"x:Q\", scale=xscale, axis=None),\n        y=alt.Y(\"y:Q\", scale=yscale, axis=None),\n        text=\"node:N\",\n        color=alt.value(INK_SOFT),\n    )\n)\n\n# Column header labels\nheader_a = (\n    alt.Chart(pd.DataFrame({\"x\": [0.22], \"y\": [0.97], \"text\": [\"Authors\"]}))\n    .mark_text(fontSize=22, fontWeight=\"bold\", color=COLOR_A)\n    .encode(x=alt.X(\"x:Q\", scale=xscale, axis=None), y=alt.Y(\"y:Q\", scale=yscale, axis=None), text=\"text:N\")\n)\n\nheader_b = (\n    alt.Chart(pd.DataFrame({\"x\": [0.78], \"y\": [0.97], \"text\": [\"Papers\"]}))\n    .mark_text(fontSize=22, fontWeight=\"bold\", color=COLOR_B)\n    .encode(x=alt.X(\"x:Q\", scale=xscale, axis=None), y=alt.Y(\"y:Q\", scale=yscale, axis=None), text=\"text:N\")\n)\n\n# Compose all layers\nchart = (\n    alt.layer(edges_chart, nodes_chart, labels_a, labels_b, header_a, header_b)\n    .properties(width=1600, height=900, title=alt.Title(TITLE), background=PAGE_BG)\n    .configure_view(fill=PAGE_BG, strokeOpacity=0)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=18,\n        titleFontSize=22,\n    )\n    .configure_title(color=INK, fontSize=28, anchor=\"start\", offset=12)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}