{"spec_id":"circos-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ncircos-basic: Circos Plot\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-15\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\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 categorical palette (first series always #009E73)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n# Data: Software module dependencies\nnp.random.seed(42)\n\n# Define segments (software modules)\nsegments = [\"Core\", \"API\", \"Database\", \"Auth\", \"Cache\", \"Queue\", \"Logger\", \"Config\"]\nn_segments = len(segments)\n\n# Segment sizes (relative importance/size of each module)\nsegment_sizes = np.array([25, 20, 18, 15, 12, 10, 8, 6])\nsegment_sizes_normalized = segment_sizes / segment_sizes.sum()\n\n# Connection matrix (dependencies between modules)\nconnections = [\n    (\"Core\", \"API\", 15),\n    (\"Core\", \"Database\", 12),\n    (\"Core\", \"Logger\", 8),\n    (\"API\", \"Auth\", 10),\n    (\"API\", \"Cache\", 8),\n    (\"Database\", \"Cache\", 6),\n    (\"Database\", \"Logger\", 5),\n    (\"Auth\", \"Logger\", 4),\n    (\"Queue\", \"Logger\", 7),\n    (\"Queue\", \"Database\", 5),\n    (\"Config\", \"Core\", 9),\n    (\"Config\", \"Logger\", 3),\n    (\"Cache\", \"Logger\", 4),\n    (\"API\", \"Queue\", 6),\n]\n\n# Inner track data (simulated importance/activity values)\ntrack_data = np.random.uniform(0.3, 1.0, n_segments)\n\n# Map segments to Okabe-Ito colors\nsegment_colors = {segments[i]: IMPRINT[i % len(IMPRINT)] for i in range(n_segments)}\n\n# Darker shades for inner track (reduce lightness while maintaining hue)\ninner_colors = {}\nfor i, seg in enumerate(segments):\n    color = IMPRINT[i % len(IMPRINT)]\n    # Create darker shade by reducing brightness\n    import colorsys\n\n    h, lightness, s = colorsys.rgb_to_hls(*bytes.fromhex(color[1:]))\n    darker = colorsys.hls_to_rgb(h, lightness * 0.6, s)\n    inner_colors[seg] = \"#{:02x}{:02x}{:02x}\".format(int(darker[0] * 255), int(darker[1] * 255), int(darker[2] * 255))\n\n# Target output: 3600x3600 px (1:1 aspect ratio for circular plot) with scale_factor=3.0\n# Internal canvas: 1200x1200 pixels\nwidth = 1200\nheight = 1200\ncenter_x = width / 2\ncenter_y = height / 2\n\n# Circle parameters\nouter_radius = 400\ninner_radius = 360\ntrack_outer_radius = 340\ntrack_inner_radius = 280\nribbon_radius = 270\n\n# Calculate segment positions\ngap = 0.05  # Gap between segments in radians\ntotal_gap = gap * n_segments\navailable_angle = 2 * np.pi - total_gap\nsegment_angles = segment_sizes_normalized * available_angle\n\n# Calculate start and end angles for each segment (starting at top)\nstart_angle = np.pi / 2\nsegment_arcs = {}\ncurrent_angle = start_angle\n\nfor i, name in enumerate(segments):\n    arc_angle = segment_angles[i]\n    segment_arcs[name] = {\"start\": current_angle, \"end\": current_angle - arc_angle, \"angle\": arc_angle, \"idx\": i}\n    current_angle = current_angle - arc_angle - gap\n\nsegment_dict = {name: i for i, name in enumerate(segments)}\n\n# Create outer ring segments data\nn_arc_points = 50\nouter_ring_data = []\n\nfor name in segments:\n    arc = segment_arcs[name]\n    theta = np.linspace(arc[\"end\"], arc[\"start\"], n_arc_points)\n\n    # Outer arc (clockwise)\n    for j, angle in enumerate(theta):\n        outer_ring_data.append(\n            {\n                \"segment\": name,\n                \"x\": center_x + outer_radius * np.cos(angle),\n                \"y\": center_y + outer_radius * np.sin(angle),\n                \"order\": j,\n                \"color\": segment_colors[name],\n            }\n        )\n\n    # Inner arc (counter-clockwise to close the shape)\n    for j, angle in enumerate(reversed(theta)):\n        outer_ring_data.append(\n            {\n                \"segment\": name,\n                \"x\": center_x + inner_radius * np.cos(angle),\n                \"y\": center_y + inner_radius * np.sin(angle),\n                \"order\": n_arc_points + j,\n                \"color\": segment_colors[name],\n            }\n        )\n\nouter_ring_df = pd.DataFrame(outer_ring_data)\n\n# Create inner track data (concentric data track)\ninner_track_data = []\n\nfor i, name in enumerate(segments):\n    arc = segment_arcs[name]\n    theta = np.linspace(arc[\"end\"], arc[\"start\"], n_arc_points)\n\n    # Height proportional to track data value\n    track_height = (track_outer_radius - track_inner_radius) * track_data[i]\n    actual_outer = track_inner_radius + track_height\n\n    # Outer arc\n    for j, angle in enumerate(theta):\n        inner_track_data.append(\n            {\n                \"segment\": name,\n                \"x\": center_x + actual_outer * np.cos(angle),\n                \"y\": center_y + actual_outer * np.sin(angle),\n                \"order\": j,\n                \"value\": track_data[i],\n            }\n        )\n\n    # Inner arc (counter-clockwise)\n    for j, angle in enumerate(reversed(theta)):\n        inner_track_data.append(\n            {\n                \"segment\": name,\n                \"x\": center_x + track_inner_radius * np.cos(angle),\n                \"y\": center_y + track_inner_radius * np.sin(angle),\n                \"order\": n_arc_points + j,\n                \"value\": track_data[i],\n            }\n        )\n\ninner_track_df = pd.DataFrame(inner_track_data)\n\n# Create ribbons (connections between segments)\nmax_value = max(c[2] for c in connections)\nn_ribbon_points = 30\nribbons_data = []\nribbon_id = 0\n\nfor source, target, value in connections:\n    arc1 = segment_arcs[source]\n    arc2 = segment_arcs[target]\n\n    # Calculate positions at segment midpoints\n    mid1 = (arc1[\"start\"] + arc1[\"end\"]) / 2\n    mid2 = (arc2[\"start\"] + arc2[\"end\"]) / 2\n\n    # Ribbon width proportional to value (minimum 0.04 for visibility)\n    width_factor = max(0.04, value / max_value * 0.12)\n\n    # Points for source segment\n    angle1_start = mid1 - width_factor\n    angle1_end = mid1 + width_factor\n\n    # Points for target segment\n    angle2_start = mid2 - width_factor\n    angle2_end = mid2 + width_factor\n\n    ribbon_points = []\n\n    # Arc at source\n    src_angles = np.linspace(angle1_start, angle1_end, 8)\n    for angle in src_angles:\n        ribbon_points.append((center_x + ribbon_radius * np.cos(angle), center_y + ribbon_radius * np.sin(angle)))\n\n    # Bezier curve from source end to target start\n    for i in range(n_ribbon_points):\n        t = i / (n_ribbon_points - 1)\n        # Quadratic bezier with control point at center\n        x = (\n            (1 - t) ** 2 * (center_x + ribbon_radius * np.cos(angle1_end))\n            + 2 * (1 - t) * t * center_x\n            + t**2 * (center_x + ribbon_radius * np.cos(angle2_start))\n        )\n        y = (\n            (1 - t) ** 2 * (center_y + ribbon_radius * np.sin(angle1_end))\n            + 2 * (1 - t) * t * center_y\n            + t**2 * (center_y + ribbon_radius * np.sin(angle2_start))\n        )\n        ribbon_points.append((x, y))\n\n    # Arc at target\n    tgt_angles = np.linspace(angle2_start, angle2_end, 8)\n    for angle in tgt_angles:\n        ribbon_points.append((center_x + ribbon_radius * np.cos(angle), center_y + ribbon_radius * np.sin(angle)))\n\n    # Bezier curve from target end back to source start\n    for i in range(n_ribbon_points):\n        t = i / (n_ribbon_points - 1)\n        x = (\n            (1 - t) ** 2 * (center_x + ribbon_radius * np.cos(angle2_end))\n            + 2 * (1 - t) * t * center_x\n            + t**2 * (center_x + ribbon_radius * np.cos(angle1_start))\n        )\n        y = (\n            (1 - t) ** 2 * (center_y + ribbon_radius * np.sin(angle2_end))\n            + 2 * (1 - t) * t * center_y\n            + t**2 * (center_y + ribbon_radius * np.sin(angle1_start))\n        )\n        ribbon_points.append((x, y))\n\n    # Add points to dataframe\n    for pt_idx, (x, y) in enumerate(ribbon_points):\n        ribbons_data.append(\n            {\n                \"ribbon_id\": f\"{source}-{target}-{ribbon_id}\",\n                \"source\": source,\n                \"target\": target,\n                \"value\": value,\n                \"x\": x,\n                \"y\": y,\n                \"order\": pt_idx,\n            }\n        )\n\n    ribbon_id += 1\n\nribbons_df = pd.DataFrame(ribbons_data)\n\n# Create segment labels data\nlabels_data = []\nfor name in segments:\n    arc = segment_arcs[name]\n    mid_angle = (arc[\"start\"] + arc[\"end\"]) / 2\n    label_radius = outer_radius + 45\n\n    labels_data.append(\n        {\n            \"segment\": name,\n            \"x\": center_x + label_radius * np.cos(mid_angle),\n            \"y\": center_y + label_radius * np.sin(mid_angle),\n        }\n    )\n\nlabels_df = pd.DataFrame(labels_data)\n\n# Create outer ring chart\nouter_ring_chart = (\n    alt.Chart(outer_ring_df)\n    .mark_line(filled=True, strokeWidth=1, stroke=INK_SOFT)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[0, width]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[0, height]), axis=None),\n        color=alt.Color(\n            \"segment:N\",\n            scale=alt.Scale(domain=list(segment_colors.keys()), range=list(segment_colors.values())),\n            legend=alt.Legend(title=\"Modules\", titleFontSize=18, labelFontSize=14, orient=\"right\", symbolSize=200),\n        ),\n        detail=\"segment:N\",\n        order=\"order:Q\",\n    )\n)\n\n# Create inner track chart with distinct styling\ninner_track_chart = (\n    alt.Chart(inner_track_df)\n    .mark_line(filled=True, strokeWidth=2, stroke=INK_SOFT, opacity=0.85)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[0, width]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[0, height]), axis=None),\n        color=alt.Color(\n            \"segment:N\",\n            scale=alt.Scale(domain=list(inner_colors.keys()), range=list(inner_colors.values())),\n            legend=None,\n        ),\n        detail=\"segment:N\",\n        order=\"order:Q\",\n    )\n)\n\n# Create ribbons chart\nribbons_chart = (\n    alt.Chart(ribbons_df)\n    .mark_line(filled=True, opacity=0.5, strokeWidth=0)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[0, width]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[0, height]), axis=None),\n        color=alt.Color(\n            \"source:N\",\n            scale=alt.Scale(domain=list(segment_colors.keys()), range=list(segment_colors.values())),\n            legend=None,\n        ),\n        detail=\"ribbon_id:N\",\n        order=\"order:Q\",\n        tooltip=[\n            alt.Tooltip(\"source:N\", title=\"From\"),\n            alt.Tooltip(\"target:N\", title=\"To\"),\n            alt.Tooltip(\"value:Q\", title=\"Dependency\"),\n        ],\n    )\n)\n\n# Create labels chart with larger font for 3600px canvas\nlabels_chart = (\n    alt.Chart(labels_df)\n    .mark_text(fontSize=22, fontWeight=\"bold\")\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[0, width])),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[0, height])),\n        text=\"segment:N\",\n        color=alt.Color(\n            \"segment:N\",\n            scale=alt.Scale(domain=list(segment_colors.keys()), range=list(segment_colors.values())),\n            legend=None,\n        ),\n    )\n)\n\n# Combine all layers\nchart = (\n    alt.layer(ribbons_chart, inner_track_chart, outer_ring_chart, labels_chart)\n    .properties(\n        width=width,\n        height=height,\n        title=alt.Title(text=\"circos-basic · altair · anyplot.ai\", fontSize=28, anchor=\"middle\"),\n        background=PAGE_BG,\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0)\n    .configure_title(color=INK, fontSize=28)\n    .configure_legend(\n        padding=15,\n        cornerRadius=5,\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        strokeWidth=1,\n        titleFontSize=20,\n        labelFontSize=16,\n        symbolSize=250,\n        offset=20,\n        titleColor=INK,\n        labelColor=INK_SOFT,\n    )\n)\n\n# Save as PNG and HTML with theme-suffixed filenames\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}