Stratigraphic Column with Lithology Patterns — Makie.jl

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.

Stratigraphic Column with Lithology Patterns rendered with Makie.jl

Julia source (Makie.jl)

# anyplot.ai
# column-stratigraphic: Stratigraphic Column with Lithology Patterns
# Library: makie 0.22.10 | Julia 1.11.9
# Quality: 90/100 | Created: 2026-06-17

using CairoMakie
using Colors
using Random

Random.seed!(42)

# --- Theme tokens (Imprint palette, theme-adaptive chrome) ------------------
const THEME       = get(ENV, "ANYPLOT_THEME", "light")
const PAGE_BG     = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17"
const ELEVATED_BG = THEME == "light" ? colorant"#FFFDF6" : colorant"#242420"
const INK         = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
const INK_SOFT    = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"
const INK_MUTED   = THEME == "light" ? colorant"#6B6A63" : colorant"#A8A79F"

# Imprint categorical palette — first series is always brand green
const IMPRINT_PALETTE = [
    colorant"#009E73",  # 1 — brand green
    colorant"#C475FD",  # 2 — lavender
    colorant"#4467A3",  # 3 — blue
    colorant"#BD8233",  # 4 — ochre
    colorant"#AE3030",  # 5 — matte red (deferred semantic anchor — skipped here)
    colorant"#2ABCCD",  # 6 — cyan
    colorant"#954477",  # 7 — rose
    colorant"#99B314",  # 8 — lime
]

const FILL_ALPHA = THEME == "light" ? 0.42 : 0.52
pattern_ink = RGBAf(INK.r, INK.g, INK.b, 0.82)

# Coal is rendered with a theme-stable dark fill so it always reads as
# semantic black/coal (rather than flipping to the near-white INK token in
# dark mode); its cleat partings use a fixed pale stroke that shows on it in
# both themes.
const COAL_FILL  = colorant"#2E2E28"
const COAL_CLEAT = RGBAf(0.80, 0.79, 0.73, 0.65)

# --- Synthetic borehole section (depth increases downward, metres) ----------
tops       = [0.0, 24.0, 58.0, 82.0, 120.0, 140.0, 176.0, 200.0, 214.0]
bottoms    = [24.0, 58.0, 82.0, 120.0, 140.0, 176.0, 200.0, 214.0, 232.0]
lithology  = ["Sandstone", "Shale", "Limestone", "Siltstone", "Conglomerate",
              "Dolostone", "Sandstone", "Coal", "Shale"]
formation  = ["Aeolian Sand Mbr", "Marl Bay Shale", "Reefal Limestone",
              "Prodelta Siltstone", "Basal Conglomerate", "Tidal Dolostone",
              "Fluvial Sandstone", "Cyclothem Coal", "Black Basinal Shale"]
age        = ["Neogene", "Paleogene", "Late Cretaceous", "Early Cretaceous",
              "Jurassic", "Triassic", "Permian", "Pennsylvanian", "Mississippian"]

# Lithology → fill colour (canonical Imprint order; coal reads as semantic black)
lith_order = ["Sandstone", "Shale", "Limestone", "Siltstone",
              "Conglomerate", "Dolostone", "Coal"]
lith_color = Dict(
    "Sandstone"    => IMPRINT_PALETTE[1],
    "Shale"        => IMPRINT_PALETTE[2],
    "Limestone"    => IMPRINT_PALETTE[3],
    "Siltstone"    => IMPRINT_PALETTE[4],
    "Conglomerate" => IMPRINT_PALETTE[6],
    "Dolostone"    => IMPRINT_PALETTE[7],
    "Coal"         => COAL_FILL,
)

# Depth of the unconformity (drawn as a wavy boundary instead of a straight rule)
const UNCONFORMITY = 120.0

