{"spec_id":"star-chart-constellation","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// star-chart-constellation: Star Chart with Constellations\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 93/100 | Created: 2026-06-17\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME;\n\n// --- Theme-adaptive chrome --------------------------------------------------\nconst INK = t.ink;\nconst INK_SOFT = t.inkSoft;\nconst GRID = t.grid;\nconst LINE_COL = THEME === \"light\" ? \"rgba(74,74,68,0.45)\" : \"rgba(184,183,176,0.45)\";\n\n// --- Named constellation stars: [RA hours, Dec degrees, apparent magnitude] -\n// Real bright northern-sky stars (J2000). The northern celestial hemisphere is\n// projected onto a circular azimuthal-equidistant sky map below.\nconst namedStars = {\n  // Ursa Major (Big Dipper)\n  Dubhe: [11.062, 61.75, 1.79], Merak: [11.030, 56.38, 2.37],\n  Phecda: [11.897, 53.69, 2.44], Megrez: [12.257, 57.03, 3.31],\n  Alioth: [12.900, 55.96, 1.77], Mizar: [13.399, 54.93, 2.27],\n  Alkaid: [13.792, 49.31, 1.86],\n  // Ursa Minor (Little Dipper)\n  Polaris: [2.530, 89.26, 1.98], Yildun: [17.537, 86.59, 4.36],\n  EpsUMi: [16.766, 82.04, 4.21], ZetaUMi: [15.734, 77.79, 4.32],\n  Kochab: [14.845, 74.16, 2.08], Pherkad: [15.345, 71.83, 3.05],\n  // Cassiopeia (the W)\n  Segin: [1.906, 63.67, 3.35], Ruchbah: [1.430, 60.24, 2.68],\n  Navi: [0.945, 60.72, 2.47], Schedar: [0.675, 56.54, 2.24],\n  Caph: [0.153, 59.15, 2.28],\n  // Cepheus (house)\n  Alderamin: [21.310, 62.59, 2.45], Alfirk: [21.477, 70.56, 3.23],\n  Errai: [23.656, 77.63, 3.21], CepIota: [22.828, 66.20, 3.52],\n  CepZeta: [22.181, 58.20, 3.35],\n  // Cygnus (Northern Cross)\n  Deneb: [20.690, 45.28, 1.25], Sadr: [20.370, 40.26, 2.23],\n  Gienah: [20.770, 33.97, 2.48], CygDelta: [19.749, 45.13, 2.87],\n  Albireo: [19.512, 27.96, 3.05],\n  // Lyra\n  Vega: [18.615, 38.78, 0.03], Sheliak: [18.835, 33.36, 3.52],\n  Sulafat: [18.982, 32.69, 3.25], LyrZeta: [18.746, 37.60, 4.36],\n  // Bootes (kite)\n  Arcturus: [14.261, 19.18, -0.05], Izar: [14.750, 27.07, 2.35],\n  Seginus: [14.534, 38.31, 3.03], Nekkar: [15.032, 40.39, 3.49],\n  BooDelta: [15.258, 33.31, 3.46], BooRho: [14.532, 30.37, 3.58],\n  // Corona Borealis (arc)\n  Alphecca: [15.578, 26.71, 2.22], CrBBeta: [15.464, 29.11, 3.66],\n  CrBTheta: [15.553, 31.36, 4.14], CrBGamma: [15.713, 26.30, 3.81],\n  CrBDelta: [15.825, 26.07, 4.59],\n  // Gemini\n  Pollux: [7.755, 28.03, 1.14], Castor: [7.577, 31.89, 1.58],\n  Alhena: [6.629, 16.40, 1.93], Wasat: [7.335, 21.98, 3.53],\n  Mebsuta: [6.382, 25.13, 3.06], Tejat: [6.383, 22.51, 2.87],\n  // Auriga (pentagon)\n  Capella: [5.278, 45.998, 0.08], Menkalinan: [5.992, 44.95, 1.90],\n  AurTheta: [5.995, 37.21, 2.62], ElNath: [5.438, 28.61, 1.65],\n  AurIota: [4.950, 33.17, 2.69],\n  // Leo\n  Regulus: [10.139, 11.97, 1.40], Denebola: [11.818, 14.57, 2.14],\n  Algieba: [10.333, 19.84, 2.08], Zosma: [11.235, 20.52, 2.56],\n  LeoTheta: [11.237, 15.43, 3.34], LeoEps: [9.879, 23.77, 2.98],\n  LeoEta: [10.122, 16.76, 3.48],\n  // Andromeda\n  Alpheratz: [0.140, 29.09, 2.06], Mirach: [1.162, 35.62, 2.05],\n  Almach: [2.065, 42.33, 2.10], AndDelta: [0.655, 30.86, 3.27],\n  // Perseus\n  Mirfak: [3.405, 49.86, 1.79], Algol: [3.136, 40.96, 2.12],\n  PerZeta: [3.902, 31.88, 2.85], PerEps: [3.964, 40.01, 2.89],\n  PerGamma: [3.080, 53.51, 2.91], PerDelta: [3.715, 47.79, 3.01],\n  // Draco (head + body)\n  Eltanin: [17.943, 51.49, 2.36], Rastaban: [17.507, 52.30, 2.79],\n  Grumium: [17.892, 56.87, 3.73], DraNu: [17.690, 55.18, 4.88],\n  Altais: [19.209, 67.66, 3.07], DraZeta: [17.146, 65.71, 3.17],\n  Edasich: [15.415, 58.97, 3.29], DraEta: [16.400, 61.51, 2.73],\n  // Triangulum\n  TriBeta: [2.159, 34.99, 3.00], TriGamma: [2.288, 33.85, 4.01],\n  TriAlpha: [1.884, 29.58, 3.41],\n  // Aries\n  Hamal: [2.119, 23.46, 2.00], Sheratan: [1.911, 20.81, 2.64],\n  Mesarthim: [1.892, 19.29, 3.86],\n  // Pegasus (great square, with shared Alpheratz)\n  Markab: [23.079, 15.21, 2.48], Scheat: [23.063, 28.08, 2.42],\n  Algenib: [0.220, 15.18, 2.83],\n  // Hercules (keystone)\n  HerZeta: [16.688, 31.60, 2.81], HerEta: [16.715, 38.92, 3.48],\n  HerPi: [17.251, 36.81, 3.16], HerEps: [17.005, 30.93, 3.92],\n  // Canes Venatici\n  CorCaroli: [12.934, 38.32, 2.89], Chara: [12.563, 41.36, 4.24],\n  // Delphinus\n  Sualocin: [20.660, 15.91, 3.77], Rotanev: [20.626, 14.60, 3.64],\n  DelGamma: [20.776, 16.12, 4.27], DelDelta: [20.725, 15.07, 4.43],\n};\n\n// Constellation stick-figures: name + edges between named stars.\nconst constellations = [\n  { name: \"Ursa Major\", edges: [[\"Dubhe\", \"Merak\"], [\"Merak\", \"Phecda\"], [\"Phecda\", \"Megrez\"], [\"Megrez\", \"Dubhe\"], [\"Megrez\", \"Alioth\"], [\"Alioth\", \"Mizar\"], [\"Mizar\", \"Alkaid\"]] },\n  { name: \"Ursa Minor\", edges: [[\"Polaris\", \"Yildun\"], [\"Yildun\", \"EpsUMi\"], [\"EpsUMi\", \"ZetaUMi\"], [\"ZetaUMi\", \"Kochab\"], [\"Kochab\", \"Pherkad\"], [\"Pherkad\", \"EpsUMi\"]] },\n  { name: \"Cassiopeia\", edges: [[\"Segin\", \"Ruchbah\"], [\"Ruchbah\", \"Navi\"], [\"Navi\", \"Schedar\"], [\"Schedar\", \"Caph\"]] },\n  { name: \"Cepheus\", edges: [[\"Alderamin\", \"Alfirk\"], [\"Alfirk\", \"Errai\"], [\"Errai\", \"CepIota\"], [\"CepIota\", \"CepZeta\"], [\"CepZeta\", \"Alderamin\"]] },\n  { name: \"Cygnus\", edges: [[\"Deneb\", \"Sadr\"], [\"Sadr\", \"Albireo\"], [\"CygDelta\", \"Sadr\"], [\"Sadr\", \"Gienah\"]] },\n  { name: \"Lyra\", edges: [[\"Vega\", \"LyrZeta\"], [\"LyrZeta\", \"Sulafat\"], [\"Sulafat\", \"Sheliak\"], [\"Sheliak\", \"LyrZeta\"]] },\n  { name: \"Bootes\", edges: [[\"Arcturus\", \"Izar\"], [\"Izar\", \"Nekkar\"], [\"Nekkar\", \"Seginus\"], [\"Seginus\", \"BooRho\"], [\"BooRho\", \"Arcturus\"], [\"Izar\", \"BooDelta\"]] },\n  { name: \"Corona Borealis\", edges: [[\"CrBTheta\", \"CrBBeta\"], [\"CrBBeta\", \"Alphecca\"], [\"Alphecca\", \"CrBGamma\"], [\"CrBGamma\", \"CrBDelta\"]] },\n  { name: \"Gemini\", edges: [[\"Pollux\", \"Castor\"], [\"Pollux\", \"Wasat\"], [\"Wasat\", \"Alhena\"], [\"Castor\", \"Mebsuta\"], [\"Mebsuta\", \"Tejat\"]] },\n  { name: \"Auriga\", edges: [[\"Capella\", \"Menkalinan\"], [\"Menkalinan\", \"AurTheta\"], [\"AurTheta\", \"ElNath\"], [\"ElNath\", \"AurIota\"], [\"AurIota\", \"Capella\"]] },\n  { name: \"Leo\", edges: [[\"Regulus\", \"LeoEta\"], [\"LeoEta\", \"Algieba\"], [\"Algieba\", \"LeoEps\"], [\"Regulus\", \"LeoTheta\"], [\"LeoTheta\", \"Zosma\"], [\"Zosma\", \"Algieba\"], [\"Zosma\", \"Denebola\"]] },\n  { name: \"Andromeda\", edges: [[\"Alpheratz\", \"AndDelta\"], [\"AndDelta\", \"Mirach\"], [\"Mirach\", \"Almach\"]] },\n  { name: \"Perseus\", edges: [[\"Mirfak\", \"PerGamma\"], [\"Mirfak\", \"PerDelta\"], [\"PerDelta\", \"PerEps\"], [\"PerEps\", \"PerZeta\"], [\"Mirfak\", \"Algol\"]] },\n  { name: \"Draco\", edges: [[\"Eltanin\", \"Rastaban\"], [\"Rastaban\", \"Grumium\"], [\"Grumium\", \"DraNu\"], [\"DraNu\", \"Eltanin\"], [\"Eltanin\", \"Altais\"], [\"Altais\", \"DraZeta\"], [\"DraZeta\", \"DraEta\"], [\"DraEta\", \"Edasich\"]] },\n  { name: \"Triangulum\", edges: [[\"TriAlpha\", \"TriBeta\"], [\"TriBeta\", \"TriGamma\"], [\"TriGamma\", \"TriAlpha\"]] },\n  { name: \"Aries\", edges: [[\"Hamal\", \"Sheratan\"], [\"Sheratan\", \"Mesarthim\"]] },\n  { name: \"Pegasus\", edges: [[\"Markab\", \"Scheat\"], [\"Scheat\", \"Alpheratz\"], [\"Alpheratz\", \"Algenib\"], [\"Algenib\", \"Markab\"]] },\n  { name: \"Hercules\", edges: [[\"HerZeta\", \"HerEta\"], [\"HerEta\", \"HerPi\"], [\"HerPi\", \"HerEps\"], [\"HerEps\", \"HerZeta\"]] },\n  { name: \"Canes Venatici\", edges: [[\"CorCaroli\", \"Chara\"]] },\n  { name: \"Delphinus\", edges: [[\"Sualocin\", \"DelGamma\"], [\"DelGamma\", \"DelDelta\"], [\"DelDelta\", \"Rotanev\"], [\"Rotanev\", \"Sualocin\"]] },\n];\n\n// --- Projection: azimuthal equidistant from the north celestial pole --------\n// Radius = angular distance from pole (90 - dec); azimuth = right ascension.\n// Disk boundary sits at the celestial equator (dec = 0, r = 90).\nconst project = (raHours, decDeg) => {\n  const r = 90 - decDeg;\n  const ang = (raHours / 24) * 2 * Math.PI;\n  return { x: r * Math.sin(ang), y: r * Math.cos(ang) };\n};\n\n// --- Magnitude → continuous Imprint colour + point size ---------------------\n// Continuous data (brightness), so use the Imprint sequential ramp: the\n// brightest stars anchor at #009E73 (palette position 1), faint stars → blue.\nconst MAG_MIN = -0.1;\nconst MAG_MAX = 6.5;\nconst hexToRgb = (h) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16));\nconst seqA = hexToRgb(t.seq[0]);\nconst seqB = hexToRgb(t.seq[1]);\nconst magColor = (mag) => {\n  const f = Math.min(1, Math.max(0, (mag - MAG_MIN) / (MAG_MAX - MAG_MIN)));\n  const c = seqA.map((a, i) => Math.round(a + (seqB[i] - a) * f));\n  return `rgb(${c[0]},${c[1]},${c[2]})`;\n};\nconst magRadius = (mag) => 1.5 + (MAG_MAX - mag) * 1.45;\n\n// --- Build the star point set -----------------------------------------------\nconst points = [];\nconst radii = [];\nconst colors = [];\nconst projected = {}; // name → {x, y} for the line/label plugin\n\nfor (const [name, [ra, dec, mag]] of Object.entries(namedStars)) {\n  const p = project(ra, dec);\n  projected[name] = p;\n  points.push(p);\n  radii.push(magRadius(mag));\n  colors.push(magColor(mag));\n}\n\n// Deterministic faint field stars (fixed-seed LCG) for an even, areal sky fill.\nlet seed = 20260617;\nconst rng = () => {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n};\nfor (let i = 0; i < 240; i++) {\n  const r = 90 * Math.sqrt(rng()); // uniform over the disk area\n  const ang = rng() * 2 * Math.PI;\n  const mag = 3.6 + rng() * 2.7;\n  points.push({ x: r * Math.sin(ang), y: r * Math.cos(ang) });\n  radii.push(magRadius(mag));\n  colors.push(magColor(mag));\n}\n\n// --- Plugin: RA/Dec grid, constellation lines, labels, magnitude key --------\nconst skyPlugin = {\n  id: \"skyChart\",\n  beforeDatasetsDraw(chart) {\n    const { ctx, scales: { x, y } } = chart;\n    const toPx = (X, Y) => [x.getPixelForValue(X), y.getPixelForValue(Y)];\n\n    // Dec circles (dec = 0, 30, 60) sampled through the projection so they stay\n    // aligned with the star coordinates regardless of plot-area aspect.\n    ctx.save();\n    ctx.strokeStyle = GRID;\n    ctx.lineWidth = 1;\n    for (const decRing of [0, 30, 60]) {\n      const rad = 90 - decRing;\n      ctx.lineWidth = decRing === 0 ? 1.8 : 1;\n      ctx.beginPath();\n      for (let k = 0; k <= 120; k++) {\n        const a = (k / 120) * 2 * Math.PI;\n        const [px, py] = toPx(rad * Math.sin(a), rad * Math.cos(a));\n        k === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);\n      }\n      ctx.closePath();\n      ctx.stroke();\n    }\n\n    // RA spokes every 2h from centre to the equator boundary.\n    ctx.lineWidth = 1;\n    const [cx, cy] = toPx(0, 0);\n    for (let h = 0; h < 24; h += 2) {\n      const a = (h / 24) * 2 * Math.PI;\n      const [ex, ey] = toPx(90 * Math.sin(a), 90 * Math.cos(a));\n      ctx.beginPath();\n      ctx.moveTo(cx, cy);\n      ctx.lineTo(ex, ey);\n      ctx.stroke();\n    }\n    ctx.restore();\n\n    // Constellation stick-figure lines (thin, semi-transparent).\n    ctx.save();\n    ctx.strokeStyle = LINE_COL;\n    ctx.lineWidth = 1.6;\n    ctx.lineCap = \"round\";\n    for (const c of constellations) {\n      for (const [a, b] of c.edges) {\n        const [ax, ay] = toPx(projected[a].x, projected[a].y);\n        const [bx, by] = toPx(projected[b].x, projected[b].y);\n        ctx.beginPath();\n        ctx.moveTo(ax, ay);\n        ctx.lineTo(bx, by);\n        ctx.stroke();\n      }\n    }\n    ctx.restore();\n  },\n\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea, scales: { x, y } } = chart;\n    const toPx = (X, Y) => [x.getPixelForValue(X), y.getPixelForValue(Y)];\n\n    // RA labels (hours) just outside the boundary.\n    ctx.save();\n    ctx.fillStyle = INK_SOFT;\n    ctx.font = \"600 13px -apple-system, Segoe UI, Roboto, sans-serif\";\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    for (const h of [0, 3, 6, 9, 12, 15, 18, 21]) {\n      const a = (h / 24) * 2 * Math.PI;\n      const [px, py] = toPx(98 * Math.sin(a), 98 * Math.cos(a));\n      ctx.fillText(`${h}h`, px, py);\n    }\n\n    // Dec ring labels along the 0h spoke (top); skip the boundary to avoid\n    // colliding with the 0h RA label.\n    ctx.font = \"12px -apple-system, Segoe UI, Roboto, sans-serif\";\n    ctx.textAlign = \"left\";\n    for (const decRing of [30, 60]) {\n      const [px, py] = toPx(0, 90 - decRing);\n      ctx.fillText(`+${decRing}°`, px + 5, py - 8);\n    }\n    ctx.restore();\n\n    // Constellation name labels near each group's centroid.\n    ctx.save();\n    ctx.fillStyle = INK;\n    ctx.font = \"italic 600 13px -apple-system, Segoe UI, Roboto, sans-serif\";\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    for (const c of constellations) {\n      const names = [...new Set(c.edges.flat())];\n      let sx = 0, sy = 0;\n      for (const n of names) { sx += projected[n].x; sy += projected[n].y; }\n      const [px, py] = toPx(sx / names.length, sy / names.length);\n      ctx.fillText(c.name, px, py);\n    }\n    ctx.restore();\n\n    // Magnitude size key (bottom-left): a legend, not a fake control.\n    ctx.save();\n    const keyX = chartArea.left + 20;\n    let keyY = chartArea.bottom - 96;\n    ctx.fillStyle = INK;\n    ctx.font = \"600 14px -apple-system, Segoe UI, Roboto, sans-serif\";\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"middle\";\n    ctx.fillText(\"Apparent magnitude\", keyX, keyY);\n    keyY += 26;\n    ctx.font = \"12px -apple-system, Segoe UI, Roboto, sans-serif\";\n    for (const mag of [0, 2, 4]) {\n      ctx.beginPath();\n      ctx.fillStyle = magColor(mag);\n      ctx.arc(keyX + 8, keyY, magRadius(mag), 0, 2 * Math.PI);\n      ctx.fill();\n      ctx.fillStyle = INK_SOFT;\n      const note = mag === 0 ? \" · bright\" : mag === 4 ? \" · faint\" : \"\";\n      ctx.fillText(`mag ${mag}${note}`, keyX + 30, keyY);\n      keyY += 24;\n    }\n    ctx.restore();\n  },\n};\n\n// --- Mount ------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [{\n      label: \"Stars\",\n      data: points,\n      pointRadius: radii,\n      pointBackgroundColor: colors,\n      pointBorderWidth: 0,\n      pointHoverRadius: radii,\n    }],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: 12 },\n    plugins: {\n      title: {\n        display: true,\n        text: \"star-chart-constellation · javascript · chartjs · anyplot.ai\",\n        color: INK,\n        font: { size: 22, weight: \"600\" },\n        padding: { top: 4, bottom: 14 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: { type: \"linear\", min: -98, max: 98, display: false },\n      y: { type: \"linear\", min: -98, max: 98, display: false },\n    },\n  },\n  plugins: [skyPlugin],\n});\n"}