twitter/clockworkraven

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

Summary

Maintainability
B
5 hrs
Test Coverage
/* Copyright 2012 Twitter, Inc. and others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// task_responses.js: Code for the bar chart visualization and data table on the
//                    evaluations results page

TaskResponses = {
    // the current set of filters applied to the chart
    filters: {},

    // Initializes the bar chart
    initChart: function() {
        TaskResponses.barChartOptions = {
            width: '100%',
            height: '100%',
            title: 'Results Summary',
            isStacked: true,
            animation: {
                duration: 1000,
                easing: 'out',
            }
        };

        TaskResponses.barChart = new google.visualization.ColumnChart(document.getElementById('bar_chart'));

        // update chart when options change
        $('input[type=radio]').click(TaskResponses.updateChart);

        // load in initial data
        TaskResponses.updateChart();
    },

    // Updates the bar chart to reflect the selected parameters
    updateChart: function() {
        // copy default options
        var options = $.extend(true, {}, TaskResponses.barChartOptions);

        // which question are we charting?
        var chartQuestionRadio = $('input[name=chart_option]:checked');
        options.hAxis = {title: chartQuestionRadio.get(0).nextSibling.nodeValue.trim()}
        var chartQuestionId = chartQuestionRadio.val();
        var chartQuestion = DATA.mcQuestions[chartQuestionId];

        // what are we segmenting by?
        var segmentQuestionId = $('input[name=segment_option]:checked').val();
        var segmentQuestion = DATA.mcQuestions[segmentQuestionId];

        if(!_.isObject(segmentQuestion)) {
            // we're not segmenting, so set segmentQuestion to chartQuestion
            // for reasonable behavior
            segmentQuestion = chartQuestion;
            segmentQuestionId = chartQuestionId;
        }

        if(segmentQuestion == chartQuestion) {
            // disable legend
            options.legend = {position: 'none'};
        }

        // what values are we displaying?
        var displayRadio = $('input[name=display_option]:checked');
        options.vAxis = {title: displayRadio.get(0).nextSibling.nodeValue.trim()}
        var display = displayRadio.val();

        // is this a change just to filtering?
        var justFilter = ((chartQuestionId == TaskResponses.oldChartQuestionId) &&
                          (segmentQuestionId == TaskResponses.oldSegmentQuestionId));

        TaskResponses.oldChartQuestionId = chartQuestionId;
        TaskResponses.oldSegmentQuestionId = segmentQuestionId;

        // update filtering
        TaskResponses.filters = {};
        $('.filter-pane').each(function(index, pane) {
            var selected = $(pane).children('input[type=radio]:checked');
            var questionId = selected.attr('name').split('_')[1];
            var value = selected.val();

            if(value != 'none') {
                TaskResponses.filters[questionId] = value;
            }
        });

        // build a table
        var header = ['chartQuestion']
        header = header.concat(_.map(segmentQuestion.options, TaskResponses.labelForOption));
        var table = [header];
        table = table.concat(_.map(chartQuestion.options, function(optionId, i) {
            var label = TaskResponses.labelForOption(optionId);
            var filtered = _.map(segmentQuestion.options, function(segOptionId) {
                return _.filter(DATA.responses, function(response) {
                    return (
                        (response.approved) &&
                        (response.mcQuestions[chartQuestionId] == optionId) &&
                        (response.mcQuestions[segmentQuestionId] == segOptionId) &&
                        _.all(TaskResponses.filters, function(value, key) {
                            // check filters
                            return (response.mcQuestions[key] == value);
                        })
                    );
                });
            });


            var values;
            if(display == 'normalized') {
                // we're normalizing the bars
                var sum = _.reduce(filtered, function(memo, item){
                    return memo + item.length;
                }, 0);

                values = _.map(filtered, function(item) {
                    return item.length / sum;
                })
            }
            else if(display == 'count'){
                // we're just displaying counts
                values = _.pluck(filtered, 'length');
            }
            else {
                // we're showing an average value
                var sum = _.reduce(filtered, function(memo, item){
                    return memo + item.length;
                }, 0);

                values = _.map(filtered, function(item) {
                    return TaskResponses.totalValueOfResponses(item, display) / sum;
                })
            }

            return [label].concat(values);
        }));

        if(!justFilter) {
            // no animations
            delete options.animation;
        }

        TaskResponses.barChart.draw(google.visualization.arrayToDataTable(table), options)

        // calculate the average value
        var hasValues = TaskResponses.hasValues(chartQuestionId)
        if(hasValues) {
            $('#average_value').text(TaskResponses.averageValue(chartQuestionId))
            $('#average_value_text').show();
        }
        else {
            $('#average_value_text').hide();
        }

    },

    // returns the label for the given option id, with the option's
    // value appended if it has one (e.g. "Tweet A was more relvant (value: -1)")
    labelForOption: function(optionId) {
        var label = DATA.mcQuestionOptions[optionId].label;

        // add value to the label if there is one
        if(DATA.mcQuestionOptions[optionId].value) {
            label += (" (value: " + DATA.mcQuestionOptions[optionId].value + ")")
        }

        return label
    },

    hasValues: function(questionId) {
        return _.any(DATA.mcQuestions[questionId].options, function(optionId) {
            return DATA.mcQuestionOptions[optionId].value;
        })
    },

    averageValue: function(questionId) {
        var filteredResponses = _.filter(DATA.responses, function(response) {
            // Only examine responses that match the current filters
            return _.all(TaskResponses.filters, function(value, key) {
                return (response.mcQuestions[key] == value);
            })
        });

        return TaskResponses.totalValueOfResponses(filteredResponses, questionId) / filteredResponses.length;
    },

    totalValueOfResponses: function(responses, questionId) {
        return _.chain(responses)
                .map(function(response) {
                    // retrieve the value of the options selected by the
                    // response
                    var option = response.mcQuestions[questionId];
                    if(option && DATA.mcQuestionOptions[option].value) {
                        return DATA.mcQuestionOptions[option].value;
                    }
                    return 0;
                })
                .reduce(function(memo, value) {
                    // sum
                    return memo + value;
                }, 0)
                .value();
    },

    // initialize the table of responses
    initDataTable: function() {
        TaskResponses.dt = $("#data_table").dataTable({
            // use jquery UI
            "bJQueryUI": true,
            // numbers, not just next/prev
            "sPaginationType": "full_numbers",
            // don't sort on actions
            "aoColumnDefs": [
                {
                    "bSortable": false,
                    "sWidth": "200px",
                    "bSearchable": false,
                    "aTargets": [ 0 ]
                }
            ],
            // header: length, search box
            // footer: page info, pagination controls
            "sDom": '<"H"lCfr>t<"F"ip>',
            // active column visibility selection on mouseover and always
            // make actions visible
            "oColVis": {
                "activate": "mouseover",
                "aiExclude": [ 0 ]
            },
            // save state across page loads
            "bStateSave": true,
            // disable auto-width
            "bAutoWidth": true
        });
    },

    // initalize buttons for approve/reject, ban, trust, etc.
    initButtons: function() {
        // approval buttons

        $('.btn-approval').bind('ajax:before', function() {
            console.log('starting approval');
            $(this).parents('.approval-controls').fadeOut(200).delay(200).siblings('.approval-spinner').fadeIn();
        }).bind('ajax:complete', function(evt, data) {
            console.log("approval success", evt, data);
            // switch to success
            var status = JSON.parse(data.responseText).status;

            // replace spinner
            $(this).parents('.approval-controls').siblings('.approval-spinner').stop().clearQueue().fadeOut(200, function() {
                $(this).siblings('.approval-controls').text(status).fadeIn();
            });

            // replace approval cell
            var pos = TaskResponses.dt.fnGetPosition($(this).parents('td').siblings('td.approval').get(0));
            TaskResponses.dt.fnUpdate(status, pos[0], pos[1]);

            // update chart data
            if(status == 'Rejected') {
                delete DATA.responses[$(this).attr("data-response")]
                TaskResponses.updateChart();
            }
        }).bind('ajax:error', function() {
            console.log('approval error')
            // switch to failure
            $(this).parents('.approval-controls').siblings('.approval-spinner').stop().clearQueue().fadeOut(200, function() {
                $(this).siblings('.approval-controls').text("Operation Failed").fadeIn();
            });
        });

        // banning and trusting buttons
        TaskResponses.setupButtons('Ban/Unban', '.btn-ban', '.btn-unban',
                                   '.ban-controls', '.ban-spinner', 'Banned');

        TaskResponses.setupButtons('Trust/Untrust', '.btn-trust', '.btn-untrust',
                                   '.trust-controls', '.trust-spinner', 'Trusted');
    },

    // helper function to set up the trust/untrust buttons and the ban/unban buttons
    //
    // name: name for logging purposes
    // positive selector: selector for buttons that activate a state (e.g. .trust, .ban)
    // negative selector: selector for buttons that deactivate a state (e.g. .untrust, .unban)
    // controls selector: selector for wrapper around the buttons (e.g. .trust-wrapper)
    // spinner selector: selector for spinner GIF (e.g. .trust-spinner)
    // positive response: server response for a positive action (e.g. "Trusted", "Banned")
    setupButtons: function(name, posSelector, negSelector, controlsSelector, spinnerSelector, positiveResponse) {
        $(posSelector).add(negSelector).bind('ajax:before', function() {
            // fade out the controls and fade in a spinner while we're loading
            // the response
            console.log(name, 'starting request');
            $(this).parents(controlsSelector).fadeOut(200).delay(200).siblings(spinnerSelector).fadeIn(200);
        }).bind('ajax:complete', function(evt, data) {
            // we got a successful response
            console.log(name, 'got a success response');

            // get the response to determine whether the action was positive
            // (e.g. "ban") or negative (e.g. "unban")
            var status = JSON.parse(data.responseText).status;

            // replace the spinner with the new action button. Clear the
            // spinner's animation queue in case it's still fading in.
            $(this).parents(controlsSelector).siblings(spinnerSelector).stop().clearQueue().fadeOut(200, function() {
                // find all rows that belong to this user and fade out the controls
                userid = $(this).parents('tr').children(".mturk-user").text();
                TaskResponses.dt.$('tr:contains(' + userid + ')').find(controlsSelector).fadeOut(function() {
                    if (status == positiveResponse) {
                        // we serviced a request for a positive action. Hide the
                        // positve action button and show the negative action
                        // button.
                        $(this).children(posSelector).hide();
                        $(this).children(negSelector).css('display', 'block');
                    }
                    else {
                        // other way around
                        $(this).children(posSelector).css('display', 'block');
                        $(this).children(negSelector).hide();
                    }

                    // fade the controls back in
                    $(this).fadeIn();
                });
            })
        }).bind('ajax:error', function() {
            console.log(name, 'got an error response');

            // switch to failure
            $(this).parents(controlsSelector).siblings(spinnerSelector).stop().clearQueue().fadeOut(200, function() {
                $(this).siblings(controlsSelector).text("Operation Failed").fadeIn();
            });
        });
    }
}

google.load('visualization', '1.0', {'packages':['corechart']});
google.setOnLoadCallback(function() {
    if ($('body').attr('class') != "task_responses index") {
        // we're not on the task responses index page; abort
        return;
    }

    // initialize everything
    TaskResponses.initButtons();
    TaskResponses.initChart();
    TaskResponses.initDataTable();
});