md`A dialyzer is the filter in a dialysis machine that pulls excess fluid out of a patient's blood. Twenty of them were tested, each run at seven pressure settings and two blood-flow settings, and the amount of fluid removed per hour (the rate, in ml/hr) was recorded. The rate rises with pressure, then levels off, and sits higher at the higher blood flow.
I modelled all twenty devices together so the model learns one shared curve and how much each device strays from it. Because it's Bayesian, the result is a range of plausible answers rather than a single number, which is what lets the next pages say things like "there is a 90% chance the rate stays under 50 ml/hr" or "device 10 is almost certainly the lowest."
Thumbnail icon from [Magnific from Flaticon](https://www.flaticon.com/free-icon/filtration_12041804?term=mesh+filter&related_id=12041804)
`svg`<svg viewBox="0 0 720 160" style="width:100%;height:auto;max-height:100%;display:block;font-family:inherit">
<defs>
<marker id="arr" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="12" markerHeight="12" markerUnits="userSpaceOnUse" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#e4e6e8"/>
</marker>
</defs>
${edge(185, 66, 286, 103)}
${edge(360, 66, 360, 101)}
${edge(535, 66, 434, 103)}
${node(120, 40, "pressure", "#434a52", "continuous, set")}
${node(360, 40, "blood_flow", "#434a52", "categorical, 200 or 300")}
${node(600, 40, "dialyzer", "#434a52", "categorical, 20 devices")}
${node(360, 128, "rate", "#8a6a2a", "continuous, measured")}
</svg>`Plot.plot({
width: width * 0.46,
height: 300,
marginLeft: 50,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: "pressure (dmHg)"},
y: {labelArrow: "none", labelAnchor: "top", label: "rate (ml/hr)", grid: true},
color: {legend: true, type: "categorical", domain: ["200", "300"], range: ["#3ddc84", "#4f8ef7"], label: "blood flow (ml/min)"},
marks: [
Plot.line(obs, {x: "pressure", y: "rate", z: "dialyzer", stroke: d => String(d.blood_flow), strokeOpacity: 0.8}),
Plot.dot(obs, {x: "pressure", y: "rate", fill: "#dfe3e6", r: 2, fillOpacity: 0.8})
]
})box = (v) => Plot.plot({
width: width * 0.21,
height: 240,
marginLeft: 50,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: "blood flow (ml/min)", type: "band"},
y: {labelArrow: "none", labelAnchor: "top", label: v === "rate" ? "rate (ml/hr)" : "pressure (dmHg)", grid: true},
marks: [Plot.boxY(obs_flow, {x: "flow", y: v, fill: "#434a52", stroke: "#e4e6e8"})]
})node = (x, y, label, fill, sub) => svg`<g>
<rect x="${x - 76}" y="${y - 26}" width="152" height="52" rx="10" fill="${fill}" stroke="#e4e6e8" stroke-width="1.5"/>
<text x="${x}" y="${y + (sub ? -3 : 6)}" text-anchor="middle" font-size="17" font-weight="600" fill="#e4e6e8">${label}</text>
${sub ? svg`<text x="${x}" y="${y + 16}" text-anchor="middle" font-size="11" fill="#b4b9bf">${sub}</text>` : ""}
</g>`throttle = (input) => Generators.observe(notify => {
// sliders fire on every pixel; apply the latest value once per screen frame
let pending = false
const fire = () => { pending = false; notify(input.value) }
const onInput = () => { if (!pending) { pending = true; requestAnimationFrame(fire) } }
notify(input.value)
input.addEventListener("input", onInput)
return () => input.removeEventListener("input", onInput)
})settings_card = html`<div class="pm-settings">
${viewof who}
${q === 5 || one !== null ? "" : viewof blood_flow}
${q === 1 || q === 2 || q === 5 ? viewof pressure_in : ""}
${q === 2 || q === 4 ? viewof limit_in : ""}
${q === 3 ? viewof target_in : ""}
${q === 4 ? viewof prob_in : ""}
${q === 5 ? viewof threshold_in : ""}
${viewof ci_in}
<div class="pm-answer" id="pm-answer-2"></div>
</div>`html`${tex.block`
\begin{aligned}
\text{rate} &= a\,\bigl(1 - e^{-c\,\text{pressure}}\bigr) + b \\[4pt]
b &= b_0 + b_{\text{bf}}\,\text{blood\_flow} + \sigma_b\, z_b \\
c &= \exp\!\bigl(c_0 + c_{\text{bf}}\,\text{blood\_flow} + \sigma_c\, z_c\bigr) \\[4pt]
\text{measured rate} &= \text{rate} + \sigma\,\varepsilon
\end{aligned}
`}
<p style="margin-top:0.5rem">${tex`z_b, z_c, \varepsilon \sim \mathrm{N}(0,1)`}. For "all dialyzers" the ${tex`z`} values are random, a different hypothetical device in every sample, so the spread between devices is included. For a chosen dialyzer its own estimated ${tex`z_b, z_c`} are used. Every answer is about the measured rate, so the noise ${tex`\sigma\varepsilon`} is always included.</p>`Plot.plot({
width: width * 0.62,
height: 310,
marginLeft: 50,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: q === 3 ? "pressure (dmHg)" : q === 5 ? "Δ rate (ml/hr), 300 minus 200" : "rate (ml/hr)"},
y: {labelArrow: "none", labelAnchor: "top", label: "density", grid: true, ticks: 0},
marks: [
Plot.areaY(hist.curve, {x: "x", y: "y", fill: "#4f8ef7", fillOpacity: 0.3}),
Plot.areaY(hist.curve.filter(d => d.counted), {x: "x", y: "y", fill: "#f5a623", fillOpacity: 0.7}),
Plot.line(hist.curve, {x: "x", y: "y", stroke: "#8dc0ff", strokeWidth: 1.5}),
Plot.ruleX([hist.mean], {stroke: "#f5a623", strokeWidth: 3}),
Plot.ruleX([hist.lo, hist.hi], {stroke: "#f5a623", strokeDasharray: "4,3"}),
...(hist.cut === null ? [] : [
Plot.ruleX([hist.cut], {stroke: "#ff6b6b", strokeWidth: 2}),
Plot.text([hist.cut], {x: d => d, y: 0, text: () => hist.cut_label, dy: -8, dx: 4, textAnchor: "start", fill: "#ff6b6b"})
])
]
})hist = {
// which draws the histogram shows, and which of them the answer counts
let all, lo, hi, mean, cut = null, cut_label = "", counted
if (q === 5) {
all = diff.draws; mean = diff.mean; lo = diff.lo; hi = diff.hi
cut = threshold; cut_label = `Δ rate ${threshold} ml/hr`
counted = all.filter(v => v > threshold)
} else if (q === 3) {
all = x_here.filter(v => v !== null); mean = inv.mean; lo = inv.lo; hi = inv.hi
counted = all.filter(v => v >= lo && v <= hi)
} else if (q === 4) {
all = x_keep === null ? y_here : y_at(x_keep)
mean = d3.mean(all); lo = d3.quantile(all, lo_q); hi = d3.quantile(all, hi_q)
cut = limit; cut_label = `limit ${limit} ml/hr`
counted = all.filter(v => v < limit)
} else if (q === 2) {
all = y_here; mean = pred.mean; lo = pred.lo; hi = pred.hi
cut = limit; cut_label = `limit ${limit} ml/hr`
counted = all.filter(v => v < limit)
} else {
all = y_here; mean = pred.mean; lo = pred.lo; hi = pred.hi
counted = all.filter(v => v >= lo && v <= hi)
}
const inside = q === 5 ? (x => x > threshold) : (q === 2 || q === 4) ? (x => x < limit) : (x => x >= lo && x <= hi)
return {all, counted, mean, lo, hi, cut, cut_label, curve: kde(all, inside)}
}kde = (values, inside) => {
// gaussian kernel density, Silverman bandwidth. Draws are binned first and the
// bins smoothed, so the cost is bins x kernel width instead of draws x grid.
const n = values.length, sd = d3.deviation(values)
const iqr = d3.quantile(values, 0.75) - d3.quantile(values, 0.25)
const h = 0.9 * Math.min(sd, iqr / 1.34) * Math.pow(n, -0.2)
const lo = d3.min(values) - 3 * h, hi = d3.max(values) + 3 * h
const m = 240, step = (hi - lo) / m
const counts = new Float64Array(m)
for (const v of values) { const k = Math.floor((v - lo) / step); if (k >= 0 && k < m) counts[k] += 1 }
const half = Math.ceil(3 * h / step)
const kern = d3.range(-half, half + 1).map(j => Math.exp(-0.5 * (j * step / h) ** 2))
const out = []
for (let i = 0; i < m; i++) {
let sum = 0
for (let j = -half; j <= half; j++) { const k = i + j; if (k >= 0 && k < m) sum += counts[k] * kern[j + half] }
const x = lo + (i + 0.5) * step
out.push({x, y: sum / (n * h * Math.sqrt(2 * Math.PI)), counted: inside(x)})
}
return out
}q === 5 ? Plot.plot({
width: width * 0.62,
height: 310,
marginLeft: 50,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: "pressure (dmHg)"},
y: {labelArrow: "none", labelAnchor: "top", label: "rate (ml/hr)", grid: true},
color: {legend: true, type: "categorical", domain: ["200", "300"], range: ["#3ddc84", "#4f8ef7"], label: "blood flow (ml/min)"},
marks: [
Plot.areaY(band_both, {x: "x", y1: "lo", y2: "hi", fill: "bf", fillOpacity: 0.25}),
Plot.line(band_both, {x: "x", y: "mean", stroke: "bf", strokeWidth: 2.5}),
Plot.dot(obs, {x: "pressure", y: "rate", fill: d => String(d.blood_flow), r: 2.5, fillOpacity: 0.6}),
Plot.ruleX([pressure], {stroke: "#f5a623", strokeWidth: 2})
]
}) : Plot.plot({
width: width * 0.62,
height: 310,
marginLeft: 50,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: "pressure (dmHg)"},
y: {labelArrow: "none", labelAnchor: "top", label: "rate (ml/hr)", grid: true},
marks: [
Plot.areaY(band, {x: "x", y1: "lo", y2: "hi", fill: "#8dc0ff", fillOpacity: 0.35}),
Plot.line(band, {x: "x", y: "mean", stroke: "#4f8ef7", strokeWidth: 2.5}),
Plot.dot(obs.filter(d => d.blood_flow === bf), {x: "pressure", y: "rate", fill: "#dfe3e6", r: 2.5, fillOpacity: one === null ? 0.6 : 0.2}),
...(one === null ? [] : [Plot.dot(obs.filter(d => d.dialyzer === one), {x: "pressure", y: "rate", fill: "#dfe3e6", r: 4})]),
Plot.ruleX([x_mark], {stroke: "#f5a623", strokeWidth: 2}),
...(q === 2 || q === 4 ? [Plot.ruleY([limit], {stroke: "#ff6b6b", strokeDasharray: "4,3"})] : []),
...(q === 3 ? [Plot.ruleY([target], {stroke: "#ff6b6b", strokeDasharray: "4,3"})] : [])
]
})viewof question = Inputs.radio(
["1. At this pressure, what will the rate be?",
"2. At this pressure, how likely is it for the rate to stay below the limit?",
"3. What pressure gives the target rate?",
"4. What is the highest pressure that keeps the rate below the limit?",
"5. At this pressure, how likely is it for blood flow 300 to add more than Δ rate over 200?"],
{value: "1. At this pressure, what will the rate be?", label: ""})viewof who = Inputs.select(["all dialyzers", ...d3.range(1, 21).map(String)], {value: "all dialyzers", label: "dialyzer"})z = {
const r = d3.randomNormal.source(d3.randomLcg(42))(0, 1)
if (one === null) return draws.map(() => ({b: r(), c: r(), e: r(), e2: r()}))
const bf1 = obs.find(d => d.dialyzer === one).blood_flow
return draws.map(d => ({
b: (d[`b_${one}`] - (d.b_0 + d.b_blood_flow * bf1)) / d.sigma_b,
c: (Math.log(d[`c_${one}`]) - (d.c_0 + d.c_blood_flow * bf1)) / d.sigma_c,
e: r(),
e2: r()
}))
}params_at = (flow) => draws.map((d, i) => ({
a: d.a,
b: d.b_0 + d.b_blood_flow * flow + d.sigma_b * z[i].b,
c: Math.exp(d.c_0 + d.c_blood_flow * flow + d.sigma_c * z[i].c),
sigma: d.sigma
}))diff = {
const hi = params_at(300), lo = params_at(200)
const d = hi.map((p, i) => (curve(p, pressure) + p.sigma * z[i].e) - (curve(lo[i], pressure) + lo[i].sigma * z[i].e2))
return {
draws: d,
mean: d3.mean(d),
lo: d3.quantile(d, lo_q),
hi: d3.quantile(d, hi_q),
p_above: d3.mean(d, v => v > threshold ? 1 : 0)
}
}pred = {
const ys = y_here
return {
mean: d3.mean(ys),
lo: d3.quantile(ys, lo_q),
hi: d3.quantile(ys, hi_q),
p_below: d3.mean(ys, v => v < limit ? 1 : 0)
}
}band = d3.range(0.2, 3.21, 0.05).map(x => {
const ys = y_at(x)
return {x: x, mean: d3.mean(ys), lo: d3.quantile(ys, lo_q), hi: d3.quantile(ys, hi_q), p_below: d3.mean(ys, v => v < limit ? 1 : 0)}
})band_both = ["200", "300"].flatMap(bf => {
const ps = params_at(Number(bf))
return d3.range(0.2, 3.21, 0.05).map(x => {
const ys = ps.map(p => curve(p, x))
return {bf, x, mean: d3.mean(ys), lo: d3.quantile(ys, lo_q), hi: d3.quantile(ys, hi_q)}
})
})x_keep = {
const ok = band.filter(b => b.p_below >= prob)
return ok.length ? d3.max(ok, b => b.x) : null
}x_here = params.map((p, i) => {
const f = (target - p.sigma * z[i].e - p.b) / p.a
return f > 0 && f < 1 ? -Math.log(1 - f) / p.c : null
})inv = {
const ok = x_here.filter(v => v !== null)
return {
mean: ok.length ? d3.mean(ok) : null,
lo: ok.length ? d3.quantile(ok, lo_q) : null,
hi: ok.length ? d3.quantile(ok, hi_q) : null,
reach: ok.length / x_here.length
}
}answer = {
const f1 = d3.format(".1f"), f2 = d3.format(".2f")
const whom = one === null ? `a new dialyzer at blood flow ${bf} ml/min` : `dialyzer ${one} (blood flow ${bf} ml/min)`
const pct = d3.format(".0%")(ci)
const kind = "measured rate"
if (q === 1) return html`<b>${f1(pred.mean)} ml/hr</b> ${pct} interval ${f1(pred.lo)} to ${f1(pred.hi)}<br><small>${kind} at pressure ${pressure} dmHg, ${whom}</small>`
if (q === 2) return html`<b>P(rate below ${limit} ml/hr) = ${f2(pred.p_below)}</b><br><small>${kind} at pressure ${pressure} dmHg, ${whom}</small>`
if (q === 3) return inv.mean === null
? html`<b>rate ${target} ml/hr is not reachable</b><br><small>the measured rate never gets there for ${whom}</small>`
: html`<b>pressure ${f2(inv.mean)} dmHg</b> ${pct} interval ${f2(inv.lo)} to ${f2(inv.hi)}<br><small>for a measured rate of ${target} ml/hr, ${whom}, reachable in ${d3.format(".0%")(inv.reach)} of draws</small>`
if (q === 5) return html`<b>${f1(diff.mean)} ml/hr</b> ${pct} interval ${f1(diff.lo)} to ${f1(diff.hi)}<br><b>P(Δ rate above ${threshold} ml/hr) = ${f2(diff.p_above)}</b><br><small>measured rate at pressure ${pressure} dmHg at blood flow 300 minus at 200 ml/min, ${one === null ? "a new dialyzer, same z at both flows" : `dialyzer ${one}'s own effects at both flows`}</small>`
return x_keep === null
? html`<b>no pressure in range keeps P(rate below ${limit} ml/hr) at ${prob}</b>`
: html`<b>pressure ${f2(x_keep)} dmHg</b><br><small>highest pressure with P(rate below ${limit} ml/hr) ≥ ${prob}, ${whom}</small>`
}html`${tex.block`
\begin{aligned}
\text{deviation of } b &= b_i - \hat b_i, \qquad \hat b_i = \mu_b \;\text{or}\; b_0 + b_{\text{bf}}\,\text{blood\_flow}_i \\[4pt]
\text{deviation of } c &= c_i / \hat c_i - 1, \qquad \hat c_i = \exp(\log\mu_c) \;\text{or}\; \exp\!\bigl(c_0 + c_{\text{bf}}\,\text{blood\_flow}_i\bigr)
\end{aligned}
`}
<p style="margin-top:0.5rem">Each dialyzer's own value minus what the population part predicts for it: a single mean when the model has no blood flow on that parameter, its blood-flow line when it does. Zero means the device is functioning as the average of all devices would. Model 1 shows the deviations splitting into two clumps by blood flow. Putting blood flow on the parameter (model 2 for c, model 3 for b) turns them into one scatter around zero.</p>`Plot.plot({
width: width * 0.62,
height: 330,
marginLeft: 70,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: show === "deviation" ? (par === "c" ? "deviation of c (relative)" : "deviation of b (ml/hr)") : par === "b" ? "b (ml/hr)" : "c (per dmHg)", grid: true},
y: {labelArrow: "none", labelAnchor: "top", label: "dialyzer", domain: order},
color: {legend: true, type: "categorical", domain: ["200", "300"], range: ["#3ddc84", "#4f8ef7"], label: "blood flow (ml/min)"},
marks: [
...(show === "deviation" ? [Plot.ruleX([0], {stroke: "#b4b9bf", strokeDasharray: "4,3"})] : []),
Plot.ruleY(per, {y: d => String(d.dialyzer), x1: "lo", x2: "hi", stroke: d => String(d.blood_flow), strokeWidth: d => d.dialyzer === picked ? 4 : 2, strokeOpacity: d => picked === null || d.dialyzer === picked ? 1 : 0.35}),
Plot.dot(per, {y: d => String(d.dialyzer), x: "mean", fill: d => String(d.blood_flow), r: d => d.dialyzer === picked ? 7 : 5,
stroke: d => d.dialyzer === picked ? "#f5a623" : "none", strokeWidth: 2,
tip: {fontSize: 18, lineHeight: 1.5, format: {x: false, y: false, fill: false, r: false, stroke: false}},
channels: {
"Dialyzer": d => `${d.dialyzer}, blood flow ${d.blood_flow}`,
"Mean effect": d => fmt(d.mean),
"Credible interval": d => `${fmt(d.lo)} to ${fmt(d.hi)} (${d3.format(".0%")(ci2)})`
}})
]
})Plot.plot({
width: width * 0.62,
height: 300,
marginLeft: 50,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: show === "deviation" ? "deviation" : par === "b" ? "b (ml/hr)" : "c (per dmHg)"},
y: {labelArrow: "none", labelAnchor: "top", label: "density", grid: true, ticks: 0},
marks: [
Plot.areaY(pooled_curve, {x: "x", y: "y", fill: "#4f8ef7", fillOpacity: 0.3}),
Plot.line(pooled_curve, {x: "x", y: "y", stroke: "#8dc0ff", strokeWidth: 1.5}),
...(picked === null ? [] : [
Plot.areaY(picked_curve, {x: "x", y: "y", fill: "#f5a623", fillOpacity: 0.6}),
Plot.line(picked_curve, {x: "x", y: "y", stroke: "#f5a623", strokeWidth: 1.5}),
Plot.ruleX([per.find(d => d.dialyzer === picked).mean], {stroke: "#f5a623", strokeWidth: 3}),
Plot.ruleX([per.find(d => d.dialyzer === picked).lo, per.find(d => d.dialyzer === picked).hi], {stroke: "#f5a623", strokeDasharray: "4,3"})
]),
...(show === "deviation" ? [Plot.ruleX([0], {stroke: "#b4b9bf", strokeDasharray: "4,3"})] : [])
]
})picked_curve = {
// scaled to the pooled curve: one dialyzer holds 1/20 of the pooled draws
if (picked === null) return []
return kde(vals[picked], () => false).map(d => ({x: d.x, y: d.y / 20}))
}viewof model = Inputs.radio(["1: no blood flow", "2: on c", "3: on b and c"], {value: "3: on b and c", label: "model"})viewof pick = Inputs.select(["none", ...d3.range(1, 21).map(String)], {value: "none", label: "dialyzer"})flow_of = Object.fromEntries(d3.range(1, 21).map(i => [i, obs.find(d => d.dialyzer === i).blood_flow]))vals = Object.fromEntries(d3.range(1, 21).map(i => {
const bf_i = flow_of[i]
const v = dr.map(d => {
const raw = d[`${par}_${i}`]
if (show === "raw") return raw
if (par === "b") {
const hat = "b_blood_flow" in d ? d.b_0 + d.b_blood_flow * bf_i : d.mu_b
return raw - hat
}
const hat = "c_blood_flow" in d ? Math.exp(d.c_0 + d.c_blood_flow * bf_i) : Math.exp(d.log_mu_c)
return raw / hat - 1
})
return [i, v]
}))ranks = {
const n = dr.length, ids = d3.range(1, 21)
const low = Object.fromEntries(ids.map(i => [i, 0])), high = Object.fromEntries(ids.map(i => [i, 0]))
for (let k = 0; k < n; k++) {
let lo = ids[0], hi = ids[0]
for (const i of ids) {
if (vals[i][k] < vals[lo][k]) lo = i
if (vals[i][k] > vals[hi][k]) hi = i
}
low[lo] += 1; high[hi] += 1
}
return {low, high, n}
}per = d3.range(1, 21).map(i => ({
dialyzer: i,
blood_flow: flow_of[i],
mean: d3.mean(vals[i]),
lo: d3.quantile(vals[i], (1 - ci2) / 2),
hi: d3.quantile(vals[i], 1 - (1 - ci2) / 2),
p_low: ranks.low[i] / ranks.n,
p_high: ranks.high[i] / ranks.n
}))order = (sort_by === "value" ? per.slice().sort((p, q) => p.mean - q.mean) : per.slice().sort((p, q) => p.dialyzer - q.dialyzer)).map(d => String(d.dialyzer))fmt = show === "deviation" && par === "c" ? d3.format("+.0%") : show === "deviation" ? d3.format("+.2f") : d3.format(".3g")sentence = {
const pct = d3.format(".0%")(ci2), f2 = d3.format(".2f")
if (picked === null) {
const lo = per.slice().sort((p, q) => q.p_low - p.p_low)[0], hi = per.slice().sort((p, q) => q.p_high - p.p_high)[0]
return html`<b>Lowest ${par}: dialyzer ${lo.dialyzer}</b> P = ${f2(lo.p_low)}<br><b>Highest ${par}: dialyzer ${hi.dialyzer}</b> P = ${f2(hi.p_high)}<br><small>pick a dialyzer above for its own numbers</small>`
}
const d = per.find(p => p.dialyzer === picked)
const what = show === "raw" ? `${par} = ${fmt(d.mean)}` :
par === "b" ? `${par} is ${d3.format(".2f")(Math.abs(d.mean))} ${d.mean < 0 ? "below" : "above"} its blood-flow line`
: `${par} is ${d3.format(".0%")(Math.abs(d.mean))} ${d.mean < 0 ? "slower" : "faster"} than its blood-flow line`
return html`<b>Dialyzer ${picked}</b> (blood flow ${d.blood_flow} ml/min)<br>${what}<br><small>${pct} interval ${fmt(d.lo)} to ${fmt(d.hi)}</small>`
}