This Gist uses Mike Bostock’s Crossfilter against a Dataset of mulitple entries. See in action at http://bl.ocks.org/Rnhatch/raw/3039755/
Data here has been scrubbed and anonymized
<!DOCTYPE html>
<meta charset="utf-8">
<title>Comparative Filtering</title>
<style>
body {
font-family: "Verdana";
margin: 40px auto;
width: 960px;
min-height: 2000px;
font-weight: 300;
font-size: 12px;
}
#body {
position: relative;
}
footer {
padding: 2em 0 1em 0;
font-size: 12px;
}
h1 + h2 {
margin-top: 0;
}
h2 {
font-weight: 400;
font-size: 28px;
}
h3 {
font-weight: 300;
font-size: 18px;
}
#body > p {
line-height: 1.5em;
width: 640px;
text-rendering: optimizeLegibility;
}
#charts {
padding: 10px 0;
}
.chart {
display: inline-block;
height: 151px;
margin-bottom: 20px;
}
.reset {
padding-left: 1em;
font-size: smaller;
color: #ccc;
}
.background.bar {
fill: #ccc;
}
.foreground.bar {
fill: steelblue;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis text {
font: 10px sans-serif;
}
.brush rect.extent {
fill: steelblue;
fill-opacity: .125;
}
.brush .resize path {
fill: #eee;
stroke: #666;
}
#Tenure-chart {
width: 420px;
}
#Age-chart {
width: 420px;
}
#date-chart {
width: 900px;
}
#TotalPTD-chart {
width: 420px;
}
#Division-chart {
width: 420px;
}
#claim-list {
min-height: 1024px;
}
#claim-list .date,
#claim-list .day {
margin-bottom: .4em;
}
#claim-list .claim {
line-height: 1.5em;
background: #eee;
width: 920px;
margin-bottom: 1px;
}
#claim-list .time {
color: #999;
}
#claim-list .claim div {
display: inline-block;
width: 100px;
}
#claim-list div.Diag_Cat {
width: 240px;
padding-right: 10px;
text-align: left;
}
#claim-list div.Age {
width: 50px;
padding-right: 10px;
text-align: right;
}
#claim-list div.State {
width: 30px;
padding-right: 10px;
text-align: right;
}
#claim-list div.Division {
width: 150px;
padding-right: 10px;
text-align: right;
}
#claim-list div.TotPTD,
#claim-list div.Tenure {
width: 100px;
padding-right: 10px;
text-align: right;
}
#claim-list .early {
color: green;
}
aside {
position: absolute;
left: 740px;
font-size: smaller;
width: 220px;
}
</style>
<div id="body">
<h2>Test of Rapid Filtering</h2>
<h3><span id="active">-</span> of <span id="total">-</span> objects selected.</h3>
<div id="charts">
<div id="Tenure-chart" class="chart">
<div class="title">Tenure (months)</div>
</div>
<div id="Age-chart" class="chart">
<div class="title">Age</div>
</div>
<div id="date-chart" class="chart">
<div class="title">Date</div>
</div>
<div id="TotalPTD-chart" class="chart">
<div class="title">Total Paid to Date</div>
</div>
<div id="Division-chart" class="chart">
<div class="title">Division</div>
</div>
</div>
<h3> List of representative objects </h3>
<div id="lists">
<div id="claim-list" class="list"></div>
</div>
</div>
<script src="//square.github.com/crossfilter/crossfilter.v1.min.js"></script>
<script src="//square.github.com/crossfilter/d3.v2.min.js"></script>
<script>
d3.csv("claims.csv", function(claims) {
// Various formatters.
var formatNumber = d3.format(",r"),
formatChange = d3.format("+,d"),
formatDate = d3.time.format("%B %d, %Y"),
formatTime = d3.time.format("%I:%M %p");
// A nest operator, for grouping the claim list.
var nestByDate = d3.nest()
.key(function(d) { return d3.time.day(d.date); });
// A little coercion, since the CSV is untyped.
claims.forEach(function(d, i) {
d.index = i;
d.date = parseDate(d.date);
d.Tenure = +d.Tenure;
d.Age = +d.Age;
d.TotalPTD = +d.TotalPTD;
d.Division = +d.Division;
});
// Create the crossfilter for the relevant dimensions and groups.
var claim = crossfilter(claims),
all = claim.groupAll(),
date = claim.dimension(function(d) { return d3.time.week(d.date); }),
dates = date.group(),
// hour = claim.dimension(function(d) { return d.date.getHours() + d.date.getMinutes() / 60; }),
// hours = hour.group(Math.floor),
Tenure = claim.dimension(function(d) { return Math.max(0, Math.min(800, d.Tenure)); }),
Tenures = Tenure.group(function(d) { return Math.floor(d / 20) * 20; }),
Age = claim.dimension(function(d) { return Math.min(80, d.Age); }),
Ages = Age.group(function(d) { return Math.floor(d / 3) * 3; });
TotalPTD = claim.dimension(function(d) { return Math.min(100000, d.TotalPTD); }),
TotalPTDs = TotalPTD.group(function(d) { return Math.floor(d / 5000) * 5000; });
Division = claim.dimension(function(d) { return Math.min(125, d.Division); }),
Divisions = Division.group(function(d) { return Math.floor(d / 5) * 5; });
var charts = [
// barChart()
// .dimension(hour)
// .group(hours)
// .x(d3.scale.linear()
// .domain([0, 1])
// .rangeRound([0, 10])),
barChart()
.dimension(Tenure)
.group(Tenures)
.x(d3.scale.linear()
.domain([0, 800])
.rangeRound([0, 400])),
barChart()
.dimension(Age)
.group(Ages)
.x(d3.scale.linear()
.domain([0, 80])
.rangeRound([0, 300])),
barChart()
.dimension(date)
.group(dates)
.round(d3.time.week.round)
.x(d3.time.scale()
.domain([new Date(2011, 0, 1), new Date(2011, 12, 1)])
.rangeRound([0, 800]))
.filter([new Date(2011, 1, 1), new Date(2011, 2, 1)]),
barChart()
.dimension(TotalPTD)
.group(TotalPTDs)
.x(d3.scale.linear()
.domain([0, 100000])
.rangeRound([0, 400])),
barChart()
.dimension(Division)
.group(Divisions)
.x(d3.scale.linear()
.domain([0, 125])
.rangeRound([0, 300]))
];
// Given our array of charts, which we assume are in the same order as the
// .chart elements in the DOM, bind the charts to the DOM and render them.
// We also listen to the chart's brush events to update the display.
var chart = d3.selectAll(".chart")
.data(charts)
.each(function(chart) { chart.on("brush", renderAll).on("brushend", renderAll); });
// Render the initial lists.
var list = d3.selectAll(".list")
.data([claimList]);
// Render the total.
d3.selectAll("#total")
.text(formatNumber(claim.size()));
renderAll();
// Renders the specified chart or list.
function render(method) {
d3.select(this).call(method);
}
// Whenever the brush moves, re-rendering everything.
function renderAll() {
chart.each(render);
list.each(render);
d3.select("#active").text(formatNumber(all.value()));
}
// Like d3.time.format, but faster.
function parseDate(d) {
return new Date(2011,
d.substring(0, 2) - 1,
d.substring(2, 4),
d.substring(4, 6),
d.substring(6, 8));
}
window.filter = function(filters) {
filters.forEach(function(d, i) { charts[i].filter(d); });
renderAll();
};
window.reset = function(i) {
charts[i].filter(null);
renderAll();
};
function claimList(div) {
var claimsByDate = nestByDate.entries(date.top(40));
div.each(function() {
var date = d3.select(this).selectAll(".date")
.data(claimsByDate, function(d) { return d.key; });
date.enter().append("div")
.attr("class", "date")
.append("div")
.attr("class", "day")
.text(function(d) { return formatDate(d.values[0].date); });
date.exit().remove();
var claim = date.order().selectAll(".claim")
.data(function(d) { return d.values; }, function(d) { return d.index; });
var claimEnter = claim.enter().append("div")
.attr("class", "claim");
claimEnter.append("div")
.attr("class", "CL_Number")
.text(function(d) { return d.CL_Number; });
claimEnter.append("div")
.attr("class", "Diag_Cat")
.text(function(d) { return d.Diag_Cat; });
claimEnter.append("div")
.attr("class", "Age")
.text(function(d) { return formatNumber(d.Age) + " yrs."; });
claimEnter.append("div")
.attr("class", "Tenure")
.text(function(d) { return formatNumber(d.Tenure) + " mos."; });
claimEnter.append("div")
.attr("class", "State")
.text(function(d) { return d.State; });
claimEnter.append("div")
.attr("class", "Division")
.text(function(d) { return "Division " + formatNumber(d.Division); });
claimEnter.append("div")
.attr("class", "TotalPTD")
.text(function(d) { return "$ " + formatNumber(d.TotalPTD); });
claim.exit().remove();
claim.order();
});
}
function barChart() {
if (!barChart.id) barChart.id = 0;
var margin = { top: 10, right: 10, bottom: 20, left: 10 },
x,
y = d3.scale.linear().range([100, 0]),
id = barChart.id++,
axis = d3.svg.axis().orient("bottom"),
brush = d3.svg.brush(),
brushDirty,
dimension,
group,
round;
function chart(div) {
var width = x.range()[1],
height = y.range()[0];
y.domain([0, group.top(1)[0].value]);
div.each(function() {
var div = d3.select(this),
g = div.select("g");
// Create the skeletal chart.
if (g.empty()) {
div.select(".title").append("a")
.attr("href", "javascript:reset(" + id + ")")
.attr("class", "reset")
.text("reset")
.style("display", "none");
g = div.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 + ")");
g.append("clipPath")
.attr("id", "clip-" + id)
.append("rect")
.attr("width", width)
.attr("height", height);
g.selectAll(".bar")
.data(["background", "foreground"])
.enter().append("path")
.attr("class", function(d) { return d + " bar"; })
.datum(group.all());
g.selectAll(".foreground.bar")
.attr("clip-path", "url(#clip-" + id + ")");
g.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(axis);
// Initialize the brush component with pretty resize handles.
var gBrush = g.append("g").attr("class", "brush").call(brush);
gBrush.selectAll("rect").attr("height", height);
gBrush.selectAll(".resize").append("path").attr("d", resizePath);
}
// Only redraw the brush if set externally.
if (brushDirty) {
brushDirty = false;
g.selectAll(".brush").call(brush);
div.select(".title a").style("display", brush.empty() ? "none" : null);
if (brush.empty()) {
g.selectAll("#clip-" + id + " rect")
.attr("x", 0)
.attr("width", width);
} else {
var extent = brush.extent();
g.selectAll("#clip-" + id + " rect")
.attr("x", x(extent[0]))
.attr("width", x(extent[1]) - x(extent[0]));
}
}
g.selectAll(".bar").attr("d", barPath);
});
function barPath(groups) {
var path = [],
i = -1,
n = groups.length,
d;
while (++i < n) {
d = groups[i];
path.push("M", x(d.key), ",", height, "V", y(d.value), "h9V", height);
}
return path.join("");
}
function resizePath(d) {
var e = +(d == "e"),
x = e ? 1 : -1,
y = height / 3;
return "M" + (.5 * x) + "," + y
+ "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6)
+ "V" + (2 * y - 6)
+ "A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y)
+ "Z"
+ "M" + (2.5 * x) + "," + (y + 8)
+ "V" + (2 * y - 8)
+ "M" + (4.5 * x) + "," + (y + 8)
+ "V" + (2 * y - 8);
}
}
brush.on("brushstart.chart", function() {
var div = d3.select(this.parentNode.parentNode.parentNode);
div.select(".title a").style("display", null);
});
brush.on("brush.chart", function() {
var g = d3.select(this.parentNode),
extent = brush.extent();
if (round) g.select(".brush")
.call(brush.extent(extent = extent.map(round)))
.selectAll(".resize")
.style("display", null);
g.select("#clip-" + id + " rect")
.attr("x", x(extent[0]))
.attr("width", x(extent[1]) - x(extent[0]));
dimension.filterRange(extent);
});
brush.on("brushend.chart", function() {
if (brush.empty()) {
var div = d3.select(this.parentNode.parentNode.parentNode);
div.select(".title a").style("display", "none");
div.select("#clip-" + id + " rect").attr("x", null).attr("width", "100%");
dimension.filterAll();
}
});
chart.margin = function(_) {
if (!arguments.length) return margin;
margin = _;
return chart;
};
chart.x = function(_) {
if (!arguments.length) return x;
x = _;
axis.scale(x);
brush.x(x);
return chart;
};
chart.y = function(_) {
if (!arguments.length) return y;
y = _;
return chart;
};
chart.dimension = function(_) {
if (!arguments.length) return dimension;
dimension = _;
return chart;
};
chart.filter = function(_) {
if (_) {
brush.extent(_);
dimension.filterRange(_);
} else {
brush.clear();
dimension.filterAll();
}
brushDirty = true;
return chart;
};
chart.group = function(_) {
if (!arguments.length) return group;
group = _;
return chart;
};
chart.round = function(_) {
if (!arguments.length) return round;
round = _;
return chart;
};
return d3.rebind(chart, brush, "on");
}
});
</script>