ministryofjustice/prison-visits

View on GitHub
app/assets/javascripts/metrics.js

Summary

Maintainability
D
2 days
Test Coverage
/*jslint indent: 4, maxstatements: 27, unused: false */
/*global d3 */
//= require d3.chart.bubble-matrix

'use strict';

var  margin = {top: 10, right: 100, bottom: 100, left: 100};


function percentile(array, n) {
    array.sort(function(a, b) {
        return a - b;
    });
    return array[parseInt(0.01 * n * array.length)];
}

function formatSeconds(s) {
    var output = [],
        d, h, m;

    if (s === 0) {
        return '';
    }

    d = s / (24 * 3600);
    s -= parseInt(d) * 24 * 3600;
    if (d > 1) {
        output = output.concat([d.toPrecision(2), 'days']);
    } else {
        h = parseInt(s / 3600);
        s -= h * 3600;
        if (h > 1) {
            output = output.concat([h, 'hrs']);
        } else {
            m = parseInt(s / 60);
            s -= m * 60;
            if (m > 1) {
                output = output.concat([m, 'mins']);
            } else {
                output = output.concat([s, 'secs']);
            }
        }
    }
    return output.join(' ');
}

function formatDate(date) {
    var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
    return [date.getDate(), months[date.getMonth()]].join(' ');
}
function displayHistogram(where, dataSource, displayLines) {
    var width, height, x, y, n, data, maxY, svg, bars, xAxis, medianValue, percentileValue;

    width = 960 - margin.left - margin.right;
    height = 500 - margin.top - margin.bottom;

    n = dataSource.length;

    x = d3.scale.linear().domain([0, d3.max(dataSource)]).range([0, width]);
    data = d3.layout.histogram().bins(40)(dataSource);
    maxY = d3.max(data, function(d) { return d.y; });
    y = d3.scale.linear().domain([0, maxY]).range([height, 0]);
    svg = d3.select(where).append('svg')
        .attr('width', width + margin.left + margin.right)
        .attr('height', height + margin.top + margin.bottom)
        .append('g')
        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

    bars = svg.selectAll('.bar').data(data).enter().append('g').attr('class', 'bar').attr('transform', function(d) {
        return 'translate(' + x(d.x) + ',' + y(d.y) + ')';
    });
    bars.append('rect').attr('x', 1).attr('width', x(data[0].dx)).attr('height', function(d) { return height - y(d.y); });
    bars.append('text')
        .attr('x', x(data[0].dx / 2))
        .attr('y', -3)
        .attr('font-size', 11)
        .attr('text-anchor', 'middle')
        .text(function(d) { var v = d.y; if (v > 0) { return v; } });
    xAxis = d3.svg.axis().scale(x).orient('bottom').tickFormat(formatSeconds);
    svg.append('g')
        .attr('class', 'x axis')
        .attr('transform', 'translate(0,' + height + ')')
        .call(xAxis);

    if (n === 0) {
        svg.append('text')
            .attr('x', 0)
            .attr('y', 0)
            .attr('class', 'no-data-label')
            .attr('transform', 'translate(' + (width - 100) / 2 + ',' + height / 2 + ')')
            .text('No data to display');
        return;
    }

    if (displayLines) {
        medianValue = percentile(dataSource, 50);
        percentileValue = percentile(dataSource, 95);

        svg.append('line')
            .attr('x1', x(3 * 24 * 3600))
            .attr('x2', x(3 * 24 * 3600))
            .attr('y1', y(0))
            .attr('y2', y(maxY))
            .attr('class', 'three-days');
        svg.append('line')
            .attr('x1', x(percentileValue))
            .attr('x2', x(percentileValue))
            .attr('y1', y(0))
            .attr('y2', y(maxY))
            .attr('class', 'percentile');
        svg.append('line')
            .attr('x1', x(medianValue))
            .attr('x2', x(medianValue))
            .attr('y1', y(0))
            .attr('y2', y(maxY))
            .attr('class', 'median');
        svg.append('text')
            .attr('x', 0)
            .attr('y', 0)
            .attr('class', 'three-days-label')
            .attr('transform', 'translate(' + (x(3 * 24 * 3600) + 4) + ',' + y(maxY) + '),rotate(90)')
            .text('three days');
        svg.append('text')
            .attr('x', 0)
            .attr('y', 0)
            .attr('class', 'percentile-label')
            .attr('transform', 'translate(' + (x(percentileValue) + 4) + ',' + y(maxY) + '),rotate(90)')
            .text('95-th percentile');
        svg.append('text')
            .attr('x', 0)
            .attr('y', 0)
            .attr('class', 'median-label')
            .attr('transform', 'translate(' + (x(medianValue) + 4) + ',' + y(maxY) + '),rotate(90)')
            .text('median');
    }
}

