{"spec_id":"column-stratigraphic","library":"d3","language":"javascript","code":"// anyplot.ai\n// column-stratigraphic: Stratigraphic Column with Lithology Patterns\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-17\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 92, right: 48, bottom: 56, left: 96 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: synthetic sedimentary section, depth increasing downward ---------\n// Imprint palette is assigned per lithology; the fill PATTERN is the primary\n// (redundant) encoding so the section reads even in grayscale.\nconst lithologies = {\n  Sandstone: { color: t.palette[0] }, // brand green — first categorical series\n  Shale: { color: t.palette[1] },\n  Limestone: { color: t.palette[2] },\n  Siltstone: { color: t.palette[3] },\n  Conglomerate: { color: t.palette[4] },\n  Mudstone: { color: t.palette[5] },\n};\n\nconst layers = [\n  { top: 0, bottom: 24, lithology: \"Sandstone\", formation: \"Mesa Verde Sandstone\", age: \"Cretaceous\" },\n  { top: 24, bottom: 58, lithology: \"Shale\", formation: \"Mancos Shale\", age: \"Cretaceous\" },\n  { top: 58, bottom: 72, lithology: \"Limestone\", formation: \"Greenhorn Limestone\", age: \"Cretaceous\" },\n  { top: 72, bottom: 104, lithology: \"Siltstone\", formation: \"Morrison Siltstone\", age: \"Jurassic\" },\n  { top: 104, bottom: 132, lithology: \"Sandstone\", formation: \"Entrada Sandstone\", age: \"Jurassic\" },\n  { top: 132, bottom: 168, lithology: \"Conglomerate\", formation: \"Chinle Conglomerate\", age: \"Triassic\" },\n  { top: 168, bottom: 192, lithology: \"Mudstone\", formation: \"Moenkopi Mudstone\", age: \"Triassic\" },\n  { top: 192, bottom: 222, lithology: \"Limestone\", formation: \"Kaibab Limestone\", age: \"Permian\" },\n  { top: 222, bottom: 260, lithology: \"Sandstone\", formation: \"Coconino Sandstone\", age: \"Permian\" },\n];\nconst maxDepth = layers[layers.length - 1].bottom;\n\n// --- Layout columns (x positions inside g) ----------------------------------\nconst axisX = 64; // depth axis line\nconst ageX = 168; // centre of the age/period band\nconst colLeft = 296;\nconst colWidth = 232;\nconst colRight = colLeft + colWidth;\nconst formX = colRight + 26; // formation labels begin here\n\n// --- SVG mount --------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst defs = svg.append(\"defs\");\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Depth scale (increases downward) ---------------------------------------\nconst y = d3.scaleLinear().domain([0, maxDepth]).range([0, ih]);\n\n// --- Lithology fill patterns (approximate FGDC/USGS symbols) -----------------\n// One <pattern> per lithology; the pattern strokes use the lithology colour so\n// the encoding is identical between light and dark (only chrome flips).\nfunction buildPattern(id, kind, color) {\n  const sw = 2; // pattern stroke width\n  if (kind === \"Sandstone\") {\n    // stipple dots\n    const p = defs.append(\"pattern\").attr(\"id\", id).attr(\"width\", 18).attr(\"height\", 18).attr(\"patternUnits\", \"userSpaceOnUse\");\n    [[4, 5], [13, 12], [9, 2], [2, 15]].forEach(([cx, cy]) =>\n      p.append(\"circle\").attr(\"cx\", cx).attr(\"cy\", cy).attr(\"r\", 1.8).attr(\"fill\", color)\n    );\n  } else if (kind === \"Shale\") {\n    // fine horizontal dashes\n    const p = defs.append(\"pattern\").attr(\"id\", id).attr(\"width\", 30).attr(\"height\", 12).attr(\"patternUnits\", \"userSpaceOnUse\");\n    [[2, 3, 12], [16, 3, 12], [9, 9, 12], [23, 9, 5]].forEach(([x0, yy, len]) =>\n      p.append(\"line\").attr(\"x1\", x0).attr(\"y1\", yy).attr(\"x2\", x0 + len).attr(\"y2\", yy).attr(\"stroke\", color).attr(\"stroke-width\", sw).attr(\"stroke-linecap\", \"round\")\n    );\n  } else if (kind === \"Limestone\") {\n    // brick pattern\n    const p = defs.append(\"pattern\").attr(\"id\", id).attr(\"width\", 44).attr(\"height\", 24).attr(\"patternUnits\", \"userSpaceOnUse\");\n    p.append(\"path\")\n      .attr(\"d\", \"M0,0 H44 M0,12 H44 M0,24 H44 M0,0 V12 M22,0 V12 M44,0 V12 M11,12 V24 M33,12 V24\")\n      .attr(\"stroke\", color).attr(\"stroke-width\", sw).attr(\"fill\", \"none\");\n  } else if (kind === \"Siltstone\") {\n    // mixed short dashes (random-look, fixed)\n    const p = defs.append(\"pattern\").attr(\"id\", id).attr(\"width\", 26).attr(\"height\", 26).attr(\"patternUnits\", \"userSpaceOnUse\");\n    [[3, 4, 11, 7], [16, 3, 23, 6], [6, 16, 13, 14], [17, 19, 24, 22], [4, 22, 9, 24]].forEach(([x1, y1, x2, y2]) =>\n      p.append(\"line\").attr(\"x1\", x1).attr(\"y1\", y1).attr(\"x2\", x2).attr(\"y2\", y2).attr(\"stroke\", color).attr(\"stroke-width\", sw).attr(\"stroke-linecap\", \"round\")\n    );\n  } else if (kind === \"Conglomerate\") {\n    // pebbles of varied size\n    const p = defs.append(\"pattern\").attr(\"id\", id).attr(\"width\", 40).attr(\"height\", 40).attr(\"patternUnits\", \"userSpaceOnUse\");\n    [[9, 10, 5], [27, 13, 6], [15, 27, 4.5], [32, 31, 4]].forEach(([cx, cy, r]) =>\n      p.append(\"circle\").attr(\"cx\", cx).attr(\"cy\", cy).attr(\"r\", r).attr(\"fill\", \"none\").attr(\"stroke\", color).attr(\"stroke-width\", sw)\n    );\n  } else if (kind === \"Mudstone\") {\n    // dash-dot horizontal\n    const p = defs.append(\"pattern\").attr(\"id\", id).attr(\"width\", 24).attr(\"height\", 14).attr(\"patternUnits\", \"userSpaceOnUse\");\n    p.append(\"line\").attr(\"x1\", 2).attr(\"y1\", 4).attr(\"x2\", 12).attr(\"y2\", 4).attr(\"stroke\", color).attr(\"stroke-width\", sw).attr(\"stroke-linecap\", \"round\");\n    p.append(\"circle\").attr(\"cx\", 17).attr(\"cy\", 4).attr(\"r\", 1.4).attr(\"fill\", color);\n    p.append(\"line\").attr(\"x1\", 12).attr(\"y1\", 10).attr(\"x2\", 22).attr(\"y2\", 10).attr(\"stroke\", color).attr(\"stroke-width\", sw).attr(\"stroke-linecap\", \"round\");\n    p.append(\"circle\").attr(\"cx\", 6).attr(\"cy\", 10).attr(\"r\", 1.4).attr(\"fill\", color);\n  }\n}\nObject.entries(lithologies).forEach(([name, def]) => buildPattern(`pat-${name}`, name, def.color));\n\n// --- A filled lithology box: colour tint base + pattern overlay + ink outline\nfunction drawLithoBox(parent, x, yy, w, h, name) {\n  const color = lithologies[name].color;\n  parent.append(\"rect\").attr(\"x\", x).attr(\"y\", yy).attr(\"width\", w).attr(\"height\", h).attr(\"fill\", color).attr(\"opacity\", 0.16);\n  parent.append(\"rect\").attr(\"x\", x).attr(\"y\", yy).attr(\"width\", w).attr(\"height\", h).attr(\"fill\", `url(#pat-${name})`);\n  parent.append(\"rect\").attr(\"x\", x).attr(\"y\", yy).attr(\"width\", w).attr(\"height\", h).attr(\"fill\", \"none\").attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1.2);\n}\n\n// --- Lithology column -------------------------------------------------------\nlayers.forEach((d) => {\n  drawLithoBox(g, colLeft, y(d.top), colWidth, y(d.bottom) - y(d.top), d.lithology);\n});\n\n// --- Depth axis (meters, increasing downward) -------------------------------\nconst depthAxis = g.append(\"g\").attr(\"transform\", `translate(${axisX},0)`).call(d3.axisLeft(y).tickValues(d3.range(0, maxDepth + 1, 40)).tickSize(7));\ndepthAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\ndepthAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\ndepthAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\ng.append(\"text\").attr(\"transform\", \"rotate(-90)\").attr(\"x\", -ih / 2).attr(\"y\", axisX - 62)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink).style(\"font-size\", \"17px\").text(\"Depth (m)\");\n\n// --- Age / period band (merge consecutive layers of the same age) -----------\nconst periods = d3.groups(layers, (d) => d.age).map(([age, rows]) => ({\n  age,\n  top: d3.min(rows, (r) => r.top),\n  bottom: d3.max(rows, (r) => r.bottom),\n}));\nperiods.forEach((p) => {\n  // boundary rule across the whole strip at each period change\n  g.append(\"line\").attr(\"x1\", axisX).attr(\"x2\", colRight).attr(\"y1\", y(p.top)).attr(\"y2\", y(p.top))\n    .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 2);\n  g.append(\"text\").attr(\"x\", ageX).attr(\"y\", (y(p.top) + y(p.bottom)) / 2)\n    .attr(\"text-anchor\", \"middle\").attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.ink).style(\"font-size\", \"17px\").style(\"font-weight\", \"600\").text(p.age);\n});\ng.append(\"text\").attr(\"x\", ageX).attr(\"y\", -16).attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\").style(\"font-weight\", \"600\").text(\"Geological Age\");\n\n// --- Formation labels with leader lines -------------------------------------\nlayers.forEach((d) => {\n  const midY = (y(d.top) + y(d.bottom)) / 2;\n  g.append(\"line\").attr(\"x1\", colRight).attr(\"x2\", formX - 8).attr(\"y1\", midY).attr(\"y2\", midY)\n    .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1.5);\n  g.append(\"text\").attr(\"x\", formX).attr(\"y\", midY).attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.ink).style(\"font-size\", \"16px\").text(d.formation);\n  g.append(\"text\").attr(\"x\", formX).attr(\"y\", midY + 22).attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\").text(`${d.lithology} · ${d.bottom - d.top} m`);\n});\n\n// --- Lithology legend (top-right) -------------------------------------------\nconst legendNames = Object.keys(lithologies);\nconst legCols = 2;\nconst legColW = 300;\nconst swW = 64;\nconst swH = 38;\nconst rowH = 52;\nconst legX = colRight + 470;\nconst legY = 6;\nconst legend = g.append(\"g\").attr(\"transform\", `translate(${legX},${legY})`);\nlegend.append(\"text\").attr(\"x\", 0).attr(\"y\", -10).attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\").style(\"font-weight\", \"600\").text(\"Lithology\");\nlegendNames.forEach((name, i) => {\n  const cx = (i % legCols) * legColW;\n  const cy = Math.floor(i / legCols) * rowH + 8;\n  const cell = legend.append(\"g\").attr(\"transform\", `translate(${cx},${cy})`);\n  drawLithoBox(cell, 0, 0, swW, swH, name);\n  cell.append(\"text\").attr(\"x\", swW + 14).attr(\"y\", swH / 2).attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.ink).style(\"font-size\", \"16px\").text(name);\n});\n\n// --- Title ------------------------------------------------------------------\nsvg.append(\"text\").attr(\"x\", width / 2).attr(\"y\", 50).attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"26px\").style(\"font-weight\", \"600\")\n  .text(\"column-stratigraphic · javascript · d3 · anyplot.ai\");\n"}