Stratigraphic Column with Lithology Patterns — Chart.js

A vertical column visualization showing geological rock layers with standardized lithology patterns, formation names, ages, and thickness scales. Each layer is represented as a stacked rectangular block filled with a distinctive pattern (e.g., brick pattern for limestone, dots for sandstone, dashes for shale) following FGDC/USGS conventions. This plot is essential for communicating subsurface geology and sedimentary sequences in a compact, standardized format.

Stratigraphic Column with Lithology Patterns rendered with Chart.js

JavaScript source (Chart.js)

// anyplot.ai
// column-stratigraphic: Stratigraphic Column with Lithology Patterns
// Library: chartjs 4.4.7 | JavaScript 22.22.3
// Quality: 93/100 | Created: 2026-06-17
//# anyplot-orientation: landscape

const t = window.ANYPLOT_TOKENS;
const THEME = window.ANYPLOT_THEME;
const INK = t.ink;
const INK_SOFT = t.inkSoft;

// --- Lithology types (>=5 distinct FGDC-style fill patterns) ----------------
// First-appearing lithology (sandstone) takes Imprint position 1 (#009E73);
// the rest follow the canonical Imprint order.
const LITHOS = [
  { key: "sandstone", label: "Sandstone", color: t.palette[0], kind: "dots" },
  { key: "shale", label: "Shale", color: t.palette[1], kind: "hlines" },
  { key: "limestone", label: "Limestone", color: t.palette[2], kind: "brick" },
  { key: "siltstone", label: "Siltstone", color: t.palette[3], kind: "dashes" },
  { key: "conglomerate", label: "Conglomerate", color: t.palette[4], kind: "pebbles" },
];

// --- Data: synthetic borehole BH-01, surface downward (depth increases down) -
// Layers ordered top (shallowest) -> bottom (deepest).
const layers = [
  { formation: "Dune Sandstone", litho: "sandstone", thickness: 42, age: "Neogene" },
  { formation: "Bayfield Shale", litho: "shale", thickness: 64, age: "Paleogene" },
  { formation: "Hawk Ridge Limestone", litho: "limestone", thickness: 50, age: "Cretaceous" },
  { formation: "Gray Mesa Siltstone", litho: "siltstone", thickness: 36, age: "Cretaceous" },
  { formation: "Ironcliff Conglomerate", litho: "conglomerate", thickness: 28, age: "Jurassic" },
  { formation: "Redwall Limestone", litho: "limestone", thickness: 58, age: "Triassic" },
  { formation: "Echo Sandstone", litho: "sandstone", thickness: 46, age: "Permian" },
  { formation: "Slate Creek Shale", litho: "shale", thickness: 54, age: "Permian" },
  { formation: "Foundation Siltstone", litho: "siltstone", thickness: 40, age: "Carboniferous" },
];

// Angular unconformity sits below this layer index (Jurassic over Triassic).
const UNCONFORMITY_BELOW = 4;

const totalDepth = layers.reduce((s, l) => s + l.thickness, 0);

// --- Mount ------------------------------------------------------------------
const canvas = document.createElement("canvas");
document.getElementById("container").appendChild(canvas);
const ctx2d = canvas.getContext("2d");

