Drawing cumulative density function (CDF).
- direct CDF (red line)
- via-histogram CDF (blue line)
Imspired by https://bl.ocks.org/jltran/bcd2a30fd9c08f8b9f87.
Drawing cumulative density function (CDF).
Imspired by https://bl.ocks.org/jltran/bcd2a30fd9c08f8b9f87.
<!DOCTYPE html> | |
<!-- ref: https://bl.ocks.org/jltran/bcd2a30fd9c08f8b9f87 --> | |
<meta charset="utf-8"> | |
<style> | |
svg { | |
font: 10px sans-serif; | |
} | |
.axis path, .axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
</style> | |
<body> | |
<script src="https://d3js.org/d3.v7.min.js"></script> | |
<script> | |
//Set dimensions | |
const m = {top: 50, right: 50, bottom: 50, left: 50} | |
, h = 500 - m.top - m.bottom | |
, w = 960 - m.left - m.right | |
, numBins = 50 | |
, n = 10000; | |
//Generate random numbers and calculate direct CDF | |
const norm = d3.range(n).map(d3.randomNormal(0, 1)); | |
norm.sort((first, second) => first - second); | |
const cum = norm.slice(0, n - 1).map((w, i) => ({'x': w, 'y': (i+1)/n})); | |
//Calculate histogram and derived CDR | |
const xScale = d3.scaleLinear().domain([-3, 3]).range([0, w]); | |
const data = d3.histogram().domain(xScale.domain()).thresholds(xScale.ticks(numBins))(norm).filter(d => d.x0 < d.x1); | |
for(let i = 0, cum0 = 0; i < data.length; i++){ | |
cum0 += data[i].length; | |
data[i]['cum'] = cum0 / n; | |
} | |
//Scales and axes | |
const yhist = d3.scaleLinear().range([h, 0]).domain([0, d3.max(data, d => d.length)]); | |
const ycum = d3.scaleLinear().domain([0, 1]).range([h, 0]); | |
const xAxis = d3.axisBottom(xScale); | |
const yAxis = d3.axisLeft(yhist); | |
const yAxis2 = d3.axisRight(ycum); | |
//Draw svg | |
const svg = d3.select("body").append("svg") | |
.attr("width", w + m.left + m.right) | |
.attr("height", h + m.top + m.bottom) | |
.append("g") | |
.attr("transform", "translate(" + m.left + "," + m.top + ")"); | |
//Draw histogram | |
svg.selectAll("rect") | |
.data(data) | |
.enter() | |
.append("rect") | |
.attr("x", 1) | |
.attr("transform", d => "translate(" + xScale(d.x0) + "," + yhist(d.length) + ")") | |
.attr("width", d => xScale(d.x1) - xScale(d.x0) - 1) | |
.attr("height", d => h - yhist(d.length)) | |
.style("fill", "#69b3a2") | |
.style("opacity", 0.2); | |
//Draw CDF's | |
svg.append("path") | |
.attr("d", d3.line().x(d => xScale(d.x1)).y(d => ycum(d.cum)).curve(d3.curveLinear)(data)) | |
.attr("stroke", "#00ffff") | |
.attr("stroke-width", 7) | |
.attr("fill", "none"); | |
svg.append("path") | |
.attr("d", (d3.line().x(d => xScale(d.x)).y(d => ycum(erfinv2(d.y))).curve(d3.curveLinear)(cum))) | |
.attr("stroke", "#ffc000") | |
.attr("stroke-width", 1.2) | |
.attr("fill", "none"); | |
svg.append("path") | |
.attr("d", (d3.line().x(d => xScale(d.x)).y(d => ycum(d.y)).curve(d3.curveLinear)(cum))) | |
.attr("stroke", "#ff0000") | |
.attr("stroke-width", 1.2) | |
.attr("fill", "none"); | |
//Draw axes | |
svg.append("g") | |
.attr("class", "x axis") | |
.attr("transform", "translate(0," + h + ")") | |
.call(xAxis); | |
svg.append("g") | |
.attr("class", "y axis") | |
.call(yAxis) | |
.append("text") | |
.attr("transform", "rotate(-90)") | |
.attr("y", 6) | |
.attr("dy", ".71em") | |
.style("text-anchor", "end") | |
.text("Count (Histogram)"); | |
svg.append("g") | |
.attr("class", "y axis") | |
.attr("transform", "translate(" + [w, 0] + ")") | |
.call(yAxis2) | |
.append("text") | |
.attr("transform", "rotate(-90)") | |
.attr("y", 4) | |
.attr("dy", "-.71em") | |
.style("text-anchor", "end") | |
.text("CDF"); | |
function erfinv2(x) { | |
return (erfinvapprox(x * 2 - 1) * 2 / Math.sqrt(Math.PI) + 1 ) / 2; | |
} | |
function erfinvapprox(x) { | |
// maximum relative error = .00013 | |
const a = 0.147; | |
const log1 = Math.log(1 - x**2); | |
const b = 2 / (Math.PI * a) + log1/2 | |
const sqrt1 = Math.sqrt(b**2 - log1/a) | |
return Math.sqrt(sqrt1 - b) * Math.sign(x) | |
} | |
//if (x <= 0) { return -999 } else if (x >= 1) {return 999} | |
</script> | |
</body> |