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; | |
| } |