{"spec_id":"star-chart-constellation","library":"muix","language":"javascript","code":"// anyplot.ai\n// star-chart-constellation: Star Chart with Constellations\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 94/100 | Created: 2026-06-17\n//# anyplot-orientation: square\n// anyplot.ai\n// star-chart-constellation: Star Chart with Constellations\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-06-17\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { useDrawingArea, useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\n// --- Theme tokens (chrome is theme-adaptive; data colours are not) -----------\nconst t = window.ANYPLOT_TOKENS;\nconst SIZE = window.ANYPLOT_SIZE;\n// Continuous brightness → Imprint sequential ramp (brand green → blue).\nconst SEQ = t.seq; // [\"#009E73\", \"#4467A3\"]\n\n// --- Projection: north-pole azimuthal equidistant ----------------------------\n// dec = +90 maps to the centre, the rim sits at DEC_MIN. RA spins the angle.\nconst DEC_MIN = -52;\nconst R_MAX = 90 - DEC_MIN; // angular distance (deg) from the pole at the rim\nfunction project(raHours, decDeg) {\n  const theta = (raHours / 24) * 2 * Math.PI;\n  const rr = (90 - decDeg) / R_MAX;\n  return { x: rr * Math.sin(theta), y: rr * Math.cos(theta) };\n}\n\n// --- Star catalogue (real bright stars, RA in hours, Dec in degrees) ---------\n// con = IAU constellation abbreviation. Coordinates are approximate J2000.\nconst CATALOG = [\n  // Ursa Major — the Big Dipper\n  { name: \"Dubhe\", ra: 11.06, dec: 61.75, mag: 1.79, con: \"UMa\" },\n  { name: \"Merak\", ra: 11.03, dec: 56.38, mag: 2.37, con: \"UMa\" },\n  { name: \"Phecda\", ra: 11.90, dec: 53.69, mag: 2.44, con: \"UMa\" },\n  { name: \"Megrez\", ra: 12.26, dec: 57.03, mag: 3.31, con: \"UMa\" },\n  { name: \"Alioth\", ra: 12.90, dec: 55.96, mag: 1.77, con: \"UMa\" },\n  { name: \"Mizar\", ra: 13.40, dec: 54.93, mag: 2.04, con: \"UMa\" },\n  { name: \"Alkaid\", ra: 13.79, dec: 49.31, mag: 1.86, con: \"UMa\" },\n  // Ursa Minor — the Little Dipper\n  { name: \"Polaris\", ra: 2.53, dec: 89.26, mag: 1.98, con: \"UMi\" },\n  { name: \"Yildun\", ra: 17.54, dec: 86.59, mag: 4.36, con: \"UMi\" },\n  { name: \"EpsUMi\", ra: 16.77, dec: 82.04, mag: 4.21, con: \"UMi\" },\n  { name: \"ZetUMi\", ra: 15.73, dec: 77.79, mag: 4.32, con: \"UMi\" },\n  { name: \"Kochab\", ra: 14.85, dec: 74.16, mag: 2.07, con: \"UMi\" },\n  { name: \"Pherkad\", ra: 15.35, dec: 71.83, mag: 3.05, con: \"UMi\" },\n  // Cassiopeia — the W\n  { name: \"Caph\", ra: 0.15, dec: 59.15, mag: 2.28, con: \"Cas\" },\n  { name: \"Schedar\", ra: 0.68, dec: 56.54, mag: 2.24, con: \"Cas\" },\n  { name: \"GamCas\", ra: 0.95, dec: 60.72, mag: 2.47, con: \"Cas\" },\n  { name: \"Ruchbah\", ra: 1.43, dec: 60.24, mag: 2.68, con: \"Cas\" },\n  { name: \"Segin\", ra: 1.91, dec: 63.67, mag: 3.35, con: \"Cas\" },\n  // Cepheus — the house\n  { name: \"Alderamin\", ra: 21.31, dec: 62.59, mag: 2.45, con: \"Cep\" },\n  { name: \"BetCep\", ra: 21.48, dec: 70.56, mag: 3.23, con: \"Cep\" },\n  { name: \"GamCep\", ra: 23.66, dec: 77.63, mag: 3.21, con: \"Cep\" },\n  { name: \"IotCep\", ra: 22.83, dec: 66.20, mag: 3.52, con: \"Cep\" },\n  { name: \"ZetCep\", ra: 22.18, dec: 58.20, mag: 3.35, con: \"Cep\" },\n  // Cygnus — the Northern Cross\n  { name: \"Deneb\", ra: 20.69, dec: 45.28, mag: 1.25, con: \"Cyg\" },\n  { name: \"Sadr\", ra: 20.37, dec: 40.26, mag: 2.23, con: \"Cyg\" },\n  { name: \"Gienah\", ra: 20.77, dec: 33.97, mag: 2.48, con: \"Cyg\" },\n  { name: \"DelCyg\", ra: 19.75, dec: 45.13, mag: 2.87, con: \"Cyg\" },\n  { name: \"Albireo\", ra: 19.51, dec: 27.96, mag: 3.18, con: \"Cyg\" },\n  // Lyra\n  { name: \"Vega\", ra: 18.62, dec: 38.78, mag: 0.03, con: \"Lyr\" },\n  { name: \"Sheliak\", ra: 18.83, dec: 33.36, mag: 3.52, con: \"Lyr\" },\n  { name: \"Sulafat\", ra: 18.98, dec: 32.69, mag: 3.26, con: \"Lyr\" },\n  { name: \"ZetLyr\", ra: 18.75, dec: 37.60, mag: 4.36, con: \"Lyr\" },\n  { name: \"DelLyr\", ra: 18.90, dec: 36.90, mag: 4.30, con: \"Lyr\" },\n  // Aquila\n  { name: \"Altair\", ra: 19.85, dec: 8.87, mag: 0.76, con: \"Aql\" },\n  { name: \"Tarazed\", ra: 19.77, dec: 10.61, mag: 2.72, con: \"Aql\" },\n  { name: \"Alshain\", ra: 19.92, dec: 6.41, mag: 3.71, con: \"Aql\" },\n  { name: \"DelAql\", ra: 19.42, dec: 3.11, mag: 3.36, con: \"Aql\" },\n  { name: \"ZetAql\", ra: 19.09, dec: 13.86, mag: 2.99, con: \"Aql\" },\n  // Bootes — the kite\n  { name: \"Arcturus\", ra: 14.26, dec: 19.18, mag: -0.05, con: \"Boo\" },\n  { name: \"Izar\", ra: 14.75, dec: 27.07, mag: 2.35, con: \"Boo\" },\n  { name: \"Seginus\", ra: 14.53, dec: 38.31, mag: 3.03, con: \"Boo\" },\n  { name: \"Nekkar\", ra: 15.03, dec: 40.39, mag: 3.49, con: \"Boo\" },\n  { name: \"Muphrid\", ra: 13.91, dec: 18.40, mag: 2.68, con: \"Boo\" },\n  // Corona Borealis — the crown arc\n  { name: \"ThetCrB\", ra: 15.55, dec: 31.36, mag: 4.13, con: \"CrB\" },\n  { name: \"BetCrB\", ra: 15.46, dec: 29.10, mag: 3.66, con: \"CrB\" },\n  { name: \"Alphecca\", ra: 15.58, dec: 26.71, mag: 2.22, con: \"CrB\" },\n  { name: \"GamCrB\", ra: 15.71, dec: 26.30, mag: 3.80, con: \"CrB\" },\n  { name: \"DelCrB\", ra: 15.82, dec: 26.07, mag: 4.59, con: \"CrB\" },\n  // Hercules — the keystone\n  { name: \"ZetHer\", ra: 16.69, dec: 31.60, mag: 2.81, con: \"Her\" },\n  { name: \"EtaHer\", ra: 16.72, dec: 38.92, mag: 3.48, con: \"Her\" },\n  { name: \"PiHer\", ra: 17.25, dec: 36.81, mag: 3.16, con: \"Her\" },\n  { name: \"EpsHer\", ra: 17.00, dec: 30.93, mag: 3.92, con: \"Her\" },\n  // Draco — winding tail\n  { name: \"Eltanin\", ra: 17.94, dec: 51.49, mag: 2.36, con: \"Dra\" },\n  { name: \"Rastaban\", ra: 17.51, dec: 52.30, mag: 2.79, con: \"Dra\" },\n  { name: \"Grumium\", ra: 17.89, dec: 56.87, mag: 3.75, con: \"Dra\" },\n  { name: \"DelDra\", ra: 19.21, dec: 67.66, mag: 3.07, con: \"Dra\" },\n  { name: \"ZetDra\", ra: 17.15, dec: 65.71, mag: 3.17, con: \"Dra\" },\n  { name: \"Edasich\", ra: 15.42, dec: 58.97, mag: 3.29, con: \"Dra\" },\n  { name: \"Thuban\", ra: 14.07, dec: 64.38, mag: 3.65, con: \"Dra\" },\n  // Perseus\n  { name: \"Mirfak\", ra: 3.41, dec: 49.86, mag: 1.79, con: \"Per\" },\n  { name: \"Algol\", ra: 3.14, dec: 40.96, mag: 2.12, con: \"Per\" },\n  { name: \"GamPer\", ra: 3.08, dec: 53.51, mag: 2.93, con: \"Per\" },\n  { name: \"DelPer\", ra: 3.72, dec: 47.79, mag: 3.01, con: \"Per\" },\n  { name: \"EpsPer\", ra: 3.96, dec: 40.01, mag: 2.89, con: \"Per\" },\n  // Auriga — the pentagon\n  { name: \"Capella\", ra: 5.28, dec: 46.00, mag: 0.08, con: \"Aur\" },\n  { name: \"Menkalinan\", ra: 5.99, dec: 44.95, mag: 1.90, con: \"Aur\" },\n  { name: \"ThetAur\", ra: 6.00, dec: 37.21, mag: 2.62, con: \"Aur\" },\n  { name: \"IotAur\", ra: 4.95, dec: 33.17, mag: 2.69, con: \"Aur\" },\n  // Andromeda\n  { name: \"Alpheratz\", ra: 0.14, dec: 29.09, mag: 2.06, con: \"And\" },\n  { name: \"Mirach\", ra: 1.16, dec: 35.62, mag: 2.05, con: \"And\" },\n  { name: \"Almach\", ra: 2.07, dec: 42.33, mag: 2.10, con: \"And\" },\n  { name: \"DelAnd\", ra: 0.66, dec: 30.86, mag: 3.27, con: \"And\" },\n  // Pegasus — the Great Square\n  { name: \"Markab\", ra: 23.08, dec: 15.21, mag: 2.49, con: \"Peg\" },\n  { name: \"Scheat\", ra: 23.06, dec: 28.08, mag: 2.42, con: \"Peg\" },\n  { name: \"Algenib\", ra: 0.22, dec: 15.18, mag: 2.83, con: \"Peg\" },\n  { name: \"Enif\", ra: 21.74, dec: 9.88, mag: 2.39, con: \"Peg\" },\n  // Triangulum\n  { name: \"BetTri\", ra: 2.16, dec: 34.99, mag: 3.00, con: \"Tri\" },\n  { name: \"AlpTri\", ra: 1.88, dec: 29.59, mag: 3.42, con: \"Tri\" },\n  { name: \"GamTri\", ra: 2.29, dec: 33.85, mag: 4.01, con: \"Tri\" },\n  // Aries\n  { name: \"Hamal\", ra: 2.12, dec: 23.46, mag: 2.00, con: \"Ari\" },\n  { name: \"Sheratan\", ra: 1.91, dec: 20.81, mag: 2.64, con: \"Ari\" },\n  { name: \"Mesarthim\", ra: 1.89, dec: 19.29, mag: 3.86, con: \"Ari\" },\n  // Gemini — the twins\n  { name: \"Pollux\", ra: 7.76, dec: 28.03, mag: 1.14, con: \"Gem\" },\n  { name: \"Castor\", ra: 7.58, dec: 31.89, mag: 1.58, con: \"Gem\" },\n  { name: \"Alhena\", ra: 6.63, dec: 16.40, mag: 1.92, con: \"Gem\" },\n  { name: \"Wasat\", ra: 7.34, dec: 21.98, mag: 3.53, con: \"Gem\" },\n  { name: \"Mebsuta\", ra: 6.73, dec: 25.13, mag: 3.06, con: \"Gem\" },\n  { name: \"MuGem\", ra: 6.38, dec: 22.51, mag: 2.87, con: \"Gem\" },\n  // Leo — the sickle and triangle\n  { name: \"Regulus\", ra: 10.14, dec: 11.97, mag: 1.36, con: \"Leo\" },\n  { name: \"Denebola\", ra: 11.82, dec: 14.57, mag: 2.14, con: \"Leo\" },\n  { name: \"Algieba\", ra: 10.33, dec: 19.84, mag: 2.08, con: \"Leo\" },\n  { name: \"Zosma\", ra: 11.24, dec: 20.52, mag: 2.56, con: \"Leo\" },\n  { name: \"ThetLeo\", ra: 11.24, dec: 15.43, mag: 3.33, con: \"Leo\" },\n  { name: \"EpsLeo\", ra: 9.76, dec: 23.77, mag: 2.98, con: \"Leo\" },\n  { name: \"EtaLeo\", ra: 10.12, dec: 16.76, mag: 3.48, con: \"Leo\" },\n  // Canes Venatici\n  { name: \"CorCaroli\", ra: 12.93, dec: 38.32, mag: 2.89, con: \"CVn\" },\n  { name: \"BetCVn\", ra: 12.56, dec: 41.36, mag: 4.24, con: \"CVn\" },\n  // Taurus — the V and horns (Elnath shared with Auriga)\n  { name: \"Aldebaran\", ra: 4.60, dec: 16.51, mag: 0.87, con: \"Tau\" },\n  { name: \"Elnath\", ra: 5.44, dec: 28.61, mag: 1.65, con: \"Tau\" },\n  { name: \"ZetTau\", ra: 5.63, dec: 21.14, mag: 3.00, con: \"Tau\" },\n  { name: \"GamTau\", ra: 4.33, dec: 15.63, mag: 3.65, con: \"Tau\" },\n  { name: \"EpsTau\", ra: 4.48, dec: 19.18, mag: 3.53, con: \"Tau\" },\n  { name: \"LamTau\", ra: 4.01, dec: 12.49, mag: 3.47, con: \"Tau\" },\n  // Orion — the hunter\n  { name: \"Betelgeuse\", ra: 5.92, dec: 7.41, mag: 0.45, con: \"Ori\" },\n  { name: \"Rigel\", ra: 5.24, dec: -8.20, mag: 0.18, con: \"Ori\" },\n  { name: \"Bellatrix\", ra: 5.42, dec: 6.35, mag: 1.64, con: \"Ori\" },\n  { name: \"Saiph\", ra: 5.80, dec: -9.67, mag: 2.07, con: \"Ori\" },\n  { name: \"Alnitak\", ra: 5.68, dec: -1.94, mag: 1.77, con: \"Ori\" },\n  { name: \"Alnilam\", ra: 5.60, dec: -1.20, mag: 1.69, con: \"Ori\" },\n  { name: \"Mintaka\", ra: 5.53, dec: -0.30, mag: 2.23, con: \"Ori\" },\n  // Canis Major — Sirius and the dog\n  { name: \"Sirius\", ra: 6.75, dec: -16.72, mag: -1.46, con: \"CMa\" },\n  { name: \"Mirzam\", ra: 6.38, dec: -17.96, mag: 1.98, con: \"CMa\" },\n  { name: \"Wezen\", ra: 7.14, dec: -26.39, mag: 1.83, con: \"CMa\" },\n  { name: \"Adhara\", ra: 6.98, dec: -28.97, mag: 1.50, con: \"CMa\" },\n  { name: \"Aludra\", ra: 7.40, dec: -29.30, mag: 2.45, con: \"CMa\" },\n];\n\n// --- Constellation stick figures (pairs of catalogue names) ------------------\nconst EDGES = [\n  // Ursa Major\n  [\"Dubhe\", \"Merak\"], [\"Merak\", \"Phecda\"], [\"Phecda\", \"Megrez\"], [\"Megrez\", \"Dubhe\"],\n  [\"Megrez\", \"Alioth\"], [\"Alioth\", \"Mizar\"], [\"Mizar\", \"Alkaid\"],\n  // Ursa Minor\n  [\"Polaris\", \"Yildun\"], [\"Yildun\", \"EpsUMi\"], [\"EpsUMi\", \"ZetUMi\"], [\"ZetUMi\", \"Kochab\"],\n  [\"Kochab\", \"Pherkad\"], [\"Pherkad\", \"EpsUMi\"],\n  // Cassiopeia\n  [\"Caph\", \"Schedar\"], [\"Schedar\", \"GamCas\"], [\"GamCas\", \"Ruchbah\"], [\"Ruchbah\", \"Segin\"],\n  // Cepheus\n  [\"Alderamin\", \"BetCep\"], [\"BetCep\", \"GamCep\"], [\"GamCep\", \"IotCep\"], [\"IotCep\", \"ZetCep\"],\n  [\"ZetCep\", \"Alderamin\"],\n  // Cygnus\n  [\"Deneb\", \"Sadr\"], [\"Sadr\", \"Albireo\"], [\"Sadr\", \"Gienah\"], [\"Sadr\", \"DelCyg\"],\n  // Lyra\n  [\"Vega\", \"ZetLyr\"], [\"ZetLyr\", \"Sheliak\"], [\"Sheliak\", \"Sulafat\"], [\"Sulafat\", \"DelLyr\"],\n  [\"DelLyr\", \"ZetLyr\"],\n  // Aquila\n  [\"Tarazed\", \"Altair\"], [\"Altair\", \"Alshain\"], [\"Altair\", \"DelAql\"], [\"DelAql\", \"ZetAql\"],\n  // Bootes\n  [\"Arcturus\", \"Izar\"], [\"Izar\", \"Nekkar\"], [\"Nekkar\", \"Seginus\"], [\"Seginus\", \"Arcturus\"],\n  [\"Arcturus\", \"Muphrid\"],\n  // Corona Borealis\n  [\"ThetCrB\", \"BetCrB\"], [\"BetCrB\", \"Alphecca\"], [\"Alphecca\", \"GamCrB\"], [\"GamCrB\", \"DelCrB\"],\n  // Hercules\n  [\"ZetHer\", \"EtaHer\"], [\"EtaHer\", \"PiHer\"], [\"PiHer\", \"EpsHer\"], [\"EpsHer\", \"ZetHer\"],\n  // Draco\n  [\"Eltanin\", \"Rastaban\"], [\"Rastaban\", \"Grumium\"], [\"Grumium\", \"Eltanin\"],\n  [\"Grumium\", \"DelDra\"], [\"DelDra\", \"ZetDra\"], [\"ZetDra\", \"Edasich\"], [\"Edasich\", \"Thuban\"],\n  // Perseus\n  [\"GamPer\", \"Mirfak\"], [\"Mirfak\", \"DelPer\"], [\"DelPer\", \"EpsPer\"], [\"Mirfak\", \"Algol\"],\n  // Auriga\n  [\"Capella\", \"Menkalinan\"], [\"Menkalinan\", \"ThetAur\"], [\"ThetAur\", \"Elnath\"],\n  [\"Elnath\", \"IotAur\"], [\"IotAur\", \"Capella\"],\n  // Andromeda\n  [\"Alpheratz\", \"DelAnd\"], [\"DelAnd\", \"Mirach\"], [\"Mirach\", \"Almach\"],\n  // Pegasus\n  [\"Markab\", \"Scheat\"], [\"Scheat\", \"Alpheratz\"], [\"Alpheratz\", \"Algenib\"], [\"Algenib\", \"Markab\"],\n  [\"Markab\", \"Enif\"],\n  // Triangulum\n  [\"BetTri\", \"AlpTri\"], [\"AlpTri\", \"GamTri\"], [\"GamTri\", \"BetTri\"],\n  // Aries\n  [\"Hamal\", \"Sheratan\"], [\"Sheratan\", \"Mesarthim\"],\n  // Gemini\n  [\"Castor\", \"Pollux\"], [\"Pollux\", \"Wasat\"], [\"Wasat\", \"Alhena\"],\n  [\"Castor\", \"Mebsuta\"], [\"Mebsuta\", \"MuGem\"],\n  // Leo\n  [\"Regulus\", \"EtaLeo\"], [\"EtaLeo\", \"Algieba\"], [\"Algieba\", \"EpsLeo\"],\n  [\"Regulus\", \"ThetLeo\"], [\"ThetLeo\", \"Zosma\"], [\"Zosma\", \"Denebola\"], [\"Denebola\", \"ThetLeo\"],\n  [\"Zosma\", \"Algieba\"],\n  // Canes Venatici\n  [\"CorCaroli\", \"BetCVn\"],\n  // Taurus\n  [\"LamTau\", \"GamTau\"], [\"GamTau\", \"EpsTau\"], [\"EpsTau\", \"Aldebaran\"], [\"Aldebaran\", \"ZetTau\"],\n  [\"EpsTau\", \"Elnath\"],\n  // Orion\n  [\"Betelgeuse\", \"Bellatrix\"], [\"Betelgeuse\", \"Alnitak\"], [\"Bellatrix\", \"Mintaka\"],\n  [\"Mintaka\", \"Alnilam\"], [\"Alnilam\", \"Alnitak\"], [\"Alnitak\", \"Saiph\"], [\"Saiph\", \"Rigel\"],\n  [\"Rigel\", \"Mintaka\"],\n  // Canis Major\n  [\"Sirius\", \"Mirzam\"], [\"Sirius\", \"Wezen\"], [\"Wezen\", \"Adhara\"], [\"Wezen\", \"Aludra\"],\n];\n\n// Display names placed near each constellation's centroid.\nconst CON_NAMES = {\n  UMa: \"Ursa Major\", UMi: \"Ursa Minor\", Cas: \"Cassiopeia\", Cep: \"Cepheus\",\n  Cyg: \"Cygnus\", Lyr: \"Lyra\", Aql: \"Aquila\", Boo: \"Boötes\", CrB: \"Corona Borealis\",\n  Her: \"Hercules\", Dra: \"Draco\", Per: \"Perseus\", Aur: \"Auriga\", And: \"Andromeda\",\n  Peg: \"Pegasus\", Tri: \"Triangulum\", Ari: \"Aries\", Gem: \"Gemini\", Leo: \"Leo\",\n  CVn: \"Canes Venatici\", Tau: \"Taurus\", Ori: \"Orion\", CMa: \"Canis Major\",\n};\n\n// --- Background field of faint stars (deterministic LCG, no network) ---------\nlet _seed = 1337;\nfunction rand() {\n  _seed = (_seed * 1103515245 + 12345) & 0x7fffffff;\n  return _seed / 0x7fffffff;\n}\nconst FIELD = [];\nfor (let i = 0; i < 230; i += 1) {\n  const rr = Math.sqrt(rand()) * 0.99; // sqrt → uniform across the disk area\n  const theta = rand() * 2 * Math.PI;\n  FIELD.push({\n    name: `field-${i}`,\n    x: rr * Math.sin(theta),\n    y: rr * Math.cos(theta),\n    mag: 3.6 + rand() * 2.8, // faint background population\n    con: \"\",\n  });\n}\n\n// --- Assemble every star with projected coordinates --------------------------\nconst STARS = CATALOG.map((s) => ({ ...s, ...project(s.ra, s.dec) })).concat(FIELD);\nconst STAR_BY_NAME = {};\nSTARS.forEach((s) => { STAR_BY_NAME[s.name] = s; });\n\n// --- Brightness → colour (Imprint sequential) and → marker radius ------------\nfunction lerpHex(a, b, f) {\n  const ca = [parseInt(a.slice(1, 3), 16), parseInt(a.slice(3, 5), 16), parseInt(a.slice(5, 7), 16)];\n  const cb = [parseInt(b.slice(1, 3), 16), parseInt(b.slice(3, 5), 16), parseInt(b.slice(5, 7), 16)];\n  const mix = ca.map((c, i) => Math.round(c + (cb[i] - c) * f));\n  return `#${mix.map((c) => c.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n// Group stars into integer magnitude bands so each band is one scatter series\n// (MUI X sizes/colours markers per series). Brighter band = larger, greener.\nconst MAG_LO = -1;\nconst MAG_HI = 6;\nconst bands = new Map();\nSTARS.forEach((s) => {\n  const g = Math.max(MAG_LO, Math.min(MAG_HI, Math.round(s.mag)));\n  if (!bands.has(g)) bands.set(g, []);\n  bands.get(g).push({ x: s.x, y: s.y, id: s.name });\n});\n// Fainter bands first so the brightest stars paint on top.\nconst SERIES = [...bands.keys()].sort((a, b) => b - a).map((g) => {\n  const f = (g - MAG_LO) / (MAG_HI - MAG_LO);\n  return {\n    type: \"scatter\",\n    data: bands.get(g),\n    markerSize: 11 - f * 9, // radius in CSS px: ~11 (bright) → ~2 (faint)\n    color: lerpHex(SEQ[0], SEQ[1], f),\n    disableHover: true,\n  };\n});\n\n// --- Custom SVG layers (read the chart scales via hooks) ---------------------\nconst GRID = \"rgba(120,140,170,0.30)\"; // faint cool grid, reads on both themes\nconst RIM = t.inkSoft;\nconst LINE = t.inkSoft; // constellation sticks — subtle chrome\n\nfunction SkyGrid() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const cx = xScale(0);\n  const cy = yScale(0);\n  const rOf = (rr) => Math.abs(xScale(rr) - xScale(0));\n  // Declination circles + their labels (+60°, +30°, 0°, -30°).\n  const decCircles = [60, 30, 0, -30].map((dec) => {\n    const rr = (90 - dec) / R_MAX;\n    const p = project(20.0, dec); // label sits along the 20h spoke\n    return (\n      <g key={`dec-${dec}`}>\n        <circle cx={cx} cy={cy} r={rOf(rr)} fill=\"none\" stroke={GRID} strokeWidth={1} />\n        <text\n          x={xScale(p.x)}\n          y={yScale(p.y)}\n          fill={t.inkSoft}\n          fontSize={12}\n          textAnchor=\"middle\"\n          opacity={0.85}\n        >\n          {dec > 0 ? `+${dec}°` : `${dec}°`}\n        </text>\n      </g>\n    );\n  });\n  // Right-ascension spokes every 2h + hour labels just outside the rim.\n  const rim = (90 - DEC_MIN) / R_MAX; // = 1.0\n  const spokes = [];\n  for (let h = 0; h < 24; h += 2) {\n    const edge = project(h, DEC_MIN);\n    const lab = { x: 1.08 * Math.sin((h / 24) * 2 * Math.PI), y: 1.08 * Math.cos((h / 24) * 2 * Math.PI) };\n    spokes.push(\n      <g key={`ra-${h}`}>\n        <line x1={cx} y1={cy} x2={xScale(edge.x)} y2={yScale(edge.y)} stroke={GRID} strokeWidth={0.8} />\n        <text\n          x={xScale(lab.x)}\n          y={yScale(lab.y)}\n          fill={t.inkSoft}\n          fontSize={13}\n          textAnchor=\"middle\"\n          dominantBaseline=\"middle\"\n          opacity={0.9}\n        >\n          {h}h\n        </text>\n      </g>,\n    );\n  }\n  return (\n    <g>\n      {spokes}\n      {decCircles}\n      <circle cx={cx} cy={cy} r={rOf(rim)} fill=\"none\" stroke={RIM} strokeWidth={1.8} opacity={0.6} />\n    </g>\n  );\n}\n\nfunction ConstellationLines() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  return (\n    <g stroke={LINE} strokeWidth={1.1} strokeOpacity={0.42} strokeLinecap=\"round\">\n      {EDGES.map(([a, b], i) => {\n        const pa = STAR_BY_NAME[a];\n        const pb = STAR_BY_NAME[b];\n        if (!pa || !pb) return null;\n        return (\n          <line key={i} x1={xScale(pa.x)} y1={yScale(pa.y)} x2={xScale(pb.x)} y2={yScale(pb.y)} />\n        );\n      })}\n    </g>\n  );\n}\n\nfunction ConstellationLabels() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const groups = {};\n  CATALOG.forEach((s) => {\n    (groups[s.con] = groups[s.con] || []).push(STAR_BY_NAME[s.name]);\n  });\n  return (\n    <g>\n      {Object.entries(groups).map(([con, list]) => {\n        const mx = list.reduce((acc, s) => acc + s.x, 0) / list.length;\n        const my = list.reduce((acc, s) => acc + s.y, 0) / list.length;\n        return (\n          <text\n            key={con}\n            x={xScale(mx)}\n            y={yScale(my)}\n            fill={t.inkSoft}\n            fontSize={13}\n            fontStyle=\"italic\"\n            textAnchor=\"middle\"\n            opacity={0.78}\n          >\n            {CON_NAMES[con]}\n          </text>\n        );\n      })}\n    </g>\n  );\n}\n\nfunction ChartTitle() {\n  const { left, top, width } = useDrawingArea();\n  return (\n    <text\n      x={left + width / 2}\n      y={top - 42}\n      fill={t.ink}\n      fontSize={22}\n      fontWeight={600}\n      textAnchor=\"middle\"\n    >\n      star-chart-constellation · javascript · muix · anyplot.ai\n    </text>\n  );\n}\n\nfunction MagnitudeLegend() {\n  const { left, top, height } = useDrawingArea();\n  const x0 = left + 14;\n  const y0 = top + height - 96;\n  const samples = [-1, 1, 3, 5];\n  return (\n    <g>\n      <text x={x0} y={y0 - 14} fill={t.inkSoft} fontSize={13} fontWeight={600}>\n        Apparent magnitude\n      </text>\n      {samples.map((mag, i) => {\n        const f = (mag - MAG_LO) / (MAG_HI - MAG_LO);\n        const cy = y0 + i * 23;\n        return (\n          <g key={mag}>\n            <circle cx={x0 + 8} cy={cy} r={11 - f * 9} fill={lerpHex(SEQ[0], SEQ[1], f)} />\n            <text x={x0 + 30} y={cy + 4} fill={t.inkSoft} fontSize={12}>\n              {`mag ${mag > 0 ? \"+\" : \"\"}${mag}${mag === -1 ? \"  (brightest)\" : mag === 5 ? \"  (faintest)\" : \"\"}`}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) --------------\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={SIZE.width}\n      height={SIZE.height}\n      series={SERIES}\n      margin={{ top: 96, bottom: 56, left: 76, right: 76 }}\n      xAxis={[{ id: \"x\", min: -1.12, max: 1.12 }]}\n      yAxis={[{ id: \"y\", min: -1.12, max: 1.12 }]}\n      skipAnimation\n    >\n      <SkyGrid />\n      <ConstellationLines />\n      <ScatterPlot />\n      <ConstellationLabels />\n      <ChartTitle />\n      <MagnitudeLegend />\n    </ChartContainer>\n  );\n}\n"}