Skip to content

Instantly share code, notes, and snippets.

@nstrayer
Last active September 16, 2016 03:41
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 nstrayer/ae6f31caccccc591ad6e6eb8d904bb28 to your computer and use it in GitHub Desktop.
Save nstrayer/ae6f31caccccc591ad6e6eb8d904bb28 to your computer and use it in GitHub Desktop.
Demonstration of a sliding histogram.
license: gpl-3.0

The height of the line represents the number of values falling within 1/2 of the bin width on either side of the x axis point.

So if the line is at height 200 for an x value of 20 and your binwidth is 10, then there are 200 points from the range of 15 to 25 in the data.

Eventually to be made into an R htmlwidgets library.

ToDo:

Make the bin interval sit as a legend normally but then when moused over it becomes the interval that we use on the y axis.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.axis {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis--x path {
display: none;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
svg text{
font-family:optima;
}
</style>
<body>
<div id = "chart"></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="sliding_hist.js"></script>
<script>
//Function to generate vector of normals.
var randomNormalArray = (n, mu, variance) => [...new Array(n)]
.map((_, i) => d3.randomNormal(mu, variance)());
//Generate our data
var data = randomNormalArray(500, 100, 10)
var slidey = sliding_hist()
.height(500)
.width(800)
.fillColor("#a6bddb")
.binWidth(10);
d3.select("#chart")
.datum(data)
.call(slidey);
</script>
function sliding_hist() {
var chartWidth = 960, // default width
chartHeight = 500, // default height
binWidth = 10,
stepWidth = 1,
fillColor = "steelblue"
//Need last element of array later, neater to define logic here.
Array.prototype.last = function() {
return this[this.length-1];
}
//takes your data, bin width and how much the bin slides over each step and
//returns an array of objects containing the "center" of the bin and the number
//of elements that fell in it ("in_bin").
function generate_slide_hist(data, binWidth, stepSize){
var binHalf = binWidth/2;
//Find the range of the data so we can know which window to slide overlay
var data_range = d3.extent(data)
//generate an array of the bin centers we are going to use in our sliding interval.
var bin_centers = [data_range[0] ]
while(bin_centers.last() < data_range[1]) bin_centers.push(bin_centers.last() + stepSize)
//Run through the bin centers counting how many are within the bin width of the value.
var counts = []
bin_centers.forEach(function(center){
var points_in_bin = data.filter(function(val){return val > (center - binHalf) && val < (center + binHalf) })
counts.push({"center": center, "in_bin": points_in_bin.length})
})
return counts;
}
function chart(selection) {
selection.each(function(data){
//make into the correct form.
var hist_data = generate_slide_hist(data,binWidth,stepWidth);
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = chartWidth - margin.left - margin.right,
height = chartHeight - margin.top - margin.bottom;
var dataRange = d3.extent(data)
var x = d3.scaleLinear()
.range([0, width])
.domain(dataRange);
var y = d3.scaleLinear()
.range([height, 0])
.domain([0,d3.max(hist_data, function(d) { return d.in_bin; })]);
var line = d3.line()
.x(function(d) { return x(d.center); })
.y(function(d) { return y(d.in_bin); });
//shade under the line like a proper histogram.
var area = d3.area()
.x(function(d) { return x(d.center); })
.y0(height)
.y1(function(d) { return y(d.in_bin); });
var svg = d3.select(this).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 + ")");
//shade under the line.
svg.append("path")
.datum(hist_data)
.attr("class", "area")
.style("fill", fillColor)
.style("fill-opacity", 0.5)
.attr("d", area);
svg.append("path")
.datum(hist_data)
.attr("class", "line")
.attr("d", line);
svg.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y))
.append("text")
.attr("class", "axis-title")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("# in x +- binwidth/2");
var focus = svg.append("g")
.attr("class", "focus")
.style("display", "none");
focus.append("circle")
.attr("r", 4.5)
.style("fill", "none")
.style("stroke", "black");
var drop_line = focus.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", 0)
.attr("stroke","black");
//how many pixles the interval takes up on the screen.
var interval_width = x(dataRange[0] + binWidth/2);
var interval_line = svg.append("g")
.attr("class", "interval_line")
//where the interval legend sits on plot when not being used for mouseover.
var interval_rest = "translate(" + (width - interval_width - 20) + "," + (height/7) + ")"
//place the interval line as a legend when not being interacted with.
interval_line.attr("transform", interval_rest);
var interval_legend_text = svg.append("text")
.text("Bin Width: " + binWidth)
.attr("text-anchor", "middle")
.attr("font-size", "0.9em")
.attr("y", -14)
.attr("transform", interval_rest);
interval_line.append("line")
.attr("class", "wide_line")
.attr("x1", interval_width)
.attr("y1", -10)
.attr("x2", -interval_width)
.attr("y2", -10)
.attr("stroke","black")
.attr("stroke-width","1px");
interval_line.append("line")
.attr("class", "left_line")
.attr("x1", -interval_width)
.attr("y1", 0)
.attr("x2", -interval_width)
.attr("y2", -10)
.attr("stroke","black")
.attr("stroke-width","1px");
interval_line.append("line")
.attr("class", "right_line")
.attr("x1", interval_width)
.attr("y1", 0)
.attr("x2", interval_width)
.attr("y2", -10)
.attr("stroke","black")
.attr("stroke-width","1px");
focus.append("text")
.attr("x", 9)
.attr("dy", ".35em");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.style("fill","none")
.style("pointer-events", "all")
.on("mouseover", function() {
focus.style("display", null); })
.on("mouseout", function() {
interval_line.transition().duration(200).attr("transform", interval_rest);
focus.style("display", "none"); })
.on("mousemove", mousemove);
var bisectX = d3.bisector(function(d) { return d.center; }).left;
function mousemove() {
var x0 = x.invert(d3.mouse(this)[0]),
i = bisectX(hist_data, x0, 1),
d0 = hist_data[i - 1],
d1 = hist_data[i],
d = x0 - d0.center > d1.center - x0 ? d1 : d0;
focus.attr("transform", "translate(" + x(d.center) + "," + y(d.in_bin) + ")");
drop_line
.attr("y1", 4.5)
.attr("y2", (height - y(d.in_bin) -10));
interval_line
.transition()
.duration(20)
.attr("transform", "translate(" + x(d.center) + "," + (height ) + ")");
focus.select("text").text(d.in_bin);
}
})
}
chart.binWidth = function(value) {
if (!arguments.length) return binWidth;
binWidth = value;
return chart;
};
chart.stepWidth = function(value) {
if (!arguments.length) return stepWidth;
stepWidth = value;
return chart;
};
chart.height = function(value) {
if (!arguments.length) return chartHeight;
chartHeight = value;
return chart;
};
chart.width = function(value) {
if (!arguments.length) return chartWidth;
chartWidth = value;
return chart;
};
chart.fillColor = function(value) {
if (!arguments.length) return fillColor;
fillColor = value;
return chart;
};
return chart;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment