Skip to content

Instantly share code, notes, and snippets.

@jeremycflin
Created December 19, 2017 20:37
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 jeremycflin/b4c5797b9dc4abdbad05d982fa59f411 to your computer and use it in GitHub Desktop.
Save jeremycflin/b4c5797b9dc4abdbad05d982fa59f411 to your computer and use it in GitHub Desktop.
d3 | reusable slopegraph v2
license: mit
// *****************************************
// 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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment