Transforms Reference
probability
simulation
d3
Interactive visualizations for inverse transform sampling
Author
Adam Fillion
Published
January 23, 2026
Discrete Event Simulation Heap
Code
{
const width = 600, height = 200;
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]).attr("width", "100%");
const allEvents = [
{t: 0.5, label: "A"}, {t: 1.2, label: "B"}, {t: 2.1, label: "C"},
{t: 3.0, label: "D"}, {t: 3.8, label: "E"}, {t: 4.5, label: "F"},
{t: 5.2, label: "G"}, {t: 6.1, label: "H"}, {t: 7.0, label: "I"},
{t: 8.2, label: "J"}, {t: 9.1, label: "K"}, {t: 10.0, label: "L"},
{t: 11.5, label: "M"}, {t: 12.3, label: "N"}, {t: 13.0, label: "O"}
];
const processed = allEvents.slice(0, simStep);
const heap = allEvents.slice(simStep, Math.min(simStep + 5, allEvents.length));
const currentTime = simStep > 0 ? allEvents[simStep - 1].t : 0;
const xScale = d3.scaleLinear().domain([0, 14]).range([50, width - 20]);
svg.append("line").attr("x1", 50).attr("y1", 50).attr("x2", width - 20).attr("y2", 50)
.attr("stroke", "#ccc").attr("stroke-width", 2);
svg.append("text").attr("x", width/2).attr("y", 20).attr("text-anchor", "middle")
.attr("font-size", "12px").text(`Simulation time: ${currentTime.toFixed(1)}`);
svg.append("line").attr("x1", xScale(currentTime)).attr("y1", 35).attr("x2", xScale(currentTime)).attr("y2", 65)
.attr("stroke", "#e74c3c").attr("stroke-width", 3);
processed.forEach(e => {
svg.append("circle").attr("cx", xScale(e.t)).attr("cy", 50).attr("r", 8).attr("fill", "#95a5a6");
});
svg.append("text").attr("x", 50).attr("y", 100).attr("font-size", "11px").attr("font-weight", "bold").text("Heap (next events):");
heap.forEach((e, i) => {
svg.append("rect").attr("x", 50 + i * 70).attr("y", 110).attr("width", 60).attr("height", 40)
.attr("fill", i === 0 ? "#3498db" : "#ecf0f1").attr("stroke", "#2c3e50").attr("rx", 4);
svg.append("text").attr("x", 80 + i * 70).attr("y", 125).attr("text-anchor", "middle")
.attr("font-size", "10px").text(`t=${e.t}`);
svg.append("text").attr("x", 80 + i * 70).attr("y", 143).attr("text-anchor", "middle")
.attr("font-size", "14px").attr("font-weight", "bold").text(e.label);
});
if (heap.length > 0) {
svg.append("text").attr("x", 80).attr("y", 170).attr("text-anchor", "middle")
.attr("font-size", "10px").attr("fill", "#e74c3c").text("↑ pop next");
}
return svg.node();
}Inverse Transform Sampling
Code
Code
Code
Code
eventTimes = {
const times = [];
for (const target of expTargets) {
if (target > 18) break;
let lo = 0, hi = 5;
for (let i = 0; i < 50; i++) {
const mid = (lo + hi) / 2;
if (integralFuncGlobal(mid) < target) lo = mid;
else hi = mid;
}
times.push((lo + hi) / 2);
}
return times.filter(t => t <= 5 && t >= 0);
}Code
{
const width = 650, height = 700;
const margin = {top: 30, right: 80, bottom: 40, left: 60};
const gap = 70;
const chartHeight = 140;
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]).attr("width", "100%");
const rateFunc = (t) => {
if (t < 1) return 4;
if (t < 1.5) return 8;
if (t < 2.5) return 0;
if (t < 3.5) return (t - 2.5) * 4;
return 4;
};
const integralFunc = (t) => {
if (t < 1) return 4 * t;
if (t < 1.5) return 4 + 8 * (t - 1);
if (t < 2.5) return 4 + 4 + 0;
if (t < 3.5) return 8 + 2 * (t - 2.5) * (t - 2.5);
return 8 + 2 + 4 * (t - 3.5);
};
const currentIntegral = integralFunc(integrationTime2);
const maxIntegral = 18;
const top1 = margin.top;
const top2 = margin.top + chartHeight + gap;
const top3 = margin.top + 2 * (chartHeight + gap);
const xScaleTime = d3.scaleLinear().domain([0, 5]).range([margin.left, width - margin.right]);
const xScaleArea = d3.scaleLinear().domain([0, maxIntegral]).range([margin.left, width - margin.right]);
const yScaleRate = d3.scaleLinear().domain([0, 10]).range([top1 + chartHeight, top1]);
const yScaleIntegral = d3.scaleLinear().domain([0, maxIntegral]).range([top2 + chartHeight, top2]);
const yScaleTarget = d3.scaleLinear().domain([0, maxIntegral]).range([top3 + chartHeight, top3]);
// Chart 1: Rate Function
const rateData = d3.range(100).map(i => ({ t: i / 100 * 5, rate: rateFunc(i / 100 * 5) }));
const areaData = d3.range(Math.floor(integrationTime2 * 20) + 1).map(i => ({
t: i / 20, rate: rateFunc(i / 20)
}));
const area = d3.area().x(d => xScaleTime(d.t)).y0(yScaleRate(0)).y1(d => yScaleRate(d.rate));
const rateLine = d3.line().x(d => xScaleTime(d.t)).y(d => yScaleRate(d.rate));
svg.append("path").datum(areaData).attr("fill", "#3498db").attr("opacity", 0.3).attr("d", area);
svg.append("path").datum(rateData).attr("fill", "none").attr("stroke", "#e74c3c").attr("stroke-width", 3).attr("d", rateLine);
eventTimes.forEach((et, i) => {
if (integrationTime2 >= et) {
svg.append("circle").attr("cx", xScaleTime(et)).attr("cy", yScaleRate(rateFunc(et)))
.attr("r", 5).attr("fill", "#2ecc71").attr("stroke", "#fff").attr("stroke-width", 2);
}
});
svg.append("line").attr("x1", xScaleTime(integrationTime2)).attr("y1", yScaleRate(0))
.attr("x2", xScaleTime(integrationTime2)).attr("y2", yScaleRate(rateFunc(integrationTime2)))
.attr("stroke", "#2c3e50").attr("stroke-width", 2);
svg.append("g").attr("transform", `translate(0,${top1 + chartHeight})`).call(d3.axisBottom(xScaleTime));
svg.append("g").attr("transform", `translate(${margin.left},0)`).call(d3.axisLeft(yScaleRate).ticks(4));
svg.append("text").attr("x", width/2).attr("y", top1 + chartHeight + 35).attr("text-anchor", "middle")
.attr("font-size", "10px").text("Time");
svg.append("text").attr("transform", "rotate(-90)").attr("x", -(top1 + chartHeight/2)).attr("y", 15)
.attr("text-anchor", "middle").attr("font-size", "10px").text("λ(t)");
svg.append("text").attr("x", width/2).attr("y", top1 - 10).attr("text-anchor", "middle")
.attr("font-size", "11px").attr("font-weight", "bold").text("Rate Function vs Time");
// Chart 2: Cumulative Intensity
expTargets.filter(t => t <= maxIntegral).forEach((target) => {
svg.append("line")
.attr("x1", margin.left).attr("x2", width - margin.right)
.attr("y1", yScaleIntegral(target)).attr("y2", yScaleIntegral(target))
.attr("stroke", "#ddd").attr("stroke-width", 1).attr("stroke-dasharray", "4,4");
});
const integralData = d3.range(101).map(i => ({ t: i / 100 * 5, val: integralFunc(i / 100 * 5) }));
const integralLine = d3.line().x(d => xScaleTime(d.t)).y(d => yScaleIntegral(d.val));
svg.append("path").datum(integralData).attr("fill", "none").attr("stroke", "#ccc")
.attr("stroke-width", 2).attr("d", integralLine);
const integralProgress = d3.range(Math.floor(integrationTime2 * 20) + 1).map(i => ({
t: i / 20, val: integralFunc(i / 20)
}));
svg.append("path").datum(integralProgress).attr("fill", "none").attr("stroke", "#3498db")
.attr("stroke-width", 3).attr("d", integralLine);
svg.append("circle").attr("cx", xScaleTime(integrationTime2)).attr("cy", yScaleIntegral(currentIntegral))
.attr("r", 6).attr("fill", "#3498db").attr("stroke", "#fff").attr("stroke-width", 2);
eventTimes.forEach((et, i) => {
if (integrationTime2 >= et) {
svg.append("circle").attr("cx", xScaleTime(et)).attr("cy", yScaleIntegral(expTargets[i]))
.attr("r", 5).attr("fill", "#2ecc71").attr("stroke", "#fff").attr("stroke-width", 2);
}
});
svg.append("g").attr("transform", `translate(${margin.left},0)`).call(d3.axisLeft(yScaleIntegral).ticks(5));
svg.append("g").attr("transform", `translate(0,${top2 + chartHeight})`).call(d3.axisBottom(xScaleTime));
svg.append("text").attr("transform", "rotate(-90)").attr("x", -(top2 + chartHeight/2)).attr("y", 15)
.attr("text-anchor", "middle").attr("font-size", "10px").text("∫λ ds");
svg.append("text").attr("x", width/2).attr("y", top2 + chartHeight + 35).attr("text-anchor", "middle")
.attr("font-size", "10px").text("Time");
svg.append("text").attr("x", width/2).attr("y", top2 - 10).attr("text-anchor", "middle")
.attr("font-size", "11px").attr("font-weight", "bold").text("Cumulative Intensity vs Time");
// Chart 3: Integral vs Target Area
svg.append("line")
.attr("x1", xScaleArea(0)).attr("y1", yScaleTarget(0))
.attr("x2", xScaleArea(maxIntegral)).attr("y2", yScaleTarget(maxIntegral))
.attr("stroke", "#ccc").attr("stroke-width", 2).attr("stroke-dasharray", "6,3");
expTargets.filter(t => t <= maxIntegral).forEach((target) => {
svg.append("line")
.attr("x1", xScaleArea(target)).attr("x2", xScaleArea(target))
.attr("y1", yScaleTarget(0)).attr("y2", yScaleTarget(maxIntegral))
.attr("stroke", "#ddd").attr("stroke-width", 1).attr("stroke-dasharray", "4,4");
});
svg.append("line")
.attr("x1", margin.left).attr("x2", width - margin.right)
.attr("y1", yScaleTarget(currentIntegral)).attr("y2", yScaleTarget(currentIntegral))
.attr("stroke", "#3498db").attr("stroke-width", 2);
eventTimes.forEach((et, i) => {
if (integrationTime2 >= et) {
svg.append("circle").attr("cx", xScaleArea(expTargets[i])).attr("cy", yScaleTarget(expTargets[i]))
.attr("r", 5).attr("fill", "#2ecc71").attr("stroke", "#fff").attr("stroke-width", 2);
}
});
svg.append("circle").attr("cx", xScaleArea(currentIntegral)).attr("cy", yScaleTarget(currentIntegral))
.attr("r", 6).attr("fill", "#3498db").attr("stroke", "#fff").attr("stroke-width", 2);
svg.append("g").attr("transform", `translate(${margin.left},0)`).call(d3.axisLeft(yScaleTarget).ticks(5));
svg.append("g").attr("transform", `translate(0,${top3 + chartHeight})`).call(d3.axisBottom(xScaleArea).ticks(6));
svg.append("text").attr("transform", "rotate(-90)").attr("x", -(top3 + chartHeight/2)).attr("y", 15)
.attr("text-anchor", "middle").attr("font-size", "10px").text("∫λ ds");
svg.append("text").attr("x", width/2).attr("y", top3 + chartHeight + 35).attr("text-anchor", "middle")
.attr("font-size", "10px").text("Target Area");
svg.append("text").attr("x", width/2).attr("y", top3 - 10).attr("text-anchor", "middle")
.attr("font-size", "11px").attr("font-weight", "bold").text("Integral vs Target Area");
const eventCount = eventTimes.filter(et => integrationTime2 >= et).length;
svg.append("text").attr("x", width - 5).attr("y", top1 + 15).attr("text-anchor", "end").attr("font-size", "11px")
.text(`area: ${currentIntegral.toFixed(1)}`);
svg.append("text").attr("x", width - 5).attr("y", top1 + 30).attr("text-anchor", "end").attr("font-size", "11px")
.text(`events: ${eventCount}`);
return svg.node();
}