{"spec_id":"network-bipartite","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nnetwork-bipartite: Bipartite Network Graph\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 84/100 | Created: 2026-05-14\n\"\"\"\n\nimport sys\n\n\nsys.path.pop(0)  # prevent bokeh.py from shadowing the bokeh package\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme\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\nCOLOR_A = \"#009E73\"  # Students - Okabe-Ito position 1\nCOLOR_B = \"#C475FD\"  # Courses  - Okabe-Ito position 2\n\n# Data: student-course enrollment network\nnp.random.seed(42)\n\nstudents = [\n    \"Alice\",\n    \"Bob\",\n    \"Carol\",\n    \"David\",\n    \"Emma\",\n    \"Frank\",\n    \"Grace\",\n    \"Henry\",\n    \"Iris\",\n    \"James\",\n    \"Kate\",\n    \"Liam\",\n    \"Maya\",\n    \"Noah\",\n    \"Olivia\",\n]\ncourses = [\n    \"CS101\",\n    \"MATH201\",\n    \"PHYS101\",\n    \"BIO201\",\n    \"CHEM101\",\n    \"ENG101\",\n    \"HIST201\",\n    \"ART101\",\n    \"CS201\",\n    \"STAT101\",\n    \"ECON201\",\n    \"MUSIC101\",\n]\n\nn_s = len(students)\nn_c = len(courses)\n\n# Generate enrollment edges (weighted toward popular courses)\ncourse_weights = np.array([3, 3, 2, 2, 2, 2, 1, 1, 2, 2, 1, 1], dtype=float)\ncourse_weights /= course_weights.sum()\n\nedges = set()\nfor i in range(n_s):\n    n_enroll = np.random.randint(2, 6)\n    chosen = np.random.choice(n_c, size=n_enroll, replace=False, p=course_weights)\n    for c in chosen:\n        edges.add((i, int(c)))\nedges = sorted(edges)\n\n# Node degrees\ns_degree = np.zeros(n_s)\nc_degree = np.zeros(n_c)\nfor s, c in edges:\n    s_degree[s] += 1\n    c_degree[c] += 1\n\n# Layout: students left (x=0.22), courses right (x=0.78)\nX_LEFT = 0.22\nX_RIGHT = 0.78\ns_y = np.linspace(0.92, 0.05, n_s)\nc_y = np.linspace(0.92, 0.05, n_c)\n\n# Node sizes proportional to degree\ns_sizes = 28 + 52 * (s_degree / s_degree.max())\nc_sizes = 28 + 52 * (c_degree / c_degree.max())\n\n# Edge segment coordinates\nseg_x0 = [X_LEFT] * len(edges)\nseg_y0 = [s_y[s] for s, c in edges]\nseg_x1 = [X_RIGHT] * len(edges)\nseg_y1 = [c_y[c] for s, c in edges]\n\n# Figure\np = figure(\n    width=4800,\n    height=2700,\n    x_range=(-0.05, 1.05),\n    y_range=(-0.02, 1.08),\n    toolbar_location=None,\n    tools=\"\",\n    title=\"Student-Course Enrollment · network-bipartite · bokeh · anyplot.ai\",\n)\n\n# Edges\np.segment(x0=seg_x0, y0=seg_y0, x1=seg_x1, y1=seg_y1, line_color=INK_MUTED, line_alpha=0.30, line_width=2)\n\n# Student nodes (set A)\np.scatter(\n    x=[X_LEFT] * n_s, y=s_y.tolist(), size=s_sizes.tolist(), color=COLOR_A, line_color=PAGE_BG, line_width=4, alpha=0.92\n)\n\n# Course nodes (set B)\np.scatter(\n    x=[X_RIGHT] * n_c,\n    y=c_y.tolist(),\n    size=c_sizes.tolist(),\n    color=COLOR_B,\n    line_color=PAGE_BG,\n    line_width=4,\n    alpha=0.92,\n)\n\n# Student labels (right-aligned, left of nodes)\np.text(\n    x=[X_LEFT - 0.03] * n_s,\n    y=s_y.tolist(),\n    text=students,\n    text_align=\"right\",\n    text_baseline=\"middle\",\n    text_font_size=\"18pt\",\n    text_color=INK_SOFT,\n)\n\n# Course labels (left-aligned, right of nodes)\np.text(\n    x=[X_RIGHT + 0.03] * n_c,\n    y=c_y.tolist(),\n    text=courses,\n    text_align=\"left\",\n    text_baseline=\"middle\",\n    text_font_size=\"18pt\",\n    text_color=INK_SOFT,\n)\n\n# Column headers (colored to match node sets)\np.text(\n    x=[X_LEFT, X_RIGHT],\n    y=[1.03, 1.03],\n    text=[\"Students\", \"Courses\"],\n    text_align=\"center\",\n    text_baseline=\"middle\",\n    text_font_size=\"24pt\",\n    text_font_style=\"bold\",\n    text_color=[COLOR_A, COLOR_B],\n)\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.visible = False\np.ygrid.visible = False\n\np.title.text_font_size = \"28pt\"\np.title.text_color = INK\np.title.text_font_style = \"normal\"\n\n# Save HTML + PNG\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\nW, H = 4800, 2700\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}