{"spec_id":"star-chart-constellation","library":"d3","language":"javascript","code":"// anyplot.ai\n// star-chart-constellation: Star Chart with Constellations\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 93/100 | Created: 2026-06-17\n//# anyplot-orientation: square\n// anyplot.ai\n// star-chart-constellation: Star Chart with Constellations\n// Library: d3 7.9.0 | JavaScript 22\n// Quality: pending | Created: 2026-06-17\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// The night sky is a constant dark sphere on BOTH themes (the spec requires a\n// dark navy/black field). Chrome drawn ON the sphere therefore uses constant\n// pale tones for legibility; only the page-surface title flips with the theme.\nconst SKY_MID = \"#16223A\"; // sphere centre (radial gradient)\nconst SKY = \"#0E1726\"; // sphere body\nconst SKY_EDGE = \"#090F1C\"; // sphere rim\nconst GRID_LINE = \"rgba(238,235,222,0.16)\"; // RA/Dec graticule\nconst GRID_TXT = \"rgba(238,235,222,0.50)\"; // dec tick labels (inside the sphere)\nconst PAGE_TXT = t.inkSoft; // RA tick labels (outside, on the page surface)\nconst RING = \"rgba(238,235,222,0.34)\"; // horizon boundary ring\nconst CON_LABEL = \"rgba(238,235,222,0.62)\"; // constellation names\nconst STAR_LABEL = \"rgba(244,238,222,0.92)\"; // bright-star names\nconst STAR = t.amber; // pale-yellow stars (#DDCC77, Imprint anchor)\nconst LINE = t.palette[0]; // constellation stick-figures — brand green #009E73\n\n// --- Geometry: north-polar azimuthal-equidistant projection ------------------\nconst cx = width / 2;\nconst cy = height / 2 + 30;\nconst R = Math.min(width, height) / 2 - 100; // sphere radius\nconst DEC_EDGE = -10; // declination at the horizon ring\nconst decToR = (dec) => (R * (90 - dec)) / (90 - DEC_EDGE);\nconst project = (raHours, dec) => {\n  const ang = (raHours / 24) * 2 * Math.PI; // 0h points up, RA winds clockwise\n  const r = decToR(dec);\n  return [cx + r * Math.sin(ang), cy - r * Math.cos(ang)];\n};\n\n// Apparent magnitude → point radius (brighter = lower mag = larger)\nconst MAG_LIMIT = 6.5;\nconst MAG_BRIGHT = -1.5;\nconst magToRadius = (m) => {\n  const u = Math.max(0, Math.min(1, (MAG_LIMIT - m) / (MAG_LIMIT - MAG_BRIGHT)));\n  return 0.6 + 7.0 * Math.pow(u, 1.5);\n};\n\n// --- Data: 15 northern constellations [ra(h), dec(deg), mag, label?] ----------\nconst cons = [\n  { name: \"Ursa Major\",\n    stars: { 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.04],\n      alkaid: [13.792, 49.31, 1.86] },\n    edges: [[\"dubhe\", \"merak\"], [\"merak\", \"phecda\"], [\"phecda\", \"megrez\"],\n      [\"megrez\", \"dubhe\"], [\"megrez\", \"alioth\"], [\"alioth\", \"mizar\"],\n      [\"mizar\", \"alkaid\"]] },\n  { name: \"Ursa Minor\",\n    stars: { polaris: [2.530, 89.26, 1.98, \"Polaris\"], yildun: [17.537, 86.59, 4.36],\n      eps_umi: [16.766, 82.04, 4.23], zeta_umi: [15.734, 77.79, 4.32],\n      eta_umi: [16.291, 75.76, 4.95], pherkad: [15.345, 71.83, 3.00],\n      kochab: [14.845, 74.16, 2.07] },\n    edges: [[\"polaris\", \"yildun\"], [\"yildun\", \"eps_umi\"], [\"eps_umi\", \"zeta_umi\"],\n      [\"zeta_umi\", \"eta_umi\"], [\"eta_umi\", \"pherkad\"], [\"pherkad\", \"kochab\"],\n      [\"kochab\", \"zeta_umi\"]] },\n  { name: \"Cassiopeia\",\n    stars: { segin: [1.906, 63.67, 3.35], ruchbah: [1.430, 60.24, 2.68],\n      gamma_cas: [0.945, 60.72, 2.47], schedar: [0.675, 56.54, 2.24],\n      caph: [0.153, 59.15, 2.28] },\n    edges: [[\"segin\", \"ruchbah\"], [\"ruchbah\", \"gamma_cas\"],\n      [\"gamma_cas\", \"schedar\"], [\"schedar\", \"caph\"]] },\n  { name: \"Cepheus\",\n    stars: { alderamin: [21.310, 62.59, 2.45], alfirk: [21.477, 70.56, 3.23],\n      gamma_cep: [23.656, 77.63, 3.21], iota_cep: [22.828, 66.20, 3.52],\n      zeta_cep: [22.181, 58.20, 3.35] },\n    edges: [[\"alderamin\", \"alfirk\"], [\"alfirk\", \"gamma_cep\"],\n      [\"gamma_cep\", \"iota_cep\"], [\"iota_cep\", \"alderamin\"],\n      [\"alderamin\", \"zeta_cep\"]] },\n  { name: \"Cygnus\",\n    stars: { deneb: [20.690, 45.28, 1.25, \"Deneb\"], sadr: [20.370, 40.26, 2.23],\n      gienah_cyg: [20.770, 33.97, 2.48], delta_cyg: [19.749, 45.13, 2.87],\n      albireo: [19.512, 27.96, 3.18] },\n    edges: [[\"deneb\", \"sadr\"], [\"sadr\", \"albireo\"], [\"sadr\", \"gienah_cyg\"],\n      [\"sadr\", \"delta_cyg\"]] },\n  { name: \"Lyra\",\n    stars: { vega: [18.616, 38.78, 0.03, \"Vega\"], eps_lyr: [18.737, 39.67, 3.90],\n      zeta_lyr: [18.746, 37.60, 4.30], sheliak: [18.835, 33.36, 3.52],\n      sulafat: [18.982, 32.69, 3.25], delta_lyr: [18.908, 36.90, 4.30] },\n    edges: [[\"vega\", \"eps_lyr\"], [\"vega\", \"zeta_lyr\"], [\"zeta_lyr\", \"delta_lyr\"],\n      [\"delta_lyr\", \"sulafat\"], [\"sulafat\", \"sheliak\"], [\"sheliak\", \"zeta_lyr\"]] },\n  { name: \"Boötes\",\n    stars: { arcturus: [14.261, 19.18, -0.05, \"Arcturus\"], izar: [14.749, 27.07, 2.35],\n      eta_boo: [13.911, 18.40, 2.68], gamma_boo: [14.534, 38.31, 3.00],\n      delta_boo: [15.258, 33.31, 3.46], nekkar: [15.032, 40.39, 3.49] },\n    edges: [[\"arcturus\", \"eta_boo\"], [\"arcturus\", \"izar\"], [\"izar\", \"delta_boo\"],\n      [\"delta_boo\", \"nekkar\"], [\"nekkar\", \"gamma_boo\"], [\"gamma_boo\", \"arcturus\"]] },\n  { name: \"Corona Borealis\",\n    stars: { alphecca: [15.578, 26.71, 2.22], beta_crb: [15.464, 29.11, 3.66],\n      gamma_crb: [15.711, 26.30, 3.81], delta_crb: [15.825, 26.07, 4.59],\n      theta_crb: [15.548, 31.36, 4.14] },\n    edges: [[\"theta_crb\", \"beta_crb\"], [\"beta_crb\", \"alphecca\"],\n      [\"alphecca\", \"gamma_crb\"], [\"gamma_crb\", \"delta_crb\"]] },\n  { name: \"Gemini\",\n    stars: { pollux: [7.755, 28.03, 1.14, \"Pollux\"], castor: [7.577, 31.89, 1.58, \"Castor\"],\n      alhena: [6.629, 16.40, 1.93], mu_gem: [6.383, 22.51, 2.87],\n      eps_gem: [6.732, 25.13, 3.00], delta_gem: [7.335, 21.98, 3.53] },\n    edges: [[\"castor\", \"pollux\"], [\"castor\", \"eps_gem\"], [\"eps_gem\", \"mu_gem\"],\n      [\"pollux\", \"delta_gem\"], [\"delta_gem\", \"alhena\"]] },\n  { name: \"Auriga\",\n    stars: { capella: [5.278, 45.998, 0.08, \"Capella\"], menkalinan: [5.992, 44.95, 1.90],\n      theta_aur: [5.995, 37.21, 2.62], iota_aur: [4.950, 33.17, 2.69],\n      eps_aur: [5.033, 43.82, 3.00] },\n    edges: [[\"capella\", \"menkalinan\"], [\"menkalinan\", \"theta_aur\"],\n      [\"theta_aur\", \"iota_aur\"], [\"iota_aur\", \"eps_aur\"], [\"eps_aur\", \"capella\"]] },\n  { name: \"Perseus\",\n    stars: { mirfak: [3.405, 49.86, 1.79, \"Mirfak\"], algol: [3.136, 40.96, 2.12, \"Algol\"],\n      gamma_per: [3.080, 53.51, 2.93], delta_per: [3.715, 47.79, 3.01],\n      eps_per: [3.964, 40.01, 2.89], zeta_per: [3.902, 31.88, 2.85] },\n    edges: [[\"gamma_per\", \"mirfak\"], [\"mirfak\", \"delta_per\"], [\"delta_per\", \"eps_per\"],\n      [\"eps_per\", \"zeta_per\"], [\"mirfak\", \"algol\"]] },\n  { name: \"Andromeda\",\n    stars: { alpheratz: [0.140, 29.09, 2.06], delta_and: [0.655, 30.86, 3.27],\n      mirach: [1.162, 35.62, 2.05], almach: [2.065, 42.33, 2.10] },\n    edges: [[\"alpheratz\", \"delta_and\"], [\"delta_and\", \"mirach\"],\n      [\"mirach\", \"almach\"]] },\n  { name: \"Pegasus\",\n    stars: { markab: [23.079, 15.21, 2.48], scheat: [23.063, 28.08, 2.42],\n      algenib: [0.220, 15.18, 2.83] },\n    edges: [[\"markab\", \"scheat\"], [\"scheat\", \"alpheratz\"],\n      [\"alpheratz\", \"algenib\"], [\"algenib\", \"markab\"]] },\n  { name: \"Leo\",\n    stars: { regulus: [10.139, 11.97, 1.36, \"Regulus\"], denebola: [11.818, 14.57, 2.11],\n      algieba: [10.333, 19.84, 2.01], zosma: [11.235, 20.52, 2.56],\n      eps_leo: [9.764, 23.77, 2.97], theta_leo: [11.237, 15.43, 3.33],\n      eta_leo: [10.122, 16.76, 3.48] },\n    edges: [[\"regulus\", \"eta_leo\"], [\"eta_leo\", \"algieba\"], [\"algieba\", \"eps_leo\"],\n      [\"regulus\", \"theta_leo\"], [\"theta_leo\", \"denebola\"], [\"denebola\", \"zosma\"],\n      [\"zosma\", \"algieba\"]] },\n  { name: \"Triangulum\",\n    stars: { mothallah: [1.885, 29.58, 3.41], beta_tri: [2.159, 34.99, 3.00],\n      gamma_tri: [2.288, 33.85, 4.01] },\n    edges: [[\"mothallah\", \"beta_tri\"], [\"beta_tri\", \"gamma_tri\"],\n      [\"gamma_tri\", \"mothallah\"]] },\n];\n\n// Flatten constellation stars into a lookup + a render list\nconst starMap = {};\nconst namedStars = [];\nfor (const c of cons) {\n  for (const [id, s] of Object.entries(c.stars)) {\n    const [ra, dec, mag, label] = s;\n    const [x, y] = project(ra, dec);\n    starMap[id] = { x, y, mag };\n    namedStars.push({ x, y, mag, label });\n  }\n}\n\n// Constellation stick-figure segments\nconst edgeData = [];\nfor (const c of cons) {\n  for (const [a, b] of c.edges) {\n    if (starMap[a] && starMap[b]) edgeData.push([starMap[a], starMap[b]]);\n  }\n}\n\n// Constellation labels at the centroid of each star group\nconst conLabels = cons.map((c) => {\n  const pts = Object.keys(c.stars).map((id) => starMap[id]);\n  return { name: c.name, x: d3.mean(pts, (p) => p.x), y: d3.mean(pts, (p) => p.y) };\n});\n\n// --- Faint background field stars (deterministic LCG, area-uniform on disk) ---\nlet seed = 20260617;\nconst rand = () => (seed = (seed * 1664525 + 1013904223) % 4294967296) / 4294967296;\nconst fieldStars = [];\nfor (let i = 0; i < 165; i++) {\n  const ang = rand() * 2 * Math.PI;\n  const r = R * Math.sqrt(0.01 + 0.98 * rand());\n  fieldStars.push({\n    x: cx + r * Math.cos(ang),\n    y: cy + r * Math.sin(ang),\n    mag: 3.8 + rand() * 2.6,\n    twinkle: 0.4 + rand() * 0.45,\n  });\n}\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\nconst defs = svg.append(\"defs\");\nconst sky = defs.append(\"radialGradient\").attr(\"id\", \"sky\").attr(\"cx\", \"50%\").attr(\"cy\", \"42%\").attr(\"r\", \"62%\");\nsky.append(\"stop\").attr(\"offset\", \"0%\").attr(\"stop-color\", SKY_MID);\nsky.append(\"stop\").attr(\"offset\", \"62%\").attr(\"stop-color\", SKY);\nsky.append(\"stop\").attr(\"offset\", \"100%\").attr(\"stop-color\", SKY_EDGE);\n\nconst glow = defs.append(\"filter\").attr(\"id\", \"glow\")\n  .attr(\"x\", \"-150%\").attr(\"y\", \"-150%\").attr(\"width\", \"400%\").attr(\"height\", \"400%\");\nglow.append(\"feGaussianBlur\").attr(\"stdDeviation\", 3.4).attr(\"result\", \"b\");\nconst merge = glow.append(\"feMerge\");\nmerge.append(\"feMergeNode\").attr(\"in\", \"b\");\nmerge.append(\"feMergeNode\").attr(\"in\", \"SourceGraphic\");\n\nconst clip = defs.append(\"clipPath\").attr(\"id\", \"disk\");\nclip.append(\"circle\").attr(\"cx\", cx).attr(\"cy\", cy).attr(\"r\", R);\n\n// Night-sky sphere\nsvg.append(\"circle\").attr(\"cx\", cx).attr(\"cy\", cy).attr(\"r\", R).attr(\"fill\", \"url(#sky)\");\n\nconst inner = svg.append(\"g\").attr(\"clip-path\", \"url(#disk)\");\n\n// --- Coordinate grid: Dec circles + RA spokes --------------------------------\nconst decCircles = [0, 30, 60, 80];\ninner.selectAll(\"circle.dec\").data(decCircles).join(\"circle\")\n  .attr(\"cx\", cx).attr(\"cy\", cy).attr(\"r\", (d) => decToR(d))\n  .attr(\"fill\", \"none\").attr(\"stroke\", GRID_LINE).attr(\"stroke-width\", 1);\n\nconst raSpokes = d3.range(0, 24, 2);\ninner.selectAll(\"line.ra\").data(raSpokes).join(\"line\")\n  .attr(\"x1\", cx).attr(\"y1\", cy)\n  .attr(\"x2\", (h) => project(h, DEC_EDGE)[0])\n  .attr(\"y2\", (h) => project(h, DEC_EDGE)[1])\n  .attr(\"stroke\", GRID_LINE).attr(\"stroke-width\", 1);\n\n// --- Constellation stick-figure lines (brand green) --------------------------\ninner.selectAll(\"line.con\").data(edgeData).join(\"line\")\n  .attr(\"x1\", (d) => d[0].x).attr(\"y1\", (d) => d[0].y)\n  .attr(\"x2\", (d) => d[1].x).attr(\"y2\", (d) => d[1].y)\n  .attr(\"stroke\", LINE).attr(\"stroke-width\", 1.6).attr(\"stroke-opacity\", 0.55)\n  .attr(\"stroke-linecap\", \"round\");\n\n// --- Field stars (faint background) ------------------------------------------\ninner.selectAll(\"circle.field\").data(fieldStars).join(\"circle\")\n  .attr(\"cx\", (d) => d.x).attr(\"cy\", (d) => d.y)\n  .attr(\"r\", (d) => magToRadius(d.mag))\n  .attr(\"fill\", STAR).attr(\"fill-opacity\", (d) => d.twinkle);\n\n// --- Bright-star bloom + named stars -----------------------------------------\ninner.selectAll(\"circle.bloom\").data(namedStars.filter((s) => s.mag < 2.4)).join(\"circle\")\n  .attr(\"cx\", (d) => d.x).attr(\"cy\", (d) => d.y)\n  .attr(\"r\", (d) => magToRadius(d.mag) * 1.9)\n  .attr(\"fill\", STAR).attr(\"fill-opacity\", 0.35).attr(\"filter\", \"url(#glow)\");\n\ninner.selectAll(\"circle.star\").data(namedStars).join(\"circle\")\n  .attr(\"cx\", (d) => d.x).attr(\"cy\", (d) => d.y)\n  .attr(\"r\", (d) => magToRadius(d.mag))\n  .attr(\"fill\", STAR);\n\n// --- Constellation name labels (centroids) -----------------------------------\ninner.selectAll(\"text.con\").data(conLabels).join(\"text\")\n  .attr(\"x\", (d) => d.x).attr(\"y\", (d) => d.y - 14)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", CON_LABEL)\n  .style(\"font-size\", \"12px\").style(\"font-weight\", \"500\")\n  .style(\"letter-spacing\", \"1.5px\").style(\"text-transform\", \"uppercase\")\n  .text((d) => d.name);\n\n// --- Bright-star name labels --------------------------------------------------\ninner.selectAll(\"text.star\").data(namedStars.filter((s) => s.label)).join(\"text\")\n  .attr(\"x\", (d) => d.x + magToRadius(d.mag) + 6).attr(\"y\", (d) => d.y + 4)\n  .attr(\"fill\", STAR_LABEL).style(\"font-size\", \"12.5px\").style(\"font-weight\", \"500\")\n  .text((d) => d.label);\n\n// --- Horizon boundary ring ---------------------------------------------------\nsvg.append(\"circle\").attr(\"cx\", cx).attr(\"cy\", cy).attr(\"r\", R)\n  .attr(\"fill\", \"none\").attr(\"stroke\", RING).attr(\"stroke-width\", 2);\n\n// --- Graticule tick labels (outside the ring & along the meridian) -----------\nconst raLabels = svg.append(\"g\");\nraSpokes.forEach((h) => {\n  const [x, y] = project(h, DEC_EDGE);\n  const ux = (x - cx) / R;\n  const uy = (y - cy) / R;\n  raLabels.append(\"text\")\n    .attr(\"x\", cx + ux * (R + 22)).attr(\"y\", cy + uy * (R + 22) + 5)\n    .attr(\"text-anchor\", \"middle\").attr(\"fill\", PAGE_TXT)\n    .style(\"font-size\", \"13px\").text(`${h}h`);\n});\n\ndecCircles.filter((d) => d > 0).forEach((d) => {\n  svg.append(\"text\")\n    .attr(\"x\", cx + 6).attr(\"y\", cy - decToR(d) + 16)\n    .attr(\"fill\", GRID_TXT).style(\"font-size\", \"11.5px\").text(`+${d}°`);\n});\n\n// --- Title -------------------------------------------------------------------\nsvg.append(\"text\").attr(\"x\", width / 2).attr(\"y\", 46).attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"star-chart-constellation · javascript · d3 · anyplot.ai\");\n"}