Faceted Grid Plot — Highcharts

A grid of subplots where each cell shows the same type of plot for a different subset of data, split by one or two categorical variables. Enables systematic comparison across multiple dimensions.

Faceted Grid Plot rendered with Highcharts

Renders

JavaScript source (Highcharts)

// anyplot.ai
// facet-grid: Faceted Grid Plot
// Library: highcharts 12.6.0 | JavaScript 22.23.2
// Quality: 81/100 | Created: 2026-09-05

// Only the core `highcharts` bundle is loaded (no highcharts-more / grid module),
// so the facet grid is built the way Highcharts' own "small multiples" demo does
// it: one xAxis/yAxis pair per cell, each pinned to a percentage rectangle of
// the shared plot area via top/left/width/height, all sharing the same min/max
// so every panel reads off the same scale. Row and column strip labels are
// drawn with the core SVG renderer, mirroring ggplot2's facet_grid strips.
const t = window.ANYPLOT_TOKENS;

// --- Deterministic PRNG (mulberry32 + Box-Muller) --------------------------
function mulberry32(seed) {
  return function random() {
    seed = (seed + 0x6d2b79f5) | 0;
    let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;
    return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
  };
}
const rand = mulberry32(42);
function randNormal(mean, std) {
  const u1 = rand();
  const u2 = rand();
  return mean + std * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

// --- Data: crop yield vs. rainfall, faceted by soil type (rows) x fertilizer (columns) ---
const ROW_FACETS = ["Clay Soil", "Sandy Soil"];
const COL_FACETS = ["No Fertilizer", "Organic", "Synthetic"];
const POINTS_PER_CELL = 50;
const SOIL_EFFECT = { "Clay Soil": 0.6, "Sandy Soil": 0 };
const FERTILIZER_EFFECT = { "No Fertilizer": 0, Organic: 1.1, Synthetic: 1.9 };

const cells = [];
ROW_FACETS.forEach((rowFacet) => {
  COL_FACETS.forEach((colFacet) => {
    const points = [];
    for (let i = 0; i < POINTS_PER_CELL; i += 1) {
      const rainfall = 250 + rand() * 700;
      const yieldTons =
        2.2 + rainfall * 0.0028 + SOIL_EFFECT[rowFacet] + FERTILIZER_EFFECT[colFacet] + randNormal(0, 0.35);
      points.push([Math.round(rainfall * 10) / 10, Math.round(yieldTons * 100) / 100]);
    }
    cells.push({ rowFacet, colFacet, points });
  });
});

const allX = cells.flatMap((cell) => cell.points.map((p) => p[0]));
const allY = cells.flatMap((cell) => cell.points.map((p) => p[1]));
const xPad = (Math.max(...allX) - Math.min(...allX)) * 0.08;
const yPad = (Math.max(...allY) - Math.min(...allY)) * 0.08;
const X_MIN = Math.min(...allX) - xPad;
const X_MAX = Math.max(...allX) + xPad;
const Y_MIN = Math.min(...allY) - yPad;
const Y_MAX = Math.max(...allY) + yPad;

// --- Grid geometry (percentages of the shared plot area) -------------------
const N_ROWS = ROW_FACETS.length;
const N_COLS = COL_FACETS.length;
const COL_GUTTER_PCT = 4;
const ROW_GUTTER_PCT = 6;
const CELL_WIDTH_PCT = (100 - COL_GUTTER_PCT * (N_COLS - 1)) / N_COLS;
const CELL_HEIGHT_PCT = (100 - ROW_GUTTER_PCT * (N_ROWS - 1)) / N_ROWS;
const cellLeftPct = (c) => c * (CELL_WIDTH_PCT + COL_GUTTER_PCT);
const cellTopPct = (r) => r * (CELL_HEIGHT_PCT + ROW_GUTTER_PCT);

const xAxes = [];
const yAxes = [];
const series = [];
cells.forEach((cell, idx) => {
  const r = ROW_FACETS.indexOf(cell.rowFacet);
  const c = COL_FACETS.indexOf(cell.colFacet);
  const rect = {
    top: `${cellTopPct(r)}%`,
    height: `${CELL_HEIGHT_PCT}%`,
    left: `${cellLeftPct(c)}%`,
    width: `${CELL_WIDTH_PCT}%`,
  };
  const isBottomRow = r === N_ROWS - 1;
  xAxes.push({
    ...rect,
    min: X_MIN,
    max: X_MAX,
    tickAmount: 4,
    startOnTick: false,
    endOnTick: false,
    // Only the bottom row draws the axis line + ticks — otherwise every
    // interior row leaves a floating bracket where its (hidden-label) axis
    // line would sit between facet rows.
    lineWidth: isBottomRow ? 1 : 0,
    tickWidth: isBottomRow ? 1 : 0,
    lineColor: t.inkSoft,
    tickColor: t.inkSoft,
    gridLineColor: t.grid,
    title: { text: null },
    labels: { enabled: isBottomRow, style: { color: t.inkSoft, fontSize: "14px" } },
  });
  yAxes.push({
    ...rect,
    min: Y_MIN,
    max: Y_MAX,
    tickAmount: 4,
    startOnTick: false,
    endOnTick: false,
    lineColor: t.inkSoft,
    tickColor: t.inkSoft,
    gridLineColor: t.grid,
    title: { text: null },
    labels: { enabled: c === 0, style: { color: t.inkSoft, fontSize: "14px" } },
  });

  // Least-squares trend line so the rainfall -> yield relationship reads as
  // the focal insight rather than something the viewer infers from the raw scatter.
  const n = cell.points.length;
  const sumX = cell.points.reduce((acc, [x]) => acc + x, 0);
  const sumY = cell.points.reduce((acc, [, y]) => acc + y, 0);
  const sumXY = cell.points.reduce((acc, [x, y]) => acc + x * y, 0);
  const sumXX = cell.points.reduce((acc, [x]) => acc + x * x, 0);
  const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
  const intercept = (sumY - slope * sumX) / n;

  series.push({
    type: "scatter",
    name: `${cell.rowFacet} × ${cell.colFacet}`,
    xAxis: idx,
    yAxis: idx,
    color: t.palette[0],
    data: cell.points,
    marker: { radius: 4.5, symbol: "circle", states: { hover: { enabled: true, radiusPlus: 1.5 } } },
    showInLegend: false,
  });
  series.push({
    type: "line",
    xAxis: idx,
    yAxis: idx,
    color: t.inkSoft,
    opacity: 0.55,
    lineWidth: 1.5,
    dashStyle: "ShortDash",
    marker: { enabled: false },
    enableMouseTracking: false,
    showInLegend: false,
    data: [
      [X_MIN, intercept + slope * X_MIN],
      [X_MAX, intercept + slope * X_MAX],
    ],
  });
});

const TITLE = "facet-grid · javascript · highcharts · anyplot.ai";
const titleFontSize = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length))) + "px";

