ciChart = {
const p0 = ciCtrl.proportion;
const mu1 = [20, 22], mu2 = [22, 20];
const delta = [mu1[0] - mu2[0], mu1[1] - mu2[1]];
const meanOf = (p) => [p * mu1[0] + (1 - p) * mu2[0], p * mu1[1] + (1 - p) * mu2[1]];
const S1 = mixLin.mat(ciCtrl.v1, ciCtrl.v2, ciCtrl.rho);
const S2 = mixLin.mat(ciCtrl.v1, ciCtrl.v2, 0);
const at = (p) => mixLin.laws(p, S1, S2);
const base = at(p0);
const mean0 = meanOf(p0);
let y = mean0;
if (ciDraw) {
const z1 = mixLin.normal(20261112 + 2 * ciDraw);
const z2 = mixLin.normal(20261112 + 2 * ciDraw + 1);
const L00 = Math.sqrt(Math.max(base.Slin[0][0], 1e-12));
const L10 = base.Slin[1][0] / L00;
const L11 = Math.sqrt(Math.max(base.Slin[1][1] - L10 * L10, 1e-12));
y = [mean0[0] + L00 * z1, mean0[1] + L10 * z1 + L11 * z2];
}
const fq = mixLin.fisher(delta, base.Squad, base.SpQuad);
const fl = mixLin.fisher(delta, base.Slin, base.SpLin);
const grid = d3.range(0.02, 0.981, 0.01);
const series = [
{ name: "Population", colour: "#c45c26", half: fq.half, cov: (p) => at(p).Squad },
{ name: "Single cell", colour: "#1d4e89", half: fl.half, cov: (p) => at(p).Slin }
];
for (const s of series) {
s.ll = grid.map((p) => mixLin.loglik(y, meanOf(p), s.cov(p)));
s.max = Math.max(...s.ll);
}
const W = 980, H = 250, mid = 500;
const svg = d3.create("svg").attr("viewBox", [0, 0, W, H]).attr("width", "100%")
.style("font", "14px sans-serif");
const xShare = d3.scaleLinear().domain([0, 1]).range([48, mid - 24]);
svg.append("text").attr("x", 48).attr("y", 16).attr("fill", "#1c2b24").attr("font-weight", 700)
.text("Wald interval for the true proportion");
svg.append("line").attr("x1", xShare(0)).attr("x2", xShare(1)).attr("y1", 108).attr("y2", 108)
.attr("stroke", "#cfc6b8");
svg.append("line").attr("x1", xShare(p0)).attr("x2", xShare(p0)).attr("y1", 36).attr("y2", 168)
.attr("stroke", "#1c2b24").attr("stroke-dasharray", "4 3");
series.forEach((s, i) => {
const y0 = 78 + i * 48;
const lo = Math.max(0, p0 - s.half), hi = Math.min(1, p0 + s.half);
svg.append("line").attr("x1", xShare(lo)).attr("x2", xShare(hi)).attr("y1", y0).attr("y2", y0)
.attr("stroke", s.colour).attr("stroke-width", 8).attr("stroke-linecap", "round");
svg.append("circle").attr("cx", xShare(p0)).attr("cy", y0).attr("r", 4).attr("fill", "#1c2b24");
svg.append("text").attr("x", 48).attr("y", y0 - 12).attr("fill", s.colour)
.text(`${s.name}: half-width ${s.half.toFixed(2)}`);
});
svg.append("text").attr("x", 48).attr("y", 200).attr("fill", "#5c6b63")
.text("Dashed line: true proportion. Bars are clipped to [0, 1].");
const xP = d3.scaleLinear().domain([0.02, 0.98]).range([mid + 36, W - 16]);
const lls = series.flatMap((s) => s.ll);
const cuts = series.map((s) => s.max - 1.92);
const yL = d3.scaleLinear()
.domain([Math.min(...lls, ...cuts), Math.max(...lls)])
.range([H - 36, 28])
.nice();
svg.append("text").attr("x", mid + 36).attr("y", 16).attr("fill", "#1c2b24").attr("font-weight", 700)
.text(ciDraw ? "Log-likelihood of the drawn bulk" : "Log-likelihood at the mean bulk");
svg.append("g").attr("transform", `translate(${mid + 36},0)`).call(d3.axisLeft(yL).ticks(4));
svg.append("g").attr("transform", `translate(0,${H - 36})`).call(d3.axisBottom(xP).ticks(5));
const panel = svg.append("g").attr("clip-path", "url(#ll-clip)");
svg.append("clipPath").attr("id", "ll-clip").append("rect")
.attr("x", mid + 8).attr("y", 20).attr("width", W - mid - 8).attr("height", H - 56);
const line = d3.line().x((d, i) => xP(grid[i])).y((d) => yL(d));
for (const s of series) {
panel.append("path").attr("d", line(s.ll)).attr("fill", "none")
.attr("stroke", s.colour).attr("stroke-width", 2);
const cut = s.max - 1.92;
panel.append("line").attr("x1", xP(0.02)).attr("x2", xP(0.98))
.attr("y1", yL(cut)).attr("y2", yL(cut))
.attr("stroke", s.colour).attr("stroke-dasharray", "3 3").attr("opacity", 0.85);
}
return svg.node();
}