// --- Lithology fill patterns (canvas tiles -> CanvasPattern) ----------------
const hexA = (hex, a) => {
  const n = parseInt(hex.slice(1), 16);
  return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${a})`;
};

const makePattern = (color, kind) => {
  const s = 24;
  const tile = document.createElement("canvas");
  tile.width = s;
  tile.height = s;
  const c = tile.getContext("2d");
  c.fillStyle = hexA(color, 0.2); // faint colour wash so the block reads as colour + texture
  c.fillRect(0, 0, s, s);
  c.strokeStyle = color;
  c.fillStyle = color;
  c.lineWidth = 1.6;
  c.lineCap = "round";

  if (kind === "dots") {
    // sandstone — stipple dots
    [[6, 6], [18, 7], [12, 13], [5, 19], [19, 19]].forEach(([x, y]) => {
      c.beginPath();
      c.arc(x, y, 1.8, 0, Math.PI * 2);
      c.fill();
    });
  } else if (kind === "hlines") {
    // shale — horizontal dashes
    [5, 12, 19].forEach((y) => {
      c.beginPath();
      c.moveTo(0, y);
      c.lineTo(s, y);
      c.stroke();
    });
  } else if (kind === "brick") {
    // limestone — offset brick courses
    for (let y = 0; y <= s; y += 8) {
      c.beginPath();
      c.moveTo(0, y);
      c.lineTo(s, y);
      c.stroke();
    }
    let row = 0;
    for (let y = 0; y < s; y += 8) {
      const xs = row % 2 === 0 ? [0, 12, 24] : [6, 18];
      xs.forEach((x) => {
        c.beginPath();
        c.moveTo(x, y);
        c.lineTo(x, y + 8);
        c.stroke();
      });
      row += 1;
    }
  } else if (kind === "dashes") {
    // siltstone — short random dashes
    [[3, 5, 9, 6], [14, 8, 21, 9], [5, 15, 11, 17], [16, 17, 22, 18], [8, 20, 14, 22]].forEach(
      ([x1, y1, x2, y2]) => {
        c.beginPath();
        c.moveTo(x1, y1);
        c.lineTo(x2, y2);
        c.stroke();
      }
    );
  } else if (kind === "pebbles") {
    // conglomerate — outlined pebbles
    c.lineWidth = 1.4;
    [[7, 7, 4], [17, 9, 3], [12, 17, 4.5], [21, 19, 2.4], [3, 19, 2.6]].forEach(([x, y, r]) => {
      c.beginPath();
      c.arc(x, y, r, 0, Math.PI * 2);
      c.stroke();
    });
  }
  return ctx2d.createPattern(tile, "repeat");
};

const patterns = {};
LITHOS.forEach((l) => {
  patterns[l.key] = makePattern(l.color, l.kind);
});

// --- Datasets: one stacked segment per layer (thickness on a reversed depth axis)
const datasets = layers.map((l) => ({
  label: l.formation,
  data: [l.thickness],
  backgroundColor: patterns[l.litho],
  borderColor: INK,
  borderWidth: 1.6,
  borderSkipped: false,
  categoryPercentage: 0.92,
  barPercentage: 0.34,
}));

// --- Plugin: formation / age / thickness labels, unconformity --------------
const annotate = {
  id: "strataLabels",
  afterDatasetsDraw(chart) {
    const ctx = chart.ctx;
    const first = chart.getDatasetMeta(0).data[0];
    if (!first) return;
    const colX = first.x;
    const halfW = first.width / 2;
    const leftEdge = colX - halfW;
    const rightEdge = colX + halfW;

    ctx.save();
    ctx.textBaseline = "middle";

    layers.forEach((layer, i) => {
      const bar = chart.getDatasetMeta(i).data[0];
      if (!bar) return;
      const top = Math.min(bar.y, bar.base);
      const bot = Math.max(bar.y, bar.base);
      const cy = (top + bot) / 2;

      // Age label — left of the column
      ctx.fillStyle = INK_SOFT;
      ctx.textAlign = "right";
      ctx.font = "500 15px -apple-system, Segoe UI, Roboto, sans-serif";
      ctx.fillText(layer.age, leftEdge - 18, cy);

      // Formation name + thickness — right of the column
      ctx.textAlign = "left";
      ctx.fillStyle = INK;
      ctx.font = "600 16px -apple-system, Segoe UI, Roboto, sans-serif";
      ctx.fillText(layer.formation, rightEdge + 18, cy - 9);
      ctx.fillStyle = INK_SOFT;
      ctx.font = "400 13px -apple-system, Segoe UI, Roboto, sans-serif";
      ctx.fillText(`${layer.thickness} m`, rightEdge + 18, cy + 11);
    });

    // Angular unconformity — wavy boundary line drawn over the straight border
    const uncBar = chart.getDatasetMeta(UNCONFORMITY_BELOW).data[0];
    if (uncBar) {
      const yU = Math.max(uncBar.y, uncBar.base);
      ctx.strokeStyle = INK;
      ctx.lineWidth = 3;
      ctx.beginPath();
      const amp = 4;
      const period = 26;
      for (let x = leftEdge; x <= rightEdge; x += 2) {
        const y = yU + amp * Math.sin(((x - leftEdge) / period) * Math.PI * 2);
        x === leftEdge ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
      }
      ctx.stroke();
      ctx.fillStyle = INK_SOFT;
      ctx.textAlign = "left";
      ctx.font = "italic 13px -apple-system, Segoe UI, Roboto, sans-serif";
      ctx.fillText("~ angular unconformity", rightEdge + 18, yU);
    }
    ctx.restore();
  },
};

// --- Chart ------------------------------------------------------------------
new Chart(canvas, {
  type: "bar",
  data: { labels: ["BH-01 · Onshore Basin"], datasets },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    animation: false,
    layout: { padding: { top: 6, right: 24, bottom: 6, left: 12 } },
    plugins: {
      title: {
        display: true,
        text: "column-stratigraphic · javascript · chartjs · anyplot.ai",
        color: INK,
        font: { size: 22, weight: "600" },
        padding: { bottom: 14 },
      },
      legend: {
        position: "top",
        onClick: () => {},
        labels: {
          color: INK,
          font: { size: 15 },
          boxWidth: 26,
          boxHeight: 15,
          padding: 20,
          generateLabels: () =>
            LITHOS.map((l) => ({
              text: l.label,
              fillStyle: patterns[l.key],
              strokeStyle: INK,
              lineWidth: 1.4,
            })),
        },
      },
      tooltip: { enabled: false },
    },
    scales: {
      x: {
        stacked: true,
        grid: { display: false },
        ticks: { color: INK_SOFT, font: { size: 15 } },
      },
      y: {
        stacked: true,
        reverse: true,
        min: 0,
        max: totalDepth,
        title: { display: true, text: "Depth below surface (m)", color: INK, font: { size: 16 } },
        ticks: { color: INK_SOFT, font: { size: 14 }, stepSize: 50 },
        grid: { color: t.grid },
      },
    },
  },
  plugins: [annotate],
});

Part of Stratigraphic Column with Lithology Patterns on anyplot.ai.

Other implementations