// Factory pattern: returns a function that creates a *new* SVG DOM node
// each call, so the same chart can be rendered on multiple slides
// independently while sharing reactive state. The reactive deps must be
// referenced at the top of this block (not inside the returned function),
// because Quarto's OJS runtime does not scan function bodies for deps.
tariffSoeChartFactory = {
// Reactive-dep references — every slider/derived value the chart consumes.
[tariff, stage, K, L, beta,
qSFree, qSTar, qDFree, qDTar, pWorld, pHome,
cloth, incomeRef];
return () => {
const w = 720, h = 440;
const margin = {top: 20, right: 140, bottom: 50, left: 70};
const iw = w - margin.left - margin.right;
const ih = h - margin.top - margin.bottom;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, w, h])
.attr("width", w)
.attr("height", h)
.style("max-width", "100%")
.style("font-family", "serif");
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// ── Fixed axis bounds. Shrunk to 120 × 3 to keep the welfare regions
// and X_F horizontal prominent at default (Home K=160, L=80). Higher
// K, L will push curves past the right edge — accepted trade-off in
// favor of pedagogical clarity at default.
const xMax = 120;
const yMax = 3;
const x = d3.scaleLinear().domain([0, xMax]).range([0, iw]);
const y = d3.scaleLinear().domain([0, yMax]).range([ih, 0]);
g.append("defs").append("clipPath")
.attr("id", "tariff-soe-clip")
.append("rect").attr("width", iw).attr("height", ih);
const plot = g.append("g").attr("clip-path", "url(#tariff-soe-clip)");
// ── Helpers: sample supply / demand between two prices ──
// (returns points in [q, p] form for polygon construction in data space)
const supplyBetween = (pLow, pHigh, N = 30) => {
const pts = [];
for (let i = 0; i <= N; i++) {
const p = pLow + (i / N) * (pHigh - pLow);
pts.push([produce(p, K, L).qc, p]);
}
return pts;
};
const demandBetween = (pLow, pHigh, N = 30) => {
const pts = [];
for (let i = 0; i <= N; i++) {
const p = pLow + (i / N) * (pHigh - pLow);
pts.push([beta * incomeRef / p, p]);
}
return pts;
};
const polyStr = (pts) => pts.map(p => `${x(p[0])},${y(p[1])}`).join(" ");
// ── Shaded welfare regions (drawn first so curves sit on top) ──
// Stage 3+: CS loss (blue tint)
if (stage >= 3 && tariff > 0) {
const csPts = [
[0, pHome],
[qDTar, pHome],
...demandBetween(pHome, pWorld).slice(1, -1),
[qDFree, pWorld],
[0, pWorld]
];
plot.append("polygon")
.attr("points", polyStr(csPts))
.attr("fill", "#005ab5").attr("fill-opacity", 0.14);
}
// Stage 4+: PS gain (maroon tint) and tariff revenue (gold)
if (stage >= 4 && tariff > 0) {
const psPts = [
[0, pWorld],
[qSFree, pWorld],
...supplyBetween(pWorld, pHome).slice(1, -1),
[qSTar, pHome],
[0, pHome]
];
plot.append("polygon")
.attr("points", polyStr(psPts))
.attr("fill", "#800000").attr("fill-opacity", 0.14);
plot.append("rect")
.attr("x", x(qSTar)).attr("y", y(pHome))
.attr("width", x(qDTar) - x(qSTar))
.attr("height", y(pWorld) - y(pHome))
.attr("fill", "#ffa319").attr("fill-opacity", 0.45);
}
// Stage 5: DWL triangles (red)
if (stage >= 5 && tariff > 0) {
const dwlLeft = [
[qSFree, pWorld],
[qSTar, pWorld],
...supplyBetween(pWorld, pHome).slice().reverse()
];
plot.append("polygon")
.attr("points", polyStr(dwlLeft))
.attr("fill", "#dc3230").attr("fill-opacity", 0.35);
const dwlRight = [
[qDTar, pHome],
[qDTar, pWorld],
[qDFree, pWorld],
...demandBetween(pWorld, pHome).slice().reverse()
];
plot.append("polygon")
.attr("points", polyStr(dwlRight))
.attr("fill", "#dc3230").attr("fill-opacity", 0.35);
}
// ── Foreign export supply (horizontal extension of S_H) ──
// Conceptual reframing: instead of a "world price line" spanning the full
// plot, the horizontal segment starts at the kink — where the rising
// domestic supply S_H meets the world price level — and extends right.
// This represents Foreign's perfectly-elastic export supply (SOE limit:
// λ→∞). The combined supply curve is one L-shape: S_H rising + horizontal
// extension by Foreign.
//
// Free trade : gold horizontal at p* from (qSFree, p*) → right edge.
// Tariff t>0 : red horizontal at p*+t from (qSTar, p*+t) → right edge,
// with the gold free-trade horizontal shown dashed for reference.
// Faint dashed reference levels at p* and p*+t from y-axis to the kink —
// keeps the welfare-shading boundaries visible without competing visually
// with the active supply curve to their right.
plot.append("line")
.attr("x1", x(0)).attr("x2", x(qSFree))
.attr("y1", y(pWorld)).attr("y2", y(pWorld))
.attr("stroke", "#ffa319").attr("stroke-width", 0.8)
.attr("stroke-dasharray", "1.5 2.5").attr("opacity", 0.55);
if (stage >= 2 && tariff > 0) {
plot.append("line")
.attr("x1", x(0)).attr("x2", x(qSTar))
.attr("y1", y(pHome)).attr("y2", y(pHome))
.attr("stroke", "#dc3230").attr("stroke-width", 0.8)
.attr("stroke-dasharray", "1.5 2.5").attr("opacity", 0.55);
}
// Free-trade Foreign export supply (gold). Solid pre-tariff; dashed once a
// tariff is in place (it is the obsolete free-trade reference).
plot.append("line")
.attr("x1", x(qSFree)).attr("x2", x(xMax))
.attr("y1", y(pWorld)).attr("y2", y(pWorld))
.attr("stroke", "#ffa319").attr("stroke-width", 2.4)
.attr("stroke-dasharray", stage >= 2 && tariff > 0 ? "5 3" : "none");
// Tariff Foreign export supply (red). Active when stage ≥ 2 with t > 0.
if (stage >= 2 && tariff > 0) {
plot.append("line")
.attr("x1", x(qSTar)).attr("x2", x(xMax))
.attr("y1", y(pHome)).attr("y2", y(pHome))
.attr("stroke", "#dc3230").attr("stroke-width", 2.4);
}
// ── Vertical drop lines at key quantities ──
if (stage >= 2 && tariff > 0) {
const drops = [
{q: qSFree, p: pWorld, color: "#800000"},
{q: qDFree, p: pWorld, color: "#005ab5"},
{q: qSTar, p: pHome, color: "#800000"},
{q: qDTar, p: pHome, color: "#005ab5"}
];
for (const d of drops) {
if (d.q > xMax || d.q < 0) continue;
plot.append("line")
.attr("x1", x(d.q)).attr("x2", x(d.q))
.attr("y1", ih).attr("y2", y(Math.min(d.p, yMax)))
.attr("stroke", d.color).attr("stroke-width", 1)
.attr("stroke-dasharray", "2 2").attr("opacity", 0.55);
}
}
// ── Curves ──
const line = d3.line()
.defined(d => d.q >= 0 && d.q <= xMax && d.p >= 0 && d.p <= yMax)
.x(d => x(d.q)).y(d => y(d.p));
plot.append("path")
.attr("d", line(cloth.supply))
.attr("fill", "none").attr("stroke", "#800000").attr("stroke-width", 2.2);
plot.append("path")
.attr("d", line(cloth.demand))
.attr("fill", "none").attr("stroke", "#005ab5").attr("stroke-width", 2.2);
// ── Equilibrium dots ──
const dot = (cx, cy, color) => plot.append("circle")
.attr("cx", x(cx)).attr("cy", y(cy)).attr("r", 3.5)
.attr("fill", color).attr("stroke", "white").attr("stroke-width", 1);
dot(qSFree, pWorld, "#800000");
dot(qDFree, pWorld, "#005ab5");
if (stage >= 2 && tariff > 0) {
dot(qSTar, pHome, "#800000");
dot(qDTar, pHome, "#005ab5");
}
// ── Axes ──
g.append("g").attr("transform", `translate(0,${ih})`)
.call(d3.axisBottom(x).ticks(6))
.selectAll("text").style("font-family", "serif");
g.append("g")
.call(d3.axisLeft(y).ticks(5))
.selectAll("text").style("font-family", "serif");
texLabel(g, iw / 2, ih + 38, "Home cloth quantity ($Q_C$)", {fontSize: 13});
texLabel(g, -50, ih / 2, "Relative price of cloth ($p$)", {rotated: true, fontSize: 13});
// ── Curve labels (at the right edge of the plot) ──
const sEdge = cloth.supply.filter(d => d.q <= xMax - 3 && d.p <= yMax - 0.05).slice(-1)[0];
if (sEdge) {
texLabel(g, x(sEdge.q) + 6, y(sEdge.p), "S_H", {
math: true, color: "#800000", bold: true, anchor: "start", fontSize: 13
});
}
const dEdge = cloth.demand.find(d => d.q <= xMax - 3);
if (dEdge) {
texLabel(g, x(dEdge.q) - 6, y(dEdge.p) - 4, "D_H", {
math: true, color: "#005ab5", bold: true, anchor: "end", fontSize: 13
});
}
// ── Price labels on the right margin ──
texLabel(g, iw + 8, y(pWorld), "p^{*}", {
math: true, color: "#ffa319", bold: true, anchor: "start", fontSize: 13
});
if (stage >= 2 && tariff > 0) {
texLabel(g, iw + 8, y(pHome), "p^{*}+t", {
math: true, color: "#dc3230", bold: true, anchor: "start", fontSize: 13
});
}
// X_F label on the active Foreign export supply horizontal — companion to
// the S_H label on the rising portion, making the two-component framing
// (domestic supply + foreign export supply) visually explicit.
{
const isTariff = stage >= 2 && tariff > 0;
const xfX = xMax * 0.78;
const xfY = isTariff ? pHome : pWorld;
texLabel(g, x(xfX), y(xfY) - 9, "X_{F}",
{math: true, bold: true, fontSize: 13,
color: isTariff ? "#dc3230" : "#ffa319", anchor: "middle"});
}
// ── Tariff wedge bracket (stage >= 2) ──
if (stage >= 2 && tariff > 0) {
const bx = iw + 55;
plot.append("line")
.attr("x1", bx - 4).attr("x2", bx).attr("y1", y(pWorld)).attr("y2", y(pWorld))
.attr("stroke", "#333").attr("stroke-width", 1);
plot.append("line")
.attr("x1", bx - 4).attr("x2", bx).attr("y1", y(pHome)).attr("y2", y(pHome))
.attr("stroke", "#333").attr("stroke-width", 1);
plot.append("line")
.attr("x1", bx).attr("x2", bx).attr("y1", y(pHome)).attr("y2", y(pWorld))
.attr("stroke", "#333").attr("stroke-width", 1);
texLabel(g, iw + 64, (y(pHome) + y(pWorld)) / 2, "t",
{math: true, anchor: "start", fontSize: 13, bold: true, color: "#333"});
}
// ── Area labels: use letters a / b / c / d (textbook convention) ──
// a = PS gain (strip between y-axis and S_H, in [p*, p*+t])
// b = left DWL (production-distortion triangle)
// c = Tariff rev. (rectangle from Q_S^t to Q_D^t)
// d = right DWL (consumption-distortion triangle)
// CS loss = a + b + c + d
const midY = (pWorld + pHome) / 2;
// Supply curve quantity at the mid-strip price (used to separate "a" vs "b"):
// PS trapezoid lies left of this x, left DWL triangle lies right of it.
const qSMid = produce(midY, K, L).qc;
if (stage >= 4 && tariff > 0) {
// "a" — PS trapezoid, centered strictly to the left of the supply curve
texLabel(g, x(qSMid * 0.45), y(midY), "a",
{fontSize: 13, bold: true, color: "#3e3e3e", anchor: "middle", math: true});
// "c" — Revenue rectangle center
texLabel(g, x((qSTar + qDTar) / 2), y(midY), "c",
{fontSize: 13, bold: true, color: "#3e3e3e", anchor: "middle", math: true});
}
if (stage >= 5 && tariff > 0) {
// "b" — left DWL triangle, placed between supply curve and qSTar at mid-y
texLabel(g, x((qSMid + qSTar) / 2), y(midY), "b",
{fontSize: 12, bold: true, color: "#3e3e3e", anchor: "middle", math: true});
// "d" — right DWL triangle, between qDTar and demand curve at mid-y
const qDMid = beta * incomeRef / midY;
texLabel(g, x((qDTar + qDMid) / 2), y(midY), "d",
{fontSize: 12, bold: true, color: "#3e3e3e", anchor: "middle", math: true});
}
return svg.node();
};
}