Interactive DAGs
  • Confounders
  • Mediators
  • Colliders

Confounders

d3 = require("d3@7")
dag = import(new URL("js/dag-utils.js", document.baseURI).href)
Relationships between nodes
viewof strength_zx = Inputs.range([0, 1], {
  value: 0.5, 
  step: 0.05, 
  label: html`<span class="node node-z">Z</span> → <span class="node node-x">X</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.7, 
  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 backdoor path</em>)`
})
// ----------------
// Status readout
// ----------------
{
  const pctPure = Math.round(y_pure_x / yMax * 100);
  const pctConf = Math.round(y_confounded / yMax * 100);
  const pctZ = Math.round(y_direct_z / yMax * 100);
  const pctOwn = Math.max(0, 100 - pctPure - pctConf - 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>${pctPure}%</td>
      </tr>
      <tr>
        <td><svg width="12" height="12">
          <defs>
            <pattern id="legend-hatch-conf" patternUnits="userSpaceOnUse"
              width="6" height="6" patternTransform="rotate(45)">
              <rect width="6" height="6" fill="${dag.colorZ}"/>
              <line x1="0" y1="0" x2="0" y2="6"
                stroke="${dag.colorX}" stroke-width="2.5"/>
            </pattern>
          </defs>
          <rect width="12" height="12" fill="url(#legend-hatch-conf)"/>
        </svg></td>
        <td><span class="node node-z">Z</span>'s influence on <span class="node node-y">Y</span> via <span class="node node-x">X</span></td>
        <td>${pctConf}%</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 direct influence on <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-conf)"/>
          </svg>
        </td>
        <td>Apparent <span class="node node-x">X</span> → <span class="node node-y">Y</span> effect</td>
        <td>${pctPure + pctConf}%</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>Unconfounded <span class="node node-x">X</span> → <span class="node node-y">Y</span> effect</td>
        <td>${pctPure}%</td>
      </tr>
    </table>
  </div>`;
}
yMax = 150
baseVal = 50

x_from_z = adjust_z ? 0 : strength_zx * baseVal

y_pure_x = strength_xy * baseVal
y_confounded = adjust_z ? 0 : strength_xy * x_from_z
y_direct_z = adjust_z ? 0 : strength_zy * baseVal
// -----------------
// Interactive DAG
// -----------------
{
  const width = 600;
  const height = 250;
  const nodeRadius = 36;

  const nodes = {
    Z: { x: width / 2, y: 60, label: "Z" },
    X: { x: 130, y: 200, label: "X" },
    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);
  dag.addHatchPattern(
    defs, "hatch-confounded", dag.colorZ, dag.colorX, 45
  );
  dag.addCircleClip(
    defs, "x-clip", nodes.X.x, nodes.X.y, nodeRadius
  );
  dag.addCircleClip(
    defs, "y-clip", nodes.Y.x, nodes.Y.y, nodeRadius
  );

  // Arrows
  const edges = [
    {
      id: "zx", from: nodes.Z, to: nodes.X,
      strength: strength_zx, blocked: adjust_z
    },
    {
      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: strength directly controls fill proportion
  dag.drawNode(svg, nodes.X.x, nodes.X.y, nodeRadius, "x-clip", {
    bottomUp: [],
    topDown: [
      { prop: strength_zx, fill: dag.colorZ }
    ]
  }, undefined, dag.colorX);

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

  dag.drawSolidNode(
    svg, nodes.Z.x, nodes.Z.y, nodeRadius, dag.colorZ
  );

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

  return svg.node();
}