Skip to content

Instantly share code, notes, and snippets.

@wernersa
Last active September 6, 2018 19:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wernersa/135f652c2f82125c2167d508dfba9685 to your computer and use it in GitHub Desktop.
Save wernersa/135f652c2f82125c2167d508dfba9685 to your computer and use it in GitHub Desktop.
Building a Normal Distribution Histogram v4
license: mit
height: 600
border: yes
scrolling: no

A randomly-generated normal distribution with a mean of 0 and a standard deviation of 3 on an ordinal x-axis. The histogram will add one datapoint at a time until it has reached 100 datapoints.

Use the controls in the upper right corner. The first button starts auto iteration, the second refresh the sample, the third resets to n = 0. Try scrolling on top of the sample counter to change the current iteration number or total sample size.

Based on the d3 v3 block by Tommy Ogden: Building a Normal Distribution Histogram.

This block was remade by Werner Sævland.

<!DOCTYPE html>
<meta charset="utf-8">
<html>
<head>
<title>Building a Normal Distribution Histogram</title>
<script src="https://use.fontawesome.com/35aa61260c.js"></script>
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jstat/1.7.0/jstat.js"></script>
<script src="https://unpkg.com/simple-statistics@4.1.0/dist/simple-statistics.min.js"></script>
<style>
body {
/*background-color: rgba(245,243,242,1);*/
font-family: "Helvetica", sans-serif;
margin: 8px;
}
svg {
background-color: white;
/*border: solid 1px rgba(208,199,198,1);*/
}
.axis {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
text.axis.label {
fill: #000;
}
.bar {
fill: rgba(30, 80, 230, 1);
shape-rendering: crispEdges;
opacity: 0.9;
}
text.bar.label {
fill: white;
text-anchor: middle;
}
.interactive {
transition: all 0.2s ease 0s;
user-select: none;
cursor: pointer;
}
.interactive:hover {
fill: #888888;
}
.btn {
font-family: 'FontAwesome';
font-size: 15px;
text-anchor: end;
letter-spacing: 10px
}
</style>
<body>
<script>
(function () {
var margin = {
top: 20,
right: 20,
bottom: 40,
left: 40
},
width = 950 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
//////////////////////////
// Generate data //
//////////////////////////
var normalMean = 0,
normalStdDev = 3;
function update_xValues() {
xSteps = normalStdDev * 4 * 2; // We want to generate 4 standard deviations to each side of the mean
xDomain = [Math.floor(normalMean - xSteps / 2), Math.ceil(normalMean + xSteps / 2)];
}
update_xValues();
var autoIterate = false,
iteration = 0,
i_step = 1,
numDataPoints = 100,
numDataPointsMax = 100000; //For memory purposes when scrolling
var updateDelay = 500;
var transitionDuration = updateDelay * 0.8;
// Data
function update_dataPoints(adjust) {
let rand = function () {
var a = d3.randomNormal(normalMean, normalStdDev);
return Math.round(a());
}
if (adjust == true) {
var new_length = numDataPoints - dataPoints.length;
if (new_length > 0) {
// Add additional points
let additional = d3.range(new_length).map(rand);
return dataPoints.concat(additional);
} else if (new_length <= 0) {
// Reduce points
iteration = Math.min(iteration, numDataPoints); //Iteration should not be larger than total
return dataPoints.slice(0, numDataPoints);
}
} else {
// New set of data points:
return d3.range(numDataPoints).map(rand);
}
}
var dataPoints = update_dataPoints();
// Initial histogram
function update_dataHist(data) {
return d3.histogram()
.domain(xDomain)
.thresholds(21)
(data);
}
var dataHist = update_dataHist(dataPoints);
//////////////////////////////////
// Build the barchart //
//////////////////////////////////
// Draw SVG
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// X scale (Continous) step generator for generating even spaced ticks
// Neccesary for continous precision of descriptives
var xStep = d3.scaleLinear()
.domain(xDomain);
// X scale (Ordinal)
var x = d3.scaleBand()
.domain(d3.ticks(xDomain[0], xDomain[1], 21))
.rangeRound([0, width])
.paddingInner(0.15);
// Recalibrate the Continous X scale to fit the Ordinal X scale endpoints
xStep.range([(x(xDomain[0]) + (x.bandwidth() / 2)), (x(xDomain[1]) + (x.bandwidth() / 2))]);
// X axis
var xAxis = d3.axisBottom(x);
var xAxisGroup = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Y scale
var y = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(dataHist, function (d) {
return Math.ceil(d.length / 10) * 10
})]);
// Y axis
var yAxis = d3.axisLeft()
.scale(y);
var yAxisGroup = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// Y Label
yAxisGroup
.call(yAxis)
.append("text")
.attr("class", "axis label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Frequency");
//////////////////////////////////////////
// Bars and labels within the plot area //
//////////////////////////////////////////
var barGroup = svg.append("g")
.attr("class", "bars")
var barGroupEntries = barGroup.selectAll("g")
.data(dataHist);
var barGroupEnter = barGroupEntries.enter()
.append("g")
.attr("class", "entry");
// Draw initial bars
var barRects = barGroupEnter.append("rect")
.attr("class", "bar")
.attr("stroke", "1")
.attr("width", x.bandwidth())
.attr("transform", function (d) {
return "translate(" + x(d.x0) +
"," + y(0) + ")";
})
.attr("height", function (d) {
return height - y(0);
});
// Add bar labels
var barLabels = barGroupEnter.append("text")
.attr("class", "bar label")
.text(function (d) {
return "";
}) // Start out with blank labels
.attr("text-anchor", "middle")
.attr("transform", function (d) {
return "translate(" + (x(d.x0) + x.bandwidth() / 2) +
"," + (y(0) + 16) + ")";
});
////////////////////////////////////////////
// Descriptives: //
// Mean, median, standard deviation, etc. //
////////////////////////////////////////////
var desciptiveData = [];
// Colors for each descriptive line and label
var colors10 = d3.scaleOrdinal(d3.schemeCategory10);
function update_descriptiveData(data) {
// See js statistic libraries benchmarks here:
// https://github.com/simple-statistics/simple-statistics/tree/master/benchmarks
var desciptiveFunctions = [{
id: 1,
type: "central_tendency",
name: "Median",
fun: ss.median
}, // 20.1 times faster than d3.median
{
id: 2,
type: "central_tendency",
name: "Mean",
fun: jStat.mean
}, // 1.6 times faster than d3.mean
{
id: 3,
type: "spread",
name: "SD",
fun: x => {
return jStat.stdev(x, true);
}
}, // 6.67 times faster than d3.deviation
{
id: 4,
type: "spread",
name: "SEM",
fun: x => {
return jStat.stdev(x, true) / Math.sqrt(x.length);
}
},
{
id: 5,
type: "spread",
name: "CI",
fun: x => {
// For the CI we centralize the value which we later adjust with the mean:
return jStat.tci(0, 0.05, x)[1];
}
} // See documentation: http://jstat.github.io/all.html#jStat.tci
];
// ES2015 anonymous function(arg) mapped over each function returns named dict
desciptiveData = desciptiveFunctions.map(arg => ({
id: arg.id,
type: arg.type,
name: arg.name,
val: arg.fun(data)
}));
}
// Set the standard (not neccesary)
desciptiveData = [{
id: 1,
type: "central_tendency",
name: "Median",
val: normalMean
},
{
id: 2,
type: "central_tendency",
name: "Mean",
val: normalMean
},
{
id: 3,
type: "spread",
name: "SD",
val: normalStdDev
},
{
id: 4,
type: "spread",
name: "SEM",
val: normalStdDev
},
{
id: 5,
type: "spread",
name: "CI",
val: normalStdDev * 1.96
} // Temp 95% CI
];
// We insert instead of append to make it appear behind the bars.
var descGroup = svg.insert("g", ":first-child")
.attr("class", "descriptives")
.selectAll("g")
.data(desciptiveData)
.enter().append("g")
.attr("class", "entry")
.attr("id", function (d) {
return d.name;
})
.style("opacity", 0);
// Add descriptive lines
var descLines = descGroup.filter(function (d) {
return d.type == "central_tendency";
}).append("line")
.attr("stroke", function (d) {
return colors10(d.id);
})
.attr('x1', function (d) {
return xStep(d.val);
}) // Start all central tendencies at the mean, initially 0
.attr('x2', function (d) {
return xStep(d.val);
})
.attr('y1', 0)
.attr('y2', height);
// Add descriptive spreads
var descAreas = descGroup.filter(function (d) {
return d.type == "spread";
}).append("rect")
.attr("fill", function (d) {
return colors10(d.id);
})
.attr("opacity", 0.1)
.attr('x', function (d) {
return xStep(normalMean - d.val);
}) // Start all central tendencies at the mean, initially 0
.attr('width', function (d) {
return ((xStep(d.val) - xStep(normalMean)) * 2);
})
.attr('y', 0)
.attr('height', height);
// Add labels for each descriptives
var descLabels = descGroup.append("text")
.attr("class", "line label")
.style("fill", function (d) {
return colors10(d.id);
})
.style("font-size", "150%")
.attr("text-anchor", "end")
.attr("dy", "-2px")
.text(function (d) {
return d.name;
})
.attr("transform", function (d) {
return "translate(" + xStep(d.val) + "," + 0 + ") rotate(-90)";
})
.on('mouseover', function (d) {
d3.select(".descriptives #" + d.name)
.transition()
.style("opacity", visible ? 0 : 1)
})
.on('mouseout', function (d) {
d3.select(this).style({
opacity: '0.0',
})
d3.select("text").style({
opacity: '0.0'
});
});
///////////////////////////////////////////////
// Upper right controls and legend
///////////////////////////////////////////////
var controls = svg.append("g")
.attr("class", "controls");
// Add descriptives legend
var legend = controls.append("g")
.attr("class", "legend")
.attr("text-anchor", "end")
.selectAll("g")
.data(desciptiveData)
.enter().append("g")
.attr("transform", function (d, i) {
return "translate(0," + (40 + i * 20) + ")";
});
legend.append("text")
.attr("class", "btnToggleDescriptive interactive")
.attr("x", width)
.attr("y", 14)
.style("text-anchor", "end")
.style('font-family', 'FontAwesome')
.style('font-size', '15px')
.text("\uf0c8")
.on("click", function (d, i) {
// Toggle on/off descriptive
var visible = d3.select(this).classed("visible");
d3.select(this)
.classed("visible", !visible)
.text(visible ? "\uf0c8" : "\uf14a"); // Unchecked box
d3.select(".descriptives #" + d.name)
.transition()
.style("opacity", visible ? 0 : 1)
});
legend.append("text")
.attr("x", width - 18)
.attr("y", 9.5)
.attr("dy", "0.32em")
.text(function (d) {
return d.name;
})
.style("color", function (d) {
return colors10(d.id);
});
/////////////////////////////
// Controls top right area //
/////////////////////////////
// Draw walker number group, iteration, and total
var walkNum = controls.append("text")
.attr("class", "walknum")
.style("text-anchor", "end")
.attr("x", width)
.attr("y", 4);
var walkNumCurrent = walkNum.append("tspan")
.attr("class", "walkNumCurrent interactive")
.text("0 / ")
.on("wheel", scrollN);
var walkNumTot = walkNum.append("tspan")
.attr("class", "walkNumTotal interactive")
.style("text-anchor", "end")
.text("100")
.on("wheel", scrollTot);
// Draw font awesome control buttons
// Play/Pause button
var buttons = controls.append("text")
.attr("x", width)
.attr("y", 28)
.attr("class", "buttons");
var toggleAutoButton = buttons.append("tspan")
.attr("id", "btnToggleAuto")
.attr("class", "interactive btn")
.text("\uf04b")
.on("click", function toggleAuto() {
autoIterate = !autoIterate;
if (autoIterate) {
// Start auto iterate and display pause button
toggleAutoButton.text("\uf04c");
updateBars();
} else {
// Pause auto iterate and display play button
clearTimeout(timer); // Stop ongoing timer
toggleAutoButton.text("\uf04b");
}
});
// Randomize button
var randomizeButton = buttons.append("tspan")
.attr("id", "btnRandomize")
.attr("class", "interactive btn")
.text("\uf074")
.on("click", function () {
update_variables(shuffle = true);
});
// Refresh button
var refreshButton = buttons.append("tspan")
.attr("id", "btnRefresh")
.attr("class", "interactive btn")
.text("\uf021")
.on("click", function () {
update_variables(shuffle = true, restart = true);
});
////////////////////////////////////////////////////////
// Functions for scroll control and auto-incrementing //
////////////////////////////////////////////////////////
function scrollN() {
d3.event.preventDefault();
var dy = d3.event.wheelDeltaY;
var minor_step = Math.ceil(numDataPoints / 100);
var major_step = Math.ceil(numDataPoints / 100) * 5;
if (dy > 0) {
if (d3.event.altKey === true) {
iteration = Math.min(numDataPoints, iteration + major_step)
} else {
iteration = Math.min(numDataPoints, iteration + minor_step)
}
} else {
if (d3.event.altKey === true) {
iteration = Math.max(0, iteration - major_step)
} else {
iteration = Math.max(0, iteration - minor_step)
}
}
updateBars()
}
function scrollTot() {
d3.event.preventDefault();
var dy = d3.event.wheelDeltaY;
var step = Math.pow(10, numDataPoints.toString().length - 1);
if (dy > 0) {
if (d3.event.altKey === true) {
numDataPoints = step * 10;
} else {
numDataPoints += step;
}
} else {
if (numDataPoints % step == 0) step = step / 10; //Minus from a number raised to 10 should decrement by 10^x-1
// Minimum n = 10 total sample
if (d3.event.altKey === true) {
numDataPoints = Math.max(10, step / 10)
} else {
numDataPoints = Math.max(10, numDataPoints - (step / 10))
}
}
// Check that the Total does is not more than max to prevent memory consumption memory
numDataPoints = Math.min(numDataPoints, numDataPointsMax);
dataPoints = update_dataPoints(true);
update_variables();
}
var timer;
function updateBars() {
// Stop any potential ongoing transition
clearTimeout(timer);
// Slice to current i-step
var DataPointsSliced = dataPoints.slice(0, iteration);
// Bin the data
var dataHist = update_dataHist(DataPointsSliced);
walkNumCurrent.text(iteration + " / ");
walkNumTot.text(numDataPoints);
// Standard enter + update pattern not working. Due to completely new histogram data, and not an update of recycled datums?
//barGroupEnties = barGroup.selectAll("g")
// .data(dataHist);
barRects
.data(dataHist)
.transition()
.duration(transitionDuration)
.ease(d3.easeBounce)
.attr("transform", function (d) {
return "translate(" + x(d.x0) +
"," + y(d.length) + ")";
})
.attr("height", function (d) {
return height - y(d.length);
});
//Remove surplus bars if bins have been reduced
barGroupEnter.exit().remove()
// Update bar labels
barLabels
.data(dataHist)
.transition()
.duration(transitionDuration)
.ease(d3.easeBounce)
.text(function (d) {
if (y(d.length) < (height - 15)) {
return d.length;
};
})
.attr("transform", function (d) {
return "translate(" + (x(d.x0) + x.bandwidth() / 2) +
"," + (y(d.length) + 16) + ")";
});
// Update descriptives if iteration is 2 or more
if (iteration > 1) {
update_descriptiveData(DataPointsSliced);
console.log("Descriptives:\n", desciptiveData,
"\nData points:\n", DataPointsSliced,
"\nData histogram:\n", dataHist);
descGroup
.data(desciptiveData)
.select("line")
.transition()
.duration(transitionDuration)
.ease(d3.easeBounce)
.attr('x1', function (d) {
return xStep(d.val);
})
.attr('x2', function (d) {
return xStep(d.val);
});
descGroup
.select("text")
.transition()
.duration(transitionDuration)
.ease(d3.easeBounce)
.attr("transform", function (d) {
if (d.type == "spread") {
var value = xStep(desciptiveData[1].val - d.val);
} else {
var value = xStep(d.val);
}
return "translate(" + value + "," + 0 + ") rotate(-90)";
});
descGroup
.select("rect")
.transition()
.duration(transitionDuration)
.ease(d3.easeBounce)
.attr('x', function (d) {
return xStep(desciptiveData[1].val - d.val);
}) // Mean - spread value (lower bound)
.attr('width', function (d) {
return ((xStep(desciptiveData[1].val + d.val) - xStep(desciptiveData[1].val)) *
2);
}) // Spread value - mean (width to upper bound)
.attr('y', 0)
.attr('height', height);
}
// Iterate after timer has passed if autoIterate = true
if (autoIterate) {
//Do the update first, then wait. Immediate transitions when updateBars() is run.
if (iteration + 1 <= numDataPoints) {
timer = setTimeout(function () {
iteration += i_step;
updateBars();
}, updateDelay);
}
}
}
function update_variables(shuffle, restart) {
// Update X-Axis
update_xValues();
xStep.domain(xDomain);
x.domain(d3.ticks(xDomain[0], xDomain[1], 21));
// Recalibrate the Continous X scale to fit the Ordinal X scale endpoints
xStep.range([(x(xDomain[0]) + (x.bandwidth() / 2)), (x(xDomain[1]) + (x.bandwidth() / 2))]);
xAxisGroup
.transition()
.duration(500)
.ease(d3.easeLinear)
.call(xAxis);
// Update bars width in case the X-axis has changed tick spacing
barRects.attr("width", x.bandwidth());
//TODO: Remove bars that are left-over from previous distributions
//Change sigma to 2 after a larger sample to see why
// Start the update iteration over again from scratch
if (shuffle) {
dataPoints = update_dataPoints();
}
if (restart) {
iteration = 0;
}
dataHist = update_dataHist(dataPoints);
// Update Y-Axis
y.domain([0, d3.max(dataHist, function (d) {
return Math.ceil(d.length / 10) * 10
})]);
yAxisGroup
.transition()
.duration(500)
.ease(d3.easeLinear)
.call(yAxis);
// If the updateDelay has changed, the transition duration should change accordingly
transitionDuration = updateDelay * 0.8;
updateBars();
}
/////////////////////////////////////////////////////
// Add listener to check for variable input change //
/////////////////////////////////////////////////////
document.addEventListener('DOMContentLoaded', function () {
document.querySelector('#variables').onchange = change_variable;
}, true);
function change_variable(event) {
if (!event.target.value) return;
console.log(event.target.id, " was ", eval(event.target.id));
eval(event.target.id + " = " + event.target.valueAsNumber);
console.log(event.target.id, " is now ", eval(event.target.id));
eval(event.target.getAttribute('fun'));
// Refresh view
update_variables();
}
})()
</script>
<div id="variables">
<label>Sample:</label>
<input type="number" class="variable" id="numDataPoints" placeholder="n = 100" fun="dataPoints = update_dataPoints(true);">
<input type="number" class="variable" id="normalMean" placeholder="μ = 0">
<input type="number" class="variable" id="normalStdDev" placeholder="σ = 3">
</br>
</br>
<label>Speed:</label>
<input type="number" class="variable" id="i_step" placeholder="1 / i">
<input type="number" class="variable" id="updateDelay" placeholder="500 ms">
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment