{"spec_id":"network-hierarchical","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nnetwork-hierarchical: Hierarchical Network Graph with Tree Layout\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 95/100 | Updated: 2026-05-17\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Ensure we import the real bokeh, not this script\nsys.path = [p for p in sys.path if p not in (\"\", os.path.dirname(__file__))]\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, LabelSet, Legend, LegendItem\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\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\n# Okabe-Ito palette for level differentiation\nLEVEL_COLORS = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data: Software project team hierarchy (24 employees, 4 levels)\nnp.random.seed(42)\n\nnodes = [\n    # Level 0 - CEO\n    {\"id\": 0, \"label\": \"CEO\", \"level\": 0},\n    # Level 1 - VPs\n    {\"id\": 1, \"label\": \"VP Engineering\", \"level\": 1},\n    {\"id\": 2, \"label\": \"VP Product\", \"level\": 1},\n    {\"id\": 3, \"label\": \"VP Operations\", \"level\": 1},\n    # Level 2 - Directors/Managers\n    {\"id\": 4, \"label\": \"Frontend Dir.\", \"level\": 2},\n    {\"id\": 5, \"label\": \"Backend Dir.\", \"level\": 2},\n    {\"id\": 6, \"label\": \"PM Lead\", \"level\": 2},\n    {\"id\": 7, \"label\": \"UX Lead\", \"level\": 2},\n    {\"id\": 8, \"label\": \"IT Manager\", \"level\": 2},\n    {\"id\": 9, \"label\": \"HR Manager\", \"level\": 2},\n    # Level 3 - Team Members\n    {\"id\": 10, \"label\": \"FE Dev 1\", \"level\": 3},\n    {\"id\": 11, \"label\": \"FE Dev 2\", \"level\": 3},\n    {\"id\": 12, \"label\": \"BE Dev 1\", \"level\": 3},\n    {\"id\": 13, \"label\": \"BE Dev 2\", \"level\": 3},\n    {\"id\": 14, \"label\": \"BE Dev 3\", \"level\": 3},\n    {\"id\": 15, \"label\": \"PM 1\", \"level\": 3},\n    {\"id\": 16, \"label\": \"PM 2\", \"level\": 3},\n    {\"id\": 17, \"label\": \"Designer 1\", \"level\": 3},\n    {\"id\": 18, \"label\": \"Designer 2\", \"level\": 3},\n    {\"id\": 19, \"label\": \"IT Support 1\", \"level\": 3},\n    {\"id\": 20, \"label\": \"IT Support 2\", \"level\": 3},\n    {\"id\": 21, \"label\": \"HR Spec 1\", \"level\": 3},\n    {\"id\": 22, \"label\": \"HR Spec 2\", \"level\": 3},\n    {\"id\": 23, \"label\": \"Recruiter\", \"level\": 3},\n]\n\nedges = [\n    # CEO to VPs\n    (0, 1),\n    (0, 2),\n    (0, 3),\n    # VP Engineering to Directors\n    (1, 4),\n    (1, 5),\n    # VP Product to Leads\n    (2, 6),\n    (2, 7),\n    # VP Operations to Managers\n    (3, 8),\n    (3, 9),\n    # Directors to Team Members\n    (4, 10),\n    (4, 11),\n    (5, 12),\n    (5, 13),\n    (5, 14),\n    (6, 15),\n    (6, 16),\n    (7, 17),\n    (7, 18),\n    (8, 19),\n    (8, 20),\n    (9, 21),\n    (9, 22),\n    (9, 23),\n]\n\n# Compute hierarchical layout positions\n# Group nodes by level\nlevels = {}\nfor node in nodes:\n    lvl = node[\"level\"]\n    if lvl not in levels:\n        levels[lvl] = []\n    levels[lvl].append(node)\n\n# Calculate positions: levels spread vertically, nodes at each level spread horizontally\npositions = {}\ny_spacing = 2.0\nfor lvl in sorted(levels.keys()):\n    nodes_at_level = levels[lvl]\n    n = len(nodes_at_level)\n    spread = max(n * 1.4, 6)\n    x_positions = np.linspace(-spread / 2, spread / 2, n)\n    y_pos = -lvl * y_spacing\n    for i, node in enumerate(nodes_at_level):\n        positions[node[\"id\"]] = (x_positions[i], y_pos)\n\n# Prepare node data\nnode_x = [positions[n[\"id\"]][0] for n in nodes]\nnode_y = [positions[n[\"id\"]][1] for n in nodes]\nnode_labels = [n[\"label\"] for n in nodes]\nnode_levels = [n[\"level\"] for n in nodes]\n\n# Color by level using Okabe-Ito\nnode_colors = [LEVEL_COLORS[lvl] for lvl in node_levels]\n\n# Node sizes based on level - larger for better visibility at Level 3\nsize_map = {0: 95, 1: 80, 2: 70, 3: 65}\nnode_sizes = [size_map[lvl] for lvl in node_levels]\n\n# Prepare edge data\nedge_x0 = [positions[e[0]][0] for e in edges]\nedge_y0 = [positions[e[0]][1] for e in edges]\nedge_x1 = [positions[e[1]][0] for e in edges]\nedge_y1 = [positions[e[1]][1] for e in edges]\n\n# Create figure\np = figure(\n    width=4800,\n    height=2700,\n    title=\"network-hierarchical · bokeh · anyplot.ai\",\n    x_axis_label=\"\",\n    y_axis_label=\"\",\n    tools=\"\",\n    toolbar_location=None,\n)\n\n# Apply theme-adaptive styling\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\n# Hide axes for cleaner network visualization\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.visible = False\np.ygrid.visible = False\n\n# Draw edges\nedge_source = ColumnDataSource(data={\"x0\": edge_x0, \"y0\": edge_y0, \"x1\": edge_x1, \"y1\": edge_y1})\np.segment(x0=\"x0\", y0=\"y0\", x1=\"x1\", y1=\"y1\", source=edge_source, line_width=4, line_color=INK_SOFT, line_alpha=0.4)\n\n# Draw nodes by level\nrenderers_by_level = {}\nlevel_names = [\"CEO\", \"VPs\", \"Directors\", \"Team Members\"]\n\nfor lvl in range(4):\n    lvl_indices = [i for i, n in enumerate(nodes) if n[\"level\"] == lvl]\n    lvl_x = [node_x[i] for i in lvl_indices]\n    lvl_y = [node_y[i] for i in lvl_indices]\n    lvl_labels = [node_labels[i] for i in lvl_indices]\n    lvl_sizes = [node_sizes[i] for i in lvl_indices]\n\n    source = ColumnDataSource(\n        data={\"x\": lvl_x, \"y\": lvl_y, \"labels\": lvl_labels, \"sizes\": lvl_sizes, \"level\": [lvl] * len(lvl_indices)}\n    )\n    renderer = p.scatter(\n        x=\"x\",\n        y=\"y\",\n        source=source,\n        size=\"sizes\",\n        fill_color=LEVEL_COLORS[lvl],\n        line_color=INK_SOFT,\n        line_width=3,\n        alpha=0.9,\n    )\n    renderers_by_level[lvl] = renderer\n\n# Create legend with theme-adaptive styling\nlegend_items = [\n    LegendItem(label=f\"Level {lvl}: {level_names[lvl]}\", renderers=[renderers_by_level[lvl]]) for lvl in range(4)\n]\nlegend = Legend(\n    items=legend_items,\n    location=\"top_right\",\n    label_text_font_size=\"26pt\",\n    label_text_color=INK_SOFT,\n    glyph_height=45,\n    glyph_width=45,\n    spacing=12,\n    padding=25,\n    border_line_color=INK_SOFT,\n    border_line_width=2,\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.95,\n)\np.add_layout(legend, \"right\")\n\n# Prepare node data for labels and hover\nall_node_source = ColumnDataSource(data={\"x\": node_x, \"y\": node_y, \"labels\": node_labels, \"levels\": node_levels})\n\n# Add labels to nodes with improved visibility\nlabels = LabelSet(\n    x=\"x\",\n    y=\"y\",\n    text=\"labels\",\n    source=all_node_source,\n    text_font_size=\"26pt\",\n    text_color=INK,\n    text_align=\"center\",\n    y_offset=60,\n)\np.add_layout(labels)\n\n# Add hover tool for interactivity\nhover = HoverTool(tooltips=[(\"Role\", \"@labels\"), (\"Level\", \"@level\")], mode=\"mouse\")\np.add_tools(hover)\n\n# Style title\np.title.text_font_size = \"28pt\"\np.title.text_color = INK\np.title.align = \"center\"\n\n# Set plot range with balanced padding\nx_vals = list(node_x)\ny_vals = list(node_y)\nx_padding = 1.0\ny_padding = 1.5\np.x_range.start = min(x_vals) - x_padding\np.x_range.end = max(x_vals) + x_padding\np.y_range.start = min(y_vals) - y_padding\np.y_range.end = max(y_vals) + y_padding\n\n# Save HTML output\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with Selenium for PNG output\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"}