function displayWeeklyBreakdown(where, rawDataSource) {
    var weekdays, processedData, maxZ, chart, i, z;

    weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
    processedData = { columns: [], rows: [] };

    for (i = 0; i < 24; i++) { processedData.columns.push(i); }

    maxZ = 0;
    rawDataSource.forEach(function(row) {
        var max = d3.max(row);
        if (max > maxZ) { maxZ = max; }
    });

    z = d3.scale.linear().domain([0, maxZ]).range([0, 1]);

    rawDataSource.forEach(function(row, i) {
        processedData.rows.push({name: weekdays[i], values: row.map(function(f) { return [z(f)]; })});
    });

    chart = d3.select(where).append('svg')
        .chart('BubbleMatrix')
        .width(960)
        .height(300);

    chart.draw(processedData);
}

function thisYear() {
    return parseInt(d3.select('#year').html());
}

function displayPerformanceLineChart(where, percentile95data, percentile50data, volumeData) {
    var xRange = [new Date(thisYear() - 1, 11, 29), new Date(thisYear() + 1, 0, 1)];
    var y1Range = [0, 1.3 * d3.max(percentile95data, function(d) { return parseInt(d.y); })];
    var y2Range = [0, 1.3 * d3.max(volumeData, function(d) { return parseInt(d.y); })];

    var width = 960 - margin.left - margin.right;
    var height = 500 - margin.top - margin.bottom;

    var x = d3.time.scale().domain(xRange).range([0, width]);
    var y1 = d3.scale.linear().domain(y1Range).range([height, 0]);
    var y2 = d3.scale.linear().domain(y2Range).range([height, 0]);

    var xAxis = d3.svg.axis().scale(x).orient('bottom').tickFormat(formatDate);
    var y1Axis = d3.svg.axis().scale(y1).orient('left').tickFormat(formatSeconds);
    var y2Axis = d3.svg.axis().scale(y2).orient('right');

    var svg = d3.select(where)
        .append('svg')
        .attr('width', width + margin.left + margin.right)
        .attr('height', height + margin.top + margin.bottom)
        .append('g');

    svg.append('g')
        .attr('class', 'x axis')
        .attr('transform', 'translate(' + margin.left + ',' + (height + margin.top) + ')')
        .call(xAxis).selectAll('text')
        .style('text-anchor', 'end')
        .attr('dx', '-.8em')
        .attr('dy', '.15em')
        .attr('transform', function(d) {
            return 'rotate(-45)'
        });

    svg.append('g')
        .attr('class', 'y axis')
        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
        .call(y1Axis);

    svg.append('g')
        .attr('class', 'y axis')
        .attr('transform', 'translate(' + (margin.left + width) + ',' + margin.top + ')')
        .call(y2Axis);

    svg.selectAll('bar')
        .data(volumeData)
        .enter().append('rect')
        .style('fill', 'pink')
        .attr('x', function(d) { return x(new Date(d.x)); })
        .attr('y', function(d) { return y2(parseInt(d.y)); })
        .attr('width', 20)
        .attr('height', function(d) { return height - y2(d.y); })
        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

    var seriesData = [
        { label: 'time to process (95th percentile)',
          color: 'steelblue' },
        { label: 'time to process (median)',
          color: 'red' },
        { label: 'requested visit volume',
          color: 'pink' }
    ];

    [percentile95data, percentile50data].forEach(function(data, i) {
        var line = d3.svg.line()
            .x(function(d) { return x(new Date(d.x)) })
            .y(function(d) { return y1(parseInt(d.y)) })
        svg.append('path')
            .datum(data)
            .attr('class', 'line')
            .style({stroke: seriesData[i].color})
            .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
            .attr('d', line);
    });

    var legend = svg.append('g')
        .attr('transform', 'translate(' + (10 + margin.left) + ',' + (10 + margin.top) + ')');

    legend.selectAll('rect')
        .data(seriesData)
        .enter()
        .append('rect')
        .attr('fill', function(d) { return d.color; })
        .attr('width', 20)
        .attr('height', 20)
        .attr('transform', function(d, i) { return 'translate(0,' + (i * 30) + ')'; })

    legend.selectAll('text')
        .data(seriesData)
        .enter()
        .append('text')
        .attr('transform', function(d, i) { return 'translate(30,' + (15 + i * 30) + ')'; })
        .text(function(d) { return d.label; })

}