{"spec_id":"heatmap-periodic-table","library":"d3","language":"javascript","code":"// anyplot.ai\n// heatmap-periodic-table: Periodic Table Property Heatmap\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 88/100 | Created: 2026-06-15\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst THEME = window.ANYPLOT_THEME;\n\n// --- Element data: [atomic_number, symbol, period, group, electronegativity, category]\n// Categories: a=alkali, e=alkaline-earth, t=transition, p=post-transition,\n//             m=metalloid, n=nonmetal, h=halogen, g=noble-gas, l=lanthanide, c=actinide\n// period 8 = lanthanide f-block row, period 9 = actinide f-block row\nconst elements = [\n  // Period 1\n  [1,\"H\",1,1,2.20,\"n\"],[2,\"He\",1,18,null,\"g\"],\n  // Period 2\n  [3,\"Li\",2,1,0.98,\"a\"],[4,\"Be\",2,2,1.57,\"e\"],\n  [5,\"B\",2,13,2.04,\"m\"],[6,\"C\",2,14,2.55,\"n\"],[7,\"N\",2,15,3.04,\"n\"],\n  [8,\"O\",2,16,3.44,\"n\"],[9,\"F\",2,17,3.98,\"h\"],[10,\"Ne\",2,18,null,\"g\"],\n  // Period 3\n  [11,\"Na\",3,1,0.93,\"a\"],[12,\"Mg\",3,2,1.31,\"e\"],\n  [13,\"Al\",3,13,1.61,\"p\"],[14,\"Si\",3,14,1.90,\"m\"],[15,\"P\",3,15,2.19,\"n\"],\n  [16,\"S\",3,16,2.58,\"n\"],[17,\"Cl\",3,17,3.16,\"h\"],[18,\"Ar\",3,18,null,\"g\"],\n  // Period 4\n  [19,\"K\",4,1,0.82,\"a\"],[20,\"Ca\",4,2,1.00,\"e\"],\n  [21,\"Sc\",4,3,1.36,\"t\"],[22,\"Ti\",4,4,1.54,\"t\"],[23,\"V\",4,5,1.63,\"t\"],\n  [24,\"Cr\",4,6,1.66,\"t\"],[25,\"Mn\",4,7,1.55,\"t\"],[26,\"Fe\",4,8,1.83,\"t\"],\n  [27,\"Co\",4,9,1.88,\"t\"],[28,\"Ni\",4,10,1.91,\"t\"],[29,\"Cu\",4,11,1.90,\"t\"],\n  [30,\"Zn\",4,12,1.65,\"t\"],[31,\"Ga\",4,13,1.81,\"p\"],[32,\"Ge\",4,14,2.01,\"m\"],\n  [33,\"As\",4,15,2.18,\"m\"],[34,\"Se\",4,16,2.55,\"n\"],[35,\"Br\",4,17,2.96,\"h\"],\n  [36,\"Kr\",4,18,3.00,\"g\"],\n  // Period 5\n  [37,\"Rb\",5,1,0.82,\"a\"],[38,\"Sr\",5,2,0.95,\"e\"],\n  [39,\"Y\",5,3,1.22,\"t\"],[40,\"Zr\",5,4,1.33,\"t\"],[41,\"Nb\",5,5,1.60,\"t\"],\n  [42,\"Mo\",5,6,2.16,\"t\"],[43,\"Tc\",5,7,1.90,\"t\"],[44,\"Ru\",5,8,2.20,\"t\"],\n  [45,\"Rh\",5,9,2.28,\"t\"],[46,\"Pd\",5,10,2.20,\"t\"],[47,\"Ag\",5,11,1.93,\"t\"],\n  [48,\"Cd\",5,12,1.69,\"t\"],[49,\"In\",5,13,1.78,\"p\"],[50,\"Sn\",5,14,1.96,\"p\"],\n  [51,\"Sb\",5,15,2.05,\"m\"],[52,\"Te\",5,16,2.10,\"m\"],[53,\"I\",5,17,2.66,\"h\"],\n  [54,\"Xe\",5,18,2.60,\"g\"],\n  // Period 6 (main table — group 3 is lanthanide placeholder)\n  [55,\"Cs\",6,1,0.79,\"a\"],[56,\"Ba\",6,2,0.89,\"e\"],\n  [72,\"Hf\",6,4,1.30,\"t\"],[73,\"Ta\",6,5,1.50,\"t\"],[74,\"W\",6,6,2.36,\"t\"],\n  [75,\"Re\",6,7,1.90,\"t\"],[76,\"Os\",6,8,2.20,\"t\"],[77,\"Ir\",6,9,2.20,\"t\"],\n  [78,\"Pt\",6,10,2.28,\"t\"],[79,\"Au\",6,11,2.54,\"t\"],[80,\"Hg\",6,12,2.00,\"t\"],\n  [81,\"Tl\",6,13,1.62,\"p\"],[82,\"Pb\",6,14,2.33,\"p\"],[83,\"Bi\",6,15,2.02,\"p\"],\n  [84,\"Po\",6,16,2.00,\"m\"],[85,\"At\",6,17,2.20,\"h\"],[86,\"Rn\",6,18,null,\"g\"],\n  // Period 7 (main table — group 3 is actinide placeholder)\n  [87,\"Fr\",7,1,0.70,\"a\"],[88,\"Ra\",7,2,0.90,\"e\"],\n  [104,\"Rf\",7,4,null,\"t\"],[105,\"Db\",7,5,null,\"t\"],[106,\"Sg\",7,6,null,\"t\"],\n  [107,\"Bh\",7,7,null,\"t\"],[108,\"Hs\",7,8,null,\"t\"],[109,\"Mt\",7,9,null,\"t\"],\n  [110,\"Ds\",7,10,null,\"t\"],[111,\"Rg\",7,11,null,\"t\"],[112,\"Cn\",7,12,null,\"t\"],\n  [113,\"Nh\",7,13,null,\"p\"],[114,\"Fl\",7,14,null,\"p\"],[115,\"Mc\",7,15,null,\"p\"],\n  [116,\"Lv\",7,16,null,\"p\"],[117,\"Ts\",7,17,null,\"h\"],[118,\"Og\",7,18,null,\"g\"],\n  // Lanthanides (f-block row, period=8, group=position 1-15)\n  [57,\"La\",8,1,1.10,\"l\"],[58,\"Ce\",8,2,1.12,\"l\"],[59,\"Pr\",8,3,1.13,\"l\"],\n  [60,\"Nd\",8,4,1.14,\"l\"],[61,\"Pm\",8,5,1.13,\"l\"],[62,\"Sm\",8,6,1.17,\"l\"],\n  [63,\"Eu\",8,7,null,\"l\"],[64,\"Gd\",8,8,1.20,\"l\"],[65,\"Tb\",8,9,null,\"l\"],\n  [66,\"Dy\",8,10,1.22,\"l\"],[67,\"Ho\",8,11,1.23,\"l\"],[68,\"Er\",8,12,1.24,\"l\"],\n  [69,\"Tm\",8,13,1.25,\"l\"],[70,\"Yb\",8,14,null,\"l\"],[71,\"Lu\",8,15,1.27,\"l\"],\n  // Actinides (f-block row, period=9, group=position 1-15)\n  [89,\"Ac\",9,1,1.10,\"c\"],[90,\"Th\",9,2,1.30,\"c\"],[91,\"Pa\",9,3,1.50,\"c\"],\n  [92,\"U\",9,4,1.38,\"c\"],[93,\"Np\",9,5,1.36,\"c\"],[94,\"Pu\",9,6,1.28,\"c\"],\n  [95,\"Am\",9,7,1.30,\"c\"],[96,\"Cm\",9,8,1.30,\"c\"],[97,\"Bk\",9,9,1.30,\"c\"],\n  [98,\"Cf\",9,10,1.30,\"c\"],[99,\"Es\",9,11,1.30,\"c\"],[100,\"Fm\",9,12,1.30,\"c\"],\n  [101,\"Md\",9,13,1.30,\"c\"],[102,\"No\",9,14,1.30,\"c\"],[103,\"Lr\",9,15,null,\"c\"],\n];\n\n// --- Category border colors (muted accents differentiating element families)\nconst CAT_COLOR = {\n  a: \"#D4762A\",  // alkali metals: amber-orange\n  e: \"#C4A020\",  // alkaline earth: golden\n  t: \"#5080A0\",  // transition metals: steel blue\n  p: \"#708070\",  // post-transition: sage\n  m: \"#8060A0\",  // metalloids: violet\n  n: \"#308878\",  // nonmetals: teal-green\n  h: \"#9050B0\",  // halogens: purple\n  g: \"#909090\",  // noble gases: gray\n  l: \"#B06040\",  // lanthanides: terra cotta\n  c: \"#A08030\",  // actinides: bronze\n};\n\n// --- Layout (landscape 1600×900 CSS → 3200×1800 PNG at dpr=2)\nconst TILE = 70;\nconst INNER = 68;\nconst MAIN_LEFT = (width - 18 * TILE) / 2;   // (1600-1260)/2 = 170\nconst MAIN_TOP = 72;\nconst FB_LEFT = MAIN_LEFT + 2 * TILE;          // f-block aligns under group-3 placeholder\nconst FB_TOP = MAIN_TOP + 7 * TILE + 30;\nconst CB_TOP = FB_TOP + 2 * TILE + 24;\nconst CB_H = 20;\nconst CB_W = 18 * TILE;\n\n// --- Color scale: imprint_seq (green → blue) for Pauling electronegativity\nconst knownVals = elements.filter(d => d[4] !== null).map(d => d[4]);\nconst valMin = Math.min(...knownVals);  // Fr: 0.70\nconst valMax = Math.max(...knownVals);  // F:  3.98\nconst colorScale = d3.scaleSequential()\n  .domain([valMin, valMax])\n  .interpolator(d3.interpolateRgbBasis(t.seq));\n\n// --- SVG\nconst svg = d3.select(\"#container\")\n  .append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// --- Colorbar linearGradient definition (idiomatic SVG — single rect, no rect-per-step)\nconst defs = svg.append(\"defs\");\nconst grad = defs.append(\"linearGradient\")\n  .attr(\"id\", \"cb-grad\")\n  .attr(\"x1\", \"0%\").attr(\"x2\", \"100%\")\n  .attr(\"y1\", \"0%\").attr(\"y2\", \"0%\");\nd3.range(11).forEach(i => {\n  grad.append(\"stop\")\n    .attr(\"offset\", `${i * 10}%`)\n    .attr(\"stop-color\", colorScale(valMin + (i / 10) * (valMax - valMin)));\n});\n\n// --- Title\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 45)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .style(\"font-family\", \"system-ui, sans-serif\")\n  .text(\"heatmap-periodic-table · javascript · d3 · anyplot.ai\");\n\n// --- Luminance-based text color for tile contrast (handles hex and rgb() strings)\nfunction textOnFill(c) {\n  let r, g, b;\n  if (c[0] === \"#\") {\n    r = parseInt(c.slice(1, 3), 16);\n    g = parseInt(c.slice(3, 5), 16);\n    b = parseInt(c.slice(5, 7), 16);\n  } else {\n    [r, g, b] = c.match(/\\d+/g).map(Number);\n  }\n  return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 > 0.38 ? \"#1A1A17\" : \"#F0EFE8\";\n}\n\nconst EMPTY_FILL = THEME === \"light\" ? \"#CCCAC3\" : \"#3A3A36\";\n\n// --- Idiomatic D3 data-join tile renderer\nfunction drawTiles(sel, data, xFn, yFn) {\n  const tileG = sel.selectAll(\"g.el-tile\")\n    .data(data)\n    .join(\"g\")\n    .attr(\"class\", \"el-tile\")\n    .attr(\"transform\", d => `translate(${xFn(d)},${yFn(d)})`);\n\n  tileG.append(\"rect\")\n    .attr(\"width\", INNER).attr(\"height\", INNER).attr(\"rx\", 2)\n    .attr(\"fill\", d => d[4] !== null ? colorScale(d[4]) : EMPTY_FILL)\n    .attr(\"stroke\", d => CAT_COLOR[d[5]] || \"none\")\n    .attr(\"stroke-width\", 1.5)\n    .attr(\"stroke-opacity\", 0.7);\n\n  // atomic number — top-left\n  tileG.append(\"text\").attr(\"x\", 4).attr(\"y\", 12)\n    .style(\"font-size\", \"9px\").style(\"font-family\", \"sans-serif\")\n    .attr(\"fill\", d => textOnFill(d[4] !== null ? colorScale(d[4]) : EMPTY_FILL))\n    .text(d => d[0]);\n\n  // element symbol — centered\n  tileG.append(\"text\")\n    .attr(\"x\", INNER / 2).attr(\"y\", INNER / 2 + 6)\n    .attr(\"text-anchor\", \"middle\")\n    .style(\"font-size\", \"15px\").style(\"font-weight\", \"700\")\n    .style(\"font-family\", \"sans-serif\")\n    .attr(\"fill\", d => textOnFill(d[4] !== null ? colorScale(d[4]) : EMPTY_FILL))\n    .text(d => d[1]);\n\n  // EN value — bottom center (known values only)\n  tileG.filter(d => d[4] !== null)\n    .append(\"text\")\n    .attr(\"x\", INNER / 2).attr(\"y\", INNER - 5)\n    .attr(\"text-anchor\", \"middle\")\n    .style(\"font-size\", \"8px\").style(\"font-family\", \"sans-serif\")\n    .attr(\"fill\", d => textOnFill(colorScale(d[4])))\n    .text(d => d[4].toFixed(2));\n\n  return tileG;\n}\n\n// --- Main table: periods 1-7\nconst mainG = svg.append(\"g\");\nconst mainTiles = drawTiles(\n  mainG,\n  elements.filter(d => d[2] <= 7),\n  d => MAIN_LEFT + (d[3] - 1) * TILE,\n  d => MAIN_TOP + (d[2] - 1) * TILE\n);\n\n// --- Fluorine annotation: global electronegativity maximum\nmainTiles.filter(d => d[1] === \"F\")\n  .append(\"text\")\n  .attr(\"x\", INNER - 2).attr(\"y\", 11)\n  .attr(\"text-anchor\", \"end\")\n  .style(\"font-size\", \"9px\").style(\"font-family\", \"sans-serif\")\n  .attr(\"fill\", \"#F5D020\")\n  .text(\"★\");\n\n// --- F-block placeholder tiles at group 3, periods 6-7\n[[\"57-71\", 5], [\"89-103\", 6]].forEach(([label, per]) => {\n  const fill = THEME === \"light\" ? \"#DEDAD3\" : \"#2D2D29\";\n  const x = MAIN_LEFT + 2 * TILE;\n  const y = MAIN_TOP + per * TILE;\n  const g = svg.append(\"g\").attr(\"transform\", `translate(${x},${y})`);\n  g.append(\"rect\")\n    .attr(\"width\", INNER).attr(\"height\", INNER)\n    .attr(\"rx\", 2).attr(\"fill\", fill)\n    .attr(\"stroke\", t.grid).attr(\"stroke-width\", 0.8);\n  g.append(\"text\").attr(\"x\", INNER / 2).attr(\"y\", INNER / 2 + 4)\n    .attr(\"text-anchor\", \"middle\")\n    .style(\"font-size\", \"8px\").style(\"font-family\", \"sans-serif\")\n    .attr(\"fill\", t.inkSoft).text(label);\n});\n\n// --- F-block rows (period 8 = lanthanides, period 9 = actinides)\nconst fbG = svg.append(\"g\");\ndrawTiles(\n  fbG,\n  elements.filter(d => d[2] >= 8),\n  d => FB_LEFT + (d[3] - 1) * TILE,\n  d => FB_TOP + (d[2] - 8) * TILE\n);\n\n// --- F-block row labels (Ln / An) — idiomatic D3 data join\nsvg.selectAll(\"text.fblock-label\")\n  .data([[\"Ln\", 0], [\"An\", 1]])\n  .join(\"text\")\n  .attr(\"class\", \"fblock-label\")\n  .attr(\"x\", MAIN_LEFT - 10)\n  .attr(\"y\", ([, i]) => FB_TOP + i * TILE + INNER / 2 + 5)\n  .attr(\"text-anchor\", \"end\")\n  .style(\"font-size\", \"11px\").style(\"font-family\", \"sans-serif\")\n  .attr(\"fill\", t.inkSoft)\n  .text(([label]) => label);\n\n// --- Period labels (1-7)\nsvg.selectAll(\"text.period-label\")\n  .data(d3.range(1, 8))\n  .join(\"text\")\n  .attr(\"class\", \"period-label\")\n  .attr(\"x\", MAIN_LEFT - 10)\n  .attr(\"y\", p => MAIN_TOP + (p - 1) * TILE + INNER / 2 + 5)\n  .attr(\"text-anchor\", \"end\")\n  .style(\"font-size\", \"11px\").style(\"font-family\", \"sans-serif\")\n  .attr(\"fill\", t.inkSoft)\n  .text(p => p);\n\n// --- Group labels (1-18)\nsvg.selectAll(\"text.group-label\")\n  .data(d3.range(1, 19))\n  .join(\"text\")\n  .attr(\"class\", \"group-label\")\n  .attr(\"x\", grp => MAIN_LEFT + (grp - 1) * TILE + INNER / 2)\n  .attr(\"y\", MAIN_TOP - 10)\n  .attr(\"text-anchor\", \"middle\")\n  .style(\"font-size\", \"9.5px\").style(\"font-family\", \"sans-serif\")\n  .attr(\"fill\", t.inkSoft)\n  .text(grp => grp);\n\n// --- Colorbar: single rect filled with linearGradient\nsvg.append(\"rect\")\n  .attr(\"x\", MAIN_LEFT).attr(\"y\", CB_TOP)\n  .attr(\"width\", CB_W).attr(\"height\", CB_H)\n  .attr(\"fill\", \"url(#cb-grad)\");\nsvg.append(\"rect\")\n  .attr(\"x\", MAIN_LEFT).attr(\"y\", CB_TOP)\n  .attr(\"width\", CB_W).attr(\"height\", CB_H)\n  .attr(\"fill\", \"none\").attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 0.5);\n\n// --- Colorbar axis\nconst cbScale = d3.scaleLinear()\n  .domain([valMin, valMax])\n  .range([MAIN_LEFT, MAIN_LEFT + CB_W]);\nconst cbAxisG = svg.append(\"g\")\n  .attr(\"transform\", `translate(0,${CB_TOP + CB_H})`)\n  .call(\n    d3.axisBottom(cbScale)\n      .tickValues([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0])\n      .tickFormat(d3.format(\".1f\"))\n      .tickSize(4)\n  );\ncbAxisG.selectAll(\"text\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .style(\"font-family\", \"sans-serif\");\ncbAxisG.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\ncbAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- Colorbar property label\nsvg.append(\"text\")\n  .attr(\"x\", MAIN_LEFT + CB_W / 2)\n  .attr(\"y\", CB_TOP + CB_H + 44)\n  .attr(\"text-anchor\", \"middle\")\n  .style(\"font-size\", \"14px\").style(\"font-family\", \"sans-serif\")\n  .attr(\"fill\", t.ink)\n  .text(\"Electronegativity (Pauling scale)\");\n"}