# --- Lithology pattern painter ----------------------------------------------
# Draws an FGDC/USGS-style texture clipped to the rectangle [x0,x1]×[ytop,ybot].
# Reused for the column blocks and the legend swatches.
function add_pattern!(ax, lith, x0, x1, ytop, ybot; ink)
    w  = x1 - x0
    h  = ybot - ytop
    px = w * 0.05

    if lith == "Sandstone"
        # stipple dots
        n = clamp(round(Int, w * h * 0.55), 12, 360)
        xs = x0 .+ px .+ rand(n) .* (w - 2px)
        ys = ytop .+ rand(n) .* h
        scatter!(ax, xs, ys; marker = :circle, markersize = 2.6,
                 color = ink, strokewidth = 0)

    elseif lith == "Shale"
        # fine continuous laminae
        segs = Point2f[]
        yy = ytop + 4.0
        while yy < ybot - 0.5
            push!(segs, Point2f(x0 + px, yy), Point2f(x1 - px, yy))
            yy += 4.6
        end
        linesegments!(ax, segs; color = ink, linewidth = 0.8)

    elseif lith == "Limestone"
        # brick: horizontal courses + staggered vertical joints
        bh = 7.5
        bw = (x1 - x0) / 4
        segs = Point2f[]
        yy = ytop
        row = 0
        while yy <= ybot + 0.01
            yclip = min(yy, ybot)
            push!(segs, Point2f(x0, yclip), Point2f(x1, yclip))
            yy += bh
            row += 1
        end
        yy = ytop
        row = 0
        while yy < ybot - 0.5
            off = isodd(row) ? bw / 2 : 0.0
            xx = x0 + off
            while xx <= x1 + 0.01
                if xx >= x0 && xx <= x1
                    push!(segs, Point2f(xx, yy), Point2f(xx, min(yy + bh, ybot)))
                end
                xx += bw
            end
            yy += bh
            row += 1
        end
        linesegments!(ax, segs; color = ink, linewidth = 0.9)

    elseif lith == "Siltstone"
        # broken, staggered dashes
        dyl = 6.0
        dl  = w / 6
        gap = dl * 0.7
        segs = Point2f[]
        yy = ytop + dyl
        row = 0
        while yy < ybot - 0.5
            off = isodd(row) ? (dl + gap) / 2 : 0.0
            xx = x0 + px + off
            while xx < x1 - px
                push!(segs, Point2f(xx, yy), Point2f(min(xx + dl, x1 - px), yy))
                xx += dl + gap
            end
            yy += dyl
            row += 1
        end
        linesegments!(ax, segs; color = ink, linewidth = 1.0)

    elseif lith == "Conglomerate"
        # scattered clasts (open circles of varying size) over a faint matrix
        n = clamp(round(Int, w * h * 0.10), 5, 70)
        cx = x0 .+ 1.5px .+ rand(n) .* (w - 3px)
        cy = ytop .+ 0.06h .+ rand(n) .* (h * 0.88)
        sz = 8.0 .+ rand(n) .* 12.0
        scatter!(ax, cx, cy; marker = :circle, markersize = sz,
                 color = RGBAf(0, 0, 0, 0), strokecolor = ink, strokewidth = 1.2)
        nm = clamp(round(Int, w * h * 0.18), 8, 120)
        scatter!(ax, x0 .+ px .+ rand(nm) .* (w - 2px), ytop .+ rand(nm) .* h;
                 marker = :circle, markersize = 1.4, color = ink, strokewidth = 0)

    elseif lith == "Dolostone"
        # rhombic texture: staggered open diamonds
        dx = w / 4
        dy = 9.0
        cx = Float64[]
        cy = Float64[]
        yy = ytop + dy / 2
        row = 0
        while yy < ybot - dy / 4
            off = isodd(row) ? dx / 2 : 0.0
            xx = x0 + dx / 2 + off
            while xx < x1 + 0.01
                if xx >= x0 && xx <= x1
                    push!(cx, xx)
                    push!(cy, yy)
                end
                xx += dx
            end
            yy += dy
            row += 1
        end
        scatter!(ax, cx, cy; marker = :diamond, markersize = 11,
                 color = RGBAf(0, 0, 0, 0), strokecolor = ink, strokewidth = 1.0)

    elseif lith == "Coal"
        # solid block already filled; add a few light cleat partings
        segs = Point2f[]
        for f in (0.3, 0.55, 0.78)
            yy = ytop + f * h
            push!(segs, Point2f(x0 + px, yy), Point2f(x1 - px, yy))
        end
        linesegments!(ax, segs; color = COAL_CLEAT, linewidth = 1.2)
    end
end

# --- Figure -----------------------------------------------------------------
title_str = "column-stratigraphic · julia · makie · anyplot.ai"
title_sz  = round(Int, 20 * min(1.0, 67 / length(title_str)))

fig = Figure(
    resolution      = (1600, 900),
    fontsize        = 14,
    backgroundcolor = PAGE_BG,
)

ax = Axis(
    fig[1, 1];
    backgroundcolor   = PAGE_BG,
    title             = title_str,
    titlesize         = title_sz,
    titlecolor        = INK,
    titlegap          = 14,
    ylabel            = "Depth (m)",
    ylabelsize        = 15,
    ylabelcolor       = INK,
    yticks            = 0:40:240,
    yticklabelsize    = 13,
    yticklabelcolor   = INK_SOFT,
    ytickcolor        = INK_SOFT,
    leftspinecolor    = INK_SOFT,
    topspinevisible   = false,
    rightspinevisible = false,
    bottomspinevisible = false,
    xgridvisible      = false,
    ygridvisible      = false,
)

