{"spec_id":"facet-grid","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// facet-grid: Faceted Grid Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 81/100 | Created: 2026-09-05\n\n// Only the core `highcharts` bundle is loaded (no highcharts-more / grid module),\n// so the facet grid is built the way Highcharts' own \"small multiples\" demo does\n// it: one xAxis/yAxis pair per cell, each pinned to a percentage rectangle of\n// the shared plot area via top/left/width/height, all sharing the same min/max\n// so every panel reads off the same scale. Row and column strip labels are\n// drawn with the core SVG renderer, mirroring ggplot2's facet_grid strips.\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (mulberry32 + Box-Muller) --------------------------\nfunction mulberry32(seed) {\n  return function random() {\n    seed = (seed + 0x6d2b79f5) | 0;\n    let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n    x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;\n    return ((x ^ (x >>> 14)) >>> 0) / 4294967296;\n  };\n}\nconst rand = mulberry32(42);\nfunction randNormal(mean, std) {\n  const u1 = rand();\n  const u2 = rand();\n  return mean + std * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// --- Data: crop yield vs. rainfall, faceted by soil type (rows) x fertilizer (columns) ---\nconst ROW_FACETS = [\"Clay Soil\", \"Sandy Soil\"];\nconst COL_FACETS = [\"No Fertilizer\", \"Organic\", \"Synthetic\"];\nconst POINTS_PER_CELL = 50;\nconst SOIL_EFFECT = { \"Clay Soil\": 0.6, \"Sandy Soil\": 0 };\nconst FERTILIZER_EFFECT = { \"No Fertilizer\": 0, Organic: 1.1, Synthetic: 1.9 };\n\nconst cells = [];\nROW_FACETS.forEach((rowFacet) => {\n  COL_FACETS.forEach((colFacet) => {\n    const points = [];\n    for (let i = 0; i < POINTS_PER_CELL; i += 1) {\n      const rainfall = 250 + rand() * 700;\n      const yieldTons =\n        2.2 + rainfall * 0.0028 + SOIL_EFFECT[rowFacet] + FERTILIZER_EFFECT[colFacet] + randNormal(0, 0.35);\n      points.push([Math.round(rainfall * 10) / 10, Math.round(yieldTons * 100) / 100]);\n    }\n    cells.push({ rowFacet, colFacet, points });\n  });\n});\n\nconst allX = cells.flatMap((cell) => cell.points.map((p) => p[0]));\nconst allY = cells.flatMap((cell) => cell.points.map((p) => p[1]));\nconst xPad = (Math.max(...allX) - Math.min(...allX)) * 0.08;\nconst yPad = (Math.max(...allY) - Math.min(...allY)) * 0.08;\nconst X_MIN = Math.min(...allX) - xPad;\nconst X_MAX = Math.max(...allX) + xPad;\nconst Y_MIN = Math.min(...allY) - yPad;\nconst Y_MAX = Math.max(...allY) + yPad;\n\n// --- Grid geometry (percentages of the shared plot area) -------------------\nconst N_ROWS = ROW_FACETS.length;\nconst N_COLS = COL_FACETS.length;\nconst COL_GUTTER_PCT = 4;\nconst ROW_GUTTER_PCT = 6;\nconst CELL_WIDTH_PCT = (100 - COL_GUTTER_PCT * (N_COLS - 1)) / N_COLS;\nconst CELL_HEIGHT_PCT = (100 - ROW_GUTTER_PCT * (N_ROWS - 1)) / N_ROWS;\nconst cellLeftPct = (c) => c * (CELL_WIDTH_PCT + COL_GUTTER_PCT);\nconst cellTopPct = (r) => r * (CELL_HEIGHT_PCT + ROW_GUTTER_PCT);\n\nconst xAxes = [];\nconst yAxes = [];\nconst series = [];\ncells.forEach((cell, idx) => {\n  const r = ROW_FACETS.indexOf(cell.rowFacet);\n  const c = COL_FACETS.indexOf(cell.colFacet);\n  const rect = {\n    top: `${cellTopPct(r)}%`,\n    height: `${CELL_HEIGHT_PCT}%`,\n    left: `${cellLeftPct(c)}%`,\n    width: `${CELL_WIDTH_PCT}%`,\n  };\n  const isBottomRow = r === N_ROWS - 1;\n  xAxes.push({\n    ...rect,\n    min: X_MIN,\n    max: X_MAX,\n    tickAmount: 4,\n    startOnTick: false,\n    endOnTick: false,\n    // Only the bottom row draws the axis line + ticks — otherwise every\n    // interior row leaves a floating bracket where its (hidden-label) axis\n    // line would sit between facet rows.\n    lineWidth: isBottomRow ? 1 : 0,\n    tickWidth: isBottomRow ? 1 : 0,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    title: { text: null },\n    labels: { enabled: isBottomRow, style: { color: t.inkSoft, fontSize: \"14px\" } },\n  });\n  yAxes.push({\n    ...rect,\n    min: Y_MIN,\n    max: Y_MAX,\n    tickAmount: 4,\n    startOnTick: false,\n    endOnTick: false,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    title: { text: null },\n    labels: { enabled: c === 0, style: { color: t.inkSoft, fontSize: \"14px\" } },\n  });\n\n  // Least-squares trend line so the rainfall -> yield relationship reads as\n  // the focal insight rather than something the viewer infers from the raw scatter.\n  const n = cell.points.length;\n  const sumX = cell.points.reduce((acc, [x]) => acc + x, 0);\n  const sumY = cell.points.reduce((acc, [, y]) => acc + y, 0);\n  const sumXY = cell.points.reduce((acc, [x, y]) => acc + x * y, 0);\n  const sumXX = cell.points.reduce((acc, [x]) => acc + x * x, 0);\n  const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);\n  const intercept = (sumY - slope * sumX) / n;\n\n  series.push({\n    type: \"scatter\",\n    name: `${cell.rowFacet} × ${cell.colFacet}`,\n    xAxis: idx,\n    yAxis: idx,\n    color: t.palette[0],\n    data: cell.points,\n    marker: { radius: 4.5, symbol: \"circle\", states: { hover: { enabled: true, radiusPlus: 1.5 } } },\n    showInLegend: false,\n  });\n  series.push({\n    type: \"line\",\n    xAxis: idx,\n    yAxis: idx,\n    color: t.inkSoft,\n    opacity: 0.55,\n    lineWidth: 1.5,\n    dashStyle: \"ShortDash\",\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n    data: [\n      [X_MIN, intercept + slope * X_MIN],\n      [X_MAX, intercept + slope * X_MAX],\n    ],\n  });\n});\n\nconst TITLE = \"facet-grid · javascript · highcharts · anyplot.ai\";\nconst titleFontSize = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length))) + \"px\";\n\n// --- Chart -------------------------------------------------------------------\nconst chart = Highcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    margin: [125, 90, 90, 100],\n  },\n  credits: { enabled: false },\n  title: { text: TITLE, style: { color: t.ink, fontSize: titleFontSize, fontWeight: \"600\" } },\n  xAxis: xAxes,\n  yAxis: yAxes,\n  legend: { enabled: false },\n  tooltip: { pointFormat: \"Rainfall: {point.x} mm<br/>Yield: {point.y} t/ha\" },\n  plotOptions: { series: { animation: false } },\n  series,\n});\n\n// --- Facet strip labels (column headers on top, row headers on the right) --\nCOL_FACETS.forEach((label, c) => {\n  const x = chart.plotLeft + ((cellLeftPct(c) + CELL_WIDTH_PCT / 2) / 100) * chart.plotWidth;\n  chart.renderer\n    .text(label, x, chart.plotTop - 14)\n    .attr({ align: \"center\", zIndex: 5 })\n    .css({ color: t.ink, fontSize: \"15px\", fontWeight: \"600\" })\n    .add();\n});\n\nROW_FACETS.forEach((label, r) => {\n  const y = chart.plotTop + ((cellTopPct(r) + CELL_HEIGHT_PCT / 2) / 100) * chart.plotHeight;\n  chart.renderer\n    .text(label, chart.plotLeft + chart.plotWidth + 22, y)\n    .attr({ align: \"center\", rotation: -90, zIndex: 5 })\n    .css({ color: t.ink, fontSize: \"15px\", fontWeight: \"600\" })\n    .add();\n});\n\n// --- Shared axis titles ------------------------------------------------------\nchart.renderer\n  .text(\"Rainfall (mm)\", chart.plotLeft + chart.plotWidth / 2, chart.plotTop + chart.plotHeight + 55)\n  .attr({ align: \"center\", zIndex: 5 })\n  .css({ color: t.inkSoft, fontSize: \"16px\" })\n  .add();\n\nchart.renderer\n  .text(\"Yield (tons/hectare)\", 28, chart.plotTop + chart.plotHeight / 2)\n  .attr({ align: \"center\", rotation: -90, zIndex: 5 })\n  .css({ color: t.inkSoft, fontSize: \"16px\" })\n  .add();\n"}