Updated reusable slopegraph sketch, from my previous slopegraph version
This version allows for multiple sets/columns and will adapted accordingly. Added some interaction to toggle the sets/columns and also toggle highlighted line.
Updated reusable slopegraph sketch, from my previous slopegraph version
This version allows for multiple sets/columns and will adapted accordingly. Added some interaction to toggle the sets/columns and also toggle highlighted line.
// ***************************************** | |
// reusable multiple slopegraph chart | |
// ***************************************** | |
(function() { | |
'use strict'; | |
d3.eesur.slopegraph_v2 = function module() { | |
// input vars for getter setters | |
var w = 200, // width of the set | |
h = 600, | |
margin = {top: 40, bottom: 40, left: 80, right: 80}, | |
gutter = 50, | |
strokeColour = 'black', | |
// key data values (in order) | |
keyValues = [], | |
// key value (used for ref/titles) | |
keyName = '', | |
format = d3.format(''), | |
sets; | |
var dispatch = d3.dispatch('_hover'); | |
var svg, yScale; | |
function exports(_selection) { | |
_selection.each(function(data) { | |
var allValues = [], | |
maxValue; | |
// format/clean data | |
data.forEach(function(d) { | |
_.times(keyValues.length, function (n) { | |
d[keyValues[n]] = +d[keyValues[n]]; | |
allValues.push(d[keyValues[n]]); | |
}); | |
}); | |
// create max value so scale is consistent | |
maxValue = _.max(allValues); | |
// adapt the size against number of sets | |
w = w * keyValues.length; | |
// have reference for number of sets | |
sets = keyValues.length -1; | |
// use same scale for both sides | |
yScale = d3.scale.linear() | |
.domain([0, maxValue]) | |
.range([h - margin.top, margin.bottom]); | |
// clean start | |
d3.select(this).select('svg').remove(); | |
svg = d3.select(this).append('svg') | |
.attr({ | |
width: w, | |
height: h | |
}); | |
render(data, 0); | |
}); | |
} | |
// recursive function to apply each set | |
// then the start and end labels (as only needed once) | |
function render (data, n) { | |
if (n < keyValues.length-1 ) { | |
lines(data, n); | |
middleLabels(data, n); | |
return render(data, n+1); | |
} else { | |
startLabels(data); | |
endLabels(data); | |
return n; | |
} | |
} | |
// render connecting lines | |
function lines(data, n) { | |
var lines = svg.selectAll('.s-line-' + n) | |
.data(data); | |
lines.enter().append('line'); | |
lines.attr({ | |
x1: function () { | |
if (n === 0) { | |
return margin.left; | |
} else { | |
return ((w / sets) * n) + margin.left/2; | |
} | |
}, | |
y1: function(d) { return yScale(d[keyValues[n]]); }, | |
x2: function () { | |
if (n === sets-1) { | |
return w - margin.right; | |
} else { | |
return ((w / sets) * (n+1)) - gutter; | |
} | |
}, | |
y2: function(d) { return yScale(d[keyValues[n+1]]); }, | |
stroke: strokeColour, | |
'stroke-width': 1, | |
class: function (d, i) { return 'elm s-line-' + n + ' sel-' + i; } | |
}) | |
.on('mouseover', dispatch._hover); | |
// lines.exit().remove(); | |
} | |
// middle labels in-between sets | |
function middleLabels(data, n) { | |
if (n !== sets-1) { | |
var middleLabels = svg.selectAll('.m-labels-' + n) | |
.data(data); | |
middleLabels.enter().append('text') | |
.attr({ | |
class: function (d, i) { return 'labels m-labels-' + n + ' elm ' + 'sel-' + i; }, | |
x: ((w / sets) * (n+1)) + 15, | |
y: function(d) { return yScale(d[keyValues[n+1]]) + 4; }, | |
}) | |
.text(function (d) { | |
return format(d[keyValues[n+1]]); | |
}) | |
.style('text-anchor','end') | |
.on('mouseover', dispatch._hover); | |
// title | |
svg.append('text') | |
.attr({ | |
class: 's-title', | |
x: ((w / sets) * (n+1)), | |
y: margin.top/2 | |
}) | |
.text(keyValues[n+1] + ' ↓') | |
.style('text-anchor','end'); | |
} | |
} | |
// start labels applied left of chart sets | |
function startLabels(data) { | |
var startLabels = svg.selectAll('.l-labels') | |
.data(data); | |
startLabels.enter().append('text') | |
.attr({ | |
class: function (d, i) { return 'labels l-labels elm ' + 'sel-' + i; }, | |
x: margin.left - 3, | |
y: function(d) { return yScale(d[keyValues[0]]) + 4; } | |
}) | |
.text(function (d) { | |
return d[keyName] + ' ' + format(d[keyValues[0]]); | |
}) | |
.style('text-anchor','end') | |
.on('mouseover', dispatch._hover); | |
// title | |
svg.append('text') | |
.attr({ | |
class: 's-title', | |
x: margin.left - 3, | |
y: margin.top/2 | |
}) | |
.text(keyValues[0] + ' ↓') | |
.style('text-anchor','end'); | |
} | |
// end labels applied right of chart sets | |
function endLabels(data) { | |
var i = keyValues.length-1; | |
var endLabels = svg.selectAll('r.labels') | |
.data(data); | |
endLabels.enter().append('text') | |
.attr({ | |
class: function (d, i) { return 'labels r-labels elm ' + 'sel-' + i; }, | |
x: w - margin.right + 3, | |
y: function(d) { return yScale(d[keyValues[i]]) + 4; }, | |
}) | |
.text(function (d) { | |
return d[keyName] + ' ' + format(d[keyValues[i]]); | |
}) | |
.style('text-anchor','start') | |
.on('mouseover', dispatch._hover); | |
// title | |
svg.append('text') | |
.attr({ | |
class: 's-title', | |
x: w - margin.right + 3, | |
y: margin.top/2 | |
}) | |
.text('↓ ' + keyValues[i]) | |
.style('text-anchor','start'); | |
} | |
// getter/setters for overrides | |
exports.w = function(value) { | |
if (!arguments.length) return w; | |
w = value; | |
return this; | |
}; | |
exports.h = function(value) { | |
if (!arguments.length) return h; | |
h = value; | |
return this; | |
}; | |
exports.margin = function(value) { | |
if (!arguments.length) return margin; | |
margin = value; | |
return this; | |
}; | |
exports.gutter = function(value) { | |
if (!arguments.length) return gutter; | |
gutter = value; | |
return this; | |
}; | |
exports.format = function(value) { | |
if (!arguments.length) return format; | |
format = value; | |
return this; | |
}; | |
exports.strokeColour = function(value) { | |
if (!arguments.length) return strokeColour; | |
strokeColour = value; | |
return this; | |
}; | |
exports.keyValues = function(value) { | |
if (!arguments.length) return keyValues; | |
keyValues = value; | |
return this; | |
}; | |
exports.keyName = function(value) { | |
if (!arguments.length) return keyName; | |
keyName = value; | |
return this; | |
}; | |
d3.rebind(exports, dispatch, 'on'); | |
return exports; | |
}; | |
}()); |
[ | |
{ | |
"2000": 1.56, | |
"2001": 1.67, | |
"2002": 1.79, | |
"2003": 1.89, | |
"2004": 2.02, | |
"2005": 2.05, | |
"2006": 2.12, | |
"2007": 2.19, | |
"2008": 2.27, | |
"2009": 2.34, | |
"2010": 2.47, | |
"2011": 2.75, | |
"2012": 3, | |
"country": "US" | |
}, | |
{ | |
"2000": 0.74, | |
"2001": 0.83, | |
"2002": 0.94, | |
"2003": 1.05, | |
"2004": 1.17, | |
"2005": 1.3, | |
"2006": 1.38, | |
"2007": 1.49, | |
"2008": 1.54, | |
"2009": 1.66, | |
"2010": 1.73, | |
"2011": 1.78, | |
"2012": 1.79, | |
"country": "Germany" | |
}, | |
{ | |
"2000": 0.75, | |
"2001": 0.81, | |
"2002": 0.88, | |
"2003": 0.95, | |
"2004": 1.31, | |
"2005": 1.65, | |
"2006": 1.75, | |
"2007": 1.85, | |
"2008": 1.97, | |
"2009": 1.95, | |
"2010": 2.02, | |
"2011": 2.12, | |
"2012": 2.22, | |
"country": "UK" | |
}, | |
{ | |
"2000": 0.09, | |
"2001": 0.11, | |
"2002": 0.15, | |
"2003": 0.21, | |
"2004": 0.22, | |
"2005": 0.24, | |
"2006": 0.3, | |
"2007": 0.35, | |
"2008": 0.43, | |
"2009": 0.49, | |
"2010": 0.55, | |
"2011": 0.61, | |
"2012": 0.67, | |
"country": "China" | |
}, | |
{ | |
"2000": 1.02, | |
"2001": 1.16, | |
"2002": 1.22, | |
"2003": 1.3, | |
"2004": 1.37, | |
"2005": 1.45, | |
"2006": 1.53, | |
"2007": 1.61, | |
"2008": 1.73, | |
"2009": 1.83, | |
"2010": 1.92, | |
"2011": 2.01, | |
"2012": 2.1, | |
"country": "Japan" | |
}, | |
{ | |
"2000": 0.02, | |
"2001": 0.03, | |
"2002": 0.04, | |
"2003": 0.05, | |
"2004": 0.06, | |
"2005": 0.08, | |
"2006": 0.14, | |
"2007": 0.16, | |
"2008": 0.19, | |
"2009": 0.24, | |
"2010": 0.3, | |
"2011": 0.37, | |
"2012": 0.44, | |
"country": "India" | |
}, | |
{ | |
"2000": 0.04, | |
"2001": 0.04, | |
"2002": 0.05, | |
"2003": 0.05, | |
"2004": 0.06, | |
"2005": 0.06, | |
"2006": 0.08, | |
"2007": 0.12, | |
"2008": 0.15, | |
"2009": 0.18, | |
"2010": 0.23, | |
"2011": 0.27, | |
"2012": 0.33, | |
"country": "Indonesia" | |
}, | |
{ | |
"2000": 0.26, | |
"2001": 0.31, | |
"2002": 0.37, | |
"2003": 0.43, | |
"2004": 0.47, | |
"2005": 0.58, | |
"2006": 0.6, | |
"2007": 0.63, | |
"2008": 0.73, | |
"2009": 1.12, | |
"2010": 1.27, | |
"2011": 1.43, | |
"2012": 1.57, | |
"country": "Mexico" | |
}, | |
{ | |
"2000": 0.02, | |
"2001": 0.03, | |
"2002": 0.03, | |
"2003": 0.04, | |
"2004": 0.05, | |
"2005": 0.06, | |
"2006": 0.09, | |
"2007": 0.09, | |
"2008": 0.1, | |
"2009": 0.11, | |
"2010": 0.12, | |
"2011": 0.14, | |
"2012": 0.15, | |
"country": "Kenya" | |
} | |
] |
<!DOCTYPE html> | |
<html lang='en'> | |
<head> | |
<meta charset='UTF-8'> | |
<title>d3 | reusable slopegraph v2</title> | |
<meta name="author" content="Sundar Singh | eesur.com"> | |
<link rel="stylesheet" href="main.css"> | |
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js" charset="utf-8"></script> | |
</head> | |
<body> | |
<header> | |
<h1>Reusable slopegraph v2</h1> | |
<p>Number of personal computers installed in a country per household.</p> | |
<nav id='filter'></nav> | |
<nav id='nav-alt'></nav> | |
</header> | |
<section id="slopegraph" class="slope"></section> | |
<!-- *************** start js/d3 code ***************** --> | |
<!-- namespace --> | |
<script> d3.eesur = {}; </script> | |
<!-- reusable slopegraph --> | |
<script src="d3_code_slopegraph_v2.js"></script> | |
<script> | |
// render slopegraph chart | |
(function() { | |
'use strict'; | |
var data, | |
// keys values from data to be applied | |
keyValues = ['2000', '2002', '2004', '2006', '2008', '2010', '2012']; | |
// store chart | |
var slopegraph; | |
// track any user interactions | |
var state = { | |
// have an array to mutate | |
keys: keyValues, | |
// track filtered sets | |
filter: [], | |
// toggle highlights | |
navToggle: [], | |
// track line selection | |
highlight: null | |
}; | |
d3.json('data.json', function(error, json) { | |
if (error) throw error; | |
// access data outside this callback | |
data = json; | |
// initial render chart | |
render(data, keyValues); | |
// alternative navigation | |
navAlt(data); | |
// add some filter options | |
filterFunc(); | |
}); | |
// filter sets via user interaction | |
function filterFunc() { | |
// create array values | |
_.times(keyValues.length, function(n) { | |
state.filter.push(true); | |
}); | |
d3.select('#filter').append('ul') | |
.selectAll('li') | |
.data(keyValues) | |
.enter().append('li') | |
.on('click', function (d, i) { | |
if (!state.filter[i]) { | |
// set toggle | |
state.filter[i] = true; | |
d3.select(this).style('opacity', 1); | |
// push key into array | |
state.keys.push(d); | |
// ensure array is kept in date order | |
state.keys = _.sortBy(state.keys); | |
// render chart with new keys | |
render(data, state.keys); | |
// ensure there at least two values | |
// so a slopegraph can be rendered | |
} else if (state.filter[i] && state.keys.length > 2) { | |
state.filter[i] = false; | |
d3.select(this).style('opacity', 0.3); | |
_.pull(state.keys, d); | |
state.keys = _.sortBy(state.keys); | |
render(data, state.keys); | |
} | |
}) | |
.text(function (d) { return d; }); | |
} | |
// navigation to highlight lines | |
function navAlt(data) { | |
// create array values | |
_.times(data.length, function(n) { | |
state.navToggle.push(true); | |
}); | |
d3.select('#nav-alt').append('ul') | |
.selectAll('li') | |
.data(data) | |
.enter().append('li') | |
.attr('class', function (d, i) { return 'navAlt li-' + i; }) | |
.on('click', function (d, i) { | |
if (!state.navToggle[i]) { | |
// update toggle state | |
state.navToggle[i] = true; | |
resetSelection(); | |
state.highlight = null; | |
} else if (state.navToggle[i]) { | |
state.navToggle[i] = false; | |
// hover to highlight line | |
highlightLine(i); | |
// highlight nav in relation to line | |
highlightNav(i); | |
// update state | |
state.highlight = i; | |
} | |
}) | |
.text(function (d) { return d['country']; }); | |
} | |
// render slopegraph chart | |
function render(data, keys) { | |
resetSelection(); | |
// create chart | |
slopegraph = d3.eesur.slopegraph_v2() | |
.margin({top: 20, bottom: 20, left: 100, right: 100}) | |
.gutter(25) | |
.keyName('country') | |
.keyValues(keys) | |
.on('_hover', function (d, i) { | |
// hover to highlight line | |
highlightLine(i); | |
// highlight nav in relation to line | |
highlightNav(i); | |
// update state of selected highlight line | |
state.highlight = i; | |
}); | |
// apply chart | |
d3.select('#slopegraph') | |
.datum(data) | |
.call(slopegraph); | |
// ensure highlight is maintained on update | |
if (!_.isNull(state.highlight)) { | |
d3.selectAll('.elm').style('opacity', 0.2); | |
d3.selectAll('.sel-' + state.highlight).style('opacity', 1); | |
highlightNav(state.highlight); | |
} | |
} | |
function highlightLine(i) { | |
d3.selectAll('.elm').transition().style('opacity', 0.2); | |
d3.selectAll('.sel-' + i).transition().style('opacity', 1); | |
} | |
function highlightNav(i) { | |
d3.selectAll('.navAlt').transition().style('opacity', 0.6); | |
d3.select('.li-' + i).transition().style('opacity', 1); | |
} | |
function resetSelection() { | |
d3.selectAll('.elm').transition().style('opacity', 1); | |
d3.selectAll('.navAlt').transition().style('opacity', 1); | |
} | |
// just for blocks viewer size | |
d3.select(self.frameElement).style('height', '800px'); | |
}()); | |
</script> | |
</body> | |
</html> | |
@import url(http://fonts.googleapis.com/css?family=Source+Code+Pro:400,600); | |
body { | |
position: relative; | |
color: #130C0E; | |
background-color: #fefefe; | |
padding: 5px 20px; | |
font-family: "Source Code Pro", Consolas, monaco, monospace; | |
line-height: 1.5; | |
font-weight: 400; | |
} | |
p { | |
padding-top: 0; | |
margin-top: 0; | |
font-size: 13px; | |
max-width: 600px; | |
} | |
h1 { | |
font-size: 18px; | |
font-weight: 400; | |
margin-bottom: 0; | |
} | |
#slopegraph { | |
min-height: 400px; | |
/*padding: 20px 0;*/ | |
} | |
.slope { | |
display: inline-block;; | |
width: 400px; | |
} | |
.labels { | |
font-size: 11px; | |
} | |
#nav-alt ul, #filter ul { | |
color: #130C0E; | |
font-size: 11px; | |
letter-spacing: 1px; | |
list-style: none; | |
padding: 0; | |
margin: 10px 0 10px 0; | |
} | |
#nav-alt ul li, #filter ul li { | |
display: inline-block; | |
padding: 2px 8px; | |
margin-right: 1px; | |
background: #A4CD39; | |
cursor: pointer; | |
} | |
#nav-alt ul li:hover, #filter ul li:hover { | |
background: #7AC143; | |
} | |
text.s-title { | |
fill: #7AC143; | |
letter-spacing: 2px; | |
font-size: 11px; | |
} |