The same plot in 14 other libraries — JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts; Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, plotnine, Pygal, Seaborn; R: ggplot2; Julia: Makie.jl. Compare all 15 side by side: Stratigraphic Column with Lithology Patterns in Python, R, Julia and JavaScript.
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.

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