{"spec_id":"column-stratigraphic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// column-stratigraphic: Stratigraphic Column with Lithology Patterns\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 93/100 | Created: 2026-06-17\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME;\nconst INK = t.ink;\nconst INK_SOFT = t.inkSoft;\n\n// --- Lithology types (>=5 distinct FGDC-style fill patterns) ----------------\n// First-appearing lithology (sandstone) takes Imprint position 1 (#009E73);\n// the rest follow the canonical Imprint order.\nconst LITHOS = [\n  { key: \"sandstone\", label: \"Sandstone\", color: t.palette[0], kind: \"dots\" },\n  { key: \"shale\", label: \"Shale\", color: t.palette[1], kind: \"hlines\" },\n  { key: \"limestone\", label: \"Limestone\", color: t.palette[2], kind: \"brick\" },\n  { key: \"siltstone\", label: \"Siltstone\", color: t.palette[3], kind: \"dashes\" },\n  { key: \"conglomerate\", label: \"Conglomerate\", color: t.palette[4], kind: \"pebbles\" },\n];\n\n// --- Data: synthetic borehole BH-01, surface downward (depth increases down) -\n// Layers ordered top (shallowest) -> bottom (deepest).\nconst layers = [\n  { formation: \"Dune Sandstone\", litho: \"sandstone\", thickness: 42, age: \"Neogene\" },\n  { formation: \"Bayfield Shale\", litho: \"shale\", thickness: 64, age: \"Paleogene\" },\n  { formation: \"Hawk Ridge Limestone\", litho: \"limestone\", thickness: 50, age: \"Cretaceous\" },\n  { formation: \"Gray Mesa Siltstone\", litho: \"siltstone\", thickness: 36, age: \"Cretaceous\" },\n  { formation: \"Ironcliff Conglomerate\", litho: \"conglomerate\", thickness: 28, age: \"Jurassic\" },\n  { formation: \"Redwall Limestone\", litho: \"limestone\", thickness: 58, age: \"Triassic\" },\n  { formation: \"Echo Sandstone\", litho: \"sandstone\", thickness: 46, age: \"Permian\" },\n  { formation: \"Slate Creek Shale\", litho: \"shale\", thickness: 54, age: \"Permian\" },\n  { formation: \"Foundation Siltstone\", litho: \"siltstone\", thickness: 40, age: \"Carboniferous\" },\n];\n\n// Angular unconformity sits below this layer index (Jurassic over Triassic).\nconst UNCONFORMITY_BELOW = 4;\n\nconst totalDepth = layers.reduce((s, l) => s + l.thickness, 0);\n\n// --- Mount ------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\nconst ctx2d = canvas.getContext(\"2d\");\n\n// --- Lithology fill patterns (canvas tiles -> CanvasPattern) ----------------\nconst hexA = (hex, a) => {\n  const n = parseInt(hex.slice(1), 16);\n  return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${a})`;\n};\n\nconst makePattern = (color, kind) => {\n  const s = 24;\n  const tile = document.createElement(\"canvas\");\n  tile.width = s;\n  tile.height = s;\n  const c = tile.getContext(\"2d\");\n  c.fillStyle = hexA(color, 0.2); // faint colour wash so the block reads as colour + texture\n  c.fillRect(0, 0, s, s);\n  c.strokeStyle = color;\n  c.fillStyle = color;\n  c.lineWidth = 1.6;\n  c.lineCap = \"round\";\n\n  if (kind === \"dots\") {\n    // sandstone — stipple dots\n    [[6, 6], [18, 7], [12, 13], [5, 19], [19, 19]].forEach(([x, y]) => {\n      c.beginPath();\n      c.arc(x, y, 1.8, 0, Math.PI * 2);\n      c.fill();\n    });\n  } else if (kind === \"hlines\") {\n    // shale — horizontal dashes\n    [5, 12, 19].forEach((y) => {\n      c.beginPath();\n      c.moveTo(0, y);\n      c.lineTo(s, y);\n      c.stroke();\n    });\n  } else if (kind === \"brick\") {\n    // limestone — offset brick courses\n    for (let y = 0; y <= s; y += 8) {\n      c.beginPath();\n      c.moveTo(0, y);\n      c.lineTo(s, y);\n      c.stroke();\n    }\n    let row = 0;\n    for (let y = 0; y < s; y += 8) {\n      const xs = row % 2 === 0 ? [0, 12, 24] : [6, 18];\n      xs.forEach((x) => {\n        c.beginPath();\n        c.moveTo(x, y);\n        c.lineTo(x, y + 8);\n        c.stroke();\n      });\n      row += 1;\n    }\n  } else if (kind === \"dashes\") {\n    // siltstone — short random dashes\n    [[3, 5, 9, 6], [14, 8, 21, 9], [5, 15, 11, 17], [16, 17, 22, 18], [8, 20, 14, 22]].forEach(\n      ([x1, y1, x2, y2]) => {\n        c.beginPath();\n        c.moveTo(x1, y1);\n        c.lineTo(x2, y2);\n        c.stroke();\n      }\n    );\n  } else if (kind === \"pebbles\") {\n    // conglomerate — outlined pebbles\n    c.lineWidth = 1.4;\n    [[7, 7, 4], [17, 9, 3], [12, 17, 4.5], [21, 19, 2.4], [3, 19, 2.6]].forEach(([x, y, r]) => {\n      c.beginPath();\n      c.arc(x, y, r, 0, Math.PI * 2);\n      c.stroke();\n    });\n  }\n  return ctx2d.createPattern(tile, \"repeat\");\n};\n\nconst patterns = {};\nLITHOS.forEach((l) => {\n  patterns[l.key] = makePattern(l.color, l.kind);\n});\n\n// --- Datasets: one stacked segment per layer (thickness on a reversed depth axis)\nconst datasets = layers.map((l) => ({\n  label: l.formation,\n  data: [l.thickness],\n  backgroundColor: patterns[l.litho],\n  borderColor: INK,\n  borderWidth: 1.6,\n  borderSkipped: false,\n  categoryPercentage: 0.92,\n  barPercentage: 0.34,\n}));\n\n// --- Plugin: formation / age / thickness labels, unconformity --------------\nconst annotate = {\n  id: \"strataLabels\",\n  afterDatasetsDraw(chart) {\n    const ctx = chart.ctx;\n    const first = chart.getDatasetMeta(0).data[0];\n    if (!first) return;\n    const colX = first.x;\n    const halfW = first.width / 2;\n    const leftEdge = colX - halfW;\n    const rightEdge = colX + halfW;\n\n    ctx.save();\n    ctx.textBaseline = \"middle\";\n\n    layers.forEach((layer, i) => {\n      const bar = chart.getDatasetMeta(i).data[0];\n      if (!bar) return;\n      const top = Math.min(bar.y, bar.base);\n      const bot = Math.max(bar.y, bar.base);\n      const cy = (top + bot) / 2;\n\n      // Age label — left of the column\n      ctx.fillStyle = INK_SOFT;\n      ctx.textAlign = \"right\";\n      ctx.font = \"500 15px -apple-system, Segoe UI, Roboto, sans-serif\";\n      ctx.fillText(layer.age, leftEdge - 18, cy);\n\n      // Formation name + thickness — right of the column\n      ctx.textAlign = \"left\";\n      ctx.fillStyle = INK;\n      ctx.font = \"600 16px -apple-system, Segoe UI, Roboto, sans-serif\";\n      ctx.fillText(layer.formation, rightEdge + 18, cy - 9);\n      ctx.fillStyle = INK_SOFT;\n      ctx.font = \"400 13px -apple-system, Segoe UI, Roboto, sans-serif\";\n      ctx.fillText(`${layer.thickness} m`, rightEdge + 18, cy + 11);\n    });\n\n    // Angular unconformity — wavy boundary line drawn over the straight border\n    const uncBar = chart.getDatasetMeta(UNCONFORMITY_BELOW).data[0];\n    if (uncBar) {\n      const yU = Math.max(uncBar.y, uncBar.base);\n      ctx.strokeStyle = INK;\n      ctx.lineWidth = 3;\n      ctx.beginPath();\n      const amp = 4;\n      const period = 26;\n      for (let x = leftEdge; x <= rightEdge; x += 2) {\n        const y = yU + amp * Math.sin(((x - leftEdge) / period) * Math.PI * 2);\n        x === leftEdge ? ctx.moveTo(x, y) : ctx.lineTo(x, y);\n      }\n      ctx.stroke();\n      ctx.fillStyle = INK_SOFT;\n      ctx.textAlign = \"left\";\n      ctx.font = \"italic 13px -apple-system, Segoe UI, Roboto, sans-serif\";\n      ctx.fillText(\"~ angular unconformity\", rightEdge + 18, yU);\n    }\n    ctx.restore();\n  },\n};\n\n// --- Chart ------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: { labels: [\"BH-01 · Onshore Basin\"], datasets },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 6, right: 24, bottom: 6, left: 12 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"column-stratigraphic · javascript · chartjs · anyplot.ai\",\n        color: INK,\n        font: { size: 22, weight: \"600\" },\n        padding: { bottom: 14 },\n      },\n      legend: {\n        position: \"top\",\n        onClick: () => {},\n        labels: {\n          color: INK,\n          font: { size: 15 },\n          boxWidth: 26,\n          boxHeight: 15,\n          padding: 20,\n          generateLabels: () =>\n            LITHOS.map((l) => ({\n              text: l.label,\n              fillStyle: patterns[l.key],\n              strokeStyle: INK,\n              lineWidth: 1.4,\n            })),\n        },\n      },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        stacked: true,\n        grid: { display: false },\n        ticks: { color: INK_SOFT, font: { size: 15 } },\n      },\n      y: {\n        stacked: true,\n        reverse: true,\n        min: 0,\n        max: totalDepth,\n        title: { display: true, text: \"Depth below surface (m)\", color: INK, font: { size: 16 } },\n        ticks: { color: INK_SOFT, font: { size: 14 }, stepSize: 50 },\n        grid: { color: t.grid },\n      },\n    },\n  },\n  plugins: [annotate],\n});\n"}