block by Golodhros dfe7c0c8be07a461e6ba

TDD D3 Template

Full Screen

Gist to serve as template for future TDD D3 blocks

index.html

<!DOCTYPE html>
<head>
    <meta charset="utf-8">
    <link type="text/css" rel="stylesheet" href="style.css"/>
</head>
<body>
    <h2 class="block-title">TDD Template</h2>
    <div class="graph"></div>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
    <script src="//d3js.org/d3.v3.min.js"></script>
    <script src="dataManager.js"></script>
    <script src="src.js"></script>
    <script type="text/javascript">
        // Code that instantiates the graph and uses the data manager to load the data
        var app = {
            // D3 Reusable API Chart
            graph: {

                dataManager: null,

                config: {
                    margin : {
                        top   : 20,
                        bottom: 30,
                        right : 20,
                        left  : 40
                    },
                    aspectWidth: 13,
                    aspectHeight: 4,
                    animation: 'linear',
                    dataURL: 'data.tsv'
                },

                init: function(ele){
                    this.$el = ele;
                    this.requestNewData();
                    this.addEvents();
                },

                addEvents: function(){
                    //Callback triggered by browser
                    window.onresize = $.proxy(this.drawGraph, this);
                },

                calculateRatioHeight: function(width) {
                    var config = this.config;

                    return Math.ceil((width * config.aspectHeight) / config.aspectWidth);
                },

                dataCleaningFunction: function(d){
                    d.frequency = +d.frequency;
                    d.letter = d.letter;
                },

                drawGraph: function(){
                    var config = this.config,
                        width  = this.$el.width(),
                        height = this.calculateRatioHeight(width);

                    this.resetGraph();
                    this.chart = graphs.chart()
                        .width(width).height(height).margin(config.margin);

                    this.container = d3.select(this.$el[0])
                        .datum(this.data)
                        .call(this.chart);
                },

                handleReceivedData: function(result){
                    this.data = result;
                    this.drawGraph();
                },

                requestNewData: function(el){
                    this.dataManager = graphs.dataManager();
                    this.dataManager.on('dataError', function(errorMsg){
                        console.log('error:', errorMsg);
                    });
                    this.dataManager.on('dataReady', $.proxy(this.handleReceivedData, this));
                    this.dataManager.loadTsvData(this.config.dataURL, this.dataCleaningFunction);
                },

                resetGraph: function(){
                    this.$el.find('svg').remove();
                }
            }
        };

        $(function(){
            app.graph.init($('.graph'));
        });
    </script>
</body>

dataManager.js

var graphs = graphs || {};

graphs.dataManager = function module() {
    var exports = {},
        dispatch = d3.dispatch('dataReady', 'dataLoading', 'dataError'),
        data;

    d3.rebind(exports, dispatch, 'on');

    exports.loadJsonData = function(_file, _cleaningFn) {
        var loadJson = d3.json(_file);

        loadJson.on('progress', function(){
            dispatch.dataLoading(d3.event.loaded);
        });

        loadJson.get(function (_err, _response){
            if(!_err){
                _response.data.forEach(function(d){
                    _cleaningFn(d);
                });
                data = _response.data;
                dispatch.dataReady(_response.data);
            }else{
                dispatch.dataError(_err.statusText);
            }
        });
    };

    exports.loadTsvData = function(_file, _cleaningFn) {
        var loadTsv = d3.tsv(_file);

        loadTsv.on('progress', function() {
            dispatch.dataLoading(d3.event.loaded);
        });

        loadTsv.get(function (_err, _response) {
            if(!_err){
                _response.forEach(function(d){
                    _cleaningFn(d);
                });
                data = _response;
                dispatch.dataReady(_response);
            }else{
                dispatch.dataError(_err.statusText);
            }
        });
    };

    // If we need more types of data geoJSON, csv, etc. we will need
    // to create methods for them

    exports.getCleanedData = function(){
        return data;
    };

    return exports;
};

src.js

