{"spec_id":"subplot-grid","library":"d3","language":"javascript","code":"// anyplot.ai\n// subplot-grid: Subplot Grid Layout\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Small fixed-seed LCG — the browser has no seeded RNG.\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function () {\n    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(20260909);\n\n// Financial dashboard scenario: 60 trading days of a single stock.\nconst numDays = 60;\nconst dayIndex = d3.range(numDays);\nconst dailyReturns = [];\nconst priceSeries = [];\nconst dailyVolumes = [];\nlet price = 150;\nfor (let i = 0; i < numDays; i++) {\n  const pctChange = (rand() - 0.47) * 3.4; // percent, slight upward drift\n  price *= 1 + pctChange / 100;\n  priceSeries.push(price);\n  dailyReturns.push(pctChange);\n  dailyVolumes.push((1.1 + Math.abs(pctChange) * 0.55) * 1e6 * (0.75 + rand() * 0.5));\n}\n\nconst priceColor = t.palette[0]; // #009E73 — brand green, headline series\nconst volumeColor = t.palette[1];\nconst upColor = t.palette[0]; // finance convention: up/profit -> green\nconst downColor = t.palette[4]; // finance convention: down/loss -> matte red\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// Diagonal hatch pattern gives \"down day\" a texture cue in addition to color,\n// so red-green color-vision-deficient viewers can still tell up/down apart.\nconst hatchDownId = \"hatch-down\";\nsvg\n  .append(\"defs\")\n  .append(\"pattern\")\n  .attr(\"id\", hatchDownId)\n  .attr(\"width\", 8)\n  .attr(\"height\", 8)\n  .attr(\"patternUnits\", \"userSpaceOnUse\")\n  .attr(\"patternTransform\", \"rotate(45)\")\n  .call((p) => {\n    p.append(\"rect\").attr(\"width\", 8).attr(\"height\", 8).attr(\"fill\", downColor);\n    p.append(\"rect\").attr(\"width\", 4).attr(\"height\", 8).attr(\"fill\", t.pageBg);\n  });\n\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\", \"24px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"subplot-grid · javascript · d3 · anyplot.ai\");\n\n// --- Grid geometry (2x2, independent axes per cell) --------------------------\nconst gridTop = 92;\nconst gridBottom = height - 24;\nconst gridLeft = 24;\nconst gridRight = width - 24;\nconst gutterX = 56;\nconst gutterY = 64;\nconst cellW = (gridRight - gridLeft - gutterX) / 2;\nconst cellH = (gridBottom - gridTop - gutterY) / 2;\n\nconst cells = [\n  { x: gridLeft, y: gridTop },\n  { x: gridLeft + cellW + gutterX, y: gridTop },\n  { x: gridLeft, y: gridTop + cellH + gutterY },\n  { x: gridLeft + cellW + gutterX, y: gridTop + cellH + gutterY },\n];\n\nconst panelMargin = { top: 40, right: 24, bottom: 46, left: 74 };\nconst pw = cellW - panelMargin.left - panelMargin.right;\nconst ph = cellH - panelMargin.top - panelMargin.bottom;\n\nfunction panel(cell, title) {\n  const cellG = svg.append(\"g\").attr(\"transform\", `translate(${cell.x},${cell.y})`);\n  cellG\n    .append(\"text\")\n    .attr(\"x\", panelMargin.left)\n    .attr(\"y\", 22)\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"18px\")\n    .style(\"font-weight\", \"600\")\n    .text(title);\n  return cellG.append(\"g\").attr(\"transform\", `translate(${panelMargin.left},${panelMargin.top})`);\n}\n\nfunction styleAxis(ax) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\nfunction legend(g, items, x, y) {\n  const lg = g.append(\"g\").attr(\"transform\", `translate(${x},${y})`);\n  items.forEach((item, i) => {\n    const row = lg.append(\"g\").attr(\"transform\", `translate(0,${i * 22})`);\n    if (item.shape) {\n      row\n        .append(\"path\")\n        .attr(\"transform\", \"translate(7,7)\")\n        .attr(\"d\", item.shape.size(110)())\n        .attr(\"fill\", item.color);\n    } else {\n      row.append(\"rect\").attr(\"width\", 14).attr(\"height\", 14).attr(\"fill\", item.fill ?? item.color);\n    }\n    row\n      .append(\"text\")\n      .attr(\"x\", 20)\n      .attr(\"y\", 12)\n      .attr(\"fill\", t.inkSoft)\n      .style(\"font-size\", \"13px\")\n      .text(item.label);\n  });\n}\n\n// --- Cell 1: price line chart --------------------------------------------------\nconst g1 = panel(cells[0], \"Stock Price ($)\");\nconst x1 = d3.scaleLinear().domain([0, numDays - 1]).range([0, pw]);\nconst y1 = d3.scaleLinear().domain(d3.extent(priceSeries)).nice().range([ph, 0]);\ng1\n  .append(\"g\")\n  .selectAll(\"line\")\n  .data(y1.ticks(5))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", pw)\n  .attr(\"y1\", (d) => y1(d))\n  .attr(\"y2\", (d) => y1(d))\n  .attr(\"stroke\", t.grid);\nconst yAxis1 = g1.append(\"g\").call(d3.axisLeft(y1).ticks(5).tickFormat(d3.format(\"$.0f\")));\nconst xAxis1 = g1\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ph})`)\n  .call(\n    d3\n      .axisBottom(x1)\n      .tickValues(dayIndex.filter((d) => d % 15 === 0))\n      .tickFormat((d) => `Day ${d + 1}`)\n  );\n[xAxis1, yAxis1].forEach(styleAxis);\nconst priceLine = d3\n  .line()\n  .x((d, i) => x1(i))\n  .y((d) => y1(d));\ng1\n  .append(\"path\")\n  .datum(priceSeries)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", priceColor)\n  .attr(\"stroke-width\", 3)\n  .attr(\"d\", priceLine);\n\n// --- Cell 2: trading volume bar chart -------------------------------------------\nconst g2 = panel(cells[1], \"Trading Volume\");\nconst x2 = d3.scaleBand().domain(dayIndex).range([0, pw]).padding(0.25);\nconst y2 = d3.scaleLinear().domain([0, d3.max(dailyVolumes)]).nice().range([ph, 0]);\ng2\n  .append(\"g\")\n  .selectAll(\"line\")\n  .data(y2.ticks(5))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", pw)\n  .attr(\"y1\", (d) => y2(d))\n  .attr(\"y2\", (d) => y2(d))\n  .attr(\"stroke\", t.grid);\nconst yAxis2 = g2.append(\"g\").call(d3.axisLeft(y2).ticks(5).tickFormat(d3.format(\".2s\")));\nconst xAxis2 = g2\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ph})`)\n  .call(\n    d3\n      .axisBottom(x2)\n      .tickValues(x2.domain().filter((d) => d % 15 === 0))\n      .tickFormat((d) => `Day ${d + 1}`)\n  );\n[xAxis2, yAxis2].forEach(styleAxis);\ng2\n  .selectAll(\"rect.bar\")\n  .data(dailyVolumes)\n  .join(\"rect\")\n  .attr(\"class\", \"bar\")\n  .attr(\"x\", (d, i) => x2(i))\n  .attr(\"y\", (d) => y2(d))\n  .attr(\"width\", x2.bandwidth())\n  .attr(\"height\", (d) => ph - y2(d))\n  .attr(\"fill\", volumeColor);\n\n// --- Cell 3: daily returns histogram --------------------------------------------\nconst g3 = panel(cells[2], \"Daily Returns Distribution\");\nconst x3 = d3.scaleLinear().domain(d3.extent(dailyReturns)).nice().range([0, pw]);\nconst bins = d3.bin().domain(x3.domain()).thresholds(10)(dailyReturns);\nconst y3 = d3\n  .scaleLinear()\n  .domain([0, d3.max(bins, (b) => b.length)])\n  .nice()\n  .range([ph, 0]);\ng3\n  .append(\"g\")\n  .selectAll(\"line\")\n  .data(y3.ticks(5))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", pw)\n  .attr(\"y1\", (d) => y3(d))\n  .attr(\"y2\", (d) => y3(d))\n  .attr(\"stroke\", t.grid);\nconst yAxis3 = g3.append(\"g\").call(d3.axisLeft(y3).ticks(5));\nconst xAxis3 = g3\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ph})`)\n  .call(\n    d3\n      .axisBottom(x3)\n      .ticks(6)\n      .tickFormat((d) => `${d.toFixed(1)}%`)\n  );\n[xAxis3, yAxis3].forEach(styleAxis);\ng3\n  .selectAll(\"rect.bin\")\n  .data(bins)\n  .join(\"rect\")\n  .attr(\"class\", \"bin\")\n  .attr(\"x\", (d) => x3(d.x0) + 1)\n  .attr(\"y\", (d) => y3(d.length))\n  .attr(\"width\", (d) => Math.max(0, x3(d.x1) - x3(d.x0) - 2))\n  .attr(\"height\", (d) => ph - y3(d.length))\n  .attr(\"fill\", (d) => ((d.x0 + d.x1) / 2 >= 0 ? upColor : `url(#${hatchDownId})`));\nlegend(\n  g3,\n  [\n    { color: upColor, fill: upColor, label: \"Up day\" },\n    { color: downColor, fill: `url(#${hatchDownId})`, label: \"Down day\" },\n  ],\n  pw - 96,\n  0\n);\n\n// --- Cell 4: volume vs. return scatter -------------------------------------------\nconst g4 = panel(cells[3], \"Volume vs. Daily Return\");\nconst x4 = d3.scaleLinear().domain(d3.extent(dailyVolumes)).nice().range([0, pw]);\nconst y4 = d3.scaleLinear().domain(d3.extent(dailyReturns)).nice().range([ph, 0]);\ng4\n  .append(\"g\")\n  .selectAll(\"line.h\")\n  .data(y4.ticks(5))\n  .join(\"line\")\n  .attr(\"class\", \"h\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", pw)\n  .attr(\"y1\", (d) => y4(d))\n  .attr(\"y2\", (d) => y4(d))\n  .attr(\"stroke\", t.grid);\ng4\n  .append(\"g\")\n  .selectAll(\"line.v\")\n  .data(x4.ticks(5))\n  .join(\"line\")\n  .attr(\"class\", \"v\")\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ph)\n  .attr(\"x1\", (d) => x4(d))\n  .attr(\"x2\", (d) => x4(d))\n  .attr(\"stroke\", t.grid);\nconst yAxis4 = g4\n  .append(\"g\")\n  .call(\n    d3\n      .axisLeft(y4)\n      .ticks(5)\n      .tickFormat((d) => `${d.toFixed(1)}%`)\n  );\nconst xAxis4 = g4\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ph})`)\n  .call(d3.axisBottom(x4).ticks(5).tickFormat(d3.format(\".2s\")));\n[xAxis4, yAxis4].forEach(styleAxis);\n// Up days are circles, down days are triangles — a shape cue alongside color\n// so the encoding still reads for red-green color-vision-deficient viewers.\nconst symbolUp = d3.symbol().type(d3.symbolCircle).size(140);\nconst symbolDown = d3.symbol().type(d3.symbolTriangle).size(150);\ng4\n  .selectAll(\"path.pt\")\n  .data(dailyVolumes.map((v, i) => ({ volume: v, ret: dailyReturns[i] })))\n  .join(\"path\")\n  .attr(\"class\", \"pt\")\n  .attr(\"transform\", (d) => `translate(${x4(d.volume)},${y4(d.ret)})`)\n  .attr(\"d\", (d) => (d.ret >= 0 ? symbolUp : symbolDown)())\n  .attr(\"fill\", (d) => (d.ret >= 0 ? upColor : downColor))\n  .attr(\"fill-opacity\", 0.85)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1);\nlegend(\n  g4,\n  [\n    { color: upColor, shape: d3.symbol().type(d3.symbolCircle), label: \"Up day\" },\n    { color: downColor, shape: d3.symbol().type(d3.symbolTriangle), label: \"Down day\" },\n  ],\n  8,\n  8\n);\n"}