from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
# The nodes are spaced across a wider canvas so the direction of the
# assignment path is easy to read. Smaller boxes also leave longer visible arrow lines.
nodes = {
"X": {"xy": (0.07, 0.74), "label": "Observed\ncontrols X", "color": "#dbeafe"},
"R": {"xy": (0.30, 0.52), "label": "Running\nscore R", "color": "#fef3c7"},
"C": {"xy": (0.52, 0.52), "label": "Cutoff\nrule R >= c", "color": "#dcfce7"},
"D": {"xy": (0.73, 0.52), "label": "Treatment\nD", "color": "#fde68a"},
"Y": {"xy": (0.94, 0.52), "label": "Outcome\nY", "color": "#fee2e2"},
"U": {"xy": (0.55, 0.86), "label": "Smooth latent\nfactors", "color": "#f3f4f6"},
}
fig, ax = plt.subplots(figsize=(14, 6.2))
ax.set_axis_off()
ax.set_xlim(-0.035, 1.035)
ax.set_ylim(0.02, 0.98)
box_w, box_h = 0.118, 0.095
arrow_gap = 0.010
def anchor(node, side):
"""
Idea: Return the plotting anchor point for a named node in the diagram.
Parameters
----------
node : object
Graph node whose position, label, or incident edges are being processed.
side : object
Endpoint side used to place an arrow or edge on the correct part of a node.
Returns
-------
tuple[float, float] or np.ndarray
Coordinate of the requested side of a plotted node.
"""
x, y = nodes[node]["xy"]
offsets = {
"left": (-box_w / 2, 0),
"right": (box_w / 2, 0),
"top": (0, box_h / 2),
"bottom": (0, -box_h / 2),
"upper_right": (box_w / 2, box_h * 0.25),
"lower_right": (box_w / 2, -box_h * 0.25),
"upper_left": (-box_w / 2, box_h * 0.25),
"lower_left": (-box_w / 2, -box_h * 0.25),
}
dx, dy = offsets[side]
return np.array([x + dx, y + dy], dtype=float)
def shorten(start, end, gap=arrow_gap):
"""
Idea: Shorten a line segment so the arrowhead remains visible outside the node box.
Parameters
----------
start : tuple[float, float]
Starting coordinate for a plotted element.
end : tuple[float, float]
Ending coordinate for a plotted element.
gap : float
Spacing used to keep plotted elements from overlapping.
Returns
-------
tuple[float, float]
Shortened endpoint coordinate that keeps an arrowhead visible.
"""
start = np.asarray(start, dtype=float)
end = np.asarray(end, dtype=float)
delta = end - start
length = np.hypot(delta[0], delta[1])
if length == 0:
return tuple(start), tuple(end)
unit = delta / length
return tuple(start + gap * unit), tuple(end - gap * unit)
def draw_arrow(start, end, color, style="solid", rad=0.0, linewidth=1.7):
"""
Idea: Draw a routed arrow between diagram nodes while keeping the arrowhead visible.
Parameters
----------
start : tuple[float, float]
Starting coordinate for a plotted element.
end : tuple[float, float]
Ending coordinate for a plotted element.
color : str
Plot color used to identify this element.
style : object
Plotting style that determines how the curve, marker, or annotation is drawn.
rad : float
Curvature parameter for the plotted arrow.
linewidth : object
Line width used to make the plotted edge or reference line readable.
Returns
-------
None
Adds an arrow annotation directly to the supplied Matplotlib axes.
"""
start, end = shorten(start, end)
arrow = FancyArrowPatch(
start,
end,
arrowstyle="-|>",
mutation_scale=18,
linewidth=linewidth,
color=color,
linestyle=style,
shrinkA=0,
shrinkB=0,
connectionstyle=f"arc3,rad={rad}",
zorder=5,
)
ax.add_patch(arrow)
# Main assignment path.
draw_arrow(anchor("X", "lower_right"), anchor("R", "upper_left"), color="#334155")
draw_arrow(anchor("R", "right"), anchor("C", "left"), color="#334155")
draw_arrow(anchor("C", "right"), anchor("D", "left"), color="#15803d")
draw_arrow(anchor("D", "right"), anchor("Y", "left"), color="#b45309")
# Smooth background paths remind us what continuity means.
draw_arrow(anchor("R", "upper_right"), anchor("Y", "upper_left"), color="#6b7280", style="dashed", rad=-0.10, linewidth=1.5)
draw_arrow(anchor("U", "lower_left"), anchor("R", "top"), color="#6b7280", style="dashed", linewidth=1.5)
draw_arrow(anchor("U", "lower_right"), anchor("Y", "top"), color="#6b7280", style="dashed", linewidth=1.5)
for spec in nodes.values():
x, y = spec["xy"]
rect = FancyBboxPatch(
(x - box_w / 2, y - box_h / 2),
box_w,
box_h,
boxstyle="round,pad=0.014",
facecolor=spec["color"],
edgecolor="#334155",
linewidth=1.2,
zorder=3,
)
ax.add_patch(rect)
ax.text(x, y, spec["label"], ha="center", va="center", fontsize=10.5, fontweight="bold", zorder=4)
ax.text(
0.50,
0.10,
"RDD estimates a local effect at the cutoff. The dashed paths should be smooth through the cutoff in the absence of treatment.",
ha="center",
va="center",
fontsize=10,
color="#475569",
)
ax.set_title("Regression Discontinuity Teaching Design", pad=18)
plt.tight_layout()
fig.savefig(FIGURE_DIR / f"{NOTEBOOK_PREFIX}_rdd_design_dag.png", dpi=160, bbox_inches="tight")
plt.show()