var graphs = graphs || {};

graphs.chart = function module(){

    var margin = {top: 20, right: 20, bottom: 30, left: 40},
        width = 960,
        height = 500,
        data,
        chartW, chartH,
        xScale, yScale,
        xAxis, yAxis;

    var svg;

    function buildContainerGroups(){
        var container = svg.append("g").classed("container-group", true);

        container.append("g").classed("chart-group", true);
        container.append("g").classed("x-axis-group", true);
        container.append("g").classed("y-axis-group", true);
    }

    function buildScales(){
        xScale = d3.scale.ordinal()
            .domain(data.map(function(d) { return d.letter; }))
            .rangeRoundBands([0, chartW], 0.1);

        yScale = d3.scale.linear()
            .domain([0, d3.max(data, function(d) { return d.frequency; })])
            .range([chartH, 0]);
    }

    function buildAxis(){
        xAxis = d3.svg.axis()
            .scale(xScale)
            .orient("bottom");

        yAxis = d3.svg.axis()
            .scale(yScale)
            .orient("left")
            .ticks(10, "%");
    }

    function drawAxis(){
        svg.select('.x-axis-group')
            .append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + chartH + ")")
            .call(xAxis);

        svg.select(".y-axis-group")
            .append("g")
            .attr("class", "y axis")
            .call(yAxis)
          .append("text")
            .attr("transform", "rotate(-90)")
            .attr("y", 6)
            .attr("dy", ".71em")
            .style("text-anchor", "end")
            .text("Frequency");
    }

    function drawBars(){
        // Setup the enter, exit and update of the actual bars in the chart.
        // Select the bars, and bind the data to the .bar elements.
        var bars = svg.select('.chart-group').selectAll(".bar")
            .data(data);

        // If there aren't any bars create them
        bars.enter().append('rect')
            .attr("class", "bar")
            .attr("x", function(d) { return xScale(d.letter); })
            .attr("width", xScale.rangeBand())
            .attr("y", function(d) { return yScale(d.frequency); })
            .attr("height", function(d) { return chartH - yScale(d.frequency); });
    }

    function exports(_selection){
        _selection.each(function(_data){
            chartW = width - margin.left - margin.right;
            chartH = height - margin.top - margin.bottom;
            data = _data;

            buildScales();
            buildAxis();

            if (!svg) {
                svg = d3.select(this)
                    .append('svg')
                    .classed('bar-chart', true);
            }
            svg.attr({
                width: width + margin.left + margin.right,
                height: height + margin.top + margin.bottom
            });

            buildContainerGroups();
            drawBars();
            drawAxis();
        });
    }

    exports.margin = function(_x) {
        if (!arguments.length) return margin;
        margin = _x;
        return this;
    };

    exports.width = function(_x) {
        if (!arguments.length) return width;
        width = _x;
        return this;
    };

    exports.height = function(_x) {
        if (!arguments.length) return height;
        height = _x;
        return this;
    };

    return exports;
};

src.spec.js

