function ptaLine(plot, x, y, slope, intc, qa, qb, attrs) {
const l = plot.append("line")
.attr("x1", x(qa)).attr("y1", y(slope * qa + intc))
.attr("x2", x(qb)).attr("y2", y(slope * qb + intc));
for (const k in attrs) l.attr(k, attrs[k]);
return l;
}
ptaModel = {
const RD_a = 4;
const C = {
L1: {key: "L1", label: "L_1", omega: 1, c: -2, size: 1.0, color: "#800000"},
L2: {key: "L2", label: "L_2", omega: 2, c: 0, size: 1.0, color: "#005ab5"},
S: {key: "S", label: "S", omega: 3, c: +2, size: 0.2, color: "#cc7000"},
};
const bloc = (keys) => {
const ms = keys.map(k => C[k]);
const W = d3.sum(ms, d => d.size);
const cbar = d3.sum(ms, d => d.size * d.c) / W;
const omegaStar = (RD_a + cbar) / 2;
const gains = ms.map(d => ({...d, dOmega: d.omega - omegaStar, gain: d.size * (d.omega - omegaStar) ** 2}));
return {keys, members: ms, cbar, omegaStar, gains, agg: d3.sum(gains, g => g.gain)};
};
const configs = [
{keys: ["S", "L1"], out: "L2", disp: "{S, L₁}", note: "small + most-dissimilar large"},
{keys: ["L1", "L2"], out: "S", disp: "{L₁, L₂}", note: "two large rivals"},
{keys: ["S", "L2"], out: "L1", disp: "{S, L₂}", note: "small + nearer large"},
].map(cfg => ({...cfg, ...bloc(cfg.keys)}));
return {RD_a, C, bloc, configs};
}
// ── Setup chart: all three RS + common RD + autarky points (no bloc) ──
ptaSetupFactory = {
[ptaModel]; const M = ptaModel;
return () => {
const w = 540, h = 410, margin = {top: 16, right: 30, bottom: 38, left: 48};
const iw = w - margin.left - margin.right, 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})`);
const x = d3.scaleLinear().domain([0, 5]).range([0, iw]);
const y = d3.scaleLinear().domain([0, 4]).range([ih, 0]);
g.append("defs").append("clipPath").attr("id", "pta-setup-clip").append("rect").attr("width", iw).attr("height", ih);
const plot = g.append("g").attr("clip-path", "url(#pta-setup-clip)");
ptaLine(plot, x, y, -1, M.RD_a, 0, 5, {stroke: "#888", "stroke-width": 1.8});
texLabel(g, x(3.7), y(0.55), "RD", {color: "#888", bold: true, fontSize: 12, anchor: "start"});
for (const k of ["L1", "L2", "S"]) {
const c = M.C[k];
ptaLine(plot, x, y, 1, c.c, 0, 5, {stroke: c.color, "stroke-width": 2.4});
const qa = (M.RD_a - c.c) / 2;
plot.append("circle").attr("cx", x(qa)).attr("cy", y(c.omega)).attr("r", 4.2).attr("fill", c.color).attr("stroke", "white").attr("stroke-width", 1.2);
texLabel(g, x(qa) + 8, y(c.omega) - 3, c.label + " (RS)", {math: false, color: c.color, bold: true, fontSize: 12.5, anchor: "start"});
}
// y-axis tick marks at the three autarky prices (S highest — its RS hugs the axis)
for (const k of ["L1", "L2", "S"]) {
const c = M.C[k];
g.append("line").attr("x1", -4).attr("x2", 0).attr("y1", y(c.omega)).attr("y2", y(c.omega)).attr("stroke", c.color).attr("stroke-width", 1.6);
}
g.append("line").attr("x1", 0).attr("y1", ih).attr("x2", iw).attr("y2", ih).attr("stroke", "#000").attr("stroke-width", 0.9);
g.append("line").attr("x1", 0).attr("y1", ih).attr("x2", 0).attr("y2", 0).attr("stroke", "#000").attr("stroke-width", 0.9);
texLabel(g, iw / 2, ih + 26, "Relative quantity $Q_C / Q_W$", {fontSize: 12});
texLabel(g, -34, ih / 2, "Relative price ω", {rotated: true, fontSize: 12});
texLabel(g, x(2.0), y(3.6), "S small (≈⅕ of each large)", {color: "#cc7000", fontSize: 9.5, anchor: "start"});
return svg.node();
};
}
// ── Three-panel comparison of the FTA configurations (small multiples) ──
ptaPanelsFactory = {
[ptaModel]; const M = ptaModel;
return () => {
const onePanel = (cfg, idx) => {
const w = 322, h = 360, margin = {top: 48, right: 12, bottom: 24, left: 30};
const iw = w - margin.left - margin.right, ih = h - margin.top - margin.bottom;
const svg = d3.create("svg").attr("viewBox", [0, 0, w, h]).attr("width", w).attr("height", h).style("font-family", "serif");
const defs = svg.append("defs");
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
defs.append("clipPath").attr("id", `pc${idx}`).append("rect").attr("width", iw).attr("height", ih);
defs.append("marker").attr("id", `pm${idx}`).attr("viewBox", "0 -5 10 10").attr("refX", 5).attr("refY", 0)
.attr("markerWidth", 5).attr("markerHeight", 5).attr("orient", "auto").append("path").attr("d", "M0,-5L10,0L0,5").attr("fill", "#333");
const x = d3.scaleLinear().domain([0, 4.5]).range([0, iw]);
const y = d3.scaleLinear().domain([0, 4]).range([ih, 0]);
const plot = g.append("g").attr("clip-path", `url(#pc${idx})`);
ptaLine(plot, x, y, -1, M.RD_a, 0, 4.5, {stroke: "#999", "stroke-width": 1.4});
for (const m of cfg.members) ptaLine(plot, x, y, 1, m.c, 0, 4.5, {stroke: m.color, "stroke-width": 2});
ptaLine(plot, x, y, 1, cfg.cbar, 0, 4.5, {stroke: "#2e7d32", "stroke-width": 2.6, "stroke-dasharray": "6 3"});
const qstar = (M.RD_a - cfg.cbar) / 2;
plot.append("line").attr("x1", 0).attr("x2", x(qstar)).attr("y1", y(cfg.omegaStar)).attr("y2", y(cfg.omegaStar))
.attr("stroke", "#2e7d32").attr("stroke-width", 0.7).attr("stroke-dasharray", "2 2").attr("opacity", 0.6);
for (const m of cfg.members) {
const qa = (M.RD_a - m.c) / 2;
plot.append("line").attr("x1", x(qa)).attr("x2", x(qa)).attr("y1", y(m.omega)).attr("y2", y(cfg.omegaStar))
.attr("stroke", m.color).attr("stroke-width", 2.4).attr("marker-end", `url(#pm${idx})`).attr("opacity", 0.9);
plot.append("circle").attr("cx", x(qa)).attr("cy", y(m.omega)).attr("r", 3.4).attr("fill", m.color).attr("stroke", "white").attr("stroke-width", 1);
texLabel(g, x(qa) + (qa > 2 ? -6 : 7), y(m.omega) + (m.omega > 2 ? -7 : 12), m.label, {math: true, color: m.color, bold: true, fontSize: 12.5, anchor: qa > 2 ? "end" : "start"});
}
plot.append("circle").attr("cx", x(qstar)).attr("cy", y(cfg.omegaStar)).attr("r", 3.6).attr("fill", "#2e7d32").attr("stroke", "white").attr("stroke-width", 1);
texLabel(g, x(qstar) + 5, y(cfg.omegaStar) - 6, "ω*", {color: "#2e7d32", bold: true, fontSize: 10, anchor: "start"});
g.append("line").attr("x1", 0).attr("y1", ih).attr("x2", iw).attr("y2", ih).attr("stroke", "#000").attr("stroke-width", 0.8);
g.append("line").attr("x1", 0).attr("y1", ih).attr("x2", 0).attr("y2", 0).attr("stroke", "#000").attr("stroke-width", 0.8);
texLabel(g, iw - 2, ih - 7, "$Q_C/Q_W$", {fontSize: 9, anchor: "end", color: "#555"});
texLabel(g, 5, 5, "ω", {fontSize: 11, anchor: "start"});
// title, note, aggregate-gain bar (in top margin)
svg.append("text").attr("x", margin.left).attr("y", 15).style("font-size", "13.5px").style("font-weight", "bold").style("font-family", "serif").text(cfg.disp);
svg.append("text").attr("x", margin.left).attr("y", 28).style("font-size", "9px").style("fill", "#666").style("font-family", "serif").text(cfg.note + (cfg.out ? ` · ${cfg.out === "S" ? "S" : cfg.out} out` : ""));
const barX = margin.left, barW = iw - 50;
svg.append("rect").attr("x", barX).attr("y", 35).attr("width", barW).attr("height", 7).attr("fill", "#eee").attr("rx", 2);
svg.append("rect").attr("x", barX).attr("y", 35).attr("width", Math.max(2, (cfg.agg / 0.7) * barW)).attr("height", 7).attr("fill", cfg.agg >= 0.6 ? "#2e7d32" : "#9e9e9e").attr("rx", 2);
svg.append("text").attr("x", barX + barW + 5).attr("y", 42).style("font-size", "9.5px").style("font-family", "serif").style("fill", cfg.agg >= 0.6 ? "#2e7d32" : "#333").style("font-weight", cfg.agg >= 0.6 ? "bold" : "normal").text(`agg ${cfg.agg.toFixed(2)}`);
return svg.node();
};
const wrap = document.createElement("div");
wrap.style.display = "flex";
wrap.style.justifyContent = "center";
wrap.style.gap = "4px";
M.configs.forEach((cfg, i) => wrap.appendChild(onePanel(cfg, i)));
return wrap;
};
}
// ── Aggregate-gain ranking bars (synthesis slide) ──
ptaRankFactory = {
[ptaModel]; const M = ptaModel;
return () => {
const max = d3.max(M.configs, c => c.agg);
const div = document.createElement("div");
div.style.fontFamily = "serif";
div.style.width = "100%";
div.style.boxSizing = "border-box";
div.style.paddingRight = "12px";
const rows = M.configs.slice().sort((a, b) => b.agg - a.agg).map(c => {
const pct = (c.agg / max * 100).toFixed(0);
const eff = Math.abs(c.agg - max) < 1e-9;
return `<div style="display:flex;align-items:center;gap:6px;margin:6px 0">
<span style="flex:0 0 56px;white-space:nowrap;font-size:0.82em">${c.disp}</span>
<div style="flex:1 1 auto;min-width:0;background:#eee;height:18px;border-radius:3px;overflow:hidden">
<div style="width:${pct}%;height:100%;background:${eff ? "#2e7d32" : "#9e9e9e"}"></div></div>
<span style="flex:0 0 36px;text-align:right;font-size:0.82em;${eff ? "font-weight:bold;color:#2e7d32" : "color:#555"}">${c.agg.toFixed(2)}</span></div>`;
}).join("");
div.innerHTML = `<div style="font-size:0.82em;font-weight:bold;margin-bottom:3px">Aggregate gains from trade</div>${rows}`;
return div;
};
}