{"spec_id":"network-bipartite","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nnetwork-bipartite: Bipartite Network Graph\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 85/100 | Created: 2026-05-14\n\"\"\"\n\nimport os\nfrom collections import defaultdict\n\nimport numpy as np\nimport plotly.graph_objects as go\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\"\n\nSTUDENT_COLOR = \"#009E73\"  # Okabe-Ito position 1\nCOURSE_COLOR = \"#C475FD\"  # Okabe-Ito position 2\nEDGE_COLOR = \"rgba(107,106,99,0.30)\" if THEME == \"light\" else \"rgba(168,167,159,0.22)\"\n\n# Data - student-course enrollment network\nnp.random.seed(42)\n\nstudents = [\"Alice\", \"Bob\", \"Carol\", \"David\", \"Emma\", \"Frank\", \"Grace\", \"Henry\", \"Iris\", \"Jack\", \"Karen\", \"Leo\"]\ncourses = [\n    \"Algorithms\",\n    \"Data Structures\",\n    \"Machine Learning\",\n    \"Statistics\",\n    \"Linear Algebra\",\n    \"Databases\",\n    \"Networks\",\n    \"Computer Vision\",\n]\n\n# (student_idx, course_idx, weight) — weight encodes credit overlap strength\nenrollments = [\n    (0, 0, 3),\n    (0, 1, 4),\n    (0, 2, 4),\n    (0, 3, 3),\n    (0, 4, 2),  # Alice: 5 courses\n    (1, 0, 3),\n    (1, 5, 3),\n    (1, 7, 2),\n    (2, 2, 4),\n    (2, 3, 3),\n    (2, 6, 2),\n    (3, 1, 3),\n    (3, 4, 3),\n    (3, 6, 2),\n    (4, 0, 2),\n    (4, 2, 3),\n    (4, 7, 3),\n    (5, 5, 3),\n    (5, 6, 3),\n    (5, 7, 4),\n    (6, 1, 2),\n    (6, 3, 4),\n    (6, 4, 2),\n    (7, 2, 4),\n    (7, 5, 3),\n    (7, 6, 2),\n    (8, 0, 3),\n    (8, 1, 2),\n    (8, 3, 3),\n    (8, 7, 4),  # Iris: 4 courses\n    (9, 4, 3),\n    (9, 6, 3),  # Jack: 2 courses\n    (10, 5, 2),\n    (10, 7, 3),  # Karen: 2 courses\n    (11, 3, 3),\n    (11, 6, 2),\n    (11, 7, 4),\n]\n\n# Degree per node\nstudent_degree = [0] * len(students)\ncourse_degree = [0] * len(courses)\nfor s_i, c_i, _ in enrollments:\n    student_degree[s_i] += 1\n    course_degree[c_i] += 1\n\n# Node positions: students on left (x=0), courses on right (x=1)\nstudent_y = np.linspace(0.05, 0.95, len(students))\ncourse_y = np.linspace(0.10, 0.90, len(courses))\n\n# Edge traces grouped by weight for variable line width\nweight_groups = defaultdict(list)\nfor s_i, c_i, w in enrollments:\n    weight_groups[w].append((s_i, c_i))\n\ntraces = []\nfor w, pairs in sorted(weight_groups.items()):\n    xs, ys = [], []\n    for s_i, c_i in pairs:\n        xs += [0.0, 1.0, None]\n        ys += [float(student_y[s_i]), float(course_y[c_i]), None]\n    traces.append(\n        go.Scatter(\n            x=xs, y=ys, mode=\"lines\", line={\"width\": w * 0.9, \"color\": EDGE_COLOR}, hoverinfo=\"none\", showlegend=False\n        )\n    )\n\n# Node size: linear scale on degree\ns_min, s_max = min(student_degree), max(student_degree)\nstudent_sizes = [22 + (d - s_min) / (s_max - s_min) * 28 for d in student_degree]\n\nc_min, c_max = min(course_degree), max(course_degree)\ncourse_sizes = [22 + (d - c_min) / (c_max - c_min) * 28 for d in course_degree]\n\ntraces.append(\n    go.Scatter(\n        x=[0.0] * len(students),\n        y=list(student_y),\n        mode=\"markers+text\",\n        marker={\"size\": student_sizes, \"color\": STUDENT_COLOR, \"line\": {\"color\": PAGE_BG, \"width\": 2}},\n        text=students,\n        textposition=\"middle left\",\n        textfont={\"size\": 16, \"color\": INK},\n        name=\"Students\",\n        customdata=student_degree,\n        hovertemplate=\"<b>%{text}</b><br>Enrolled in %{customdata} courses<extra></extra>\",\n    )\n)\n\ntraces.append(\n    go.Scatter(\n        x=[1.0] * len(courses),\n        y=list(course_y),\n        mode=\"markers+text\",\n        marker={\"size\": course_sizes, \"color\": COURSE_COLOR, \"line\": {\"color\": PAGE_BG, \"width\": 2}},\n        text=courses,\n        textposition=\"middle right\",\n        textfont={\"size\": 16, \"color\": INK},\n        name=\"Courses\",\n        customdata=course_degree,\n        hovertemplate=\"<b>%{text}</b><br>%{customdata} students enrolled<extra></extra>\",\n    )\n)\n\nfig = go.Figure(data=traces)\n\nfig.update_layout(\n    title={\n        \"text\": \"Student-Course Enrollment · network-bipartite · plotly · anyplot.ai\",\n        \"font\": {\"size\": 28, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    legend={\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"font\": {\"size\": 16, \"color\": INK_SOFT},\n        \"orientation\": \"h\",\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n        \"y\": -0.04,\n    },\n    xaxis={\"range\": [-0.55, 1.55], \"showgrid\": False, \"zeroline\": False, \"showticklabels\": False, \"showline\": False},\n    yaxis={\"range\": [-0.05, 1.12], \"showgrid\": False, \"zeroline\": False, \"showticklabels\": False, \"showline\": False},\n    margin={\"l\": 20, \"r\": 20, \"t\": 80, \"b\": 60},\n)\n\n# Column headers\nfig.add_annotation(\n    x=0.0, y=1.07, text=\"<b>Students</b>\", font={\"size\": 22, \"color\": INK}, showarrow=False, xref=\"x\", yref=\"y\"\n)\nfig.add_annotation(\n    x=1.0, y=1.07, text=\"<b>Courses</b>\", font={\"size\": 22, \"color\": INK}, showarrow=False, xref=\"x\", yref=\"y\"\n)\n\n# Subtle vertical separator\nfig.add_shape(\n    type=\"line\",\n    x0=0.5,\n    x1=0.5,\n    y0=0.0,\n    y1=1.0,\n    xref=\"x\",\n    yref=\"paper\",\n    line={\"color\": INK_SOFT, \"width\": 1, \"dash\": \"dot\"},\n)\n\nfig.write_image(f\"plot-{THEME}.png\", width=1600, height=900, scale=3)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}