{"spec_id":"column-stratigraphic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// column-stratigraphic: Stratigraphic Column with Lithology Patterns\n// Library: highcharts 12.6.0 | JavaScript 22.22.3\n// Quality: 92/100 | Created: 2026-06-17\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: synthetic borehole section, top → bottom, depth increasing downward\n// `top`/`bottom` are depths below surface in metres. Lithology drives the fill\n// pattern; the tint is assigned in canonical Imprint order by first appearance.\nconst layers = [\n  { litho: \"sandstone\", formation: \"Navajo Fm\", age: \"Jurassic · 190 Ma\", top: 0, bottom: 45 },\n  { litho: \"shale\", formation: \"Carmel Fm\", age: \"Jurassic · 170 Ma\", top: 45, bottom: 80 },\n  { litho: \"limestone\", formation: \"Kaibab Fm\", age: \"Permian · 270 Ma\", top: 80, bottom: 140 },\n  { litho: \"siltstone\", formation: \"Toroweap Fm\", age: \"Permian · 275 Ma\", top: 140, bottom: 175 },\n  { litho: \"sandstone\", formation: \"Coconino Fm\", age: \"Permian · 280 Ma\", top: 175, bottom: 230 },\n  { litho: \"shale\", formation: \"Hermit Fm\", age: \"Permian · 285 Ma\", top: 230, bottom: 262 },\n  { litho: \"conglomerate\", formation: \"Supai Group\", age: \"Pennsylvanian · 310 Ma\", top: 262, bottom: 300 },\n  { litho: \"limestone\", formation: \"Redwall Ls\", age: \"Mississippian · 340 Ma\", top: 300, bottom: 360 },\n  { litho: \"mudstone\", formation: \"Bright Angel Sh\", age: \"Cambrian · 510 Ma\", top: 360, bottom: 398 },\n];\n\nconst total = layers[layers.length - 1].bottom; // deepest depth = stack height\n\n// Assign an Imprint tint to each lithology by order of first appearance\n// (first categorical series is therefore always #009E73 — palette[0]).\nconst lithoOrder = [];\nlayers.forEach((l) => {\n  if (!lithoOrder.includes(l.litho)) lithoOrder.push(l.litho);\n});\nconst tintFor = {};\nlithoOrder.forEach((k, i) => {\n  tintFor[k] = t.palette[i % t.palette.length];\n});\n\nconst cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);\n\n// --- SVG pattern texture per lithology (approximating FGDC/USGS symbols).\n// Each entry returns the foreground texture children; the harness injects them\n// into the chart's <defs> after load so the `url(#litho-*)` fills resolve.\nconst dot = (x, y, r) => ({ tag: \"circle\", attrs: { cx: x, cy: y, r, fill: t.ink, \"fill-opacity\": 0.7 } });\nconst seg = (x1, y1, x2, y2, w) => ({\n  tag: \"line\",\n  attrs: { x1, y1, x2, y2, stroke: t.ink, \"stroke-width\": w || 1, \"stroke-opacity\": 0.6, \"stroke-linecap\": \"round\" },\n});\nconst ring = (x, y, r) => ({\n  tag: \"circle\",\n  attrs: { cx: x, cy: y, r, fill: \"none\", stroke: t.ink, \"stroke-width\": 1, \"stroke-opacity\": 0.6 },\n});\n\nconst TEXTURE = {\n  // Stipple dots → sandstone\n  sandstone: { w: 12, h: 12, kids: [dot(3, 3, 0.95), dot(9, 9, 0.95), dot(9, 3, 0.95), dot(3, 9, 0.95), dot(6, 6, 0.95)] },\n  // Horizontal dashes → shale\n  shale: { w: 14, h: 9, kids: [seg(0, 2.5, 6, 2.5), seg(8, 6.5, 14, 6.5)] },\n  // Brick / blocky → limestone\n  limestone: { w: 18, h: 10, kids: [seg(0, 5, 18, 5), seg(9, 0, 9, 5), seg(0, 5, 0, 10), seg(18, 5, 18, 10)] },\n  // Random short dashes → siltstone\n  siltstone: { w: 16, h: 16, kids: [seg(2, 4, 6, 6), seg(10, 2, 13, 6), seg(4, 12, 8, 10), seg(11, 13, 14, 11)] },\n  // Pebbles / clasts → conglomerate\n  conglomerate: { w: 20, h: 20, kids: [ring(6, 6, 3), ring(15, 9, 2.4), ring(10, 16, 2.8), ring(17, 17, 1.7)] },\n  // Fine horizontal lines → mudstone\n  mudstone: { w: 8, h: 6, kids: [seg(0, 3, 8, 3, 0.8)] },\n};\n\n// --- Build one stacked series per layer (single category column). With the\n// y-axis reversed + normal stacking, series[0] (the shallowest layer) sits at\n// the top, so depth increases downward as required.\nconst seen = new Set();\nconst series = layers.map((l) => {\n  const firstOfKind = !seen.has(l.litho);\n  seen.add(l.litho);\n  return {\n    name: cap(l.litho),\n    color: `url(#litho-${l.litho})`,\n    showInLegend: firstOfKind,\n    data: [{ y: l.bottom - l.top, formation: l.formation, age: l.age }],\n  };\n});\n\n// --- Chart ------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"column\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    spacing: [24, 28, 24, 24],\n    style: { fontFamily: \"inherit\" },\n    events: {\n      // Inject lithology patterns into the chart SVG <defs> so `url(#litho-*)`\n      // fills resolve (the core bundle ships no pattern-fill module).\n      load() {\n        const R = this.renderer;\n        lithoOrder.forEach((k) => {\n          const tex = TEXTURE[k];\n          const pat = R.createElement(\"pattern\").attr({\n            id: `litho-${k}`,\n            patternUnits: \"userSpaceOnUse\",\n            width: tex.w,\n            height: tex.h,\n          });\n          // tinted background — identical hue across light/dark themes\n          R.createElement(\"rect\").attr({ width: tex.w, height: tex.h, fill: tintFor[k], \"fill-opacity\": 0.58 }).add(pat);\n          tex.kids.forEach((c) => R.createElement(c.tag).attr(c.attrs).add(pat));\n          pat.add(R.defs);\n        });\n\n        // --- Left-side geologic time scale: group consecutive layers by period\n        // and draw a labelled bracket spanning each period's depth range. This\n        // fills the otherwise-empty left band and satisfies the spec hint that\n        // age/period names belong on the left side.\n        const periods = [];\n        layers.forEach((l) => {\n          const name = l.age.split(\" · \")[0];\n          const prev = periods[periods.length - 1];\n          if (prev && prev.name === name) prev.bottom = l.bottom;\n          else periods.push({ name, top: l.top, bottom: l.bottom });\n        });\n        const yA = this.yAxis[0];\n        const xLine = this.plotLeft + 34; // vertical spine of the bracket\n        const xTip = xLine + 16; // horizontal caps point toward the column\n        periods.forEach((p) => {\n          const yTop = yA.toPixels(p.top);\n          const yBot = yA.toPixels(p.bottom);\n          R.path([\"M\", xTip, yTop, \"L\", xLine, yTop, \"L\", xLine, yBot, \"L\", xTip, yBot])\n            .attr({ stroke: t.inkSoft, \"stroke-width\": 1.5, fill: \"none\" })\n            .add();\n          R.text(p.name, xTip + 14, (yTop + yBot) / 2 + 6)\n            .attr({ align: \"left\" })\n            .css({ color: t.ink, fontSize: \"17px\", fontWeight: \"600\" })\n            .add();\n        });\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"column-stratigraphic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Synthetic borehole BH-1 · Colorado Plateau · lithology by fill pattern\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    categories: [\"BH-1\"],\n    visible: false,\n  },\n  yAxis: {\n    min: 0,\n    max: total,\n    reversed: true, // depth increases downward\n    reversedStacks: false, // keep series[0] (shallowest) at the top of the stack\n    tickInterval: 50,\n    title: { text: \"Depth below surface (m)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    gridLineColor: t.grid,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { format: \"{value} m\", style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: {\n    title: { text: \"Lithology\", style: { color: t.ink, fontSize: \"14px\", fontWeight: \"600\" } },\n    layout: \"vertical\",\n    align: \"right\",\n    verticalAlign: \"middle\",\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n    symbolRadius: 2,\n    symbolHeight: 16,\n    symbolWidth: 16,\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    borderWidth: 1,\n    borderRadius: 6,\n    padding: 14,\n    itemMarginBottom: 6,\n  },\n  tooltip: { enabled: false },\n  plotOptions: {\n    series: {\n      animation: false,\n      states: { hover: { enabled: false }, inactive: { opacity: 1 } },\n    },\n    column: {\n      stacking: \"normal\",\n      borderColor: t.ink, // solid layer-boundary lines\n      borderWidth: 1.5,\n      groupPadding: 0.05,\n      pointPadding: 0,\n      maxPointWidth: 520,\n      dataLabels: {\n        enabled: true,\n        inside: true,\n        align: \"center\",\n        verticalAlign: \"middle\",\n        useHTML: true,\n        allowOverlap: true,\n        crop: false,\n        overflow: \"allow\",\n        formatter() {\n          const p = this.point;\n          const formation = p.formation || p.options.formation;\n          const age = p.age || p.options.age;\n          return (\n            `<div style=\"text-align:center;background:${t.pageBg}d9;border:1px solid ${t.grid};` +\n            `border-radius:5px;padding:3px 10px;white-space:nowrap;\">` +\n            `<span style=\"color:${t.ink};font-weight:600;font-size:13px;\">${formation}</span><br>` +\n            `<span style=\"color:${t.inkSoft};font-size:11px;\">${age}</span></div>`\n          );\n        },\n        style: { textOutline: \"none\" },\n      },\n    },\n  },\n  series,\n});\n"}