A simple upset chart implementation, a chart type useful for visualising set intersections, applied to flatmate-purchase assignment data from my onlineshop project.
See it live on bl.ocks
A simple upset chart implementation, a chart type useful for visualising set intersections, applied to flatmate-purchase assignment data from my onlineshop project.
See it live on bl.ocks
id | purchase_id | flatmate_id | |
---|---|---|---|
1 | 1 | 2 | |
2 | 2 | 1 | |
3 | 3 | 2 | |
4 | 3 | 1 | |
5 | 4 | 2 | |
6 | 5 | 2 | |
7 | 5 | 1 |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Upset Viz</title> | |
<link rel='stylesheet' type='text/css' href='style.css'> | |
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js' charset='utf-8'></script> | |
<script src='./UpsetChart.js'></script> | |
<script src='./main.js'></script> | |
</head> | |
<body> | |
<div id='vis'></div> | |
</body> | |
</html> | |
'use strict' | |
var dataFilepath = 'assignment.csv' | |
d3.csv(dataFilepath, function(loadedData) { | |
// remodel data for chart's requirements | |
var numFlatmates = d3.nest() | |
.key(function(a) { return a.flatmate_id }) | |
.entries(loadedData) | |
.length | |
var purchaseAssignments = d3.nest() | |
.key(function(a) { return a.purchase_id }) | |
.rollup(function(assignments) { | |
var assignment = new Array(numFlatmates).fill(0) | |
var assignedFlatmates = assignments.map(function(a) { return a.flatmate_id }) | |
assignedFlatmates.forEach(function(f) { | |
// flatmate ids start at 1 in the csv | |
assignment[f-1] = 1 | |
}) | |
return assignment | |
}) | |
.entries(loadedData) | |
var dataset = { | |
numFlatmates: numFlatmates | |
, values: purchaseAssignments | |
} | |
// the chart | |
var svg = d3.select('div#vis') | |
.append('svg') | |
.attr('width', 960) | |
.attr('height', 500) | |
function draw(data) { | |
var rows = svg.selectAll('g.upsetchart') | |
.data(data, function(d) { return d.key }) | |
rows.enter() | |
.append('g') | |
.classed('upsetchart purchase', true) | |
.attr('transform', function(d, i) { | |
return 'translate(' + 10 + ',' + (10 + i*30) + ')' | |
}) | |
rows | |
.call(UpsetChart()) | |
rows.exit() | |
.remove() | |
} | |
draw(dataset.values) | |
}) |
div#vis { | |
width: 960px; | |
margin: 0px auto; | |
} | |
.innerchart line { | |
stroke: gray; | |
stroke-width: 4px; | |
} | |
circle { | |
stroke: gray; | |
stroke-width: 3px; | |
} | |
circle.assigned { | |
fill: gray; | |
} | |
circle.not-assigned { | |
fill: white; | |
} |
function UpsetChart() { | |
// assumptions about the data passed to this chart: | |
// i.e. dataset of each selection | |
// Object with the following keys: | |
// - key: purchase id | |
// - values: flatmates assigned to this purchase | |
// chart config DEFAULTS | |
var margin = {top: 10, right: 10, bottom: 10, left: 10} | |
var width = 960 - margin.left - margin.right | |
var height = 30 - margin.top - margin.bottom | |
function drawChart(selection) { | |
selection.each(function(dataset, i) { | |
var wholeChart = this | |
// | |
// THE CHART | |
// | |
// select chart element(s) (eg. innerchart) if they exist already | |
var innerchart = d3.select(wholeChart).selectAll('g.innerchart') | |
.data(function(dataset) { return [dataset.values] }) | |
// otherwise create them and the elements they contain | |
var innerchartEnter = innerchart.enter() | |
.append('g') | |
.classed('innerchart', true) | |
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') | |
innerchartEnter | |
.append('line') | |
.attr('x1', 10) | |
.attr('y1', 10) | |
.attr('x2', 60-10-10) | |
.attr('y2', 10) | |
// update data | |
var flatmates = innerchart.selectAll('circle') | |
.data(function(d) { return d }) | |
flatmates.enter() | |
.append('circle') | |
.attr('class', function(d, i) { | |
var assigned | |
if (d) { assigned = 'assigned' } | |
else { assigned = 'not-assigned' } | |
var flatmate_id = i+1 | |
return assigned + ' ' + flatmate_id | |
}) | |
.attr('cx', function(d, i) { return 10+i*(10+20) }) | |
.attr('cy', 10) | |
.attr('r', 10) | |
}) | |
} | |
// | |
// Chart CONFIG getter setter methods | |
// | |
// TODO figure out which side effects are actually necessary to recalculate | |
drawChart.width = function(value) { | |
if (!arguments.length) return width | |
width = value | |
return drawChart | |
} | |
drawChart.height = function(value) { | |
if (!arguments.length) return height | |
height = value | |
return drawChart | |
} | |
return drawChart | |
} |