Causal highlighting

DAGMakie highlights paths and adjustment sets that you already computed. Identification algorithms live in CausalInference.jl (or CausalDynamics.jl façades). Pass the resulting sets into the plot helpers — the core package does not pull CausalInference (registry CausalInference is incompatible with GraphMakie 0.6).

using Graphs, DAGMakie, CairoMakie

g, labels = confounding_graph(["Z", "X", "Y"])

# From CausalInference.find_min_backdoor_adjustment(g, 2, 3) — or hard-code Z
adjustment = Set([1])

fig, ax, p = dagplot_backdoor(g, 2, 3; adjustment = adjustment, nlabels = labels)
fig
Example block output

dagplot_adjustment accepts an explicit set the same way:

using Graphs, DAGMakie, CairoMakie

g, labels = confounding_graph(["Z", "X", "Y"])
fig, ax, p = dagplot_adjustment(g, 2, 3; adjustment = Set([1]), nlabels = labels)
fig
Example block output

Smart / dagitty-style colours

Pass smart=true (or :ancestors) with treatment and outcome to colour nodes like dagitty “highlight ancestors”. Variables outside the ancestral closure of exposure ∪ outcome are grayed out; shared ancestors (often on backdoors) are marked in red.

ColourRole
SeagreenExposure
Royal blueOutcome
Medium seagreenAncestor of exposure only
Steel blueAncestor of outcome only
Indian redAncestor of both
Light grayOutside the ancestral closure

Ancestor sets use CausalInference.jl when it is loaded; otherwise Graphs reverse-BFS. smart=:adjustment also thickens a backdoor adjustment set (needs CausalInference, or pass adjustment=).

Default vs smart (confounding triangle)

Same graph, plain steel-blue nodes versus dagitty-style roles. Here $Z$ is an ancestor of both exposure $X$ and outcome $Y$ (indian red).

using Graphs, DAGMakie, CairoMakie

g, labels = confounding_graph(["Z", "X", "Y"])

fig = with_theme(dag_theme()) do
    fig = Figure(size = (900, 360))
    ax1 = Axis(fig[1, 1], title = "Default")
    ax2 = Axis(fig[1, 2], title = "smart = true")
    dagplot!(ax1, g; nlabels = labels)
    dagplot!(ax2, g; smart = true, treatment = 2, outcome = 3, nlabels = labels)
    fig
end
fig
Example block output

Full role palette

A slightly richer DAG shows every smart role at once: $A$ affects only the exposure, $B$ only the outcome, $Z$ both, and $W$ is irrelevant.

using Graphs, DAGMakie, CairoMakie

# A → X ← Z → Y ← B, X → Y; W isolated
g = SimpleDiGraph(6)
add_edge!(g, 1, 4)  # A → X
add_edge!(g, 2, 4)  # Z → X
add_edge!(g, 2, 5)  # Z → Y
add_edge!(g, 3, 5)  # B → Y
add_edge!(g, 4, 5)  # X → Y
labels = ["A", "Z", "B", "X", "Y", "W"]

roles = classify_smart_roles(g, 4, 5)
@show roles

fig = with_theme(dag_theme()) do
    # Place X below the Z→Y chord so A→X and Z→X do not cross it.
    layout = Point2f[
        (0.0, -2.7),  # A
        (0.0, -0.9),  # Z
        (0.0, 0.9),   # B
        (2.6, -2.2),  # X (below Z→Y so parent edges do not cross it)
        (5.2, 0.0),   # Y
        (0.0, 2.7),   # W
    ]
    fig, ax, p = dagplot_smart(g, 4, 5;
        nlabels = labels,
        layout = layout,
        figure_size = (700, 420),
    )
    ax.title = "A: anc. exposure · B: anc. outcome · Z: both · W: irrelevant"
    fig
end
fig
Example block output

Emphasise an adjustment set

smart=:adjustment keeps the ancestor colours and draws a thicker dark-red outline on adjustment nodes. Pass adjustment= explicitly when CausalInference is not loaded (as in this documentation build).

using Graphs, DAGMakie, CairoMakie

g, labels = confounding_graph(["Z", "X", "Y"])

fig = with_theme(dag_theme()) do
    fig, ax, p = dagplot(g;
        smart = :adjustment,
        treatment = 2,
        outcome = 3,
        adjustment = Set([1]),  # Z
        nlabels = labels,
    )
    ax.title = "smart = :adjustment (Z emphasised)"
    fig
end
fig
Example block output

With CausalInference available in your environment you can omit adjustment and let the extension compute a minimal backdoor set:

using CausalInference  # activates DAGMakieCausalInferenceExt
fig, ax, p = dagplot(g; smart = :adjustment, treatment = 2, outcome = 3, nlabels = labels)

That call draws the same emphasis as below (for this DAG the extension returns ${Z}$). The documentation environment does not load CausalInference (registry builds pin GraphMakie incompatible with 0.6), so the figure uses the equivalent explicit set:

using Graphs, DAGMakie, CairoMakie

g, labels = confounding_graph(["Z", "X", "Y"])

fig = with_theme(dag_theme()) do
    fig, ax, p = dagplot(g;
        smart = :adjustment,
        treatment = 2,
        outcome = 3,
        adjustment = Set([1]),  # same as find_min_backdoor_adjustment(g, 2, 3)
        nlabels = labels,
    )
    ax.title = "smart = :adjustment (Z from backdoor set)"
    fig
end
fig
Example block output

d-Separation titles

Pass separated from dsep / d_separated for axis titles. Use a fork $X ← Z → Y$ (no direct $X → Y$): conditioning on $Z$ d-separates $X$ and $Y$. A confounding triangle with $X → Y$ keeps them d-connected.

using Graphs, DAGMakie, CairoMakie

g, labels = fork_graph(["X", "Z", "Y"])  # X ← Z → Y
# X ⊥ Y | Z (Z blocks the only path)
fig, ax, p = dagplot_dsep(g, 1, 3, Set([2]); separated = true, nlabels = labels)
fig
Example block output

Combine smart colours with a d-separation title when teaching forks:

using Graphs, DAGMakie, CairoMakie

g, labels = fork_graph(["X", "Z", "Y"])
fig = with_theme(dag_theme()) do
    fig, ax, p = dagplot(g;
        smart = true,
        treatment = 1,
        outcome = 3,
        nlabels = labels,
    )
    ax.title = "X ⊥ Y | Z (d-separated) · smart ancestors"
    fig
end
fig
Example block output