{"spec_id":"arc-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\narc-basic: Basic Arc Diagram\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 84/100 | Created: 2026-05-30\n\"\"\"\n\nimport math\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT_PALETTE = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\nNODE_FILL = \"#DDCC77\"\n\nnp.random.seed(42)\nnodes = [\"science\", \"data\", \"research\", \"study\", \"analysis\", \"result\", \"method\", \"evidence\", \"theory\", \"review\"]\nn_nodes = len(nodes)\n\n# Edges: (source_idx, target_idx, weight) — weight 1=rare, 2=moderate, 3=frequent\n# \"review\" (index 9) has only one connection — demonstrates a peripheral node\nedges = [\n    (0, 2, 3),  # science–research (frequent)\n    (1, 4, 3),  # data–analysis (frequent)\n    (4, 5, 3),  # analysis–result (frequent)\n    (0, 4, 2),  # science–analysis (moderate)\n    (1, 5, 2),  # data–result (moderate)\n    (2, 6, 2),  # research–method (moderate)\n    (3, 6, 2),  # study–method (moderate)\n    (6, 7, 2),  # method–evidence (moderate)\n    (7, 8, 2),  # evidence–theory (moderate)\n    (2, 8, 1),  # research–theory (rare)\n    (3, 5, 1),  # study–result (rare)\n    (1, 8, 1),  # data–theory (rare)\n    (0, 9, 1),  # science–review (rare, long-range — \"review\" has only this edge)\n]\n\narc_colors = {3: IMPRINT_PALETTE[0], 2: IMPRINT_PALETTE[1], 1: IMPRINT_PALETTE[2]}\narc_widths = {1: 5, 2: 14, 3: 26}\nweight_labels = {1: \"Rare\", 2: \"Moderate\", 3: \"Frequent\"}\n\nx_positions = np.linspace(1, 9, n_nodes)\ny_baseline = 0.5\n\n# Degree-based node sizing: hub nodes larger, peripheral nodes visually smaller\ndegrees = [0] * n_nodes\nfor s, t, _ in edges:\n    degrees[s] += 1\n    degrees[t] += 1\n\ndegree_levels = sorted(set(degrees))  # [1, 2, 3]\ndegree_groups: dict[int, list[int]] = {}\nfor i, d in enumerate(degrees):\n    degree_groups.setdefault(d, []).append(i)\n\n# (outline dots_size, fill dots_size) per degree — hub nodes clearly dominate visually\nnode_sizes = {1: (30, 20), 2: (46, 34), 3: (62, 48)}\n\n# Colors: 3 legend + 13 arc edges + 2 series per degree level (outline, fill)\nnode_colors: list[str] = []\nfor _d in degree_levels:\n    node_colors.extend([INK, NODE_FILL])\n\ncolors = tuple([arc_colors[3], arc_colors[2], arc_colors[1]] + [arc_colors[w] for _, _, w in edges] + node_colors)\n\ntitle = \"Word Co-occurrences · arc-basic · python · pygal · anyplot.ai\"\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=colors,\n    title_font_size=54,\n    label_font_size=46,\n    major_label_font_size=36,\n    legend_font_size=36,\n    value_font_size=28,\n    stroke_width=2.5,\n    opacity=0.85,\n    opacity_hover=1.0,\n)\n\nchart = pygal.XY(\n    width=2400,\n    height=2400,\n    style=custom_style,\n    fill=False,\n    title=title,\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=3,\n    legend_box_size=26,\n    x_title=\"\",\n    y_title=\"\",\n    show_x_guides=False,\n    show_y_guides=False,\n    show_x_labels=True,\n    show_y_labels=False,\n    stroke=True,\n    dots_size=0,\n    stroke_style={\"width\": 5, \"linecap\": \"round\"},\n    range=(0, 5.0),\n    xrange=(0, 10),\n    x_labels=[{\"value\": float(x_positions[i]), \"label\": nodes[i]} for i in range(n_nodes)],\n    x_label_rotation=30,\n    truncate_label=-1,\n    css=[\n        \"file://style.css\",\n        \"file://graph.css\",\n        f\"inline:.plot .background {{fill: {PAGE_BG}; stroke: none !important;}}\",\n        \"inline:.axis .line {stroke: none !important;}\",\n        \"inline:.axis .guides .line {stroke: none !important;}\",\n        \"inline:.plot .axis {stroke: none !important;}\",\n        \"inline:.series .line {fill: none !important;}\",\n        # Hide all legend entries after the 3 weight categories\n        \"inline:.legends > g:nth-child(n+4) {display: none !important;}\",\n    ],\n    js=[],\n)\n\n# Legend entries (Frequent → Moderate → Rare, matching color assignment order)\nfor w_val, w_label in [(3, \"Frequent\"), (2, \"Moderate\"), (1, \"Rare\")]:\n    chart.add(\n        f\"{w_label} co-occurrence\",\n        [None],\n        stroke=True,\n        show_dots=False,\n        stroke_style={\"width\": arc_widths[w_val], \"linecap\": \"round\"},\n    )\n\n# Arc series: semi-circle above baseline, height ∝ node distance\narc_resolution = 50\n\nfor start_idx, end_idx, weight in edges:\n    x_start = x_positions[start_idx]\n    x_end = x_positions[end_idx]\n    x_center = (x_start + x_end) / 2\n    arc_radius = abs(x_end - x_start) / 2\n    distance = abs(end_idx - start_idx)\n    height_scale = 0.4 * distance\n\n    arc_points = [\n        {\n            \"value\": (\n                x_center - arc_radius * math.cos(math.pi * i / arc_resolution),\n                y_baseline + height_scale * math.sin(math.pi * i / arc_resolution),\n            ),\n            \"label\": f\"{nodes[start_idx]} ↔ {nodes[end_idx]} | {weight_labels[weight]} ({weight}/3)\",\n        }\n        for i in range(arc_resolution + 1)\n    ]\n    chart.add(\n        \"\", arc_points, stroke=True, show_dots=False, stroke_style={\"width\": arc_widths[weight], \"linecap\": \"round\"}\n    )\n\n# Degree-based node series: hub nodes (degree 3) largest, peripheral (degree 1) smallest\n# Pygal tooltips in HTML expose per-node degree info — the library's interactive differentiator\nfor d in degree_levels:\n    group_idx = degree_groups[d]\n    outline_sz, fill_sz = node_sizes[d]\n    conn_word = \"connection\" if d == 1 else \"connections\"\n\n    node_pts = [\n        {\"value\": (float(x_positions[i]), y_baseline), \"label\": f\"{nodes[i]} | {d} {conn_word}\"} for i in group_idx\n    ]\n    chart.add(\"\", node_pts, stroke=False, dots_size=outline_sz)\n    chart.add(\"\", node_pts, stroke=False, dots_size=fill_sz)\n\n# Save PNG and interactive HTML (HTML exposes per-edge and per-node tooltips on hover)\nchart.render_to_png(f\"plot-{THEME}.png\")\n\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}