block by renecnielsen 99ee17e86c184a616c7c

d3 | reusable slopegraph v2

Full Screen

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.

data source

forked from eesur‘s block: d3 | reusable slopegraph v2

index.html

<!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>



d3_code_slopegraph_v2.js

// *****************************************
//  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;

    };

}());

data.json

[
  {
    "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"
  }
]

main.css

@import url(http://fonts.googleapis.com/css?family=Oswald:300,400,700);

body {
  position: relative;
  color: #130C0E;
  background-color: #fefefe;
  padding: 5px 20px;
  font-family: Oswald, Consolas, monaco, monospace;
  line-height: 1.5;
  font-weight: 300;
}
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: #FFFFFF;
    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: #00aeef;
    cursor: pointer;
}
#nav-alt ul li:hover, #filter ul li:hover {
    background: #00447c;
}
text.s-title {
    fill: #00aeef;
    letter-spacing: 2px;
    font-size: 11px;
}