hidexdecorations!(ax)
xlims!(ax, 0.0, 11.8)
ylims!(ax, 240.0, -24.0)   # inverted y: 0 m at top, deepest layer at the bottom

# Column geometry
const X0 = 2.3
const X1 = 4.7
const X_AGE  = 2.05   # age labels — right-aligned, to the left of the column
const X_FORM = 5.0    # formation labels — left-aligned, to the right of the column

# --- Layers: tinted fill + lithology pattern --------------------------------
for i in eachindex(tops)
    ytop = tops[i]
    ybot = bottoms[i]
    lith = lithology[i]
    c    = lith_color[lith]
    fillc = lith == "Coal" ? RGBAf(c.r, c.g, c.b, 1.0) :
            RGBAf(c.r, c.g, c.b, FILL_ALPHA)

    poly!(ax, Rect2f(X0, ytop, X1 - X0, ybot - ytop);
          color = fillc, strokewidth = 0)
    add_pattern!(ax, lith, X0, X1, ytop, ybot; ink = pattern_ink)
end

# --- Layer boundaries (solid) + one wavy unconformity -----------------------
boundary_segs = Point2f[]
for d in unique(vcat(tops, bottoms[end]))
    d == UNCONFORMITY && continue
    push!(boundary_segs, Point2f(X0, d), Point2f(X1, d))
end
linesegments!(ax, boundary_segs; color = INK_SOFT, linewidth = 1.6)

# Wavy unconformity line
wx = range(X0, X1, length = 90)
wy = UNCONFORMITY .+ 2.6 .* sin.((wx .- X0) .* (2π * 3 / (X1 - X0)))
lines!(ax, collect(wx), collect(wy); color = INK_SOFT, linewidth = 2.4)

# Column outline (left/right edges)
lines!(ax, [X0, X0], [tops[1], bottoms[end]]; color = INK_SOFT, linewidth = 1.6)
lines!(ax, [X1, X1], [tops[1], bottoms[end]]; color = INK_SOFT, linewidth = 1.6)

# --- Age (left) and formation (right) labels --------------------------------
for i in eachindex(tops)
    ymid = (tops[i] + bottoms[i]) / 2

    text!(ax, X_AGE, ymid; text = age[i],
          align = (:right, :center), color = INK_SOFT, fontsize = 12.5)

    label = string(formation[i], "\n", Int(tops[i]), "–", Int(bottoms[i]), " m")
    text!(ax, X_FORM, ymid; text = label,
          align = (:left, :center), color = INK, fontsize = 13)
end

# Unconformity annotation: a thin leader nudges the label clear of the column
# edge into the open whitespace, between the two adjacent formation rows.
lines!(ax, [X1, 6.9], [UNCONFORMITY, UNCONFORMITY];
       color = INK_MUTED, linewidth = 1.0, linestyle = :dot)
text!(ax, 7.0, UNCONFORMITY; text = "unconformity",
      align = (:left, :center), color = INK_MUTED, fontsize = 11.5)

# --- Lithology legend (pattern key, upper right) ----------------------------
text!(ax, 8.9, -14.0; text = "Lithology", align = (:left, :center),
      color = INK, fontsize = 14)

sw_x0 = 8.9
sw_x1 = 10.0
sw_h  = 17.0
for (k, lith) in enumerate(lith_order)
    yc = 6.0 + (k - 1) * 29.0
    yt = yc - sw_h / 2
    yb = yc + sw_h / 2
    c  = lith_color[lith]
    fillc = lith == "Coal" ? RGBAf(c.r, c.g, c.b, 1.0) :
            RGBAf(c.r, c.g, c.b, FILL_ALPHA)

    poly!(ax, Rect2f(sw_x0, yt, sw_x1 - sw_x0, sw_h);
          color = fillc, strokecolor = INK_SOFT, strokewidth = 1.2)
    add_pattern!(ax, lith, sw_x0, sw_x1, yt, yb; ink = pattern_ink)

    text!(ax, sw_x1 + 0.15, yc; text = lith,
          align = (:left, :center), color = INK_SOFT, fontsize = 13)
end

# --- Save -------------------------------------------------------------------
save("plot-$(THEME).png", fig; px_per_unit = 2)

Part of Stratigraphic Column with Lithology Patterns on anyplot.ai.

Other implementations