{"spec_id":"scatter-hr-diagram","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-hr-diagram: Hertzsprung-Russell Diagram\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-26\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 150, right: 170, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Deterministic PRNG (fixed-seed LCG + Box-Muller) -----------------------\nlet seed = 42;\nconst rand = () => {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n};\nconst gauss = () => Math.sqrt(-2 * Math.log(rand())) * Math.cos(2 * Math.PI * rand());\nconst uniform = (lo, hi) => lo + rand() * (hi - lo);\nconst logUniform = (lo, hi) => Math.exp(uniform(Math.log(lo), Math.log(hi)));\n\n// --- Data: synthetic stellar population, one PRNG draw per star ------------\n// Main sequence: logL scales with logT around the Sun (T=5772K, L=1 Lsun).\nconst logTSun = Math.log10(5772);\nconst spectralRanges = [\n  { type: \"O\", lo: 30000, hi: 40000, count: 10 },\n  { type: \"B\", lo: 10000, hi: 30000, count: 26 },\n  { type: \"A\", lo: 7500, hi: 10000, count: 30 },\n  { type: \"F\", lo: 6000, hi: 7500, count: 36 },\n  { type: \"G\", lo: 5200, hi: 6000, count: 40 },\n  { type: \"K\", lo: 3700, hi: 5200, count: 46 },\n  { type: \"M\", lo: 2400, hi: 3700, count: 52 },\n];\n\n// Spectral type is always derived from actual temperature (never hardcoded per\n// region) so the conventional per-type color always matches the star's real temperature.\nfunction spectralTypeFromTemp(temp) {\n  if (temp >= 30000) return \"O\";\n  if (temp >= 10000) return \"B\";\n  if (temp >= 7500) return \"A\";\n  if (temp >= 6000) return \"F\";\n  if (temp >= 5200) return \"G\";\n  if (temp >= 3700) return \"K\";\n  return \"M\";\n}\n\nconst stars = [];\nfor (const range of spectralRanges) {\n  for (let i = 0; i < range.count; i++) {\n    const temperature = uniform(range.lo, range.hi);\n    const logL = 7 * (Math.log10(temperature) - logTSun) + gauss() * 0.35;\n    stars.push({\n      temperature,\n      luminosity: 10 ** logL,\n      spectralType: range.type,\n      region: \"main sequence\",\n    });\n  }\n}\nfor (let i = 0; i < 40; i++) {\n  const temperature = uniform(3400, 5000);\n  stars.push({\n    temperature,\n    luminosity: 10 ** uniform(1, 3.3),\n    spectralType: spectralTypeFromTemp(temperature),\n    region: \"red giants\",\n  });\n}\nfor (let i = 0; i < 25; i++) {\n  const temperature = logUniform(3000, 25000);\n  stars.push({\n    temperature,\n    luminosity: 10 ** uniform(4.3, 6),\n    spectralType: spectralTypeFromTemp(temperature),\n    region: \"supergiants\",\n  });\n}\nfor (let i = 0; i < 35; i++) {\n  const temperature = uniform(8000, 40000);\n  stars.push({\n    temperature,\n    luminosity: 10 ** uniform(-4, -1.5),\n    spectralType: spectralTypeFromTemp(temperature),\n    region: \"white dwarfs\",\n  });\n}\nconst sun = { temperature: 5772, luminosity: 1, spectralType: \"G\", region: \"main sequence\", name: \"Sun\" };\n\n// --- Scales -------------------------------------------------------------\n// X reversed: hot/blue stars on the left, following astrophysical convention.\nconst x = d3.scaleLog().domain([40000, 2000]).range([0, iw]).clamp(true);\nconst y = d3.scaleLog().domain([0.00005, 1500000]).range([ih, 0]).clamp(true);\n// Semantic exception (per style guide \"domain conventions\"): spectral type has a\n// widely-recognized discrete color convention, so this uses an ordinal scale with\n// those conventional hues rather than a continuous Imprint colormap.\nconst spectralColor = d3\n  .scaleOrdinal()\n  .domain([\"O\", \"B\", \"A\", \"F\", \"G\", \"K\", \"M\"])\n  .range([\"#4467A3\", \"#4467A3\", \"#EDE8DC\", \"#F2C744\", \"#F2C744\", \"#E07B39\", \"#AE3030\"]);\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Gridlines (both axes, per scatter convention) -------------------------\ng.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickValues([3000, 5000, 10000, 20000, 40000]).tickSize(-ih).tickFormat(\"\"))\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\ng.append(\"g\")\n  .call(d3.axisLeft(y).ticks(6).tickSize(-iw).tickFormat(\"\"))\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\ng.selectAll(\".domain\").remove();\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickValues([3000, 5000, 10000, 20000, 40000]).tickFormat(d3.format(\",\")));\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxis.selectAll(\"line\").remove();\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6, \",\").tickFormat(d3.format(\",\")));\nyAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nyAxis.selectAll(\"line\").remove();\nyAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// Secondary top axis: representative spectral class per temperature band.\nconst spectralTicks = [\n  { type: \"O\", temp: 35000 },\n  { type: \"B\", temp: 17000 },\n  { type: \"A\", temp: 8500 },\n  { type: \"F\", temp: 6750 },\n  { type: \"G\", temp: 5600 },\n  { type: \"K\", temp: 4450 },\n  { type: \"M\", temp: 3000 },\n];\nconst topAxis = g\n  .append(\"g\")\n  .call(\n    d3\n      .axisTop(x)\n      .tickValues(spectralTicks.map((d) => d.temp))\n      .tickFormat((d, i) => spectralTicks[i].type)\n      .tickSize(6)\n  );\ntopAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\").style(\"font-weight\", \"600\");\ntopAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\ntopAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- Axis labels --------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Surface Temperature (K)\");\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -78)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Luminosity (L / L_sun)\");\n\n// --- Region labels (spec explicitly requires labeling the four regions) ----\nconst regionLabels = [\n  { label: \"Main sequence\", temp: 9200, lum: 4500 },\n  { label: \"Red giants\", temp: 4300, lum: 2600 },\n  { label: \"Supergiants\", temp: 3300, lum: 300000 },\n  { label: \"White dwarfs\", temp: 20000, lum: 0.00025 },\n];\ng.selectAll(\".region-label\")\n  .data(regionLabels)\n  .join(\"text\")\n  .attr(\"class\", \"region-label\")\n  .attr(\"x\", (d) => x(d.temp))\n  .attr(\"y\", (d) => y(d.lum))\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-style\", \"italic\")\n  .text((d) => d.label);\n\n// --- Stars ------------------------------------------------------------------\ng.selectAll(\".star\")\n  .data(stars)\n  .join(\"circle\")\n  .attr(\"class\", \"star\")\n  .attr(\"cx\", (d) => x(d.temperature))\n  .attr(\"cy\", (d) => y(d.luminosity))\n  .attr(\"r\", 5)\n  .attr(\"fill\", (d) => spectralColor(d.spectralType))\n  .attr(\"fill-opacity\", 0.72)\n  .attr(\"stroke\", (d) => (d.spectralType === \"A\" ? t.ink : t.pageBg))\n  .attr(\"stroke-width\", (d) => (d.spectralType === \"A\" ? 1.1 : 0.6));\n\n// --- Sun marker (distinct reference point) ----------------------------------\ng.append(\"circle\")\n  .attr(\"cx\", x(sun.temperature))\n  .attr(\"cy\", y(sun.luminosity))\n  .attr(\"r\", 11)\n  .attr(\"fill\", spectralColor(sun.spectralType))\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2.5);\ng.append(\"text\")\n  .attr(\"x\", x(sun.temperature) + 18)\n  .attr(\"y\", y(sun.luminosity) + 5)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Sun\");\n\n// --- Spectral-type color legend (explains the conventional hue mapping) -----\nconst legendItems = [\n  { type: \"O\", label: \"O / B — blue\" },\n  { type: \"A\", label: \"A — white\" },\n  { type: \"F\", label: \"F / G — yellow\" },\n  { type: \"K\", label: \"K — orange\" },\n  { type: \"M\", label: \"M — red\" },\n];\nconst legendX = iw - 175;\nconst legendY = -118;\ng.append(\"text\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendY - 12)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Spectral type\");\nconst legendRows = g\n  .selectAll(\".legend-item\")\n  .data(legendItems)\n  .join(\"g\")\n  .attr(\"class\", \"legend-item\")\n  .attr(\"transform\", (d, i) => `translate(${legendX},${legendY + i * 20})`);\nlegendRows\n  .append(\"circle\")\n  .attr(\"r\", 6)\n  .attr(\"cx\", 6)\n  .attr(\"cy\", -4)\n  .attr(\"fill\", (d) => spectralColor(d.type))\n  .attr(\"stroke\", (d) => (d.type === \"A\" ? t.ink : \"none\"))\n  .attr(\"stroke-width\", 1);\nlegendRows\n  .append(\"text\")\n  .attr(\"x\", 18)\n  .attr(\"y\", 0)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text((d) => d.label);\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"scatter-hr-diagram · javascript · d3 · anyplot.ai\");\n"}