// --- Chart -------------------------------------------------------------------
const chart = Highcharts.chart("container", {
  chart: {
    type: "scatter",
    backgroundColor: "transparent",
    animation: false,
    style: { fontFamily: "inherit" },
    margin: [125, 90, 90, 100],
  },
  credits: { enabled: false },
  title: { text: TITLE, style: { color: t.ink, fontSize: titleFontSize, fontWeight: "600" } },
  xAxis: xAxes,
  yAxis: yAxes,
  legend: { enabled: false },
  tooltip: { pointFormat: "Rainfall: {point.x} mm<br/>Yield: {point.y} t/ha" },
  plotOptions: { series: { animation: false } },
  series,
});

// --- Facet strip labels (column headers on top, row headers on the right) --
COL_FACETS.forEach((label, c) => {
  const x = chart.plotLeft + ((cellLeftPct(c) + CELL_WIDTH_PCT / 2) / 100) * chart.plotWidth;
  chart.renderer
    .text(label, x, chart.plotTop - 14)
    .attr({ align: "center", zIndex: 5 })
    .css({ color: t.ink, fontSize: "15px", fontWeight: "600" })
    .add();
});

ROW_FACETS.forEach((label, r) => {
  const y = chart.plotTop + ((cellTopPct(r) + CELL_HEIGHT_PCT / 2) / 100) * chart.plotHeight;
  chart.renderer
    .text(label, chart.plotLeft + chart.plotWidth + 22, y)
    .attr({ align: "center", rotation: -90, zIndex: 5 })
    .css({ color: t.ink, fontSize: "15px", fontWeight: "600" })
    .add();
});

// --- Shared axis titles ------------------------------------------------------
chart.renderer
  .text("Rainfall (mm)", chart.plotLeft + chart.plotWidth / 2, chart.plotTop + chart.plotHeight + 55)
  .attr({ align: "center", zIndex: 5 })
  .css({ color: t.inkSoft, fontSize: "16px" })
  .add();

chart.renderer
  .text("Yield (tons/hectare)", 28, chart.plotTop + chart.plotHeight / 2)
  .attr({ align: "center", rotation: -90, zIndex: 5 })
  .css({ color: t.inkSoft, fontSize: "16px" })
  .add();

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/facet-grid/highcharts/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "facet-grid",
  "language": "javascript",
  "library": "highcharts",
  "page": "https://anyplot.ai/facet-grid/javascript/highcharts",
  "hub": "https://anyplot.ai/facet-grid",
  "code_json": "https://api.anyplot.ai/specs/facet-grid/highcharts/code",
  "spec_json": "https://api.anyplot.ai/specs/facet-grid",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/facet-grid/javascript/highcharts/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/facet-grid/javascript/highcharts/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/facet-grid/javascript/highcharts/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/facet-grid/javascript/highcharts/plot-dark.html",
  "quality_score": 81.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Faceted Grid Plot on anyplot.ai.

Other implementations