{"spec_id":"column-stratigraphic","library":"muix","language":"javascript","code":"// anyplot.ai\n// column-stratigraphic: Stratigraphic Column with Lithology Patterns\n// Library: muix 9.16.0 | JavaScript 22.23.3\n// Quality: 87/100 | Updated: 2026-10-24\n\nimport { ChartsContainer } from \"@mui/x-charts/ChartsContainer\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst SIZE = window.ANYPLOT_SIZE;\n\n// Constants\nconst TITLE = \"column-stratigraphic · javascript · muix · anyplot.ai\";\nconst FONT =\n  '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif';\nconst PATTERN_TYPES = [\"stipple\", \"dash\", \"brick\", \"rhomb\", \"silt\", \"clasts\"] as const;\nconst LITHOLOGY_PATTERN_MAP: Record<string, (typeof PATTERN_TYPES)[number]> = {\n  Sandstone: \"stipple\",\n  Shale: \"dash\",\n  Limestone: \"brick\",\n  Dolomite: \"rhomb\",\n  Siltstone: \"silt\",\n  Conglomerate: \"clasts\",\n};\n\nconst COL_LEFT = 4.25;\nconst COL_RIGHT = 7.7;\nconst COL_TOP_Y = 7.7;\nconst COL_BOT_Y = 0.6;\nconst AXIS_X = 4.05;\nconst BRACKET_X = 2.95;\nconst FORM_X = 7.95;\nconst LEGEND_X = 11.3;\nconst LEGEND_STEP = 0.72;\nconst TICK_COUNT = 7;\nconst WAVY_SAMPLES = 48;\n\n// Columns: set these to your data's column names\nconst TOP = \"Top depth (m)\"; // role: top\nconst BOTTOM = \"Bottom depth (m)\"; // role: bottom\nconst LITHOLOGY = \"Lithology\"; // role: lithology\nconst FORMATION = \"Formation\"; // role: formation\nconst STAGE_AGE = \"Stage age\"; // role: age\nconst PERIOD = \"Geological period\"; // role: extra\nconst PERIOD_SPAN = \"Period span\"; // role: extra\nconst UNCONFORMITY = \"Unconformity\"; // role: extra\n\n// Data: replace this block with your own records\nconst data = [\n  {\n    [TOP]: 0,\n    [BOTTOM]: 180,\n    [LITHOLOGY]: \"Sandstone\",\n    [FORMATION]: \"Horseshoe Canyon Fm\",\n    [STAGE_AGE]: \"Maastrichtian\",\n    [PERIOD]: \"Cretaceous\",\n    [PERIOD_SPAN]: \"145–66 Ma\",\n    [UNCONFORMITY]: false,\n  },\n  {\n    [TOP]: 180,\n    [BOTTOM]: 380,\n    [LITHOLOGY]: \"Shale\",\n    [FORMATION]: \"Bearpaw Shale\",\n    [STAGE_AGE]: \"Campanian\",\n    [PERIOD]: \"Cretaceous\",\n    [PERIOD_SPAN]: \"145–66 Ma\",\n    [UNCONFORMITY]: false,\n  },\n  {\n    [TOP]: 380,\n    [BOTTOM]: 560,\n    [LITHOLOGY]: \"Sandstone\",\n    [FORMATION]: \"Belly River Fm\",\n    [STAGE_AGE]: \"Campanian\",\n    [PERIOD]: \"Cretaceous\",\n    [PERIOD_SPAN]: \"145–66 Ma\",\n    [UNCONFORMITY]: false,\n  },\n  {\n    [TOP]: 560,\n    [BOTTOM]: 700,\n    [LITHOLOGY]: \"Limestone\",\n    [FORMATION]: \"Rundle Group\",\n    [STAGE_AGE]: \"Mississippian\",\n    [PERIOD]: \"Carboniferous\",\n    [PERIOD_SPAN]: \"359–299 Ma\",\n    [UNCONFORMITY]: true,\n  },\n  {\n    [TOP]: 700,\n    [BOTTOM]: 850,\n    [LITHOLOGY]: \"Dolomite\",\n    [FORMATION]: \"Palliser Fm\",\n    [STAGE_AGE]: \"Famennian\",\n    [PERIOD]: \"Devonian\",\n    [PERIOD_SPAN]: \"419–359 Ma\",\n    [UNCONFORMITY]: false,\n  },\n  {\n    [TOP]: 850,\n    [BOTTOM]: 980,\n    [LITHOLOGY]: \"Shale\",\n    [FORMATION]: \"Ireton Fm\",\n    [STAGE_AGE]: \"Frasnian\",\n    [PERIOD]: \"Devonian\",\n    [PERIOD_SPAN]: \"419–359 Ma\",\n    [UNCONFORMITY]: false,\n  },\n  {\n    [TOP]: 980,\n    [BOTTOM]: 1230,\n    [LITHOLOGY]: \"Limestone\",\n    [FORMATION]: \"Leduc Reef\",\n    [STAGE_AGE]: \"Frasnian\",\n    [PERIOD]: \"Devonian\",\n    [PERIOD_SPAN]: \"419–359 Ma\",\n    [UNCONFORMITY]: false,\n  },\n  {\n    [TOP]: 1230,\n    [BOTTOM]: 1400,\n    [LITHOLOGY]: \"Siltstone\",\n    [FORMATION]: \"Beaverhill Lake Gp\",\n    [STAGE_AGE]: \"Givetian\",\n    [PERIOD]: \"Devonian\",\n    [PERIOD_SPAN]: \"419–359 Ma\",\n    [UNCONFORMITY]: false,\n  },\n  {\n    [TOP]: 1400,\n    [BOTTOM]: 1500,\n    [LITHOLOGY]: \"Conglomerate\",\n    [FORMATION]: \"Basal Clastics\",\n    [STAGE_AGE]: \"Cambrian\",\n    [PERIOD]: \"Cambrian\",\n    [PERIOD_SPAN]: \"539–485 Ma\",\n    [UNCONFORMITY]: true,\n  },\n];\n\n// Plot: reads only data and the constants above\nconst DEPTH_UNIT = TOP.match(/\\(([^)]+)\\)/)?.[1] ?? \"\";\nconst depthUnitSuffix = DEPTH_UNIT ? ` ${DEPTH_UNIT}` : \"\";\nconst DEPTH_AXIS_TITLE = TOP.replace(/^Top\\s+/i, \"\").replace(/^d/, \"D\");\n\nconst minDepth = Math.min(...data.map((d) => Number(d[TOP])));\nconst maxDepth = Math.max(...data.map((d) => Number(d[BOTTOM])));\nconst depthSpan = maxDepth - minDepth;\nconst worldSpan = COL_TOP_Y - COL_BOT_Y;\n\nconst LITHO_NAMES = Array.from(new Set(data.map((d) => String(d[LITHOLOGY]))));\nconst LITHO_COLORS = Object.fromEntries(\n  LITHO_NAMES.map((name, idx) => [name, t.palette[idx]]),\n);\nconst LITHO_IDS = Object.fromEntries(\n  LITHO_NAMES.map((name, idx) => [name, `lith-${idx}`]),\n);\n\nconst usedPatterns = new Set<string>(\n  LITHO_NAMES.map((name) => LITHOLOGY_PATTERN_MAP[name]).filter(Boolean),\n);\nconst unusedPatterns = PATTERN_TYPES.filter((p) => !usedPatterns.has(p));\nlet unusedIdx = 0;\nconst LITHO_PATTERNS: Record<string, (typeof PATTERN_TYPES)[number]> = {};\nfor (const name of LITHO_NAMES) {\n  const mapped = LITHOLOGY_PATTERN_MAP[name];\n  if (mapped) {\n    LITHO_PATTERNS[name] = mapped;\n  } else {\n    if (unusedIdx >= unusedPatterns.length) {\n      throw new Error(`Exceeded available lithology patterns (${PATTERN_TYPES.length})`);\n    }\n    LITHO_PATTERNS[name] = unusedPatterns[unusedIdx++];\n  }\n}\n\nconst LAYERS = data.map((d) => {\n  const top = Number(d[TOP]);\n  const bottom = Number(d[BOTTOM]);\n  const litho = String(d[LITHOLOGY]);\n  const yTopW = COL_TOP_Y - ((top - minDepth) / depthSpan) * worldSpan;\n  const yBotW = COL_TOP_Y - ((bottom - minDepth) / depthSpan) * worldSpan;\n  return {\n    top,\n    bottom,\n    thickness: bottom - top,\n    litho,\n    patternId: LITHO_IDS[litho],\n    formation: String(d[FORMATION]),\n    age: String(d[STAGE_AGE]),\n    period: String(d[PERIOD]),\n    periodSpan: String(d[PERIOD_SPAN]),\n    unconf: Boolean(d[UNCONFORMITY]),\n    yTopW,\n    yBotW,\n    yMidW: (yTopW + yBotW) / 2,\n  };\n});\n\nconst PERIODS: {\n  name: string;\n  age: string;\n  top: number;\n  bottom: number;\n  yTopW: number;\n  yBotW: number;\n}[] = [];\nfor (const layer of LAYERS) {\n  const prev = PERIODS[PERIODS.length - 1];\n  if (prev && prev.name === layer.period) {\n    prev.bottom = layer.bottom;\n    prev.yBotW = layer.yBotW;\n  } else {\n    PERIODS.push({\n      name: layer.period,\n      age: layer.periodSpan,\n      top: layer.top,\n      bottom: layer.bottom,\n      yTopW: layer.yTopW,\n      yBotW: layer.yBotW,\n    });\n  }\n}\n\nconst rawStep = depthSpan / TICK_COUNT;\nconst stepMag = Math.pow(10, Math.floor(Math.log10(rawStep)));\nconst stepNorm = rawStep / stepMag;\nconst tickStep = (stepNorm <= 1.5 ? 1 : stepNorm <= 3.5 ? 2 : 5) * stepMag;\nconst DEPTH_TICKS = [];\nfor (let d = Math.ceil(minDepth / tickStep) * tickStep; d <= maxDepth + 1e-9; d += tickStep) {\n  DEPTH_TICKS.push({\n    depth: Number(d.toFixed(4)),\n    yW: COL_TOP_Y - ((d - minDepth) / depthSpan) * worldSpan,\n  });\n}\n\nconst SUBTITLE = `${LAYERS.length} stratigraphic units (${minDepth}–${maxDepth}${depthUnitSuffix}) · ${LITHO_NAMES.length} ${LITHOLOGY} types across ${PERIODS.length} periods`;\nconst WAVY_STEPS = Array.from({ length: WAVY_SAMPLES + 1 }, (_, i) => i / WAVY_SAMPLES);\n\nfunction StratigraphicLayer() {\n  const xs = useXScale() as any;\n  const ys = useYScale() as any;\n\n  const xL = xs(COL_LEFT);\n  const xR = xs(COL_RIGHT);\n  const colW = xR - xL;\n  const axisX = xs(AXIS_X);\n  const bx = xs(BRACKET_X);\n  const xLead = xs(FORM_X) - 8;\n  const xText = xs(FORM_X);\n  const legX = xs(LEGEND_X);\n  const swW = xs(LEGEND_X + 0.85) - legX;\n  const swH = Math.abs(ys(0) - ys(0.42));\n\n  return (\n    <g fontFamily={FONT}>\n      <defs>\n        {LITHO_NAMES.map((name) => {\n          const color = LITHO_COLORS[name];\n          const pid = LITHO_IDS[name];\n          const patType = LITHO_PATTERNS[name];\n          if (patType === \"stipple\") {\n            return (\n              <pattern key={pid} id={pid} patternUnits=\"userSpaceOnUse\" width={24} height={24}>\n                <rect width={24} height={24} fill={color} fillOpacity={0.26} />\n                {[\n                  [5, 5],\n                  [17, 9],\n                  [9, 17],\n                  [20, 20],\n                  [13, 13],\n                ].map(([x, y], i) => (\n                  <circle key={i} cx={x} cy={y} r={2.1} fill={color} />\n                ))}\n              </pattern>\n            );\n          }\n          if (patType === \"dash\") {\n            return (\n              <pattern key={pid} id={pid} patternUnits=\"userSpaceOnUse\" width={24} height={16}>\n                <rect width={24} height={16} fill={color} fillOpacity={0.26} />\n                {[4, 9, 14].map((y, i) => (\n                  <line key={i} x1={0} y1={y} x2={24} y2={y} stroke={color} strokeWidth={1.7} />\n                ))}\n              </pattern>\n            );\n          }\n          if (patType === \"brick\") {\n            return (\n              <pattern key={pid} id={pid} patternUnits=\"userSpaceOnUse\" width={28} height={18}>\n                <rect width={28} height={18} fill={color} fillOpacity={0.26} />\n                <g stroke={color} strokeWidth={1.7}>\n                  <line x1={0} y1={0} x2={28} y2={0} />\n                  <line x1={0} y1={9} x2={28} y2={9} />\n                  <line x1={0} y1={18} x2={28} y2={18} />\n                  <line x1={14} y1={0} x2={14} y2={9} />\n                  <line x1={0} y1={9} x2={0} y2={18} />\n                  <line x1={28} y1={9} x2={28} y2={18} />\n                </g>\n              </pattern>\n            );\n          }\n          if (patType === \"rhomb\") {\n            return (\n              <pattern key={pid} id={pid} patternUnits=\"userSpaceOnUse\" width={20} height={20}>\n                <rect width={20} height={20} fill={color} fillOpacity={0.26} />\n                <g stroke={color} strokeWidth={1.6}>\n                  <line x1={0} y1={0} x2={20} y2={20} />\n                  <line x1={0} y1={20} x2={20} y2={0} />\n                  <line x1={-10} y1={10} x2={10} y2={-10} />\n                  <line x1={10} y1={30} x2={30} y2={10} />\n                </g>\n              </pattern>\n            );\n          }\n          if (patType === \"silt\") {\n            return (\n              <pattern key={pid} id={pid} patternUnits=\"userSpaceOnUse\" width={26} height={26}>\n                <rect width={26} height={26} fill={color} fillOpacity={0.26} />\n                <g stroke={color} strokeWidth={1.8} strokeLinecap=\"round\">\n                  <line x1={3} y1={6} x2={10} y2={6} />\n                  <line x1={15} y1={12} x2={22} y2={12} />\n                  <line x1={6} y1={19} x2={13} y2={19} />\n                  <line x1={18} y1={23} x2={25} y2={23} />\n                </g>\n              </pattern>\n            );\n          }\n          return (\n            <pattern key={pid} id={pid} patternUnits=\"userSpaceOnUse\" width={32} height={32}>\n              <rect width={32} height={32} fill={color} fillOpacity={0.26} />\n              <g fill=\"none\" stroke={color} strokeWidth={1.8}>\n                <circle cx={9} cy={9} r={5.5} />\n                <circle cx={24} cy={13} r={6.5} />\n                <circle cx={15} cy={25} r={5} />\n                <circle cx={30} cy={29} r={4} />\n              </g>\n            </pattern>\n          );\n        })}\n      </defs>\n\n      {/* Column blocks */}\n      {LAYERS.map((L, i) => {\n        const yTop = ys(L.yTopW);\n        const yBot = ys(L.yBotW);\n        return (\n          <rect\n            key={`blk-${i}`}\n            x={xL}\n            y={yTop}\n            width={colW}\n            height={yBot - yTop}\n            fill={`url(#${L.patternId})`}\n            stroke={t.ink}\n            strokeWidth={1.4}\n          />\n        );\n      })}\n\n      {/* Contacts */}\n      {LAYERS.map((L, i) => {\n        if (L.top === minDepth) return null;\n        const y = ys(L.yTopW);\n        if (!L.unconf) {\n          return (\n            <line\n              key={`c-${i}`}\n              x1={xL}\n              y1={y}\n              x2={xR}\n              y2={y}\n              stroke={t.ink}\n              strokeWidth={1.6}\n            />\n          );\n        }\n        const dPath = WAVY_STEPS.map((frac, k) => {\n          const px = xL + colW * frac;\n          const py = y + 4.5 * Math.sin(frac * 5 * 2 * Math.PI);\n          return `${k === 0 ? \"M\" : \"L\"}${px.toFixed(1)} ${py.toFixed(1)}`;\n        }).join(\" \");\n        return (\n          <path\n            key={`c-${i}`}\n            d={dPath}\n            fill=\"none\"\n            stroke={t.ink}\n            strokeWidth={3}\n            strokeLinecap=\"round\"\n          />\n        );\n      })}\n\n      {/* Depth axis */}\n      <line\n        x1={axisX}\n        y1={ys(COL_TOP_Y)}\n        x2={axisX}\n        y2={ys(COL_BOT_Y)}\n        stroke={t.inkSoft}\n        strokeWidth={1.6}\n      />\n      {DEPTH_TICKS.map((tk) => {\n        const y = ys(tk.yW);\n        return (\n          <g key={`tk-${tk.depth}`}>\n            <line x1={axisX - 8} y1={y} x2={axisX} y2={y} stroke={t.inkSoft} strokeWidth={1.6} />\n            <text\n              x={axisX - 13}\n              y={y}\n              textAnchor=\"end\"\n              dominantBaseline=\"middle\"\n              fontSize={14}\n              fill={t.inkSoft}\n            >\n              {tk.depth}\n            </text>\n          </g>\n        );\n      })}\n      <text\n        x={axisX - 13}\n        y={ys(COL_TOP_Y) - 22}\n        textAnchor=\"end\"\n        dominantBaseline=\"middle\"\n        fontSize={15}\n        fontWeight={600}\n        fill={t.ink}\n      >\n        {DEPTH_AXIS_TITLE}\n      </text>\n\n      {/* Geological periods */}\n      {PERIODS.map((p, i) => {\n        const yTop = ys(p.yTopW);\n        const yBot = ys(p.yBotW);\n        const yMid = (yTop + yBot) / 2;\n        return (\n          <g key={`p-${i}`}>\n            <line x1={bx} y1={yTop + 1} x2={bx} y2={yBot - 1} stroke={t.inkSoft} strokeWidth={1.8} />\n            <line x1={bx} y1={yTop + 1} x2={bx - 10} y2={yTop + 1} stroke={t.inkSoft} strokeWidth={1.8} />\n            <line x1={bx} y1={yBot - 1} x2={bx - 10} y2={yBot - 1} stroke={t.inkSoft} strokeWidth={1.8} />\n            <text\n              x={bx - 16}\n              y={yMid - 10}\n              textAnchor=\"end\"\n              dominantBaseline=\"middle\"\n              fontSize={16}\n              fontWeight={600}\n              fill={t.ink}\n            >\n              {p.name}\n            </text>\n            <text\n              x={bx - 16}\n              y={yMid + 11}\n              textAnchor=\"end\"\n              dominantBaseline=\"middle\"\n              fontSize={14}\n              fill={t.inkSoft}\n            >\n              {p.age}\n            </text>\n          </g>\n        );\n      })}\n\n      {/* Formation labels */}\n      {LAYERS.map((L, i) => {\n        const yMid = ys(L.yMidW);\n        return (\n          <g key={`fm-${i}`}>\n            <line x1={xR} y1={yMid} x2={xLead} y2={yMid} stroke={t.grid} strokeWidth={1.2} />\n            <text\n              x={xText}\n              y={yMid - 10}\n              dominantBaseline=\"middle\"\n              fontSize={16}\n              fontWeight={600}\n              fill={t.ink}\n            >\n              {L.formation}\n            </text>\n            <text\n              x={xText}\n              y={yMid + 11}\n              dominantBaseline=\"middle\"\n              fontSize={14}\n              fill={t.inkSoft}\n            >\n              {`${L.age} · ${L.thickness}${depthUnitSuffix}`}\n            </text>\n          </g>\n        );\n      })}\n\n      {/* Legend */}\n      <text x={legX} y={ys(COL_TOP_Y + 0.55)} fontSize={18} fontWeight={700} fill={t.ink}>\n        {LITHOLOGY}\n      </text>\n      {LITHO_NAMES.map((name, i) => {\n        const y = ys(COL_TOP_Y - i * LEGEND_STEP);\n        return (\n          <g key={`leg-${name}`}>\n            <rect\n              x={legX}\n              y={y - swH / 2}\n              width={swW}\n              height={swH}\n              fill={`url(#${LITHO_IDS[name]})`}\n              stroke={t.ink}\n              strokeWidth={1.2}\n            />\n            <text x={legX + swW + 14} y={y} dominantBaseline=\"middle\" fontSize={15} fill={t.inkSoft}>\n              {name}\n            </text>\n          </g>\n        );\n      })}\n\n      <text\n        x={legX}\n        y={ys(COL_TOP_Y - LITHO_NAMES.length * LEGEND_STEP - 0.15)}\n        fontSize={16}\n        fontWeight={700}\n        fill={t.ink}\n      >\n        Contacts\n      </text>\n      <g>\n        <line\n          x1={legX}\n          y1={ys(COL_TOP_Y - (LITHO_NAMES.length + 0.6) * LEGEND_STEP)}\n          x2={legX + swW}\n          y2={ys(COL_TOP_Y - (LITHO_NAMES.length + 0.6) * LEGEND_STEP)}\n          stroke={t.ink}\n          strokeWidth={2}\n        />\n        <text\n          x={legX + swW + 14}\n          y={ys(COL_TOP_Y - (LITHO_NAMES.length + 0.6) * LEGEND_STEP)}\n          dominantBaseline=\"middle\"\n          fontSize={15}\n          fill={t.inkSoft}\n        >\n          Conformable\n        </text>\n      </g>\n      <g>\n        <path\n          d={WAVY_STEPS.map((frac, k) => {\n            const px = legX + swW * frac;\n            const py =\n              ys(COL_TOP_Y - (LITHO_NAMES.length + 1.4) * LEGEND_STEP) +\n              3.5 * Math.sin(frac * 2.5 * 2 * Math.PI);\n            return `${k === 0 ? \"M\" : \"L\"}${px.toFixed(1)} ${py.toFixed(1)}`;\n          }).join(\" \")}\n          fill=\"none\"\n          stroke={t.ink}\n          strokeWidth={2.4}\n        />\n        <text\n          x={legX + swW + 14}\n          y={ys(COL_TOP_Y - (LITHO_NAMES.length + 1.4) * LEGEND_STEP)}\n          dominantBaseline=\"middle\"\n          fontSize={15}\n          fill={t.inkSoft}\n        >\n          Unconformity (hiatus)\n        </text>\n      </g>\n\n      {/* Title block */}\n      <text x={xs(0.55)} y={ys(8.62)} fontSize={22} fontWeight={600} fill={t.ink}>\n        {TITLE}\n      </text>\n      <text x={xs(0.55)} y={ys(8.2)} fontSize={15} fill={t.inkSoft}>\n        {SUBTITLE}\n      </text>\n    </g>\n  );\n}\n\nexport default function Chart() {\n  return (\n    <ChartsContainer\n      width={SIZE.width}\n      height={SIZE.height}\n      margin={{ top: 12, right: 12, bottom: 12, left: 12 }}\n      series={[]}\n      xAxis={[{ scaleType: \"linear\", min: 0, max: 16, position: \"none\" }]}\n      yAxis={[{ scaleType: \"linear\", min: 0, max: 9, position: \"none\" }]}\n      skipAnimation\n    >\n      <StratigraphicLayer />\n    </ChartsContainer>\n  );\n}\n"}