Interactive DAGs
  • Confounders
  • Mediators
  • Colliders

Mediators

d3 = require("d3@7")
dag = import(new URL("js/dag-utils.js", document.baseURI).href)
Relationships between nodes
viewof strength_xz = Inputs.range([0, 1], {
  value: 0.5, 
  step: 0.05, 
  label: html`<span class="node node-x">X</span> → <span class="node node-z">Z</span> strength`
})

viewof strength_zy = Inputs.range([0, 1], {
  value: 0.5, 
  step: 0.05, 
  label: html`<span class="node node-z">Z</span> → <span class="node node-y">Y</span> strength`
})

viewof strength_xy = Inputs.range([0, 1], {
  value: 0.5, 
  step: 0.05, 
  label: html`<span class="node node-x">X</span> → <span class="node node-y">Y</span> strength`
})
Adjustments
viewof adjust_z = Inputs.toggle({
  label: html`<span class="node node-z">Adjust for Z</span> (<em>block indirect path</em>)`
})
// ----------------
// Status readout
// ----------------
{
  const pctDirect = Math.round(y_direct_x / yMax * 100);
  const pctMediated = Math.round(y_mediated / yMax * 100);
  const pctZ = Math.round(y_from_z_own / yMax * 100);
  const pctOwn = Math.max(0, 100 - pctDirect - pctMediated - pctZ);

  return html`<div class="alert alert-secondary status-readout">
    <h5 class="alert-heading">What Y contains</h5>
    <table>
      <tr>
        <td><svg width="12" height="12"><rect width="12" height="12" fill="${dag.colorX}"/></svg></td>
        <td><span class="node node-x">X</span>'s direct influence on <span class="node node-y">Y</span></td>
        <td>${pctDirect}%</td>
      </tr>
      <tr>
        <td><svg width="12" height="12">
          <defs>
            <pattern id="legend-hatch-med" patternUnits="userSpaceOnUse"
              width="6" height="6" patternTransform="rotate(-45)">
              <rect width="6" height="6" fill="${dag.colorX}"/>
              <line x1="0" y1="0" x2="0" y2="6"
                stroke="${dag.colorZ}" stroke-width="2.5"/>
            </pattern>
          </defs>
          <rect width="12" height="12" fill="url(#legend-hatch-med)"/>
        </svg></td>
        <td><span class="node node-x">X</span>'s influence on <span class="node node-y">Y</span> via <span class="node node-z">Z</span></td>
        <td>${pctMediated}%</td>
      </tr>
      <tr>
        <td><svg width="12" height="12"><rect width="12" height="12" fill="${dag.colorZ}"/></svg></td>
        <td><span class="node node-z">Z</span>'s own variation flowing to <span class="node node-y">Y</span></td>
        <td>${pctZ}%</td>
      </tr>
      <tr>
        <td><svg width="12" height="12"><rect width="12" height="12" fill="${dag.colorY}"/></svg></td>
        <td><span class="node node-y">Y</span>'s own variation</td>
        <td>${pctOwn}%</td>
      </tr>
      <tr class="summary ${adjust_z ? 'dimmed' : ''}">
        <td>
          <svg width="12" height="12"><rect width="12" height="12" fill="${dag.colorX}"/></svg>
          +
          <svg width="12" height="12">
            <rect width="12" height="12" fill="url(#legend-hatch-med)"/>
          </svg>
        </td>
        <td>Total <span class="node node-x">X</span> → <span class="node node-y">Y</span> effect</td>
        <td>${pctDirect + pctMediated}%</td>
      </tr>
      <tr class="summary ${adjust_z ? '' : 'dimmed'}">
        <td>
          <svg width="12" height="12"><rect width="12" height="12" fill="${dag.colorX}"/></svg>
        </td>
        <td>Direct-only <span class="node node-x">X</span> → <span class="node node-y">Y</span> effect</td>
        <td>${pctDirect}%</td>
      </tr>
    </table>
  </div>`;
}
yMax = 150
baseVal = 50

z_from_x = strength_xz * baseVal

y_direct_x = strength_xy * baseVal
y_mediated = adjust_z ? 0 : strength_zy * z_from_x
y_from_z_own = adjust_z ? 0 : strength_zy * baseVal
// -----------------
// Interactive DAG
// -----------------
{
  const width = 600;
  const height = 250;
  const nodeRadius = 36;

  // Mediator: Z is between X and Y, positioned at top
  const nodes = {
    X: { x: 130, y: 200, label: "X" },
    Z: { x: width / 2, y: 60, label: "Z" },
    Y: { x: 470, y: 200, label: "Y" }
  };

  const svg = d3.create("svg")
    .attr("viewBox", `0 0 ${width} ${height}`)
    .attr("width", width)
    .attr("height", height)
    .style("max-width", "100%");

  const defs = svg.append("defs");

  dag.addArrowMarkers(defs);

  // Mediation hatch: red stripes on gold, slope downward
  // "X flowing through Z's territory"
  dag.addHatchPattern(
    defs, "hatch-mediated", dag.colorX, dag.colorZ, -45
  );

  dag.addCircleClip(
    defs, "z-clip", nodes.Z.x, nodes.Z.y, nodeRadius
  );
  dag.addCircleClip(
    defs, "y-clip", nodes.Y.x, nodes.Y.y, nodeRadius
  );

  // Arrows
  const edges = [
    {
      id: "xz", from: nodes.X, to: nodes.Z,
      strength: strength_xz, blocked: false
    },
    {
      id: "zy", from: nodes.Z, to: nodes.Y,
      strength: strength_zy, blocked: adjust_z
    },
    {
      id: "xy", from: nodes.X, to: nodes.Y,
      strength: strength_xy, blocked: false
    }
  ];

  for (const edge of edges) {
    dag.drawEdge(svg, edge, nodeRadius);
  }

  // Nodes
  // X is solid
  dag.drawSolidNode(
    svg, nodes.X.x, nodes.X.y, nodeRadius, dag.colorX
  );

  // Z: strength directly controls fill proportion
  dag.drawNode(svg, nodes.Z.x, nodes.Z.y, nodeRadius, "z-clip", {
    bottomUp: [
      { prop: strength_xz, fill: dag.colorX }
    ],
    topDown: []
  }, "horizontal", dag.colorZ);

  // Y: blue base, incoming effects overlay
  dag.drawNode(svg, nodes.Y.x, nodes.Y.y, nodeRadius, "y-clip", {
    bottomUp: [
      { prop: Math.min(y_direct_x / yMax, 1), fill: dag.colorX },
      {
        prop: Math.min(y_mediated / yMax, 1),
        fill: "url(#hatch-mediated)"
      }
    ],
    topDown: [
      {
        prop: Math.min(y_from_z_own / yMax, 1),
        fill: dag.colorZ
      }
    ]
  }, undefined, dag.colorY);

  // Labels
  for (const n of Object.values(nodes)) {
    dag.drawLabel(svg, n.x, n.y, n.label);
  }

  return svg.node();
}