{"spec_id":"circos-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ncircos-basic: Circos Plot\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-15\n\"\"\"\n\nimport math\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    coord_fixed,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\nnp.random.seed(42)\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# Genomic-style data: 8 chromosomes with connections and expression data\nchromosomes = [\"Chr1\", \"Chr2\", \"Chr3\", \"Chr4\", \"Chr5\", \"Chr6\", \"Chr7\", \"Chr8\"]\nn_chromosomes = len(chromosomes)\n\n# Chromosome sizes (proportional to their arc length)\nchr_sizes = [120, 95, 85, 75, 70, 65, 55, 50]  # Megabases\n\n# Connections between chromosomes (inter-chromosomal rearrangements)\nconnections = [\n    (\"Chr1\", \"Chr3\", 25),\n    (\"Chr1\", \"Chr5\", 18),\n    (\"Chr2\", \"Chr4\", 22),\n    (\"Chr2\", \"Chr7\", 15),\n    (\"Chr3\", \"Chr6\", 20),\n    (\"Chr4\", \"Chr8\", 12),\n    (\"Chr5\", \"Chr7\", 16),\n    (\"Chr6\", \"Chr8\", 10),\n    (\"Chr1\", \"Chr8\", 8),\n    (\"Chr3\", \"Chr5\", 14),\n]\n\n# Track data: expression levels for each chromosome (inner tracks)\nexpression_track1 = [0.85, 0.72, 0.93, 0.68, 0.81, 0.55, 0.78, 0.62]  # Normalized 0-1\nexpression_track2 = [0.45, 0.82, 0.38, 0.91, 0.55, 0.73, 0.42, 0.88]  # Normalized 0-1\n\n# Colors for chromosomes (colorblind-friendly palette)\nchr_colors = [\"#306998\", \"#FFD43B\", \"#27AE60\", \"#E74C3C\", \"#9B59B6\", \"#1ABC9C\", \"#F39C12\", \"#3498DB\"]\n\n# Calculate angular positions for each chromosome\ntotal_size = sum(chr_sizes)\ngap_angle = 0.08  # Gap between chromosomes in radians\ntotal_gap = gap_angle * n_chromosomes\navailable_angle = 2 * math.pi - total_gap\n\n# Assign angular positions to each chromosome\nchr_arcs = {}\ncurrent_angle = 0\nfor i, chrom in enumerate(chromosomes):\n    arc_size = (chr_sizes[i] / total_size) * available_angle\n    chr_arcs[chrom] = {\n        \"start\": current_angle,\n        \"end\": current_angle + arc_size,\n        \"mid\": current_angle + arc_size / 2,\n        \"size\": chr_sizes[i],\n        \"color\": chr_colors[i],\n        \"idx\": i,\n    }\n    current_angle += arc_size + gap_angle\n\n# Radii for different elements\nouter_radius = 1.0  # Outer chromosome ring\ninner_ring_radius = 0.92  # Inner edge of chromosome ring\ntrack1_outer = 0.88  # Expression track 1\ntrack1_inner = 0.78\ntrack2_outer = 0.74  # Expression track 2\ntrack2_inner = 0.64\nchord_radius = 0.60  # Ribbons connecting chromosomes\n\n# Build outer arc segments (chromosome ring)\narc_data = []\nn_arc_points = 50\n\nfor chrom in chromosomes:\n    arc = chr_arcs[chrom]\n    angles = np.linspace(arc[\"start\"], arc[\"end\"], n_arc_points)\n\n    # Outer edge\n    for angle in angles:\n        arc_data.append(\n            {\n                \"x\": outer_radius * np.cos(angle),\n                \"y\": outer_radius * np.sin(angle),\n                \"chromosome\": chrom,\n                \"arc_id\": f\"{chrom}_arc\",\n            }\n        )\n    # Inner edge (reversed)\n    for angle in reversed(angles):\n        arc_data.append(\n            {\n                \"x\": inner_ring_radius * np.cos(angle),\n                \"y\": inner_ring_radius * np.sin(angle),\n                \"chromosome\": chrom,\n                \"arc_id\": f\"{chrom}_arc\",\n            }\n        )\n\narc_df = pd.DataFrame(arc_data)\n\n# Build track 1 data (bar heights based on expression)\ntrack1_data = []\nfor chrom in chromosomes:\n    arc = chr_arcs[chrom]\n    expr = expression_track1[arc[\"idx\"]]\n\n    # Create arc segment for this track\n    angles = np.linspace(arc[\"start\"], arc[\"end\"], n_arc_points)\n    bar_height = track1_inner + (track1_outer - track1_inner) * expr\n\n    # Outer edge at expression level\n    for angle in angles:\n        track1_data.append(\n            {\n                \"x\": bar_height * np.cos(angle),\n                \"y\": bar_height * np.sin(angle),\n                \"chromosome\": chrom,\n                \"track_id\": f\"{chrom}_track1\",\n            }\n        )\n    # Inner edge\n    for angle in reversed(angles):\n        track1_data.append(\n            {\n                \"x\": track1_inner * np.cos(angle),\n                \"y\": track1_inner * np.sin(angle),\n                \"chromosome\": chrom,\n                \"track_id\": f\"{chrom}_track1\",\n            }\n        )\n\ntrack1_df = pd.DataFrame(track1_data)\n\n# Build track 2 data\ntrack2_data = []\nfor chrom in chromosomes:\n    arc = chr_arcs[chrom]\n    expr = expression_track2[arc[\"idx\"]]\n\n    angles = np.linspace(arc[\"start\"], arc[\"end\"], n_arc_points)\n    bar_height = track2_inner + (track2_outer - track2_inner) * expr\n\n    # Outer edge at expression level\n    for angle in angles:\n        track2_data.append(\n            {\n                \"x\": bar_height * np.cos(angle),\n                \"y\": bar_height * np.sin(angle),\n                \"chromosome\": chrom,\n                \"track_id\": f\"{chrom}_track2\",\n            }\n        )\n    # Inner edge\n    for angle in reversed(angles):\n        track2_data.append(\n            {\n                \"x\": track2_inner * np.cos(angle),\n                \"y\": track2_inner * np.sin(angle),\n                \"chromosome\": chrom,\n                \"track_id\": f\"{chrom}_track2\",\n            }\n        )\n\ntrack2_df = pd.DataFrame(track2_data)\n\n# Build ribbon connections between chromosomes\nribbon_data = []\nribbon_id = 0\n\n# Track offsets for connection placement within each chromosome\nchr_offsets = {chrom: chr_arcs[chrom][\"start\"] for chrom in chromosomes}\n\nfor src, tgt, val in connections:\n    src_arc = chr_arcs[src]\n    tgt_arc = chr_arcs[tgt]\n\n    # Calculate angular width proportional to connection value\n    width_factor = val / 100.0  # Normalize\n    src_width = (src_arc[\"end\"] - src_arc[\"start\"]) * width_factor * 0.8\n    tgt_width = (tgt_arc[\"end\"] - tgt_arc[\"start\"]) * width_factor * 0.8\n\n    # Source position\n    src_start = chr_offsets[src]\n    src_end = src_start + src_width\n    chr_offsets[src] = src_end + 0.01\n\n    # Target position\n    tgt_start = chr_offsets[tgt]\n    tgt_end = tgt_start + tgt_width\n    chr_offsets[tgt] = tgt_end + 0.01\n\n    # Create bezier-like ribbon\n    n_bezier = 40\n    polygon_x = []\n    polygon_y = []\n\n    # Source arc at chord radius\n    src_angles = np.linspace(src_start, src_end, 10)\n    for angle in src_angles:\n        polygon_x.append(chord_radius * np.cos(angle))\n        polygon_y.append(chord_radius * np.sin(angle))\n\n    # Bezier curve from source end to target start\n    src_end_x = chord_radius * np.cos(src_end)\n    src_end_y = chord_radius * np.sin(src_end)\n    tgt_start_x = chord_radius * np.cos(tgt_start)\n    tgt_start_y = chord_radius * np.sin(tgt_start)\n\n    for i in range(1, n_bezier):\n        t = i / n_bezier\n        x = (1 - t) ** 2 * src_end_x + 2 * (1 - t) * t * 0 + t**2 * tgt_start_x\n        y = (1 - t) ** 2 * src_end_y + 2 * (1 - t) * t * 0 + t**2 * tgt_start_y\n        polygon_x.append(x)\n        polygon_y.append(y)\n\n    # Target arc (reversed)\n    tgt_angles = np.linspace(tgt_end, tgt_start, 10)\n    for angle in tgt_angles:\n        polygon_x.append(chord_radius * np.cos(angle))\n        polygon_y.append(chord_radius * np.sin(angle))\n\n    # Bezier curve back from target end to source start\n    tgt_end_x = chord_radius * np.cos(tgt_end)\n    tgt_end_y = chord_radius * np.sin(tgt_end)\n    src_start_x = chord_radius * np.cos(src_start)\n    src_start_y = chord_radius * np.sin(src_start)\n\n    for i in range(1, n_bezier):\n        t = i / n_bezier\n        x = (1 - t) ** 2 * tgt_end_x + 2 * (1 - t) * t * 0 + t**2 * src_start_x\n        y = (1 - t) ** 2 * tgt_end_y + 2 * (1 - t) * t * 0 + t**2 * src_start_y\n        polygon_x.append(x)\n        polygon_y.append(y)\n\n    # Add points to dataframe\n    for x, y in zip(polygon_x, polygon_y, strict=False):\n        ribbon_data.append({\"x\": x, \"y\": y, \"ribbon_id\": f\"ribbon_{ribbon_id}\", \"source\": src})\n\n    ribbon_id += 1\n\nribbon_df = pd.DataFrame(ribbon_data)\n\n# Create chromosome labels\nlabel_data = []\nlabel_radius = 1.12\nfor chrom in chromosomes:\n    arc = chr_arcs[chrom]\n    mid_angle = arc[\"mid\"]\n    label_data.append({\"x\": label_radius * np.cos(mid_angle), \"y\": label_radius * np.sin(mid_angle), \"label\": chrom})\n\nlabel_df = pd.DataFrame(label_data)\n\n# Build the circos plot\nplot = (\n    ggplot()\n    # Ribbons connecting chromosomes (innermost, with transparency)\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\", group=\"ribbon_id\", fill=\"source\"), data=ribbon_df, alpha=0.45, color=PAGE_BG, size=0.2\n    )\n    # Expression track 2 (inner track)\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\", group=\"track_id\", fill=\"chromosome\"), data=track2_df, alpha=0.65, color=PAGE_BG, size=0.3\n    )\n    # Expression track 1 (middle track)\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\", group=\"track_id\", fill=\"chromosome\"), data=track1_df, alpha=0.8, color=PAGE_BG, size=0.3\n    )\n    # Outer chromosome ring\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\", group=\"arc_id\", fill=\"chromosome\"), data=arc_df, alpha=0.95, color=PAGE_BG, size=0.8\n    )\n    # Chromosome labels\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=label_df, size=14, color=INK, fontface=\"bold\")\n    + scale_fill_manual(values=chr_colors, name=\"Chromosome\")\n    + coord_fixed(ratio=1)\n    + scale_x_continuous(limits=(-1.45, 1.45))\n    + scale_y_continuous(limits=(-1.45, 1.45))\n    + labs(title=\"circos-basic · letsplot · anyplot.ai\")\n    + ggsize(1200, 1200)\n    + theme(\n        plot_title=element_text(size=26, face=\"bold\", color=INK),\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        axis_line=element_blank(),\n        panel_grid=element_blank(),\n        legend_text=element_text(size=14, color=INK_SOFT),\n        legend_title=element_text(size=16, face=\"bold\", color=INK),\n        legend_position=\"bottom\",\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    )\n)\n\n# Save as PNG (scale 3x for 3600x3600 px output)\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=3)\n\n# Save as HTML for interactivity\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}