{"spec_id":"facet-grid","library":"d3","language":"javascript","code":"// anyplot.ai\n// facet-grid: Faceted Grid Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-05\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic LCG PRNG) -------------------------------\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (1103515245 * state + 12345) & 0x7fffffff;\n    return state / 0x7fffffff;\n  };\n}\nconst rand = makeLcg(42);\nconst randNormal = () => {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n};\n\nconst waterLevels = [\"Low water\", \"High water\"];\nconst fertilizers = [\"No fertilizer\", \"Organic\", \"Synthetic\"];\nconst waterEffect = { \"Low water\": 0, \"High water\": 9 };\nconst fertEffect = { \"No fertilizer\": 0, Organic: 6, Synthetic: 12 };\nconst pointsPerFacet = 45;\n\nconst data = [];\nfor (const water of waterLevels) {\n  for (const fert of fertilizers) {\n    for (let i = 0; i < pointsPerFacet; i++) {\n      const sunlightHours = 4 + rand() * 8;\n      const noise = randNormal() * 4;\n      const leafAreaCm2 = Math.max(\n        4,\n        18 + waterEffect[water] + fertEffect[fert] + 3.4 * (sunlightHours - 4) + noise,\n      );\n      data.push({ water, fert, sunlightHours, leafAreaCm2 });\n    }\n  }\n}\n\n// --- Layout -------------------------------------------------------------\nconst margin = { top: 92, right: 92, bottom: 86, left: 96 };\nconst stripTop = 34;\nconst cellGap = 16;\nconst nCols = fertilizers.length;\nconst nRows = waterLevels.length;\n\nconst gridLeft = margin.left;\nconst gridTop = margin.top + stripTop;\nconst gridWidth = width - margin.left - margin.right;\nconst gridHeight = height - margin.top - stripTop - margin.bottom;\nconst cellWidth = (gridWidth - (nCols - 1) * cellGap) / nCols;\nconst cellHeight = (gridHeight - (nRows - 1) * cellGap) / nRows;\n\n// --- Shared scales (same axes across every facet) ---------------------------\nconst x = d3\n  .scaleLinear()\n  .domain(d3.extent(data, (d) => d.sunlightHours))\n  .nice()\n  .range([0, cellWidth]);\nconst y = d3\n  .scaleLinear()\n  .domain(d3.extent(data, (d) => d.leafAreaCm2))\n  .nice()\n  .range([cellHeight, 0]);\nconst xTicks = x.ticks(4);\nconst yTicks = y.ticks(4);\n\n// --- Per-facet linear trend (reinforces the sunlight→leaf-area story) -------\nfunction trendLine(points) {\n  const n = points.length;\n  const sumX = d3.sum(points, (d) => d.sunlightHours);\n  const sumY = d3.sum(points, (d) => d.leafAreaCm2);\n  const sumXY = d3.sum(points, (d) => d.sunlightHours * d.leafAreaCm2);\n  const sumXX = d3.sum(points, (d) => d.sunlightHours * d.sunlightHours);\n  const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);\n  const intercept = (sumY - slope * sumX) / n;\n  const [x0, x1] = x.domain();\n  return [\n    { sunlightHours: x0, leafAreaCm2: slope * x0 + intercept },\n    { sunlightHours: x1, leafAreaCm2: slope * x1 + intercept },\n  ];\n}\nconst trendPath = d3\n  .line()\n  .x((d) => x(d.sunlightHours))\n  .y((d) => y(d.leafAreaCm2));\n\n// --- SVG mount ----------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// --- Title ----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"facet-grid · javascript · d3 · anyplot.ai\");\n\n// --- Shared axis titles -----------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", gridLeft + gridWidth / 2)\n  .attr(\"y\", height - 22)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Sunlight (hours / day)\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", 26)\n  .attr(\"y\", gridTop + gridHeight / 2)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .attr(\"transform\", `rotate(-90, 26, ${gridTop + gridHeight / 2})`)\n  .text(\"Leaf Area (cm²)\");\n\n// --- Column facet strips (fertilizer) ----------------------------------------\nfertilizers.forEach((fert, c) => {\n  const cx = gridLeft + c * (cellWidth + cellGap);\n  svg\n    .append(\"rect\")\n    .attr(\"x\", cx)\n    .attr(\"y\", margin.top)\n    .attr(\"width\", cellWidth)\n    .attr(\"height\", stripTop)\n    .attr(\"fill\", t.elevatedBg);\n  svg\n    .append(\"text\")\n    .attr(\"x\", cx + cellWidth / 2)\n    .attr(\"y\", margin.top + stripTop / 2)\n    .attr(\"dy\", \"0.32em\")\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"14px\")\n    .style(\"font-weight\", \"600\")\n    .text(fert);\n});\n\n// --- Row facet strips (water regime) -----------------------------------------\nconst rowStripX = gridLeft + gridWidth + 8;\nconst rowStripWidth = margin.right - 8;\nwaterLevels.forEach((water, r) => {\n  const cy = gridTop + r * (cellHeight + cellGap);\n  svg\n    .append(\"rect\")\n    .attr(\"x\", rowStripX)\n    .attr(\"y\", cy)\n    .attr(\"width\", rowStripWidth)\n    .attr(\"height\", cellHeight)\n    .attr(\"fill\", t.elevatedBg);\n  svg\n    .append(\"text\")\n    .attr(\"x\", rowStripX + rowStripWidth / 2)\n    .attr(\"y\", cy + cellHeight / 2)\n    .attr(\"dy\", \"0.32em\")\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"14px\")\n    .style(\"font-weight\", \"600\")\n    .attr(\n      \"transform\",\n      `rotate(-90, ${rowStripX + rowStripWidth / 2}, ${cy + cellHeight / 2})`,\n    )\n    .text(water);\n});\n\n// --- Facet panels (scatter cells) --------------------------------------------\nwaterLevels.forEach((water, r) => {\n  fertilizers.forEach((fert, c) => {\n    const cellX = gridLeft + c * (cellWidth + cellGap);\n    const cellY = gridTop + r * (cellHeight + cellGap);\n    const cell = svg.append(\"g\").attr(\"transform\", `translate(${cellX},${cellY})`);\n\n    // shared gridlines\n    cell\n      .selectAll(\".gridline-y\")\n      .data(yTicks)\n      .join(\"line\")\n      .attr(\"x1\", 0)\n      .attr(\"x2\", cellWidth)\n      .attr(\"y1\", (d) => y(d))\n      .attr(\"y2\", (d) => y(d))\n      .attr(\"stroke\", t.grid);\n    cell\n      .selectAll(\".gridline-x\")\n      .data(xTicks)\n      .join(\"line\")\n      .attr(\"y1\", 0)\n      .attr(\"y2\", cellHeight)\n      .attr(\"x1\", (d) => x(d))\n      .attr(\"x2\", (d) => x(d))\n      .attr(\"stroke\", t.grid);\n\n    // tick labels only on the outer edges (declutters the grid, scales stay shared)\n    if (r === nRows - 1) {\n      cell\n        .selectAll(\".xtick\")\n        .data(xTicks)\n        .join(\"text\")\n        .attr(\"x\", (d) => x(d))\n        .attr(\"y\", cellHeight + 22)\n        .attr(\"text-anchor\", \"middle\")\n        .attr(\"fill\", t.inkSoft)\n        .style(\"font-size\", \"14px\")\n        .text((d) => d);\n    }\n    if (c === 0) {\n      cell\n        .selectAll(\".ytick\")\n        .data(yTicks)\n        .join(\"text\")\n        .attr(\"x\", -10)\n        .attr(\"y\", (d) => y(d))\n        .attr(\"dy\", \"0.32em\")\n        .attr(\"text-anchor\", \"end\")\n        .attr(\"fill\", t.inkSoft)\n        .style(\"font-size\", \"14px\")\n        .text((d) => d);\n    }\n\n    // shared-slope trend line (reinforces the sunlight -> leaf-area story per facet)\n    const facetData = data.filter((d) => d.water === water && d.fert === fert);\n    cell\n      .append(\"path\")\n      .datum(trendLine(facetData))\n      .attr(\"d\", trendPath)\n      .attr(\"fill\", \"none\")\n      .attr(\"stroke\", t.ink)\n      .attr(\"stroke-opacity\", 0.35)\n      .attr(\"stroke-width\", 2)\n      .attr(\"stroke-dasharray\", \"5,4\");\n\n    // scatter points\n    cell\n      .selectAll(\"circle\")\n      .data(facetData)\n      .join(\"circle\")\n      .attr(\"cx\", (d) => x(d.sunlightHours))\n      .attr(\"cy\", (d) => y(d.leafAreaCm2))\n      .attr(\"r\", 4.5)\n      .attr(\"fill\", t.palette[0])\n      .attr(\"fill-opacity\", 0.75)\n      .attr(\"stroke\", t.pageBg)\n      .attr(\"stroke-width\", 0.5);\n  });\n});\n"}