{"spec_id":"star-chart-constellation","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nstar-chart-constellation: Star Chart with Constellations\nLibrary: plotly 6.8.0 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\nfrom collections import defaultdict\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme-adaptive chrome (Imprint tokens)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nif THEME == \"light\":\n    PAGE_BG = \"#FAF8F1\"\n    ELEVATED_BG = \"#FFFDF6\"\n    INK = \"#1A1A17\"\n    INK_SOFT = \"#4A4A44\"\n    INK_MUTED = \"#6B6A63\"\n    LINE_COL = \"rgba(74,74,68,0.52)\"\n    GRID_COL = \"rgba(26,26,23,0.15)\"\n    EQUATOR_COL = \"rgba(26,26,23,0.30)\"\n    BOUNDARY_COL = \"rgba(74,74,68,0.55)\"\n    BG_STAR_COL = \"rgba(107,106,99,0.45)\"\n    MW_COL = \"rgba(107,106,99,0.06)\"\nelse:\n    PAGE_BG = \"#1A1A17\"\n    ELEVATED_BG = \"#242420\"\n    INK = \"#F0EFE8\"\n    INK_SOFT = \"#B8B7B0\"\n    INK_MUTED = \"#A8A79F\"\n    LINE_COL = \"rgba(184,183,176,0.55)\"\n    GRID_COL = \"rgba(240,239,232,0.16)\"\n    EQUATOR_COL = \"rgba(240,239,232,0.32)\"\n    BOUNDARY_COL = \"rgba(184,183,176,0.55)\"\n    BG_STAR_COL = \"rgba(184,183,176,0.50)\"\n    MW_COL = \"rgba(168,167,159,0.10)\"\n\n# Continuous Imprint colormap (sequential): brand green -> blue. Maps bright\n# (low magnitude) stars to #009E73 and faint stars toward #4467A3.\nIMPRINT_SEQ = [[0.0, \"#009E73\"], [1.0, \"#4467A3\"]]\n# Amber semantic anchor for the ecliptic reference line (constant across themes).\nECLIPTIC_COL = \"rgba(221,204,119,0.60)\"\n\n\n# Azimuthal-equidistant projection centred on the North Celestial Pole.\n# Co-declination is the true radial distance (deg); RA is the azimuth.\ndef project(ra_hours, dec_deg):\n    ra_rad = np.radians(np.asarray(ra_hours, dtype=float) * 15.0)\n    r = 90.0 - np.asarray(dec_deg, dtype=float)\n    return r * np.sin(ra_rad), r * np.cos(ra_rad)\n\n\n# Galactic (l, b) -> equatorial (RA hours, Dec deg), J2000 pole constants.\ndef gal_to_eq(l_deg, b_deg):\n    lon = np.radians(l_deg)\n    b = np.radians(b_deg)\n    a_ngp = np.radians(192.85948)\n    d_ngp = np.radians(27.12825)\n    l_ncp = np.radians(122.93192)\n    sin_d = np.sin(d_ngp) * np.sin(b) + np.cos(d_ngp) * np.cos(b) * np.cos(l_ncp - lon)\n    dec = np.arcsin(sin_d)\n    yy = np.cos(b) * np.sin(l_ncp - lon)\n    xx = np.cos(d_ngp) * np.sin(b) - np.sin(d_ngp) * np.cos(b) * np.cos(l_ncp - lon)\n    ra = (a_ngp + np.arctan2(yy, xx)) % (2 * np.pi)\n    return np.degrees(ra) / 15.0, np.degrees(dec)\n\n\n# Data - Notable stars with RA (hours), Dec (degrees), magnitude, constellation\nnp.random.seed(42)\n\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    # 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    \"Graffias\": (16.09, -19.81, 2.64, \"Sco\"),\n    \"Epsilon Sco\": (16.84, -34.29, 2.29, \"Sco\"),\n    \"Kappa Sco\": (17.71, -39.03, 2.41, \"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    \"Delta1 Lyr\": (18.91, 36.90, 4.22, \"Lyr\"),\n    # Gemini\n    \"Castor\": (7.58, 31.89, 1.58, \"Gem\"),\n    \"Pollux\": (7.76, 28.03, 1.14, \"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    # 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    \"Zeta Tau\": (5.63, 21.14, 3.03, \"Tau\"),\n    \"Tau Epsilon\": (4.48, 19.18, 3.53, \"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    # 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    # Boötes\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    \"Eta Boo\": (13.85, 18.40, 2.68, \"Boo\"),\n    # Auriga\n    \"Capella\": (5.28, 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    # 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    # Virgo\n    \"Spica\": (13.42, -11.16, 1.04, \"Vir\"),\n    \"Vindemiatrix\": (13.04, 10.96, 2.83, \"Vir\"),\n    \"Porrima\": (12.69, -1.45, 2.74, \"Vir\"),\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    # Pegasus\n    \"Enif\": (21.74, 9.88, 2.39, \"Peg\"),\n    \"Scheat\": (23.06, 28.08, 2.42, \"Peg\"),\n    \"Markab\": (23.08, 15.21, 2.49, \"Peg\"),\n    \"Algenib\": (0.22, 15.18, 2.83, \"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    # Corona Borealis\n    \"Alphecca\": (15.58, 26.71, 2.23, \"CrB\"),\n    # Libra\n    \"Zubeneschamali\": (15.28, -9.38, 2.61, \"Lib\"),\n    \"Zubenelgenubi\": (14.85, -16.04, 2.75, \"Lib\"),\n    # Aries\n    \"Hamal\": (2.12, 23.46, 2.00, \"Ari\"),\n    \"Sheratan\": (1.91, 20.81, 2.64, \"Ari\"),\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    \"Eta Dra\": (16.40, 61.51, 2.74, \"Dra\"),\n    # Canis Minor\n    \"Procyon\": (7.66, 5.22, 0.34, \"CMi\"),\n    \"Gomeisa\": (7.45, 8.29, 2.90, \"CMi\"),\n    # Pisces Austrinus\n    \"Fomalhaut\": (22.96, -29.62, 1.16, \"PsA\"),\n    # Centaurus\n    \"Rigil Kentaurus\": (14.66, -60.83, -0.01, \"Cen\"),\n    \"Hadar\": (14.06, -60.37, 0.61, \"Cen\"),\n    # Crux\n    \"Acrux\": (12.44, -63.10, 0.76, \"Cru\"),\n    \"Mimosa\": (12.80, -59.69, 1.25, \"Cru\"),\n    \"Gacrux\": (12.52, -57.11, 1.64, \"Cru\"),\n    \"Delta Cru\": (12.25, -58.75, 2.80, \"Cru\"),\n}\n\n# Constellation line connections (pairs of star names)\nedges = [\n    # Orion\n    (\"Betelgeuse\", \"Bellatrix\"),\n    (\"Bellatrix\", \"Mintaka\"),\n    (\"Mintaka\", \"Alnilam\"),\n    (\"Alnilam\", \"Alnitak\"),\n    (\"Alnitak\", \"Saiph\"),\n    (\"Saiph\", \"Rigel\"),\n    (\"Rigel\", \"Mintaka\"),\n    (\"Betelgeuse\", \"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\", \"Chertan\"),\n    (\"Chertan\", \"Denebola\"),\n    (\"Denebola\", \"Zosma\"),\n    (\"Zosma\", \"Algieba\"),\n    (\"Algieba\", \"Regulus\"),\n    # Scorpius\n    (\"Graffias\", \"Dschubba\"),\n    (\"Dschubba\", \"Antares\"),\n    (\"Antares\", \"Epsilon Sco\"),\n    (\"Epsilon Sco\", \"Shaula\"),\n    (\"Shaula\", \"Kappa Sco\"),\n    (\"Kappa Sco\", \"Sargas\"),\n    # Cygnus\n    (\"Deneb\", \"Sadr\"),\n    (\"Sadr\", \"Gienah Cyg\"),\n    (\"Sadr\", \"Delta Cyg\"),\n    (\"Sadr\", \"Albireo\"),\n    # Lyra\n    (\"Vega\", \"Sheliak\"),\n    (\"Vega\", \"Sulafat\"),\n    (\"Sheliak\", \"Sulafat\"),\n    # Gemini\n    (\"Castor\", \"Pollux\"),\n    (\"Pollux\", \"Mebsuta\"),\n    (\"Mebsuta\", \"Tejat\"),\n    (\"Castor\", \"Mebsuta\"),\n    (\"Pollux\", \"Alhena\"),\n    # Taurus\n    (\"Aldebaran\", \"Tau Epsilon\"),\n    (\"Tau Epsilon\", \"Alcyone\"),\n    (\"Aldebaran\", \"Zeta Tau\"),\n    (\"Zeta Tau\", \"Elnath\"),\n    # Canis Major\n    (\"Sirius\", \"Mirzam\"),\n    (\"Sirius\", \"Adhara\"),\n    (\"Adhara\", \"Wezen\"),\n    (\"Wezen\", \"Aludra\"),\n    # Aquila\n    (\"Altair\", \"Tarazed\"),\n    (\"Altair\", \"Alshain\"),\n    # Boötes\n    (\"Arcturus\", \"Izar\"),\n    (\"Arcturus\", \"Muphrid\"),\n    # Auriga\n    (\"Capella\", \"Menkalinan\"),\n    (\"Menkalinan\", \"Theta Aur\"),\n    (\"Theta Aur\", \"Iota Aur\"),\n    (\"Iota Aur\", \"Capella\"),\n    # Perseus\n    (\"Mirfak\", \"Algol\"),\n    (\"Mirfak\", \"Epsilon Per\"),\n    (\"Epsilon Per\", \"Zeta Per\"),\n    # Andromeda\n    (\"Alpheratz\", \"Mirach\"),\n    (\"Mirach\", \"Almach\"),\n    # Pegasus (Great Square)\n    (\"Alpheratz\", \"Scheat\"),\n    (\"Scheat\", \"Markab\"),\n    (\"Markab\", \"Algenib\"),\n    (\"Algenib\", \"Alpheratz\"),\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    # Aries\n    (\"Hamal\", \"Sheratan\"),\n    # Draco\n    (\"Eltanin\", \"Rastaban\"),\n    (\"Rastaban\", \"Eta Dra\"),\n    (\"Eta Dra\", \"Thuban\"),\n    # Canis Minor\n    (\"Procyon\", \"Gomeisa\"),\n    # Centaurus\n    (\"Rigil Kentaurus\", \"Hadar\"),\n    # Crux\n    (\"Acrux\", \"Gacrux\"),\n    (\"Mimosa\", \"Delta Cru\"),\n    # Libra\n    (\"Zubeneschamali\", \"Zubenelgenubi\"),\n]\n\n# Constellation full names for labels\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    \"Aur\": \"Auriga\",\n    \"Per\": \"Perseus\",\n    \"Vir\": \"Virgo\",\n    \"Sgr\": \"Sagittarius\",\n    \"Peg\": \"Pegasus\",\n    \"And\": \"Andromeda\",\n    \"CrB\": \"Corona Borealis\",\n    \"Lib\": \"Libra\",\n    \"Ari\": \"Aries\",\n    \"Dra\": \"Draco\",\n    \"CMi\": \"Canis Minor\",\n    \"PsA\": \"Piscis Austrinus\",\n    \"Cen\": \"Centaurus\",\n    \"Cru\": \"Crux\",\n}\n\n# Geometry of the planisphere (radius = co-declination, degrees)\nR_BOUNDARY = 156.0  # outer frame, just beyond the southernmost catalogued stars\nDEC_RINGS = [60, 30, 0, -30, -60]  # labelled declination circles\n\n# Project the named stars\nstar_names = list(stars.keys())\nra_h = np.array([stars[s][0] for s in star_names])\ndec = np.array([stars[s][1] for s in star_names])\nmag = np.array([stars[s][2] for s in star_names])\nstar_x, star_y = project(ra_h, dec)\n\n# Magnitude -> marker size (brighter / lower magnitude = larger)\nmax_size, min_size = 15.0, 2.5\nmag_min, mag_max = mag.min(), mag.max()\nsizes = max_size - (mag - mag_min) / (mag_max - mag_min) * (max_size - min_size)\n\n# Dimmer background star field, projected within the visible cap\nbg_ra = np.random.uniform(0, 24, 220)\nbg_dec = np.random.uniform(-58, 87, 220)\nbg_x, bg_y = project(bg_ra, bg_dec)\n\nfig = go.Figure()\n\n# Milky Way band - faint filled great-circle ribbon (galactic latitude +/-12 deg)\ngl = np.linspace(0, 360, 361)\nmw_ra_top, mw_dec_top = gal_to_eq(gl, 12.0)\nmw_ra_bot, mw_dec_bot = gal_to_eq(gl[::-1], -12.0)\nmw_xt, mw_yt = project(mw_ra_top, mw_dec_top)\nmw_xb, mw_yb = project(mw_ra_bot, mw_dec_bot)\nfig.add_trace(\n    go.Scatter(\n        x=np.concatenate([mw_xt, mw_xb]),\n        y=np.concatenate([mw_yt, mw_yb]),\n        mode=\"lines\",\n        fill=\"toself\",\n        fillcolor=MW_COL,\n        line={\"width\": 0},\n        hoverinfo=\"skip\",\n        showlegend=False,\n    )\n)\n\n# Coordinate grid - declination rings + right-ascension spokes\nring_x, ring_y = [], []\nfor dec_ring in DEC_RINGS:\n    if dec_ring == 0:\n        continue  # equator drawn separately, emphasised\n    rr = 90.0 - dec_ring\n    th = np.linspace(0, 2 * np.pi, 240)\n    ring_x.extend((rr * np.cos(th)).tolist() + [None])\n    ring_y.extend((rr * np.sin(th)).tolist() + [None])\n\nspoke_x, spoke_y = [], []\nfor h in range(0, 24, 2):\n    a = np.radians(h * 15)\n    spoke_x.extend([0.0, 150.0 * np.sin(a), None])\n    spoke_y.extend([0.0, 150.0 * np.cos(a), None])\n\nfig.add_trace(\n    go.Scatter(\n        x=ring_x + spoke_x,\n        y=ring_y + spoke_y,\n        mode=\"lines\",\n        line={\"color\": GRID_COL, \"width\": 0.7},\n        hoverinfo=\"skip\",\n        showlegend=False,\n    )\n)\n\n# Celestial equator (dec = 0) - emphasised reference circle\nth = np.linspace(0, 2 * np.pi, 300)\nfig.add_trace(\n    go.Scatter(\n        x=90.0 * np.cos(th),\n        y=90.0 * np.sin(th),\n        mode=\"lines\",\n        line={\"color\": EQUATOR_COL, \"width\": 1.0},\n        hoverinfo=\"skip\",\n        showlegend=False,\n    )\n)\n\n# Outer sky boundary\nfig.add_trace(\n    go.Scatter(\n        x=R_BOUNDARY * np.cos(th),\n        y=R_BOUNDARY * np.sin(th),\n        mode=\"lines\",\n        line={\"color\": BOUNDARY_COL, \"width\": 1.2},\n        hoverinfo=\"skip\",\n        showlegend=False,\n    )\n)\n\n# Ecliptic - amber dashed great circle (23.44 deg inclined to the equator)\necl_lon = np.linspace(0, 360, 361)\nobliquity = np.radians(23.44)\necl_ra = np.degrees(np.arctan2(np.sin(np.radians(ecl_lon)) * np.cos(obliquity), np.cos(np.radians(ecl_lon)))) / 15.0\necl_dec = np.degrees(np.arcsin(np.sin(np.radians(ecl_lon)) * np.sin(obliquity)))\necl_x, ecl_y = project(ecl_ra % 24, ecl_dec)\nfig.add_trace(\n    go.Scatter(\n        x=ecl_x,\n        y=ecl_y,\n        mode=\"lines\",\n        line={\"color\": ECLIPTIC_COL, \"width\": 1.4, \"dash\": \"dash\"},\n        name=\"Ecliptic\",\n        hoverinfo=\"skip\",\n        showlegend=False,\n    )\n)\n\n# Constellation stick-figure lines (projected, single trace with separators)\nline_x, line_y = [], []\nfor s1, s2 in edges:\n    if s1 in stars and s2 in stars:\n        x1, y1 = project(stars[s1][0], stars[s1][1])\n        x2, y2 = project(stars[s2][0], stars[s2][1])\n        line_x.extend([float(x1), float(x2), None])\n        line_y.extend([float(y1), float(y2), None])\n\nfig.add_trace(\n    go.Scatter(\n        x=line_x, y=line_y, mode=\"lines\", line={\"color\": LINE_COL, \"width\": 1.2}, hoverinfo=\"skip\", showlegend=False\n    )\n)\n\n# Background stars (WebGL for the dense field)\nfig.add_trace(\n    go.Scattergl(\n        x=bg_x, y=bg_y, mode=\"markers\", marker={\"size\": 1.6, \"color\": BG_STAR_COL}, hoverinfo=\"skip\", showlegend=False\n    )\n)\n\n# Named stars - size + Imprint-sequential colour both encode magnitude\nfig.add_trace(\n    go.Scattergl(\n        x=star_x,\n        y=star_y,\n        mode=\"markers\",\n        marker={\n            \"size\": sizes,\n            \"color\": mag,\n            \"colorscale\": IMPRINT_SEQ,\n            \"cmin\": -1.5,\n            \"cmax\": 3.8,\n            \"opacity\": 0.95,\n            \"line\": {\"width\": 0},\n            \"colorbar\": {\n                \"title\": {\"text\": \"Apparent magnitude\", \"font\": {\"size\": 11, \"color\": INK}, \"side\": \"top\"},\n                \"orientation\": \"h\",\n                \"x\": 0.5,\n                \"xanchor\": \"center\",\n                \"y\": -0.06,\n                \"yanchor\": \"top\",\n                \"len\": 0.46,\n                \"thickness\": 14,\n                \"tickfont\": {\"size\": 9, \"color\": INK_SOFT},\n                \"tickvals\": [-1, 0, 1, 2, 3],\n                \"outlinewidth\": 0,\n            },\n        },\n        text=[\n            f\"{name}<br>Mag {stars[name][2]:.2f}<br>{constellation_names.get(stars[name][3], stars[name][3])}\"\n            for name in star_names\n        ],\n        hoverinfo=\"text\",\n        showlegend=False,\n    )\n)\n\n# Annotations: constellation labels, grid labels, brightest-star labels\nann = []\n\n# Constellation names at each group's projected centroid, nudged radially out\ngroups = defaultdict(list)\nfor i, name in enumerate(star_names):\n    groups[stars[name][3]].append((star_x[i], star_y[i]))\ncentroids = {}\nfor abbr, pts in groups.items():\n    centroids[abbr] = (float(np.mean([p[0] for p in pts])), float(np.mean([p[1] for p in pts])))\n    if len(pts) < 2:\n        continue  # skip single-star constellations to avoid clutter\n    cx, cy = centroids[abbr]\n    rad = np.hypot(cx, cy) or 1.0\n    ann.append(\n        {\n            \"x\": cx + cx / rad * 9.0,\n            \"y\": cy + cy / rad * 9.0,\n            \"text\": constellation_names.get(abbr, abbr),\n            \"showarrow\": False,\n            \"font\": {\"size\": 10, \"color\": INK_SOFT, \"family\": \"Arial\"},\n        }\n    )\n\n# Brightest stars (mag < 0.2) get individual labels, pushed away from the\n# constellation-name centroid so the two labels never collide.\nfor i, name in enumerate(star_names):\n    if mag[i] < 0.2:\n        cx, cy = centroids[stars[name][3]]\n        dx, dy = star_x[i] - cx, star_y[i] - cy\n        d = np.hypot(dx, dy)\n        if d < 1.0:  # star sits on the centroid -> push radially outward instead\n            dx, dy = star_x[i], star_y[i]\n            d = np.hypot(dx, dy) or 1.0\n        ann.append(\n            {\n                \"x\": float(star_x[i] + dx / d * 9.0),\n                \"y\": float(star_y[i] + dy / d * 9.0),\n                \"text\": name,\n                \"showarrow\": False,\n                \"font\": {\"size\": 9, \"color\": INK, \"family\": \"Arial\"},\n            }\n        )\n\n# Declination ring labels (placed along a clear gap between spokes)\nfor dec_ring in DEC_RINGS:\n    rr = 90.0 - dec_ring\n    a = np.radians(7)\n    dec_label = f\"{dec_ring:+d}°\" if dec_ring != 0 else \"0°\"\n    ann.append(\n        {\n            \"x\": float(rr * np.sin(a)),\n            \"y\": float(rr * np.cos(a)),\n            \"text\": dec_label,\n            \"showarrow\": False,\n            \"font\": {\"size\": 9, \"color\": INK_MUTED, \"family\": \"Arial\"},\n        }\n    )\n\n# Right-ascension labels around the outer rim\nfor h in range(0, 24, 2):\n    a = np.radians(h * 15)\n    ann.append(\n        {\n            \"x\": float((R_BOUNDARY + 6) * np.sin(a)),\n            \"y\": float((R_BOUNDARY + 6) * np.cos(a)),\n            \"text\": f\"{h}h\",\n            \"showarrow\": False,\n            \"font\": {\"size\": 9, \"color\": INK_SOFT, \"family\": \"Arial\"},\n        }\n    )\n\n# North Celestial Pole marker at the centre\nann.append(\n    {\"x\": 0, \"y\": -7, \"text\": \"NCP\", \"showarrow\": False, \"font\": {\"size\": 8.5, \"color\": INK_MUTED, \"family\": \"Arial\"}}\n)\n\naxis_off = {\"visible\": False, \"showgrid\": False, \"zeroline\": False, \"range\": [-(R_BOUNDARY + 14), R_BOUNDARY + 14]}\n\nfig.update_layout(\n    title={\n        \"text\": \"star-chart-constellation · python · plotly · anyplot.ai\",\n        \"font\": {\"size\": 16, \"color\": INK, \"family\": \"Arial\"},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n        \"y\": 0.97,\n    },\n    autosize=False,\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    xaxis={**axis_off, \"scaleanchor\": \"y\", \"scaleratio\": 1},\n    yaxis=dict(axis_off),\n    margin={\"l\": 40, \"r\": 40, \"t\": 70, \"b\": 90},\n    annotations=ann,\n)\n\n# Save - square 2400 x 2400 (600 x 600 @ scale 4)\nfig.write_image(f\"plot-{THEME}.png\", width=600, height=600, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}