// Simple tests for the bar chart
describe('Reusable barChart Test Suite', function() {
    var barChart, dataset, containerFixture, f;

    beforeEach(function() {
        dataset = [
            {
                letter: 'A',
                frequency: .08167
            },
            {
                letter: 'B',
                frequency: .01492
            },
            {
                letter: 'C',
                frequency: .02782
            },
            {
                letter: 'D',
                frequency: .04253
            },
            {
                letter: 'E',
                frequency: .12702
            },
            {
                letter: 'F',
                frequency: .02288
            },
            {
                letter: 'G',
                frequency: .02015
            },
            {
                letter: 'H',
                frequency: .06094
            },
            {
                letter: 'I',
                frequency: .06966
            },
            {
                letter: 'J',
                frequency: .00153
            },
            {
                letter: 'K',
                frequency: .00772
            },
            {
                letter: 'L',
                frequency: .04025
            },
            {
                letter: 'M',
                frequency: .02406
            },
            {
                letter: 'N',
                frequency: .06749
            },
            {
                letter: 'O',
                frequency: .07507
            },
            {
                letter: 'P',
                frequency: .01929
            },
            {
                letter: 'Q',
                frequency: .00095
            },
            {
                letter: 'R',
                frequency: .05987
            },
            {
                letter: 'S',
                frequency: .06327
            },
            {
                letter: 'T',
                frequency: .09056
            },
            {
                letter: 'U',
                frequency: .02758
            },
            {
                letter: 'V',
                frequency: .00978
            },
            {
                letter: 'W',
                frequency: .02360
            },
            {
                letter: 'X',
                frequency: .00150
            },
            {
                letter: 'Y',
                frequency: .01974
            },
            {
                letter: 'Z',
                frequency: .00074
            }
        ];
        barChart = graphs.barChart();
        $('body').append($('<div class="test-container"></div>'));

        containerFixture = d3.select('.test-container');
        containerFixture.datum(dataset).call(barChart);
    });

    afterEach(function() {
        containerFixture.remove();
    });

    it('should render a chart with minimal requirements', function() {
        expect(containerFixture.select('.chart')).toBeDefined(1);
    });

    it('should render container, axis and chart groups', function() {
        expect(containerFixture.select('g.container-group')[0][0]).not.toBeNull();
        expect(containerFixture.select('g.chart-group')[0][0]).not.toBeNull();
        expect(containerFixture.select('g.x-axis-group')[0][0]).not.toBeNull();
        expect(containerFixture.select('g.y-axis-group')[0][0]).not.toBeNull();
    });

    it('should render an X and Y axis', function() {
        expect(containerFixture.select('.x.axis')[0][0]).not.toBeNull();
        expect(containerFixture.select('.y.axis')[0][0]).not.toBeNull();
    });

    it('should render a bar for each data entry', function() {
        var numBars = dataset.length;

        expect(containerFixture.selectAll('.bar')[0].length).toEqual(numBars);
    });

    it('should provide margin getter and setter', function() {
        var defaultMargin = barChart.margin(),
            testMargin = {top: 4, right: 4, bottom: 4, left: 4},
            newMargin;

        barChart.margin(testMargin);
        newMargin = barChart.margin();

        expect(defaultMargin).not.toBe(testMargin);
        expect(newMargin).toBe(testMargin);
    });

    it('should provide width getter and setter', function() {
        var defaultWidth = barChart.width(),
            testWidth = 200,
            newWidth;

        barChart.width(testWidth);
        newWidth = barChart.width();

        expect(defaultWidth).not.toBe(testWidth);
        expect(newWidth).toBe(testWidth);
    });

    it('should provide height getter and setter', function() {
        var defaultHeight = barChart.height(),
            testHeight = 200,
            newHeight;

        barChart.height(testHeight);
        newHeight = barChart.height();

        expect(defaultHeight).not.toBe(testHeight);
        expect(newHeight).toBe(testHeight);
    });

});

style.css

@import url("//fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,700italic,400,300,700");

body {
  font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Helvetica, Arial, sans-serif;
}

.block-title {
  color: #222;
  font-size: 44px;
  font-style: normal;
  font-weight: 300;
  text-rendering: optimizelegibility;
}

test_runner.html

<!DOCTYPE HTML>
<html lang="en-US">
<head>
    <meta charset="UTF-8">
    <title>Jasmine Spec Runner</title>
    <link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.0/jasmine.css">
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.0/jasmine.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.0/jasmine-html.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.0/boot.js"></script>

    <!-- Favicon -->
    <link rel="shortcut icon" type="image/png" href="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.0.0/jasmine_favicon.png" />
    <!-- End Favicon -->

    <!-- source files... -->
    <script src="//d3js.org/d3.v3.min.js"></script>
    <script src="dataManager.js"></script>
    <script src="src.js"></script>

    <!-- spec files... -->
    <script src="src.spec.js"></script>
</head>
<body>

</body>
</html>