{"spec_id":"star-chart-constellation","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nstar-chart-constellation: Star Chart with Constellations\nLibrary: plotnine 0.15.7 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_fixed,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_point,\n    geom_segment,\n    geom_text,\n    ggplot,\n    guide_legend,\n    guides,\n    labs,\n    scale_alpha_continuous,\n    scale_size_continuous,\n    theme,\n    theme_void,\n)\n\n\n# ── Theme-adaptive chrome (Imprint palette + 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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — brand green is the FIRST (and primary) categorical series.\n# Here it draws the constellation stick-figures, the chart's leading data layer.\nBRAND = \"#009E73\"\n# Stars themselves are rendered in the theme ink: dark engraving on a cream\n# parchment atlas (light), pale warm-white on a night sky (dark). Magnitude is\n# encoded by size + opacity, never colour — keeping Imprint identity intact.\nSTAR_COLOR = INK\nGRID_COLOR = INK\n\n# ── Data — real star positions (RA in hours, Dec in degrees) ──────────────────\nnp.random.seed(42)\n\nstars_raw = [\n    # Orion\n    (\"Betelgeuse\", 5.92, 7.41, 0.42, \"Ori\"),\n    (\"Rigel\", 5.24, -8.20, 0.13, \"Ori\"),\n    (\"Bellatrix\", 5.42, 6.35, 1.64, \"Ori\"),\n    (\"Mintaka\", 5.53, -0.30, 2.23, \"Ori\"),\n    (\"Alnilam\", 5.60, -1.20, 1.69, \"Ori\"),\n    (\"Alnitak\", 5.68, -1.94, 1.77, \"Ori\"),\n    (\"Saiph\", 5.80, -9.67, 2.09, \"Ori\"),\n    # Ursa Major (Big Dipper)\n    (\"Dubhe\", 11.06, 61.75, 1.79, \"UMa\"),\n    (\"Merak\", 11.02, 56.38, 2.37, \"UMa\"),\n    (\"Phecda\", 11.90, 53.69, 2.44, \"UMa\"),\n    (\"Megrez\", 12.26, 57.03, 3.31, \"UMa\"),\n    (\"Alioth\", 12.90, 55.96, 1.77, \"UMa\"),\n    (\"Mizar\", 13.40, 54.93, 2.27, \"UMa\"),\n    (\"Alkaid\", 13.79, 49.31, 1.86, \"UMa\"),\n    # Cassiopeia\n    (\"Schedar\", 0.68, 56.54, 2.23, \"Cas\"),\n    (\"Caph\", 0.15, 59.15, 2.27, \"Cas\"),\n    (\"Gamma Cas\", 0.95, 60.72, 2.47, \"Cas\"),\n    (\"Ruchbah\", 1.36, 60.24, 2.68, \"Cas\"),\n    (\"Segin\", 1.91, 63.67, 3.37, \"Cas\"),\n    # Leo\n    (\"Regulus\", 10.14, 11.97, 1.35, \"Leo\"),\n    (\"Denebola\", 11.82, 14.57, 2.14, \"Leo\"),\n    (\"Algieba\", 10.33, 19.84, 2.08, \"Leo\"),\n    (\"Zosma\", 11.24, 20.52, 2.56, \"Leo\"),\n    (\"Chertan\", 11.24, 15.43, 3.34, \"Leo\"),\n    # Cygnus\n    (\"Deneb\", 20.69, 45.28, 1.25, \"Cyg\"),\n    (\"Sadr\", 20.37, 40.26, 2.20, \"Cyg\"),\n    (\"Gienah Cyg\", 20.77, 33.97, 2.46, \"Cyg\"),\n    (\"Albireo\", 19.51, 27.96, 3.08, \"Cyg\"),\n    (\"Delta Cyg\", 19.75, 45.13, 2.87, \"Cyg\"),\n    # Scorpius\n    (\"Antares\", 16.49, -26.43, 0.96, \"Sco\"),\n    (\"Shaula\", 17.56, -37.10, 1.63, \"Sco\"),\n    (\"Sargas\", 17.62, -42.99, 1.87, \"Sco\"),\n    (\"Dschubba\", 16.01, -22.62, 2.32, \"Sco\"),\n    (\"Graffias\", 16.09, -19.81, 2.62, \"Sco\"),\n    (\"Wei\", 16.84, -34.29, 2.29, \"Sco\"),\n    (\"Lesath\", 17.53, -37.29, 2.69, \"Sco\"),\n    # Gemini\n    (\"Pollux\", 7.76, 28.03, 1.14, \"Gem\"),\n    (\"Castor\", 7.58, 31.89, 1.58, \"Gem\"),\n    (\"Alhena\", 6.63, 16.40, 1.93, \"Gem\"),\n    (\"Tejat\", 6.38, 22.51, 2.88, \"Gem\"),\n    (\"Mebsuta\", 6.73, 25.13, 3.06, \"Gem\"),\n    # Lyra\n    (\"Vega\", 18.62, 38.78, 0.03, \"Lyr\"),\n    (\"Sheliak\", 18.83, 33.36, 3.45, \"Lyr\"),\n    (\"Sulafat\", 18.98, 32.69, 3.24, \"Lyr\"),\n    # Aquila\n    (\"Altair\", 19.85, 8.87, 0.77, \"Aql\"),\n    (\"Tarazed\", 19.77, 10.61, 2.72, \"Aql\"),\n    (\"Alshain\", 19.92, 6.41, 3.71, \"Aql\"),\n    # Taurus\n    (\"Aldebaran\", 4.60, 16.51, 0.85, \"Tau\"),\n    (\"Elnath\", 5.44, 28.61, 1.65, \"Tau\"),\n    (\"Alcyone\", 3.79, 24.11, 2.87, \"Tau\"),\n    (\"Tianguan\", 5.63, 21.14, 3.00, \"Tau\"),\n    # Canis Major\n    (\"Sirius\", 6.75, -16.72, -1.46, \"CMa\"),\n    (\"Adhara\", 6.98, -28.97, 1.50, \"CMa\"),\n    (\"Wezen\", 7.14, -26.39, 1.84, \"CMa\"),\n    (\"Mirzam\", 6.38, -17.96, 1.98, \"CMa\"),\n    (\"Aludra\", 7.40, -29.30, 2.45, \"CMa\"),\n    # Perseus\n    (\"Mirfak\", 3.41, 49.86, 1.79, \"Per\"),\n    (\"Algol\", 3.14, 40.96, 2.12, \"Per\"),\n    (\"Zeta Per\", 3.90, 31.88, 2.85, \"Per\"),\n    (\"Epsilon Per\", 3.96, 40.01, 2.89, \"Per\"),\n    (\"Delta Per\", 3.72, 47.79, 3.01, \"Per\"),\n    # Andromeda\n    (\"Alpheratz\", 0.14, 29.09, 2.06, \"And\"),\n    (\"Mirach\", 1.16, 35.62, 2.05, \"And\"),\n    (\"Almach\", 2.06, 42.33, 2.17, \"And\"),\n    # Bootes\n    (\"Arcturus\", 14.26, 19.18, -0.05, \"Boo\"),\n    (\"Izar\", 14.75, 27.07, 2.70, \"Boo\"),\n    (\"Muphrid\", 13.91, 18.40, 2.68, \"Boo\"),\n    (\"Nekkar\", 15.03, 40.39, 3.58, \"Boo\"),\n    # Sagittarius (teapot asterism)\n    (\"Kaus Australis\", 18.40, -34.38, 1.85, \"Sgr\"),\n    (\"Nunki\", 18.92, -26.30, 2.02, \"Sgr\"),\n    (\"Ascella\", 19.04, -29.88, 2.59, \"Sgr\"),\n    (\"Kaus Media\", 18.35, -29.83, 2.70, \"Sgr\"),\n    (\"Kaus Borealis\", 18.47, -25.42, 2.81, \"Sgr\"),\n    # Auriga\n    (\"Capella\", 5.27, 46.00, 0.08, \"Aur\"),\n    (\"Menkalinan\", 5.99, 44.95, 1.90, \"Aur\"),\n    (\"Theta Aur\", 5.99, 37.21, 2.62, \"Aur\"),\n    (\"Iota Aur\", 4.95, 33.17, 2.69, \"Aur\"),\n    # Corona Borealis\n    (\"Alphecca\", 15.58, 26.71, 2.23, \"CrB\"),\n    (\"Nusakan\", 15.46, 29.11, 3.68, \"CrB\"),\n    (\"Gamma CrB\", 15.71, 26.30, 3.84, \"CrB\"),\n    # Canis Minor\n    (\"Procyon\", 7.66, 5.22, 0.34, \"CMi\"),\n    (\"Gomeisa\", 7.45, 8.29, 2.90, \"CMi\"),\n    # Pegasus (Great Square)\n    (\"Markab\", 23.08, 15.21, 2.49, \"Peg\"),\n    (\"Scheat\", 23.06, 28.08, 2.42, \"Peg\"),\n    (\"Algenib\", 0.22, 15.19, 2.83, \"Peg\"),\n    # Virgo\n    (\"Spica\", 13.42, -11.16, 0.97, \"Vir\"),\n    (\"Porrima\", 12.69, -1.45, 2.74, \"Vir\"),\n    (\"Vindemiatrix\", 13.04, 10.96, 2.83, \"Vir\"),\n    # Ursa Minor\n    (\"Polaris\", 2.53, 89.26, 1.98, \"UMi\"),\n    (\"Kochab\", 14.85, 74.16, 2.08, \"UMi\"),\n    (\"Pherkad\", 15.35, 71.83, 3.00, \"UMi\"),\n    # Draco\n    (\"Eltanin\", 17.94, 51.49, 2.23, \"Dra\"),\n    (\"Rastaban\", 17.51, 52.30, 2.79, \"Dra\"),\n    (\"Thuban\", 14.07, 64.38, 3.65, \"Dra\"),\n]\n\n# Constellation edges (pairs of star names) — drawn as stick-figure lines\nedges_raw = [\n    (\"Betelgeuse\", \"Bellatrix\"),\n    (\"Bellatrix\", \"Mintaka\"),\n    (\"Mintaka\", \"Alnilam\"),\n    (\"Alnilam\", \"Alnitak\"),\n    (\"Betelgeuse\", \"Alnilam\"),\n    (\"Rigel\", \"Alnitak\"),\n    (\"Rigel\", \"Saiph\"),\n    (\"Saiph\", \"Alnitak\"),\n    (\"Dubhe\", \"Merak\"),\n    (\"Merak\", \"Phecda\"),\n    (\"Phecda\", \"Megrez\"),\n    (\"Megrez\", \"Alioth\"),\n    (\"Alioth\", \"Mizar\"),\n    (\"Mizar\", \"Alkaid\"),\n    (\"Megrez\", \"Dubhe\"),\n    (\"Caph\", \"Schedar\"),\n    (\"Schedar\", \"Gamma Cas\"),\n    (\"Gamma Cas\", \"Ruchbah\"),\n    (\"Ruchbah\", \"Segin\"),\n    (\"Regulus\", \"Chertan\"),\n    (\"Chertan\", \"Denebola\"),\n    (\"Denebola\", \"Zosma\"),\n    (\"Zosma\", \"Algieba\"),\n    (\"Algieba\", \"Regulus\"),\n    (\"Deneb\", \"Sadr\"),\n    (\"Sadr\", \"Gienah Cyg\"),\n    (\"Sadr\", \"Delta Cyg\"),\n    (\"Sadr\", \"Albireo\"),\n    (\"Graffias\", \"Dschubba\"),\n    (\"Dschubba\", \"Antares\"),\n    (\"Antares\", \"Wei\"),\n    (\"Wei\", \"Shaula\"),\n    (\"Shaula\", \"Lesath\"),\n    (\"Wei\", \"Sargas\"),\n    (\"Castor\", \"Pollux\"),\n    (\"Castor\", \"Tejat\"),\n    (\"Pollux\", \"Alhena\"),\n    (\"Tejat\", \"Mebsuta\"),\n    (\"Mebsuta\", \"Castor\"),\n    (\"Vega\", \"Sheliak\"),\n    (\"Sheliak\", \"Sulafat\"),\n    (\"Sulafat\", \"Vega\"),\n    (\"Altair\", \"Tarazed\"),\n    (\"Altair\", \"Alshain\"),\n    (\"Aldebaran\", \"Elnath\"),\n    (\"Aldebaran\", \"Alcyone\"),\n    (\"Elnath\", \"Tianguan\"),\n    (\"Sirius\", \"Mirzam\"),\n    (\"Sirius\", \"Adhara\"),\n    (\"Adhara\", \"Wezen\"),\n    (\"Wezen\", \"Aludra\"),\n    (\"Mirfak\", \"Delta Per\"),\n    (\"Mirfak\", \"Epsilon Per\"),\n    (\"Epsilon Per\", \"Zeta Per\"),\n    (\"Mirfak\", \"Algol\"),\n    (\"Alpheratz\", \"Mirach\"),\n    (\"Mirach\", \"Almach\"),\n    (\"Arcturus\", \"Izar\"),\n    (\"Arcturus\", \"Muphrid\"),\n    (\"Izar\", \"Nekkar\"),\n    (\"Kaus Australis\", \"Kaus Media\"),\n    (\"Kaus Media\", \"Kaus Borealis\"),\n    (\"Kaus Borealis\", \"Nunki\"),\n    (\"Nunki\", \"Ascella\"),\n    (\"Ascella\", \"Kaus Australis\"),\n    (\"Capella\", \"Menkalinan\"),\n    (\"Menkalinan\", \"Theta Aur\"),\n    (\"Theta Aur\", \"Iota Aur\"),\n    (\"Iota Aur\", \"Capella\"),\n    (\"Nusakan\", \"Alphecca\"),\n    (\"Alphecca\", \"Gamma CrB\"),\n    (\"Procyon\", \"Gomeisa\"),\n    (\"Markab\", \"Scheat\"),\n    (\"Markab\", \"Algenib\"),\n    (\"Scheat\", \"Alpheratz\"),\n    (\"Algenib\", \"Alpheratz\"),\n    (\"Spica\", \"Porrima\"),\n    (\"Porrima\", \"Vindemiatrix\"),\n    (\"Polaris\", \"Kochab\"),\n    (\"Kochab\", \"Pherkad\"),\n    (\"Eltanin\", \"Rastaban\"),\n    (\"Rastaban\", \"Thuban\"),\n]\n\n# Fainter background stars to give the sky depth (mag 3.5–5.5)\nn_bg = 230\nbg_ra = np.random.uniform(0, 24, n_bg)\nbg_dec = np.random.uniform(-50, 70, n_bg)\nbg_mag = np.random.uniform(3.5, 5.5, n_bg)\nbg_stars = [(f\"BG{i}\", bg_ra[i], bg_dec[i], bg_mag[i], \"bg\") for i in range(n_bg)]\n\nall_stars = stars_raw + bg_stars\n\nstar_names = [s[0] for s in all_stars]\nra_hours = np.array([s[1] for s in all_stars])\ndec_deg = np.array([s[2] for s in all_stars])\nmagnitudes = np.array([s[3] for s in all_stars])\nconstellations = [s[4] for s in all_stars]\n\n# ── Stereographic projection (centred on RA=12h, Dec=30° — northern sky) ──────\nra_rad = np.radians(ra_hours * 15.0)\ndec_rad = np.radians(dec_deg)\nra0 = np.radians(180.0)\ndec0 = np.radians(30.0)\n\n\ndef project(ra_r, dec_r):\n    \"\"\"Stereographic projection; x flipped so RA increases right-to-left.\"\"\"\n    cos_c = np.sin(dec0) * np.sin(dec_r) + np.cos(dec0) * np.cos(dec_r) * np.cos(ra_r - ra0)\n    k = 2.0 / (1.0 + cos_c)\n    x = -k * np.cos(dec_r) * np.sin(ra_r - ra0)\n    y = k * (np.cos(dec0) * np.sin(dec_r) - np.sin(dec0) * np.cos(dec_r) * np.cos(ra_r - ra0))\n    return x, y\n\n\nproj_x, proj_y = project(ra_rad, dec_rad)\n\ndf_stars = pd.DataFrame(\n    {\"x\": proj_x, \"y\": proj_y, \"magnitude\": magnitudes, \"name\": star_names, \"constellation\": constellations}\n)\n\n# Clip to the circular sky boundary\nradius_limit = 3.5\ndf_stars = df_stars[np.hypot(df_stars[\"x\"], df_stars[\"y\"]) < radius_limit].copy()\n\n# ── Constellation stick-figure segments ───────────────────────────────────────\nstar_lookup = {row[\"name\"]: (row[\"x\"], row[\"y\"]) for _, row in df_stars.iterrows()}\nedge_data = [\n    {\"x\": star_lookup[a][0], \"y\": star_lookup[a][1], \"xend\": star_lookup[b][0], \"yend\": star_lookup[b][1]}\n    for a, b in edges_raw\n    if a in star_lookup and b in star_lookup\n]\ndf_edges = pd.DataFrame(edge_data)\n\n# ── Constellation name labels at group centroids (with manual de-clutter) ─────\nnamed_stars = df_stars[df_stars[\"constellation\"] != \"bg\"].copy()\nconstellation_names = {\n    \"Ori\": \"ORION\",\n    \"UMa\": \"URSA MAJOR\",\n    \"Cas\": \"CASSIOPEIA\",\n    \"Leo\": \"LEO\",\n    \"Cyg\": \"CYGNUS\",\n    \"Sco\": \"SCORPIUS\",\n    \"Gem\": \"GEMINI\",\n    \"Lyr\": \"LYRA\",\n    \"Aql\": \"AQUILA\",\n    \"Tau\": \"TAURUS\",\n    \"CMa\": \"CANIS MAJOR\",\n    \"Per\": \"PERSEUS\",\n    \"And\": \"ANDROMEDA\",\n    \"Boo\": \"BOÖTES\",\n    \"Sgr\": \"SAGITTARIUS\",\n    \"Aur\": \"AURIGA\",\n    \"CrB\": \"COR. BOREALIS\",\n    \"CMi\": \"CANIS MINOR\",\n    \"Peg\": \"PEGASUS\",\n    \"Vir\": \"VIRGO\",\n    \"UMi\": \"URSA MINOR\",\n    \"Dra\": \"DRACO\",\n}\n# (dx, dy) nudges to keep labels off their stars and away from each other,\n# especially the crowded circumpolar core (UMa / UMi / Dra / Cas / Per / Aur).\nlabel_nudge = {\n    \"Ori\": (0.14, -0.34),\n    \"UMa\": (-0.42, 0.66),\n    \"Cas\": (-0.30, 0.46),\n    \"Leo\": (0.05, -0.42),\n    \"Cyg\": (0.48, 0.10),\n    \"Sco\": (0.00, -0.44),\n    \"Gem\": (0.34, -0.36),\n    \"Lyr\": (-0.62, -0.12),\n    \"Aql\": (0.62, -0.04),\n    \"Tau\": (-0.12, -0.44),\n    \"CMa\": (0.00, -0.42),\n    \"Per\": (0.54, 0.06),\n    \"And\": (-0.10, -0.46),\n    \"Boo\": (-0.66, -0.16),\n    \"Sgr\": (0.00, -0.42),\n    \"Aur\": (-0.48, -0.54),\n    \"CrB\": (0.56, 0.20),\n    \"CMi\": (0.36, -0.30),\n    \"Peg\": (-0.32, -0.32),\n    \"Vir\": (0.05, -0.42),\n    \"UMi\": (0.72, 0.58),\n    \"Dra\": (-0.76, 0.46),\n}\nlabel_data = []\nfor abbr, full_name in constellation_names.items():\n    group = named_stars[named_stars[\"constellation\"] == abbr]\n    if len(group) > 0:\n        dx, dy = label_nudge.get(abbr, (0.0, 0.0))\n        label_data.append({\"x\": group[\"x\"].mean() + dx, \"y\": group[\"y\"].mean() + dy, \"label\": full_name})\ndf_labels = pd.DataFrame(label_data)\n\n# ── Coordinate grid: declination rings + RA meridians ─────────────────────────\ngrid_segs = []\nfor dec_grid in [0, 20, 40, 60]:\n    theta = np.linspace(0, 2 * np.pi, 240)\n    gx, gy = project(theta, np.full_like(theta, np.radians(dec_grid)))\n    bad = np.hypot(gx, gy) >= radius_limit\n    gx[bad] = np.nan\n    for i in range(len(theta) - 1):\n        if not (np.isnan(gx[i]) or np.isnan(gx[i + 1])):\n            grid_segs.append({\"x\": gx[i], \"y\": gy[i], \"xend\": gx[i + 1], \"yend\": gy[i + 1]})\nfor ra_h in range(0, 24, 3):\n    dec_range = np.linspace(np.radians(-50), np.radians(70), 160)\n    gx, gy = project(np.full_like(dec_range, np.radians(ra_h * 15.0)), dec_range)\n    bad = np.hypot(gx, gy) >= radius_limit\n    gx[bad] = np.nan\n    for i in range(len(dec_range) - 1):\n        if not (np.isnan(gx[i]) or np.isnan(gx[i + 1])):\n            grid_segs.append({\"x\": gx[i], \"y\": gy[i], \"xend\": gx[i + 1], \"yend\": gy[i + 1]})\ndf_grid = pd.DataFrame(grid_segs)\n\n# RA tick labels along the Dec=-12° parallel\nra_ticks = []\nfor ra_h in range(0, 24, 3):\n    tx, ty = project(np.radians(ra_h * 15.0), np.radians(-12))\n    if np.hypot(tx, ty) < radius_limit - 0.1:\n        ra_ticks.append({\"x\": tx, \"y\": ty - 0.16, \"label\": f\"{ra_h}h\"})\ndf_ra_ticks = pd.DataFrame(ra_ticks)\n\n# Dec tick labels along the RA=3h meridian\ndec_ticks = []\nfor dec_val in [0, 20, 40, 60]:\n    tx, ty = project(np.radians(3 * 15.0), np.radians(dec_val))\n    if np.hypot(tx, ty) < radius_limit - 0.1:\n        dec_ticks.append({\"x\": tx + 0.16, \"y\": ty, \"label\": f\"{dec_val}°\"})\ndf_dec_ticks = pd.DataFrame(dec_ticks)\n\n# ── Title (mandated format; fontsize scales if it ever runs long) ─────────────\nTITLE = \"star-chart-constellation · python · plotnine · anyplot.ai\"\ntitle_size = round(13 * min(1.0, 67 / len(TITLE)))\n\n# ── Plot ──────────────────────────────────────────────────────────────────────\nplot = (\n    ggplot()\n    # Coordinate grid (very subtle structural layer)\n    + geom_segment(df_grid, aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), color=GRID_COLOR, size=0.3, alpha=0.14)\n    # Constellation stick-figures — Imprint brand green, the leading data series\n    + geom_segment(df_edges, aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), color=BRAND, size=0.7, alpha=0.6)\n    # Stars — magnitude drives both size and opacity (brighter = bigger, opaque)\n    + geom_point(df_stars, aes(x=\"x\", y=\"y\", size=\"magnitude\", alpha=\"magnitude\"), color=STAR_COLOR, stroke=0)\n    + scale_size_continuous(name=\"Apparent\\nmagnitude\", range=(5.0, 0.4), breaks=[0, 1, 2, 3, 4, 5])\n    + scale_alpha_continuous(name=\"Apparent\\nmagnitude\", range=(1.0, 0.45), breaks=[0, 1, 2, 3, 4, 5])\n    + guides(size=guide_legend(), alpha=guide_legend())\n    # Constellation names\n    + geom_text(\n        df_labels,\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        color=INK_SOFT,\n        size=7,\n        alpha=0.9,\n        fontstyle=\"italic\",\n        fontweight=\"bold\",\n    )\n    # RA / Dec tick labels\n    + geom_text(df_ra_ticks, aes(x=\"x\", y=\"y\", label=\"label\"), color=INK_MUTED, size=7)\n    + geom_text(df_dec_ticks, aes(x=\"x\", y=\"y\", label=\"label\"), color=INK_MUTED, size=7, ha=\"left\")\n    + coord_fixed(\n        ratio=1, xlim=(-radius_limit - 0.2, radius_limit + 0.2), ylim=(-radius_limit - 0.2, radius_limit + 0.2)\n    )\n    + labs(title=TITLE)\n    + theme_void()\n    + theme(\n        figure_size=(6, 6),\n        dpi=400,\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_title=element_text(color=INK_SOFT, size=title_size, ha=\"center\", va=\"top\", margin={\"b\": 12}),\n        legend_position=\"right\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_MUTED),\n        legend_title=element_text(color=INK, size=9, ha=\"left\"),\n        legend_text=element_text(color=INK_SOFT, size=8),\n        legend_key=element_blank(),\n        plot_margin=0.01,\n    )\n)\n\n# ── Save (square 2400×2400 px) ────────────────────────────────────────────────\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=6, height=6, units=\"in\", verbose=False)\n"}