{"spec_id":"subplot-mosaic","library":"d3","language":"javascript","code":"// anyplot.ai\n// subplot-mosaic: Mosaic Subplot Layout with Varying Sizes\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Deterministic PRNG (browser has no seeded Math.random) -----------------\nfunction lcg(seed) {\n  let s = seed % 2147483647;\n  if (s <= 0) s += 2147483646;\n  return () => (s = (s * 16807) % 2147483647) / 2147483647;\n}\nconst rand = lcg(42);\n\n// --- Mosaic layout: ASCII pattern -> per-letter bounding box -----------------\n// \"AAAAAA\" x2 rows = wide overview on top; \"BBBCCC\" = two medium detail\n// charts; \"DDEEFF\" = three small metric panels. Repeated letters span cells.\nconst pattern = [\"AAAAAA\", \"AAAAAA\", \"BBBCCC\", \"DDEEFF\"];\nconst nRows = pattern.length;\nconst nCols = pattern[0].length;\n\nconst cells = {};\npattern.forEach((rowStr, r) => {\n  [...rowStr].forEach((key, c) => {\n    if (key === \".\") return;\n    if (!cells[key]) cells[key] = { r0: r, r1: r, c0: c, c1: c };\n    const cell = cells[key];\n    cell.r0 = Math.min(cell.r0, r);\n    cell.r1 = Math.max(cell.r1, r);\n    cell.c0 = Math.min(cell.c0, c);\n    cell.c1 = Math.max(cell.c1, c);\n  });\n});\n\nconst margin = { top: 90, right: 40, bottom: 40, left: 40 };\nconst gutter = 24;\nconst gridW = width - margin.left - margin.right;\nconst gridH = height - margin.top - margin.bottom;\nconst colW = (gridW - gutter * (nCols - 1)) / nCols;\nconst rowH = (gridH - gutter * (nRows - 1)) / nRows;\n\nfunction cellRect(key) {\n  const c = cells[key];\n  return {\n    x: margin.left + c.c0 * (colW + gutter),\n    y: margin.top + c.r0 * (rowH + gutter),\n    w: (c.c1 - c.c0 + 1) * colW + (c.c1 - c.c0) * gutter,\n    h: (c.r1 - c.r0 + 1) * rowH + (c.r1 - c.r0) * gutter,\n  };\n}\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// A: daily page views over a quarter — wide overview\nconst pageViews = d3.range(90).map((day) => {\n  const trendline = 8000 + day * 35;\n  const weeklyCycle = 1200 * Math.sin((day / 7) * 2 * Math.PI);\n  const noise = (rand() - 0.5) * 1000;\n  return { day, views: Math.max(0, trendline + weeklyCycle + noise) };\n});\n\n// B: traffic by acquisition channel — medium detail\nconst channels = [\n  { channel: \"Organic\", visits: 18200 },\n  { channel: \"Direct\", visits: 12400 },\n  { channel: \"Social\", visits: 8300 },\n  { channel: \"Referral\", visits: 5100 },\n  { channel: \"Email\", visits: 3600 },\n];\n\n// C: session duration vs. pages viewed — medium detail\nconst sessions = d3.range(60).map(() => {\n  const duration = 1 + rand() * 9;\n  const pages = Math.max(1, 1 + duration * 0.8 + (rand() - 0.5) * 2);\n  return { duration, pages };\n});\n\n// D: bounce rate by device — small metric panel\nconst devices = [\n  { device: \"Desktop\", bounce: 38 },\n  { device: \"Mobile\", bounce: 54 },\n  { device: \"Tablet\", bounce: 47 },\n];\n\n// E: conversion rate, 14-day trend — small metric panel\nconst conversion = d3.range(14).map((i) => 2.4 + Math.sin(i / 2) * 0.4 + (rand() - 0.5) * 0.3);\n\n// F: top referral sources — small metric panel\nconst referrers = [\n  { source: \"google.com\", count: 420 },\n  { source: \"news-example.com\", count: 310 },\n  { source: \"socialhub.io\", count: 260 },\n  { source: \"partner.co\", count: 180 },\n];\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\nfunction stylePanelAxis(sel) {\n  sel.selectAll(\".tick text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\");\n  sel.selectAll(\".tick line\").attr(\"stroke\", t.grid);\n  sel.select(\".domain\").remove();\n}\n\n// Small axis-unit label, rotated for the y axis, so panels where the panel\n// title alone doesn't convey units (e.g. duration, visits) stay unambiguous.\nfunction axisTitle(cg, text, axis, iw, ih, offset) {\n  if (axis === \"y\") {\n    cg.append(\"text\")\n      .attr(\"transform\", \"rotate(-90)\")\n      .attr(\"x\", -(ih / 2))\n      .attr(\"y\", -offset)\n      .attr(\"text-anchor\", \"middle\")\n      .attr(\"fill\", t.inkSoft)\n      .style(\"font-size\", \"10px\")\n      .style(\"font-weight\", \"600\")\n      .text(text);\n  } else {\n    cg.append(\"text\")\n      .attr(\"x\", iw / 2)\n      .attr(\"y\", ih + offset)\n      .attr(\"text-anchor\", \"middle\")\n      .attr(\"fill\", t.inkSoft)\n      .style(\"font-size\", \"10px\")\n      .style(\"font-weight\", \"600\")\n      .text(text);\n  }\n}\n\nfunction panel(key, title, titleSize = \"16px\") {\n  const { x, y, w, h } = cellRect(key);\n  const g = svg.append(\"g\").attr(\"transform\", `translate(${x},${y})`);\n  g.append(\"rect\").attr(\"width\", w).attr(\"height\", h).attr(\"rx\", 10).attr(\"fill\", t.elevatedBg);\n  g.append(\"text\")\n    .attr(\"x\", 18)\n    .attr(\"y\", 28)\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", titleSize)\n    .style(\"font-weight\", \"600\")\n    .text(title);\n  return { g, w, h };\n}\n\n// --- Panel A: wide overview area+line chart ----------------------------------\n{\n  const { g, w, h } = panel(\"A\", \"Daily Page Views — Last 90 Days\", \"18px\");\n  const m = { top: 48, right: 24, bottom: 34, left: 86 };\n  const iw = w - m.left - m.right;\n  const ih = h - m.top - m.bottom;\n  const cg = g.append(\"g\").attr(\"transform\", `translate(${m.left},${m.top})`);\n\n  const x = d3.scaleLinear().domain([0, 89]).range([0, iw]);\n  const y = d3\n    .scaleLinear()\n    .domain([0, d3.max(pageViews, (d) => d.views)])\n    .nice()\n    .range([ih, 0]);\n\n  cg.append(\"g\")\n    .call(d3.axisLeft(y).ticks(5).tickSize(-iw).tickFormat(d3.format(\"~s\")))\n    .call((sel) => sel.selectAll(\"line\").attr(\"stroke\", t.grid))\n    .call(stylePanelAxis);\n  axisTitle(cg, \"Page Views\", \"y\", iw, ih, 60);\n\n  const area = d3\n    .area()\n    .x((d) => x(d.day))\n    .y0(ih)\n    .y1((d) => y(d.views))\n    .curve(d3.curveMonotoneX);\n  const line = d3\n    .line()\n    .x((d) => x(d.day))\n    .y((d) => y(d.views))\n    .curve(d3.curveMonotoneX);\n\n  cg.append(\"path\").datum(pageViews).attr(\"d\", area).attr(\"fill\", t.palette[0]).attr(\"opacity\", 0.18);\n  cg.append(\"path\")\n    .datum(pageViews)\n    .attr(\"d\", line)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", t.palette[0])\n    .attr(\"stroke-width\", 3.5);\n\n  const xAxis = cg\n    .append(\"g\")\n    .attr(\"transform\", `translate(0,${ih})`)\n    .call(\n      d3\n        .axisBottom(x)\n        .ticks(9)\n        .tickFormat((d) => `Day ${d}`)\n    );\n  stylePanelAxis(xAxis);\n}\n\n// --- Panel B: traffic by channel (vertical bar) ------------------------------\n{\n  const { g, w, h } = panel(\"B\", \"Traffic by Channel\");\n  const m = { top: 44, right: 20, bottom: 32, left: 76 };\n  const iw = w - m.left - m.right;\n  const ih = h - m.top - m.bottom;\n  const cg = g.append(\"g\").attr(\"transform\", `translate(${m.left},${m.top})`);\n\n  const x = d3\n    .scaleBand()\n    .domain(channels.map((d) => d.channel))\n    .range([0, iw])\n    .padding(0.3);\n  const y = d3\n    .scaleLinear()\n    .domain([0, d3.max(channels, (d) => d.visits)])\n    .nice()\n    .range([ih, 0]);\n  const color = d3.scaleOrdinal().domain(channels.map((d) => d.channel)).range(t.palette);\n\n  cg.append(\"g\")\n    .call(d3.axisLeft(y).ticks(4).tickSize(-iw).tickFormat(d3.format(\"~s\")))\n    .call((sel) => sel.selectAll(\"line\").attr(\"stroke\", t.grid))\n    .call(stylePanelAxis);\n  axisTitle(cg, \"Visits\", \"y\", iw, ih, 54);\n\n  cg.selectAll(\"rect.bar\")\n    .data(channels)\n    .join(\"rect\")\n    .attr(\"class\", \"bar\")\n    .attr(\"x\", (d) => x(d.channel))\n    .attr(\"y\", (d) => y(d.visits))\n    .attr(\"width\", x.bandwidth())\n    .attr(\"height\", (d) => ih - y(d.visits))\n    .attr(\"fill\", (d) => color(d.channel));\n\n  const xAxis = cg.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x));\n  stylePanelAxis(xAxis);\n  xAxis.selectAll(\"text\").style(\"font-size\", \"11px\");\n}\n\n// --- Panel C: session duration vs. pages viewed (scatter) --------------------\n{\n  const { g, w, h } = panel(\"C\", \"Session Duration vs. Pages Viewed\");\n  const m = { top: 44, right: 24, bottom: 50, left: 76 };\n  const iw = w - m.left - m.right;\n  const ih = h - m.top - m.bottom;\n  const cg = g.append(\"g\").attr(\"transform\", `translate(${m.left},${m.top})`);\n\n  const x = d3\n    .scaleLinear()\n    .domain([0, d3.max(sessions, (d) => d.duration)])\n    .nice()\n    .range([0, iw]);\n  const y = d3\n    .scaleLinear()\n    .domain([0, d3.max(sessions, (d) => d.pages)])\n    .nice()\n    .range([ih, 0]);\n\n  cg.append(\"g\")\n    .call(d3.axisLeft(y).ticks(4).tickSize(-iw))\n    .call((sel) => sel.selectAll(\"line\").attr(\"stroke\", t.grid))\n    .call(stylePanelAxis);\n  cg.append(\"g\")\n    .call(d3.axisBottom(x).ticks(4).tickSize(-ih))\n    .attr(\"transform\", `translate(0,${ih})`)\n    .call((sel) => sel.selectAll(\"line\").attr(\"stroke\", t.grid))\n    .call(stylePanelAxis);\n  axisTitle(cg, \"Pages\", \"y\", iw, ih, 54);\n  axisTitle(cg, \"Duration (min)\", \"x\", iw, ih, 40);\n\n  cg.selectAll(\"circle\")\n    .data(sessions)\n    .join(\"circle\")\n    .attr(\"cx\", (d) => x(d.duration))\n    .attr(\"cy\", (d) => y(d.pages))\n    .attr(\"r\", 6)\n    .attr(\"fill\", t.palette[0])\n    .attr(\"fill-opacity\", 0.75)\n    .attr(\"stroke\", t.elevatedBg)\n    .attr(\"stroke-width\", 1);\n}\n\n// --- Panel D: bounce rate by device (small horizontal bar) -------------------\n{\n  const { g, w, h } = panel(\"D\", \"Bounce Rate by Device\");\n  const m = { top: 44, right: 44, bottom: 16, left: 78 };\n  const iw = w - m.left - m.right;\n  const ih = h - m.top - m.bottom;\n  const cg = g.append(\"g\").attr(\"transform\", `translate(${m.left},${m.top})`);\n\n  const y = d3\n    .scaleBand()\n    .domain(devices.map((d) => d.device))\n    .range([0, ih])\n    .padding(0.35);\n  const x = d3.scaleLinear().domain([0, 100]).range([0, iw]);\n  const color = d3.scaleOrdinal().domain(devices.map((d) => d.device)).range(t.palette);\n\n  cg.selectAll(\"rect.bar\")\n    .data(devices)\n    .join(\"rect\")\n    .attr(\"class\", \"bar\")\n    .attr(\"x\", 0)\n    .attr(\"y\", (d) => y(d.device))\n    .attr(\"width\", (d) => x(d.bounce))\n    .attr(\"height\", y.bandwidth())\n    .attr(\"fill\", (d) => color(d.device));\n\n  cg.selectAll(\"text.label\")\n    .data(devices)\n    .join(\"text\")\n    .attr(\"class\", \"label\")\n    .attr(\"x\", -10)\n    .attr(\"y\", (d) => y(d.device) + y.bandwidth() / 2)\n    .attr(\"dy\", \"0.35em\")\n    .attr(\"text-anchor\", \"end\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"12px\")\n    .text((d) => d.device);\n\n  cg.selectAll(\"text.value\")\n    .data(devices)\n    .join(\"text\")\n    .attr(\"class\", \"value\")\n    .attr(\"x\", (d) => x(d.bounce) + 8)\n    .attr(\"y\", (d) => y(d.device) + y.bandwidth() / 2)\n    .attr(\"dy\", \"0.35em\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"12px\")\n    .style(\"font-weight\", \"600\")\n    .text((d) => `${d.bounce}%`);\n}\n\n// --- Panel E: conversion rate 14-day sparkline --------------------------------\n{\n  const { g, w, h } = panel(\"E\", \"Conversion Rate — 14 Days\");\n  const m = { top: 46, right: 60, bottom: 16, left: 20 };\n  const iw = w - m.left - m.right;\n  const ih = h - m.top - m.bottom;\n  const cg = g.append(\"g\").attr(\"transform\", `translate(${m.left},${m.top})`);\n\n  const x = d3.scaleLinear().domain([0, conversion.length - 1]).range([0, iw]);\n  const y = d3\n    .scaleLinear()\n    .domain(d3.extent(conversion))\n    .nice()\n    .range([ih, 0]);\n\n  const line = d3\n    .line()\n    .x((_, i) => x(i))\n    .y((d) => y(d))\n    .curve(d3.curveMonotoneX);\n\n  cg.append(\"path\")\n    .datum(conversion)\n    .attr(\"d\", line)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", t.palette[0])\n    .attr(\"stroke-width\", 3);\n\n  const last = conversion[conversion.length - 1];\n  cg.append(\"circle\").attr(\"cx\", x(conversion.length - 1)).attr(\"cy\", y(last)).attr(\"r\", 5).attr(\"fill\", t.palette[0]);\n  cg.append(\"text\")\n    .attr(\"x\", x(conversion.length - 1) + 10)\n    .attr(\"y\", y(last))\n    .attr(\"dy\", \"0.35em\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"13px\")\n    .style(\"font-weight\", \"600\")\n    .text(`${last.toFixed(1)}%`);\n}\n\n// --- Panel F: top referral sources (dot plot) ---------------------------------\n{\n  const { g, w, h } = panel(\"F\", \"Top Referral Sources\");\n  const m = { top: 44, right: 20, bottom: 16, left: 130 };\n  const iw = w - m.left - m.right;\n  const ih = h - m.top - m.bottom;\n  const cg = g.append(\"g\").attr(\"transform\", `translate(${m.left},${m.top})`);\n\n  const y = d3\n    .scaleBand()\n    .domain(referrers.map((d) => d.source))\n    .range([0, ih])\n    .padding(0.4);\n  const x = d3\n    .scaleLinear()\n    .domain([0, d3.max(referrers, (d) => d.count)])\n    .nice()\n    .range([0, iw]);\n\n  cg.selectAll(\"line.stem\")\n    .data(referrers)\n    .join(\"line\")\n    .attr(\"class\", \"stem\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", (d) => x(d.count))\n    .attr(\"y1\", (d) => y(d.source) + y.bandwidth() / 2)\n    .attr(\"y2\", (d) => y(d.source) + y.bandwidth() / 2)\n    .attr(\"stroke\", t.grid)\n    .attr(\"stroke-width\", 2);\n\n  cg.selectAll(\"circle.dot\")\n    .data(referrers)\n    .join(\"circle\")\n    .attr(\"class\", \"dot\")\n    .attr(\"cx\", (d) => x(d.count))\n    .attr(\"cy\", (d) => y(d.source) + y.bandwidth() / 2)\n    .attr(\"r\", 7)\n    .attr(\"fill\", t.palette[0]);\n\n  cg.selectAll(\"text.label\")\n    .data(referrers)\n    .join(\"text\")\n    .attr(\"class\", \"label\")\n    .attr(\"x\", -10)\n    .attr(\"y\", (d) => y(d.source) + y.bandwidth() / 2)\n    .attr(\"dy\", \"0.35em\")\n    .attr(\"text-anchor\", \"end\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"12px\")\n    .text((d) => d.source);\n}\n\n// --- Title --------------------------------------------------------------------\n// Measure the actual rendered width (real browser layout engine) instead of a\n// char-count heuristic, so the title reliably fills ~60% of the canvas width\n// without guessing at font metrics.\nconst title = \"Website Analytics Dashboard · subplot-mosaic · javascript · d3 · anyplot.ai\";\nconst baseFontSize = 28;\nconst titleEl = svg\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\", `${baseFontSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\nconst measuredWidth = titleEl.node().getBBox().width;\nconst targetWidth = width * 0.62;\nconst scale = Math.max(0.55, Math.min(1.25, targetWidth / measuredWidth));\ntitleEl.style(\"font-size\", `${Math.round(baseFontSize * scale)}px`);\n"}