viewof complementsPlot = {
const container = document.createElement("div");
container.style.display = "flex";
container.style.width = "100%";
container.style.justifyContent = "space-between";
container.style.alignItems = "flex-start";
const tc = {
foreground: "#222222", accent: "#FF3E00",
lightGray: "#e5e5e5", mediumGray: "#9e9e9e", darkGray: "#555555"
};
const plotArea = document.createElement("div");
plotArea.style.width = "73%";
const cp = document.createElement("div");
cp.style.width = "27%";
cp.style.padding = "8px";
cp.style.backgroundColor = tc.lightGray;
cp.style.borderRadius = "4px";
cp.style.fontSize = "0.72em";
cp.style.color = tc.foreground;
cp.style.fontFamily = "Inter,-apple-system,BlinkMacSystemFont,sans-serif";
function mkSlider(html, min, max, step, val) {
const d = document.createElement("div");
d.style.marginBottom = "6px";
const l = document.createElement("label");
l.innerHTML = html;
l.style.fontWeight = "500";
const v = document.createElement("span");
v.textContent = " " + Number(val).toFixed(2);
v.style.fontWeight = "500";
const s = document.createElement("input");
s.type = "range";
s.min = String(min);
s.max = String(max);
s.step = String(step);
s.value = String(val);
s.style.width = "100%";
s.style.accentColor = tc.accent;
d.appendChild(l);
d.appendChild(v);
d.appendChild(document.createElement("br"));
d.appendChild(s);
return {d, s, v};
}
function mkH(text) {
const h = document.createElement("div");
h.textContent = text;
h.style.fontWeight = "700";
h.style.margin = "8px 0 4px";
h.style.borderBottom = "1px solid " + tc.mediumGray;
h.style.paddingBottom = "2px";
return h;
}
cp.appendChild(mkH("Utilidad"));
const sA = mkSlider("a:", 0.1, 5, 0.1, 1);
const sB = mkSlider("b:", 0.1, 5, 0.1, 1);
cp.appendChild(sA.d);
cp.appendChild(sB.d);
cp.appendChild(mkH("Presupuesto"));
const sPX = mkSlider("p<sub>X</sub>:", 0.1, 5, 0.1, 1);
const sPY = mkSlider("p<sub>Y</sub>:", 0.1, 5, 0.1, 1);
const sM = mkSlider("m:", 1, 30, 0.5, 10);
cp.appendChild(sPX.d);
cp.appendChild(sPY.d);
cp.appendChild(sM.d);
let a = 1, b = 1, pX = 1, pY = 1, m = 10;
const W = 550, H = 350;
const mg = {top: 20, right: 15, bottom: 40, left: 50};
const pW = W - mg.left - mg.right, pH = H - mg.top - mg.bottom;
const svg = d3.create("svg")
.attr("width", W).attr("height", H)
.style("font-family", "Inter,-apple-system,BlinkMacSystemFont,sans-serif");
svg.append("defs").append("clipPath").attr("id", "clip-comp")
.append("rect").attr("x", mg.left).attr("y", mg.top)
.attr("width", pW).attr("height", pH);
const xSc = d3.scaleLinear().range([mg.left, W - mg.right]);
const ySc = d3.scaleLinear().range([H - mg.bottom, mg.top]);
const gridG = svg.append("g");
const xAxG = svg.append("g").attr("transform", `translate(0,${H - mg.bottom})`);
const yAxG = svg.append("g").attr("transform", `translate(${mg.left},0)`);
const cG = svg.append("g").attr("clip-path", "url(#clip-comp)");
const bFill = cG.append("polygon").attr("fill", tc.lightGray).attr("fill-opacity", 0.4);
const bLine = cG.append("line").attr("stroke", tc.foreground).attr("stroke-width", 2.5);
// Expansion path (aX = bY → Y = (a/b)X)
const expPath = cG.append("line")
.attr("stroke", tc.mediumGray).attr("stroke-width", 1.5)
.attr("stroke-dasharray", "5,4");
const cc = ["#4285F4", "#0F9D58", "#F4B400", "#DB4437", "#AB47BC"];
const ps = [], ls = [];
for (let i = 0; i < 5; i++) {
ps.push(cG.append("path").attr("fill", "none").attr("stroke", cc[i]).attr("stroke-width", i === 2 ? 3 : 2));
ls.push(cG.append("text").attr("fill", cc[i]).style("font-size", "10px").style("font-weight", "600"));
}
const optDot = cG.append("circle").attr("r", 6)
.attr("fill", tc.accent).attr("stroke", "white").attr("stroke-width", 1.5);
const optLbl = cG.append("text").attr("fill", tc.accent)
.style("font-size", "11px").style("font-weight", "600");
svg.append("text").attr("x", W / 2).attr("y", H - 2)
.attr("text-anchor", "middle").attr("fill", tc.darkGray)
.style("font-weight", "500").style("font-size", "14px").text("X");
svg.append("text").attr("transform", "rotate(-90)")
.attr("x", -H / 2).attr("y", 15)
.attr("text-anchor", "middle").attr("fill", tc.darkGray)
.style("font-weight", "500").style("font-size", "14px").text("Y");
const lineGen = d3.line().x(d => xSc(d[0])).y(d => ySc(d[1]));
function update() {
const bxI = m / pX, byI = m / pY;
// Optimal: intersection of aX = bY with budget line
const optX = b * m / (b * pX + a * pY);
const optY = a * m / (b * pX + a * pY);
const uOpt = Math.min(a * optX, b * optY);
const maxV = Math.max(bxI, byI) * 1.5;
xSc.domain([0, maxV]);
ySc.domain([0, maxV]);
xAxG.call(d3.axisBottom(xSc)).attr("color", tc.darkGray);
yAxG.call(d3.axisLeft(ySc)).attr("color", tc.darkGray);
gridG.selectAll("*").remove();
const gx = gridG.append("g").attr("transform", `translate(0,${H - mg.bottom})`);
gx.call(d3.axisBottom(xSc).tickSize(-pH).tickFormat(""));
gx.selectAll("line").attr("stroke", tc.lightGray).attr("stroke-opacity", 0.5);
gx.select(".domain").attr("stroke", "none");
const gy = gridG.append("g").attr("transform", `translate(${mg.left},0)`);
gy.call(d3.axisLeft(ySc).tickSize(-pW).tickFormat(""));
gy.selectAll("line").attr("stroke", tc.lightGray).attr("stroke-opacity", 0.5);
gy.select(".domain").attr("stroke", "none");
bFill.attr("points",
`${xSc(0)},${ySc(0)} ${xSc(bxI)},${ySc(0)} ${xSc(0)},${ySc(byI)}`);
bLine.attr("x1", xSc(bxI)).attr("y1", ySc(0))
.attr("x2", xSc(0)).attr("y2", ySc(byI));
// Expansion path: Y = (a/b)X
const slope = a / b;
const epX = slope <= 1 ? maxV : maxV / slope;
const epY = slope <= 1 ? slope * maxV : maxV;
expPath.attr("x1", xSc(0)).attr("y1", ySc(0))
.attr("x2", xSc(epX)).attr("y2", ySc(epY));
const mults = [0.5, 0.75, 1, 1.25, 1.5];
for (let i = 0; i < 5; i++) {
const u = uOpt * mults[i];
if (u <= 0) {
ps[i].attr("visibility", "hidden");
ls[i].attr("visibility", "hidden");
continue;
}
// L-shaped: from (u/a, maxV) → (u/a, u/b) → (maxV, u/b)
const kx = u / a, ky = u / b;
const pts = [[kx, maxV], [kx, ky], [maxV, ky]];
ps[i].attr("d", lineGen(pts)).attr("visibility", "visible");
ls[i].attr("x", xSc(kx) + 4).attr("y", ySc(ky) - 6)
.text("ū=" + u.toFixed(1)).attr("visibility", "visible");
}
optDot.attr("cx", xSc(optX)).attr("cy", ySc(optY));
optLbl.attr("x", xSc(optX) + 8).attr("y", ySc(optY) - 8)
.text(`(${optX.toFixed(1)}, ${optY.toFixed(1)})`);
}
update();
sA.s.addEventListener("input", function() { a = +this.value; sA.v.textContent = " " + a.toFixed(2); update(); });
sB.s.addEventListener("input", function() { b = +this.value; sB.v.textContent = " " + b.toFixed(2); update(); });
sPX.s.addEventListener("input", function() { pX = +this.value; sPX.v.textContent = " " + pX.toFixed(2); update(); });
sPY.s.addEventListener("input", function() { pY = +this.value; sPY.v.textContent = " " + pY.toFixed(2); update(); });
sM.s.addEventListener("input", function() { m = +this.value; sM.v.textContent = " " + m.toFixed(2); update(); });
plotArea.appendChild(svg.node());
container.appendChild(plotArea);
container.appendChild(cp);
return container;
}