This block experiments two things:
Usage : use the controller to hide/show objects, and hover a cell or a bin for details.
Indeed, the underlying dataset does not suit the experimentation. I have to find another one.
==original README==
Kernel density estimation is a method of estimating the probability distribution of a random variable based on a random sample. In contrast to a histogram, kernel density estimation produces a smooth estimate. The smoothness can be tuned via the kernel’s bandwidth parameter. With the correct choice of bandwidth, important features of the distribution can be seen, while an incorrect choice results in undersmoothing or oversmoothing and obscured features.
This example shows a histogram and a kernel density estimation for times between eruptions of Old Faithful Geyser in Yellowstone National Park, taken from R’s faithful
dataset. The data follow a bimodal distribution; short eruptions are followed by a wait time averaging about 55 minutes, and long eruptions by a wait time averaging about 80 minutes. In recent years, wait times have been increasing, possibly due to the effects of earthquakes on the geyser’s geohydrology.
This example is based on a Protovis version by John Firebaugh. See also a two-dimensional density estimation of this dataset using d3-contour.
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>weighted KDE + Voronoï maps</title>
<meta name="description" content="Weighted KDE filled with Voronoï maps in D3.js">
<style>
.hide {
display: none;
}
.standard {
fill: #bbb;
stroke: #bbb;
}
.weighted {
fill: #ddd;
stroke: #ddd;
}
.bin {
stroke: white;
}
.kde {
stroke-width: 1.5;
stroke-linejoin: round;
fill: none;
stroke: black;
}
text.standard {
stroke: none;
}
text.weighted {
stroke: none;
}
.cell {
fill: transparent;
stroke-width: 1;
stroke: grey;
}
.axis--y .domain {
display: none;
}
.dg .property-name {
width: 80% !important;
}
.dg .c {
width: 20% !important;
}
</style>
</head>
<body>
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://rawcdn.githack.com/kcnarf/d3-weighted-voronoi/v1.0.0/build/d3-weighted-voronoi.js"></script>
<script src="d3-voronoi-map-fixed-x.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.3/seedrandom.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.5/dat.gui.min.js"></script>
<script>
var settings = {
standard: {
showBins: true,
showKDE: true
},
weighted: {
showWeightedBins: true,
showWeightedKDE: true,
showVoronoiMaps: true
}
};
var binCount = 40;
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
margin = {top: 20, right: 30, bottom: 30, left: 40};
var x = d3.scaleLinear()
.domain([30, 110])
.range([margin.left, width - margin.right]);
var y = d3.scaleLinear()
.range([height - margin.bottom, margin.top]);
var valueAccessor = d => d.value,
weightAccessor = d => d.weight;
var bins, // will store histograms data
density, // will store KDE data
weightedDensity, // will store weighted KDE data
maps; // will store each Voronoi map's data of each bin
d3.json("faithful.json").then(function(data) {
// I know the data is not suitable for such an experimentation :-(
// Data dealing with sales (with the possibility to count the number the sales, or to count the total sales' prices), would be much more better
faithful = data.map((d)=>{
return {
value: d,
weight: Math.round(d/10) // weird weight feature :-(
}
});
//begin: compute histograms data
bins = d3.histogram()
.domain(x.domain())
.value(valueAccessor)
.thresholds(binCount)
(faithful);
bins.forEach(b=>{
b.totalWeight = d3.sum(b, weightAccessor);
})
//end: compute histograms data
y.domain([0, d3.max(bins, b=>b.totalWeight)*1.1]);
//compute KDE's data, using a weight of 1 for each datum
density = kernelDensityEstimator(x.ticks(binCount), scaledEpanechnikov(7, valueAccessor), d=>1)(faithful);
//compute weighted KDE's data, using a particular weight for each datum
weightedDensity = kernelDensityEstimator(x.ticks(binCount), scaledEpanechnikov(7, valueAccessor), weightAccessor)(faithful);
//begin: weighted voronoï map for each right trapeze bin
var seededprng = new Math.seedrandom('my seed'),
voronoiMap = d3.voronoiMap().weight(weightAccessor).prng(seededprng);
maps = [];
weightedDensity.forEach(function (d,i) {
if (i > weightedDensity.length-2) return;
//begin: retrieve weighted KDE of bin's extent for right trapeze bins
var e = weightedDensity[i+1],
x0 = x(d[0]),
y0 = y(d[1]),
x1 = x(e[0]),
y1 = y(e[1]),
baseY = y(0),
data = bins[i];
//begin: retrieve weighted KDE of bin's extent for right trapeze bins
var map;
if (data.length <= 0) return; // no real data
//begin: voronoï map computation of a specific bin, considering weighted KDE
map = voronoiMap
.clip([[x0,baseY], [x0,y0], [x1,y1], [x1,baseY]])
.weight(weightAccessor)
.initialPosition(randomYInitialPosition())
(data).polygons;
//begin: voronoï map computation of a specific bin, considering weighted KDE
maps.push(map);
})
//end: weighted voronoï map for each right trapeze bin
drawBins();
drawVoronoiMaps();
drawKDEs();
drawAxes();
drawDatGui();
});
/******************************/
/* Weighted KDE */
/******************************/
function kernelDensityEstimator(samples, kernel, weightAccessor) {
return function(data) {
return samples.map(function(sample) {
return [sample, d3.sum(data, function(d) {
//return a value depending on kernel function AND weight
return kernel(sample, d) * weightAccessor(d);
})];
});
};
}
function scaledEpanechnikov(bw, valueAccessor) {
// 'bd' states for 'bandwidth'
return function(sample, d) {
var diff = sample - valueAccessor(d);
return epanechnikov(diff/bw)/bw;
};
}
function epanechnikov(diff) {
return Math.abs(diff) <= 1 ? 0.75 * (1 - diff * diff) : 0;
};
/******************************************************/
/* Voronoï map initial positioning with fixed x-coord */
/******************************************************/
function randomYInitialPosition () {
var clippingPolygon,
extent,
minY, maxY,
dy;
function _randomY(d, i, arr, voronoiMap) {
var shouldUpdateInternals = false;
var y;
if (clippingPolygon !== voronoiMap.clip()) {
clippingPolygon = voronoiMap.clip();
extent = voronoiMap.extent();
shouldUpdateInternals = true;
}
if (shouldUpdateInternals) {
updateInternals();
}
y = minY + dy * voronoiMap.prng()();
while (!d3.polygonContains(clippingPolygon, [x(valueAccessor(d)), y])) {
y = minY + dy * voronoiMap.prng()();
}
return [x(valueAccessor(d)), y];
};
///////////////////////
/////// Private ///////
///////////////////////
function updateInternals() {
minY = extent[0][1];
maxY = extent[1][1];
dy = maxY - minY;
};
return _randomY;
};
/******************************/
/* Drawings */
/******************************/
function drawBins() {
//begin: weighted bin
svg.append("g")
.attr("id", "weighted-histogram")
.classed("hide", !settings.weighted.showWeightedBins)
.selectAll("rect")
.data(bins)
.enter().append("rect")
.classed("weighted bin", true)
.attr("x", function(d) { return x(d.x0); })
.attr("y", function(d) { return y(d.totalWeight); })
.attr("width", function(d) { return x(d.x1) - x(d.x0); })
.attr("height", function(d) { return y(0) - y(d.totalWeight); })
.append("title")
.text(d => { return "weighted count: "+d.totalWeight+"\ncount: "+d.length; });;
//end: weighted bin
//begin: standard bin
svg.append("g")
.attr("id", "standard-histogram")
.classed("hide", !settings.standard.showBins)
.selectAll("rect")
.data(bins)
.enter().append("rect")
.classed("standard bin", true)
.attr("x", function(d) { return x(d.x0); })
.attr("y", function(d) { return y(d.length); })
.attr("width", function(d) { return x(d.x1) - x(d.x0); })
.attr("height", function(d) { return y(0) - y(d.length); })
.append("title")
.text(d => { return "count: " + d.length; });
//end: standard bin
}
function drawKDEs () {
//begin: weighted KDE
svg.append("path")
.attr("id", "weighted-kde")
.classed("weighted kde", true)
.classed("hide", !settings.weighted.showWeightedKDE)
.datum(weightedDensity)
.attr("d", d3.line()
.curve(d3.curveBasis)
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[1]); }));
//begin: weighted KDE
//begin: standard KDE
svg.append("path")
.attr("id", "standard-kde")
.classed("standard kde", true)
.classed("hide", !settings.standard.showKDE)
.datum(density)
.attr("d", d3.line()
.curve(d3.curveLinear)
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[1]); }));
//end: standard KDE
}
function drawVoronoiMaps () {
//begin: draw each cells of each voronoï map
var mapContainer = svg.append("g").attr("id", "voronoi-map-container").classed("hide", !settings.weighted.showVoronoiMaps);
maps.forEach(function(d){
mapContainer.append("g").classed("map", true)
.selectAll("path")
.data(d)
.enter()
.append("path")
.classed("cell", true)
.attr("d", d => { return d3.line().curve(d3.curveLinear)(d) + "z"; })
.append("title")
.text(d => { return "weight: " + weightAccessor(d.site.originalObject.data.originalData); })
})
//end: draw each cells of each voronoï map
}
function drawAxes () {
svg.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + (height - margin.bottom) + ")")
.call(d3.axisBottom(x))
.append("text")
.attr("x", width - margin.right)
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "end")
.attr("font-weight", "bold")
.text("Time between eruptions (min.)");
var yAxis = svg.append("g")
.attr("class", "axis axis--y")
.attr("transform", "translate(" + margin.left + ",0)")
.call(d3.axisLeft(y));
yAxis.append("text")
.classed("standard", true)
.attr("x", -margin.top-80)
.attr("y", 10)
.style("text-anchor", "end")
.style("font-weight", "bold")
.style("transform", "rotate(-90deg)")
.text("Count (i.e. number of eruptions) /");
yAxis.append("text")
.classed("weighted", true)
.attr("x", -margin.top)
.attr("y", 10)
.style("text-anchor", "end")
.style("font-weight", "bold")
.style("transform", "rotate(-90deg)")
.text("Weighted Count");
}
function drawDatGui() {
var controls = new dat.GUI({width: 200});
var standardControler = controls.addFolder("Standard");
standardControler.add(settings.standard, "showBins")
.listen().onChange(function(value) {
d3.select("#standard-histogram").classed("hide", !value);
});
standardControler.add(settings.standard, "showKDE")
.listen().onChange(function(value) {
d3.select("#standard-kde").classed("hide", !value);
});
standardControler.open();
var weightedControler = controls.addFolder("Weighted");
weightedControler.add(settings.weighted, "showWeightedBins")
.listen().onChange(function(value) {
d3.select("#weighted-histogram").classed("hide", !value);
});
weightedControler.add(settings.weighted, "showWeightedKDE")
.listen().onChange(function(value) {
d3.select("#weighted-kde").classed("hide", !value);
});
weightedControler.add(settings.weighted, "showVoronoiMaps")
.listen().onChange(function(value) {
d3.select("#voronoi-map-container").classed("hide", !value);
});
weightedControler.open();
}
</script>
</body>
</html>
Updated missing url https://rawcdn.githack.com/Kcnarf/d3-weighted-voronoi/v1.0.0/build/d3-weighted-voronoi.js to https://rawcdn.githack.com/kcnarf/d3-weighted-voronoi/v1.0.0/build/d3-weighted-voronoi.js
https://d3js.org/d3.v5.min.js
https://rawcdn.githack.com/Kcnarf/d3-weighted-voronoi/v1.0.0/build/d3-weighted-voronoi.js
https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.3/seedrandom.min.js
https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.5/dat.gui.min.js