{"spec_id":"star-chart-constellation","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nstar-chart-constellation: Star Chart with Constellations\nLibrary: matplotlib 3.11.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\n\nimport matplotlib.patheffects as pe\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.colors import LinearSegmentedColormap\nfrom matplotlib.lines import Line2D\nfrom matplotlib.patches import Patch\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\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\"\nBRAND = \"#009E73\"  # Imprint palette position 1 — brightest stars\nAMBER = \"#DDCC77\"  # semantic anchor — ecliptic reference line\n\nnp.random.seed(42)\n\n# Star catalog: name -> (RA hours, Dec degrees, magnitude, constellation abbrev)\nstars = {\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\n    \"Dubhe\": (11.06, 61.75, 1.79, \"UMa\"),\n    \"Merak\": (11.03, 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.13, \"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.33, \"Leo\"),\n    \"Eta Leo\": (10.12, 16.76, 3.52, \"Leo\"),\n    # Scorpius\n    \"Antares\": (16.49, -26.43, 1.09, \"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    \"Acrab\": (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    # 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    \"Delta Cyg\": (19.75, 45.13, 2.87, \"Cyg\"),\n    \"Albireo\": (19.51, 27.96, 3.08, \"Cyg\"),\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    \"Epsilon1 Lyr\": (18.74, 39.67, 4.67, \"Lyr\"),\n    \"Epsilon2 Lyr\": (18.75, 39.61, 4.59, \"Lyr\"),\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    \"Wasat\": (7.07, 21.98, 3.53, \"Gem\"),\n    \"Mebsuta\": (6.73, 25.13, 2.98, \"Gem\"),\n    \"Tejat\": (6.38, 22.51, 2.88, \"Gem\"),\n    # Taurus\n    \"Aldebaran\": (4.60, 16.51, 0.85, \"Tau\"),\n    \"Elnath\": (5.44, 28.61, 1.68, \"Tau\"),\n    \"Alcyone\": (3.79, 24.11, 2.87, \"Tau\"),\n    \"Tianguan\": (5.63, 21.14, 3.00, \"Tau\"),\n    \"Prima Hyadum\": (4.33, 15.63, 3.65, \"Tau\"),\n    \"Ain\": (4.48, 19.18, 3.54, \"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    \"Furud\": (6.34, -30.06, 3.02, \"CMa\"),\n    # Aquila\n    \"Altair\": (19.85, 8.87, 0.76, \"Aql\"),\n    \"Tarazed\": (19.77, 10.61, 2.72, \"Aql\"),\n    \"Alshain\": (19.92, 6.41, 3.71, \"Aql\"),\n    \"Theta Aql\": (20.19, -0.82, 3.23, \"Aql\"),\n    \"Delta Aql\": (19.42, 3.11, 3.36, \"Aql\"),\n    # Bootes\n    \"Arcturus\": (14.26, 19.18, -0.05, \"Boo\"),\n    \"Izar\": (14.75, 27.07, 2.37, \"Boo\"),\n    \"Muphrid\": (13.91, 18.40, 2.68, \"Boo\"),\n    \"Nekkar\": (15.03, 40.39, 3.58, \"Boo\"),\n    \"Seginus\": (14.53, 38.31, 3.03, \"Boo\"),\n    # Perseus\n    \"Mirfak\": (3.41, 49.86, 1.80, \"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    # Auriga\n    \"Capella\": (5.27, 45.99, 0.08, \"Aur\"),\n    \"Menkalinan\": (5.99, 44.95, 1.90, \"Aur\"),\n    \"Theta Aur\": (5.99, 37.21, 2.62, \"Aur\"),\n    \"Hassaleh\": (4.95, 33.17, 2.69, \"Aur\"),\n    \"Almaaz\": (5.03, 43.82, 2.99, \"Aur\"),\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    \"Heze\": (13.58, -0.60, 3.37, \"Vir\"),\n    \"Zaniah\": (12.33, -0.67, 3.89, \"Vir\"),\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    \"Delta CrB\": (15.83, 26.07, 4.59, \"CrB\"),\n    # Pegasus\n    \"Enif\": (21.74, 9.88, 2.39, \"Peg\"),\n    \"Markab\": (23.08, 15.21, 2.49, \"Peg\"),\n    \"Scheat\": (23.06, 28.08, 2.42, \"Peg\"),\n    \"Algenib\": (0.22, 15.18, 2.83, \"Peg\"),\n    \"Matar\": (22.72, 30.22, 2.94, \"Peg\"),\n    # Andromeda\n    \"Alpheratz\": (0.14, 29.09, 2.06, \"And\"),\n    \"Mirach\": (1.16, 35.62, 2.05, \"And\"),\n    \"Almach\": (2.07, 42.33, 2.17, \"And\"),\n    # Draco\n    \"Eltanin\": (17.94, 51.49, 2.24, \"Dra\"),\n    \"Rastaban\": (17.51, 52.30, 2.79, \"Dra\"),\n    \"Thuban\": (14.07, 64.38, 3.65, \"Dra\"),\n    \"Grumium\": (17.89, 56.87, 3.75, \"Dra\"),\n    \"Kuma\": (17.53, 55.17, 4.87, \"Dra\"),\n    # Hercules\n    \"Kornephoros\": (16.50, 21.49, 2.77, \"Her\"),\n    \"Zeta Her\": (16.69, 31.60, 2.81, \"Her\"),\n    \"Rasalgethi\": (17.24, 14.39, 3.37, \"Her\"),\n    \"Sarin\": (17.25, 24.84, 3.14, \"Her\"),\n    \"Pi Her\": (17.25, 36.81, 3.16, \"Her\"),\n    \"Eta Her\": (16.71, 38.92, 3.53, \"Her\"),\n    # Sagittarius\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    \"Nash\": (18.10, -30.42, 3.11, \"Sgr\"),\n}\n\n# Constellation stick-figure edges\nedges = [\n    # Orion\n    (\"Betelgeuse\", \"Bellatrix\"),\n    (\"Betelgeuse\", \"Mintaka\"),\n    (\"Bellatrix\", \"Mintaka\"),\n    (\"Mintaka\", \"Alnilam\"),\n    (\"Alnilam\", \"Alnitak\"),\n    (\"Rigel\", \"Alnitak\"),\n    (\"Rigel\", \"Saiph\"),\n    (\"Saiph\", \"Alnitak\"),\n    # Ursa Major (Big Dipper)\n    (\"Dubhe\", \"Merak\"),\n    (\"Merak\", \"Phecda\"),\n    (\"Phecda\", \"Megrez\"),\n    (\"Megrez\", \"Alioth\"),\n    (\"Alioth\", \"Mizar\"),\n    (\"Mizar\", \"Alkaid\"),\n    (\"Megrez\", \"Dubhe\"),\n    # Cassiopeia\n    (\"Caph\", \"Schedar\"),\n    (\"Schedar\", \"Gamma Cas\"),\n    (\"Gamma Cas\", \"Ruchbah\"),\n    (\"Ruchbah\", \"Segin\"),\n    # Leo\n    (\"Regulus\", \"Eta Leo\"),\n    (\"Eta Leo\", \"Algieba\"),\n    (\"Algieba\", \"Zosma\"),\n    (\"Zosma\", \"Denebola\"),\n    (\"Regulus\", \"Chertan\"),\n    (\"Chertan\", \"Denebola\"),\n    # Scorpius\n    (\"Acrab\", \"Dschubba\"),\n    (\"Dschubba\", \"Antares\"),\n    (\"Antares\", \"Wei\"),\n    (\"Wei\", \"Shaula\"),\n    (\"Shaula\", \"Lesath\"),\n    (\"Wei\", \"Sargas\"),\n    # Cygnus (Northern Cross)\n    (\"Deneb\", \"Sadr\"),\n    (\"Sadr\", \"Albireo\"),\n    (\"Sadr\", \"Gienah Cyg\"),\n    (\"Sadr\", \"Delta Cyg\"),\n    # Lyra\n    (\"Vega\", \"Sheliak\"),\n    (\"Vega\", \"Sulafat\"),\n    (\"Sheliak\", \"Sulafat\"),\n    # Gemini\n    (\"Castor\", \"Pollux\"),\n    (\"Castor\", \"Mebsuta\"),\n    (\"Mebsuta\", \"Tejat\"),\n    (\"Pollux\", \"Wasat\"),\n    (\"Wasat\", \"Alhena\"),\n    # Taurus\n    (\"Aldebaran\", \"Ain\"),\n    (\"Ain\", \"Prima Hyadum\"),\n    (\"Aldebaran\", \"Elnath\"),\n    (\"Ain\", \"Alcyone\"),\n    (\"Elnath\", \"Tianguan\"),\n    # Canis Major\n    (\"Sirius\", \"Mirzam\"),\n    (\"Sirius\", \"Adhara\"),\n    (\"Adhara\", \"Wezen\"),\n    (\"Wezen\", \"Aludra\"),\n    (\"Adhara\", \"Furud\"),\n    # Aquila\n    (\"Altair\", \"Tarazed\"),\n    (\"Altair\", \"Alshain\"),\n    (\"Altair\", \"Theta Aql\"),\n    (\"Altair\", \"Delta Aql\"),\n    # Bootes\n    (\"Arcturus\", \"Muphrid\"),\n    (\"Arcturus\", \"Izar\"),\n    (\"Izar\", \"Seginus\"),\n    (\"Seginus\", \"Nekkar\"),\n    # Perseus\n    (\"Mirfak\", \"Delta Per\"),\n    (\"Mirfak\", \"Epsilon Per\"),\n    (\"Epsilon Per\", \"Algol\"),\n    (\"Algol\", \"Zeta Per\"),\n    # Auriga\n    (\"Capella\", \"Menkalinan\"),\n    (\"Menkalinan\", \"Theta Aur\"),\n    (\"Theta Aur\", \"Hassaleh\"),\n    (\"Hassaleh\", \"Almaaz\"),\n    (\"Almaaz\", \"Capella\"),\n    # Virgo\n    (\"Spica\", \"Heze\"),\n    (\"Heze\", \"Porrima\"),\n    (\"Porrima\", \"Zaniah\"),\n    (\"Porrima\", \"Vindemiatrix\"),\n    # Corona Borealis\n    (\"Alphecca\", \"Nusakan\"),\n    (\"Alphecca\", \"Gamma CrB\"),\n    (\"Gamma CrB\", \"Delta CrB\"),\n    # Pegasus (Great Square)\n    (\"Markab\", \"Scheat\"),\n    (\"Scheat\", \"Algenib\"),\n    (\"Markab\", \"Enif\"),\n    (\"Markab\", \"Algenib\"),\n    # Andromeda\n    (\"Alpheratz\", \"Mirach\"),\n    (\"Mirach\", \"Almach\"),\n    (\"Alpheratz\", \"Scheat\"),\n    # Draco\n    (\"Eltanin\", \"Rastaban\"),\n    (\"Rastaban\", \"Kuma\"),\n    (\"Kuma\", \"Grumium\"),\n    (\"Eltanin\", \"Grumium\"),\n    # Hercules\n    (\"Kornephoros\", \"Zeta Her\"),\n    (\"Zeta Her\", \"Eta Her\"),\n    (\"Eta Her\", \"Pi Her\"),\n    (\"Kornephoros\", \"Rasalgethi\"),\n    (\"Rasalgethi\", \"Sarin\"),\n    (\"Sarin\", \"Pi Her\"),\n    # Sagittarius (Teapot)\n    (\"Kaus Australis\", \"Kaus Media\"),\n    (\"Kaus Media\", \"Kaus Borealis\"),\n    (\"Kaus Borealis\", \"Nunki\"),\n    (\"Nunki\", \"Ascella\"),\n    (\"Ascella\", \"Kaus Australis\"),\n    (\"Nash\", \"Kaus Media\"),\n]\n\n# Limit of the circular sky chart: declination floor (stars below this are off-map)\nDEC_FLOOR = -48.0\nR_MAX = 90.0 - DEC_FLOOR  # radial distance of the chart boundary\n\n\n# North-polar azimuthal-equidistant projection: equally spaced declination\n# circles give a natural *circular* sky boundary (the spec's preferred style).\n#   theta = RA (radians)   r = colatitude = 90deg - Dec\ndef project(ra_h, dec_d):\n    theta = np.asarray(ra_h, dtype=float) * (np.pi / 12.0)\n    r = 90.0 - np.asarray(dec_d, dtype=float)\n    return theta, r\n\n\n# Background field stars (faint, for celestial-sphere context)\nn_bg = 320\nbg_ra_hours = np.random.uniform(0, 24, n_bg)\nbg_dec_deg = np.random.uniform(DEC_FLOOR, 72, n_bg)\nbg_mag = np.random.uniform(4.0, 6.0, n_bg)\nbg_theta, bg_r = project(bg_ra_hours, bg_dec_deg)\n\n# Milky Way band — soft cloud of faint points along the galactic plane.\n# Convert galactic (l, b) -> equatorial (RA, Dec) via the standard NGP frame.\nra_ngp, dec_ngp, l_ncp = np.radians(192.85948), np.radians(27.12825), np.radians(122.93192)\nn_mw = 2600\nmw_l = np.random.uniform(0, 2 * np.pi, n_mw)\nmw_b = np.random.normal(0.0, np.radians(8.5), n_mw)  # band thickness ~ +/-8.5deg\nmw_dec = np.arcsin(np.sin(dec_ngp) * np.sin(mw_b) + np.cos(dec_ngp) * np.cos(mw_b) * np.cos(l_ncp - mw_l))\nmw_ra = ra_ngp + np.arctan2(\n    np.cos(mw_b) * np.sin(l_ncp - mw_l),\n    np.sin(mw_b) * np.cos(dec_ngp) - np.cos(mw_b) * np.sin(dec_ngp) * np.cos(l_ncp - mw_l),\n)\nmw_theta, mw_r = project(np.degrees(mw_ra) / 15.0, np.degrees(mw_dec))\n\n# Extract named-star arrays\nstar_names = list(stars.keys())\nstar_ra_h = np.array([stars[s][0] for s in star_names])\nstar_dec_d = np.array([stars[s][1] for s in star_names])\nstar_mag = np.array([stars[s][2] for s in star_names])\nstar_theta, star_r = project(star_ra_h, star_dec_d)\n\n# Size mapping: brighter stars (lower magnitude) map to larger points\nmax_mag, min_mag = 6.5, -1.5\nmin_size, max_size = 5, 175\nstar_sizes = max_size * ((max_mag - star_mag) / (max_mag - min_mag)) ** 1.5 + min_size\n\n# Color mapping by magnitude — Imprint sequential cmap (brand green -> blue).\n# Brightest stars (low magnitude) get brand green; faintest fade to blue.\nimprint_seq = LinearSegmentedColormap.from_list(\"imprint_seq\", [BRAND, \"#4467A3\"])\nstar_color_norm = np.clip((star_mag - min_mag) / (max_mag - min_mag), 0, 1)\nstar_colors = imprint_seq(star_color_norm)\n\n# Plot — square axes centered in the 3200x1800 canvas so the polar plot reads\n# as a true circle, leaving the side margins for the legends.\nfig = plt.figure(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax = fig.add_axes([0.305, 0.085, 0.39, 0.83], projection=\"polar\", facecolor=PAGE_BG)\nax.set_theta_zero_location(\"N\")\nax.set_theta_direction(-1)  # RA increases clockwise from the top\nax.set_rlim(0, R_MAX)\nax.set_rorigin(0)\n\n# Milky Way band (deepest layer) — faint diffuse glow\nax.scatter(mw_theta, mw_r, s=7, c=INK_MUTED, alpha=0.045, edgecolors=\"none\", zorder=0)\n\n# Faint background field stars (context layer, muted)\nbg_sizes = max_size * ((max_mag - bg_mag) / (max_mag - min_mag)) ** 1.5 + min_size\nax.scatter(bg_theta, bg_r, s=bg_sizes, c=INK_MUTED, alpha=0.30, edgecolors=\"none\", zorder=1)\n\n# Constellation stick-figure lines (thin, semi-transparent structural ink)\nfor s1, s2 in edges:\n    t1, r1 = project(stars[s1][0], stars[s1][1])\n    t2, r2 = project(stars[s2][0], stars[s2][1])\n    # Unwrap so the segment takes the short way around the RA circle\n    if t2 - t1 > np.pi:\n        t2 -= 2 * np.pi\n    elif t1 - t2 > np.pi:\n        t2 += 2 * np.pi\n    ax.plot([t1, t2], [r1, r2], color=INK_SOFT, linewidth=0.8, alpha=0.5, zorder=2)\n\n# Soft halo behind the brightest stars (mag < 1.0) for a gentle glow\nbright_mask = star_mag < 1.0\nax.scatter(\n    star_theta[bright_mask],\n    star_r[bright_mask],\n    s=star_sizes[bright_mask] * 2.6,\n    c=BRAND,\n    alpha=0.10,\n    edgecolors=\"none\",\n    zorder=2,\n)\n\n# Named stars — sized by magnitude, colored along the Imprint sequential ramp\nax.scatter(star_theta, star_r, s=star_sizes, c=star_colors, alpha=0.95, edgecolors=PAGE_BG, linewidth=0.4, zorder=3)\n\n# Ecliptic — dashed amber reference curve. The ecliptic is the great circle\n# 23.44deg from the ecliptic pole; trace it in RA/Dec then project.\neps = np.radians(23.44)\necl_lon = np.linspace(0, 2 * np.pi, 720)\necl_dec = np.degrees(np.arcsin(np.sin(eps) * np.sin(ecl_lon)))\necl_ra_h = (np.degrees(np.arctan2(np.cos(eps) * np.sin(ecl_lon), np.cos(ecl_lon))) / 15.0) % 24\necl_theta, ecl_r = project(ecl_ra_h, ecl_dec)\n# Break the curve where it wraps past the RA seam to avoid a chord across the chart\nseam = np.abs(np.diff(ecl_theta)) > np.pi\necl_theta[:-1][seam] = np.nan\nax.plot(ecl_theta, ecl_r, color=AMBER, linewidth=1.4, alpha=0.8, linestyle=(0, (7, 4)), zorder=2)\n\n# Label the brightest stars (mag < 1.0)\nbright_offsets = {\n    \"Sirius\": (7, -10),\n    \"Vega\": (8, 6),\n    \"Arcturus\": (8, -4),\n    \"Capella\": (-6, 9),\n    \"Rigel\": (7, -10),\n    \"Betelgeuse\": (8, 6),\n    \"Altair\": (8, -8),\n    \"Aldebaran\": (-10, 8),\n    \"Spica\": (7, -10),\n    \"Pollux\": (8, 7),\n}\nfor name, (ra, dec, mag, _) in stars.items():\n    if mag < 1.0:\n        t, r = project(ra, dec)\n        ax.annotate(\n            name,\n            (t, r),\n            fontsize=7.5,\n            color=INK,\n            alpha=0.9,\n            xytext=bright_offsets.get(name, (7, 6)),\n            textcoords=\"offset points\",\n            zorder=5,\n            path_effects=[pe.withStroke(linewidth=2, foreground=PAGE_BG, alpha=0.8)],\n        )\n\n# Constellation names placed near each group's centroid (circular mean in RA)\nconstellation_names = {\n    \"Ori\": \"Orion\",\n    \"UMa\": \"Ursa Major\",\n    \"Cas\": \"Cassiopeia\",\n    \"Leo\": \"Leo\",\n    \"Sco\": \"Scorpius\",\n    \"Cyg\": \"Cygnus\",\n    \"Lyr\": \"Lyra\",\n    \"Gem\": \"Gemini\",\n    \"Tau\": \"Taurus\",\n    \"CMa\": \"Canis Major\",\n    \"Aql\": \"Aquila\",\n    \"Boo\": \"Boötes\",\n    \"Per\": \"Perseus\",\n    \"Aur\": \"Auriga\",\n    \"Vir\": \"Virgo\",\n    \"CrB\": \"Corona Bor.\",\n    \"Peg\": \"Pegasus\",\n    \"And\": \"Andromeda\",\n    \"Dra\": \"Draco\",\n    \"Her\": \"Hercules\",\n    \"Sgr\": \"Sagittarius\",\n}\n\n# Seed each label at its constellation centroid (circular mean handles the\n# RA=0h seam), then relax overlaps with a force-based pass in projected\n# Cartesian space so the dense northern groups spread out legibly.\nlabel_seeds = []\nfor abbr, full_name in constellation_names.items():\n    member_t = np.array([project(stars[s][0], stars[s][1])[0] for s in star_names if stars[s][3] == abbr])\n    member_r = np.array([project(stars[s][0], stars[s][1])[1] for s in star_names if stars[s][3] == abbr])\n    ct = np.arctan2(np.mean(np.sin(member_t)), np.mean(np.cos(member_t)))\n    cr = np.mean(member_r)\n    label_seeds.append((ct, cr, full_name))\n\nxy = np.array([[r * np.cos(t), r * np.sin(t)] for t, r, _ in label_seeds])\n# Width-aware half-extents (data units) so long names reserve more room.\nhalf_w = np.array([len(full) * 0.95 + 2.0 for _, _, full in label_seeds])\n# Fixed obstacles: bright-star labels keep their place, so push names off them.\nfixed_xy = np.array(\n    [\n        [project(ra, dec)[1] * np.cos(project(ra, dec)[0]), project(ra, dec)[1] * np.sin(project(ra, dec)[0])]\n        for ra, dec, mag, _ in stars.values()\n        if mag < 1.0\n    ]\n)\nMIN_D_FIXED = 12.0\nfor _ in range(200):\n    moved = False\n    for i in range(len(xy)):\n        for j in range(i + 1, len(xy)):\n            d = xy[i] - xy[j]\n            dist = np.hypot(d[0], d[1])\n            need = half_w[i] + half_w[j]\n            if 1e-6 < dist < need:\n                shift = (d / dist) * (need - dist) / 2.0\n                xy[i] += shift\n                xy[j] -= shift\n                moved = True\n        for fx in fixed_xy:\n            d = xy[i] - fx\n            dist = np.hypot(d[0], d[1])\n            if 1e-6 < dist < MIN_D_FIXED:\n                xy[i] += (d / dist) * (MIN_D_FIXED - dist)\n                moved = True\n    if not moved:\n        break\n\nfor (x, y), (_, _, full_name) in zip(xy, label_seeds, strict=True):\n    nr = min(np.hypot(x, y), R_MAX - 4.0)\n    nt = np.arctan2(y, x)\n    ax.text(\n        nt,\n        nr,\n        full_name,\n        fontsize=8,\n        color=INK_SOFT,\n        alpha=0.9,\n        ha=\"center\",\n        va=\"center\",\n        fontstyle=\"italic\",\n        fontweight=\"medium\",\n        zorder=4,\n        path_effects=[pe.withStroke(linewidth=2.5, foreground=PAGE_BG, alpha=0.7)],\n    )\n\n# Style the circular coordinate grid (RA spokes + Dec circles)\nax.grid(True, color=INK, linewidth=0.5, alpha=0.16, linestyle=(0, (4, 6)))\nax.set_axisbelow(True)\nax.spines[\"polar\"].set_color(INK_SOFT)\nax.spines[\"polar\"].set_alpha(0.5)\nax.spines[\"polar\"].set_linewidth(1.0)\n\n# RA spokes every 2 hours, labelled in hours\nax.set_xticks(np.arange(0, 2 * np.pi, np.pi / 6))\nax.set_xticklabels([f\"{(2 * k) % 24}h\" for k in range(12)])\n\n# Dec circles: r = 90 - Dec  ->  +60, +30, 0 (equator), -30\nax.set_rticks([30, 60, 90, 120])\nax.set_yticklabels([\"+60°\", \"+30°\", \"0°\", \"−30°\"])\nax.set_rlabel_position(99)\nax.tick_params(axis=\"x\", colors=INK_SOFT, labelsize=8, pad=2)\nax.tick_params(axis=\"y\", colors=INK_MUTED, labelsize=7)\nfor lbl in ax.get_yticklabels():\n    lbl.set_path_effects([pe.withStroke(linewidth=2, foreground=PAGE_BG, alpha=0.7)])\n\n# Feature legend (ecliptic + Milky Way) — upper-right margin\nfeature_handles = [\n    Line2D([0], [0], color=AMBER, lw=1.4, linestyle=(0, (5, 3)), label=\"Ecliptic\"),\n    Patch(facecolor=INK_MUTED, alpha=0.35, edgecolor=\"none\", label=\"Milky Way\"),\n]\nleg1 = ax.legend(\n    handles=feature_handles,\n    loc=\"upper left\",\n    bbox_to_anchor=(1.04, 1.02),\n    fontsize=8,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n    framealpha=0.9,\n    handlelength=1.8,\n    borderpad=0.7,\n)\nplt.setp(leg1.get_texts(), color=INK_SOFT)\nax.add_artist(leg1)\n\n# Magnitude size legend — lower-right margin (size encodes brightness)\nmag_legend = [0.0, 2.0, 4.0]\nmag_labels = [\"0  (bright)\", \"2\", \"4  (faint)\"]\nsize_handles = [\n    Line2D(\n        [0],\n        [0],\n        marker=\"o\",\n        color=\"none\",\n        markerfacecolor=imprint_seq((m - min_mag) / (max_mag - min_mag)),\n        markeredgecolor=PAGE_BG,\n        markeredgewidth=0.4,\n        markersize=np.sqrt(max_size * ((max_mag - m) / (max_mag - min_mag)) ** 1.5 + min_size),\n        label=lab,\n    )\n    for m, lab in zip(mag_legend, mag_labels, strict=True)\n]\nleg2 = ax.legend(\n    handles=size_handles,\n    loc=\"lower left\",\n    bbox_to_anchor=(1.04, -0.02),\n    fontsize=8,\n    title=\"Apparent magnitude\",\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n    framealpha=0.9,\n    labelspacing=1.1,\n    borderpad=0.8,\n    handletextpad=1.0,\n)\nplt.setp(leg2.get_texts(), color=INK_SOFT)\nleg2.get_title().set_color(INK)\nleg2.get_title().set_fontsize(8)\n\n# Title + footer subtitle\nfig.text(\n    0.5,\n    0.95,\n    \"star-chart-constellation · python · matplotlib · anyplot.ai\",\n    ha=\"center\",\n    fontsize=12,\n    fontweight=\"medium\",\n    color=INK,\n)\nfig.text(\n    0.5,\n    0.035,\n    \"Azimuthal-equidistant sky chart  |  Right Ascension (hours) · Declination (°)  |  point size ∝ brightness\",\n    ha=\"center\",\n    fontsize=9,\n    color=INK_SOFT,\n)\n\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=fig.get_facecolor())\n"}