Source: analytics.js

/*
Copyright (C) 2016
Developed at University of Toronto

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

const users = require('./users.js');
const questions = require('./questions.js');
const logger = require('./log.js');
const common = require('./common.js');
const db = require('./db.js');

const classId = 'class';
const analyticsTimeInterval = 86400000; // 24 * 60 * 60 * 1000

var totalStudentsCount = 0;

/**
 * start the analytics daily process
 *
 * @param {function} callback
 */
exports.initialize = function(callback) {
    if (process.env.DEBUG) {
        getAnalytics(callback);
    } else {
        var d = new Date();
        var h = d.getHours();
        var m = d.getMinutes();
        var s = d.getSeconds();
        var secondsTilMidNight = ((24 * 60 * 60) - (h * 60 * 60) - (m * 60) - s) * 1000;

        setTimeout(function () {
            getAnalytics(callback);
            setInterval(function () {
                getAnalytics(callback);
            }, analyticsTimeInterval);
        }, secondsTilMidNight);

        users.getStudentsList(function (err, studentsList) {
            if (err) {
                logger.error(JSON.stringify(err));
            }
            totalStudentsCount = studentsList.length;
        });
    }
}

/**
 * get the charts based on type
 *
 * @param {object} query
 * @param {function} callback
 */
exports.getChart = function(query, callback) {
    switch(query.type) {
        case 'QuestionsAnsweredVsClass':
            return getQuestionsAnsweredVsClass(query, callback);
        case 'overallVsClass':
            return getOverallVsClass(query, callback);
        case 'pointsPerAttemptVsClass':
            return getPointsPerAttemptVsClass(query, callback);
        case 'AccuracyVsClass':
            return getAccuracyVsClass(query, callback);
        case 'PointsVsClass':
            return getPointsVsClass(query, callback);
        case 'RatingVsClass':
            return getRatingVsClass(query, callback);
        case 'classAnswered':
            return getClassAnswered(query, callback);
        case 'classPointsPerAttempt':
            return getClassPointsPerAttempt(query, callback);
        case 'classOverall':
            return getClassOverall(query, callback);
        case 'classAnsweredOverTime':
            return getClassAnsweredOverTime(query, callback);
        case 'classPointsPerAttemptOverTime':
            return getClassPointsPerAttemptOverTime(query, callback);
        case 'classOverallOverTime':
            return getClassOverallOverTime(query, callback);
        case 'classAccuracyOverTime':
            return getClassAccuracyOverTime(query, callback);
        case 'classPointsOverTime':
            return getClassPointsOverTime(query, callback);
        case 'classAccuracy':
            return getClassAccuracy(query, callback);
        case 'classPoints':
            return getClassPoints(query, callback);
        case 'classRating':
            return getClassRating(query, callback);
        case 'correctAttemptsOverTime':
            return getCorrectAttemptsOverTime(query, callback);
        case 'accuracyOverTime':
            return getAccuracyOverTime(query, callback);
        case 'pointsOverTime':
            return getPointsOverTime(query, callback);
        case 'overallRankOverTime':
            return getOverallRankOverTime(query, callback);
        case 'pointsRankOverTime':
            return getPointsRankOverTime(query, callback);
        case 'attemptRankOverTime':
            return getAttemptRankOverTime(query, callback);
        case 'correctAttemptRankOverTime':
            return getCorrectAttemptRankOverTime(query, callback);
        case 'accuracyRankOverTime':
            return getAccuracyRankOverTime(query, callback);
        case 'pointsPerAttemptsOverTime':
            return getPointsPerAttemptsOverTime(query, callback);
        case 'overallOverTime':
            return getOverallOverTime(query, callback);
        case 'pointsPerTopicVsClass':
            return getPointsPerTopicVsClass(query, callback);
        case 'accuracyPerTopicVsClass':
            return getAccuracyPerTopicVsClass(query, callback);
        case 'pointsPerTypeVsClass':
            return getPointsPerTypeVsClass(query, callback);
        case 'accuracyPerTypeVsClass':
            return getAccuracyPerTypeVsClass(query, callback);
        case 'classPointsPerTopicVsClass':
            return getClassPointsPerTopicVsClass(query, callback);
        case 'classAccuracyPerTopicVsClass':
            return getClassAccuracyPerTopicVsClass(query, callback);
        case 'classPointsPerTypeVsClass':
            return getClassPointsPerTypeVsClass(query, callback);
        case 'classAccuracyPerTypeVsClass':
            return getClassAccuracyPerTypeVsClass(query, callback);
        case 'classRatingPerTopicVsClass':
            return getClassRatingPerTopicVsClass(query, callback);
        case 'classRatingPerTypeVsClass':
            return getClassRatingPerTypeVsClass(query, callback);
        default:
            return callback('notFound', null);
    }
}

/**
 * get analytics for a admins
 *
 * @param {function} callback
 */
var getAnalytics = function(callback) {
    var studentsCount = 0;
    var classObject = {
        correctAttemptsCount: 0,
        wrongAttemptsCount: 0,
        totalAttemptsCount: 0,
        points: 0,
        accuracy: 0
    };
    var currentDate = common.getDateFormatedMinutesAgo('YYYY-MM-DD', 1);

    users.getFullLeaderboard(function (err, leaderboardList) {
        if (err) {
            logger.error(JSON.stringify(err));
            return callback(err, null);
        }

        if (!leaderboardList || leaderboardList.length === 0) {
            logger.log('Finished updating analytics. Status: warning. (No students available/active)');
            return callback(null, 'ok');
        }

        var classCorrectAttemptsCount = 0;
        var classWrongAttemptsCount = 0;
        var classTotalAttemptsCount = 0;
        var classPoints = 0;

        var getRanks = function (query) {
            var objList = {};
            var list = sortLeaderBoard(leaderboardList, query);
            var rank = 0;
            var score = -1;
            var repeated = 1;
            for (var i = 0; i < list.length; i++) {
                if (score !== list[i][query]) {
                    score = list[i][query];
                    rank += repeated;
                    repeated = 1;
                } else {
                    repeated ++;
                }
                objList[list[i]._id] = rank;
            }
            return objList;
        }

        var overallList = getRanks('overall');
        var attemptList = getRanks('attempt');
        var accuracyList = getRanks('accuracy');
        var pointsList = getRanks('points');
        var correctAttemptsList = getRanks('correctAttemptsCount');

        var overallSum = common.sumListOfNumbers(common.getIdsListFromJSONList(sortLeaderBoard(leaderboardList, 'overall'), 'overall'));
        var attemptSum = common.sumListOfNumbers(common.getIdsListFromJSONList(sortLeaderBoard(leaderboardList, 'attempt'), 'attempt'));
        var accuracySum = common.sumListOfNumbers(common.getIdsListFromJSONList(sortLeaderBoard(leaderboardList, 'accuracy'), 'accuracy'));
        var pointsSum = common.sumListOfNumbers(common.getIdsListFromJSONList(sortLeaderBoard(leaderboardList, 'points'), 'points'));
        var correctAttemptsSum = common.sumListOfNumbers(common.getIdsListFromJSONList(sortLeaderBoard(leaderboardList, 'correctAttemptsCount'), 'correctAttemptsCount'));

        users.getStudentsList(function (err, studentsList) {
            if (err) {
                logger.error(JSON.stringify(err));
                return callback(err, null);
            }

            totalStudentsCount = studentsList.length;
            for (var i in studentsList) {
                var student = studentsList[i];
                var row = {};

                row.correctAttemptsCount = student.correctAttemptsCount;
                row.wrongAttemptsCount = student.wrongAttemptsCount;
                row.totalAttemptsCount = student.totalAttemptsCount;
                row.points = student.points;
                row.accuracy = student.totalAttemptsCount === 0 ? 0 : ((student.correctAttemptsCount / student.totalAttemptsCount) * 100).toFixed(2);

                row.overallValue = leaderboardList.find((element) => {return element._id === student._id}).overall;
                row.pointsPerAttemptValue = leaderboardList.find((element) => {return element._id === student._id}).attempt;
                row.overallRank = overallList[student._id];
                row.pointsPerAttemptRank = attemptList[student._id];
                row.accuracyRank = accuracyList[student._id];
                row.pointsRank = pointsList[student._id];
                row.correctAttemptsRank = correctAttemptsList[student._id];

                classCorrectAttemptsCount += student.correctAttemptsCount;
                classWrongAttemptsCount += student.wrongAttemptsCount;
                classTotalAttemptsCount += student.totalAttemptsCount;
                classPoints += student.points;

                addStudentAnalyticsWithDate(
                    student._id,
                    currentDate,
                    row,
                    function (err, result) {
                        if (err) {
                            logger.error(JSON.stringify(err));
                            return callback(err, null);
                        }

                        studentsCount++;
                        if (studentsCount === studentsList.length) {
                            classObject.points = studentsCount === 0 ? 0 :  (classPoints / studentsCount).toFixed(0);
                            classObject.accuracy = classCorrectAttemptsCount === 0 ? 0 :  ((classCorrectAttemptsCount / classTotalAttemptsCount) * 100).toFixed(2);

                            classObject.correctAttemptsCount = studentsCount === 0 ? 0 :  (classCorrectAttemptsCount / studentsCount).toFixed(0);
                            classObject.wrongAttemptsCount = studentsCount === 0 ? 0 :  (classWrongAttemptsCount / studentsCount).toFixed(0);
                            classObject.totalAttemptsCount = studentsCount === 0 ? 0 :  (classTotalAttemptsCount / studentsCount).toFixed(0);

                            classObject.overallAverage = studentsCount === 0 ? 0 :  (overallSum / studentsCount).toFixed(2);
                            classObject.pointsPerAttemptAverage = studentsCount === 0 ? 0 :  (attemptSum / studentsCount).toFixed(2);
                            classObject.accuracyAverage = studentsCount === 0 ? 0 :  (accuracySum / studentsCount).toFixed(2);
                            classObject.pointsAverage = studentsCount === 0 ? 0 :  (pointsSum / studentsCount).toFixed(2);
                            classObject.correctAttemptsAverage = studentsCount === 0 ? 0 :  (correctAttemptsSum / studentsCount).toFixed(2);

                            addStudentAnalyticsWithDate(
                                classId,
                                currentDate,
                                classObject,
                                function (err, result) {
                                    if (err) {
                                        logger.error(JSON.stringify(err));
                                        return callback(err, null);
                                    }

                                    logger.log('Finished updating analytics. Status: ok.');
                                    return callback(null, 'ok');
                                }
                            );
                        }
                    }
                );
            }
        });
    });
}


/**
 * sort leaderboard based on criteria
 *
 * @param {list} list
 * @param {string} criteria
 */
var sortLeaderBoard = function(list, criteria) {
    return list.sort(function(student1,student2){
        return student2[criteria] - student1[criteria];
    });
}

/**
 * clean all previous analytics data
 *
 * @param {function} callback
 */
exports.removeAnalytics = function (callback) {
    db.removeAnalytics(callback);
}

/**
 * add student analytics
 * if there are no records of the student, create a new record
 * if there are recards of the student, get the last recard and compute the deltas
 *
 * @param {string} studentId
 * @param {string} date
 * @param {object} info
 * @param {function} callback
 */
var addStudentAnalyticsWithDate = function (studentId, date, info, callback) {
    db.addStudentAnalyticsWithDate(studentId, date, info, callback);
}

/**
 * get the list of data on the user by date
 *
 * @param {object} findQuery
 * @param {function} callback
 */
var getTimeBasedAnalytics = function (findQuery, callback) {
    db.getTimeBasedAnalytics(findQuery, callback);
}

/**
 * get questions answered vs class
 *
 * @param {object} query
 * @param {function} callback
 */
var getQuestionsAnsweredVsClass = function(query, callback) {
    users.getStudentsList(function(err, students) {
        if (err) {
            return callback(err, null);
        }

        if (!students) {
            return  callback('no results', null);
        }

        var studentId = query.userId;
        var studentAnswered = 0;
        var classAnswered = 0;
        var classCount = 0;
        var classAnsweredAverage = 0;

        for (i in students) {
            if (students[i]._id === studentId) {
                studentAnswered = students[i].correctAttemptsCount;
            } else {
                classAnswered += students[i].correctAttemptsCount;
                classCount++;
            }
        }

        classAnsweredAverage = classCount === 0 ? 0 : Math.ceil(classAnswered / classCount);
        return callback(null, [studentAnswered, classAnsweredAverage]);
    });
}

/**
 * get questions answered vs class
 *
 * @param {object} query
 * @param {function} callback
 */
var getOverallVsClass = function(query, callback) {
    users.getFullLeaderboard(function(err, leaderboardList) {
        if (err) {
            logger.error(JSON.stringify(err));
            return callback(err, null);
        }

        var studentId = query.userId;
        var studentOverall = 0;
        var classOverall = 0;
        var classCount = 0;

        for (i in leaderboardList) {
            if (leaderboardList[i]._id === studentId) {
                studentOverall = parseFloat(leaderboardList[i].overall);
            } else {
                classOverall += parseFloat(leaderboardList[i].overall);
                classCount++;
            }
        }

        var classOverallAverage = classCount === 0 ? 0 : Math.ceil(classOverall / classCount);
        return callback(null, [studentOverall, classOverallAverage]);
    });
}

/**
 * get questions answered vs class
 *
 * @param {object} query
 * @param {function} callback
 */
var getPointsPerAttemptVsClass = function(query, callback) {
    users.getFullLeaderboard(function(err, leaderboardList) {
        if (err) {
            logger.error(JSON.stringify(err));
            return callback(err, null);
        }

        var studentId = query.userId;
        var studentPointsPerAttempt = 0;
        var classPointsPerAttempt = 0;
        var classCount = 0;

        for (i in leaderboardList) {
            if (leaderboardList[i]._id === studentId) {
                studentPointsPerAttempt = parseFloat(leaderboardList[i].attempt);
            } else {
                classPointsPerAttempt += parseFloat(leaderboardList[i].attempt);
                classCount++;
            }
        }

        var classAttemptsAverage = classCount === 0 ? 0 : Math.ceil(classPointsPerAttempt / classCount);
        return callback(null, [studentPointsPerAttempt, classAttemptsAverage]);
    });
}

/**
 * get questions answered vs class
 *
 * @param {object} query
 * @param {function} callback
 */
var getAccuracyVsClass = function(query, callback) {
    users.getStudentsList(function(err, students) {
        if (err) {
            return callback(err, null);
        }

        if (!students) {
            return  callback('no results', null);
        }

        var studentId = query.userId;
        var studentAccuracy = 0;
        var classAccuracy = 0;
        var classCount = 0;
        var classAverageAccuracy = 0;

        for (i in students) {
            if (students[i]._id === studentId) {
                studentAccuracy = parseFloat(students[i].totalAttemptsCount ? ((students[i].correctAttemptsCount / students[i].totalAttemptsCount)*100).toFixed(2) : 0);
            } else {
                classAccuracy += parseFloat(students[i].totalAttemptsCount ? ((students[i].correctAttemptsCount / students[i].totalAttemptsCount)*100).toFixed(2) : 0);
                classCount++;
            }
        }

        classAverageAccuracy = classCount === 0 ? 0 : parseFloat(((classAccuracy / classCount)).toFixed(2));
        return callback(null, [studentAccuracy, classAverageAccuracy]);
    });
}

/**
 * get points vs class
 *
 * @param {object} query
 * @param {function} callback
 */
var getPointsVsClass = function(query, callback) {
    users.getStudentsList(function(err, students) {
        if (err) {
            return callback(err, null);
        }

        if (!students) {
            return  callback('no results', null);
        }

        var studentId = query.userId;
        var studentPoints = 0;
        var classPoints = 0;
        var classCount = 0;
        var classPointsAverage = 0;

        for (i in students) {
            if (students[i]._id === studentId) {
                studentPoints = students[i].points;
            } else {
                classPoints += students[i].points;
                classCount++;
            }
        }

        classPointsAverage = classCount === 0 ? 0 : Math.ceil(classPoints / classCount);
        return callback(null, [studentPoints, classPointsAverage]);
    });
}

/**
 * get rating vs class
 *
 * @param {object} query
 * @param {function} callback
 */
var getRatingVsClass = function(query, callback) {
    users.getUsersList(function(err, students) {
        if (err) {
            return callback(err, null);
        }

        if (!students) {
            return  callback('no results', null);
        }

        var studentId = query.userId;
        var studentRating = 0;
        var classRating = 0;
        var classCount = 0;
        var classRatingAverage = 0;

        for (i in students) {
            if (students[i].ratings.length !== 0) {
                if (students[i]._id === studentId) {
                    studentRating = getAverageRating(students[i].ratings);
                } else {
                    classRating += getAverageRating(students[i].ratings);
                    classCount++;
                }
            }
        }

        classRatingAverage = classRating ? Math.ceil(classRating / classCount) : 0;
        return callback(null, [studentRating, classRatingAverage]);
    });
}

/**
 * get average rating
 *
 * @param {list} ratingsList
 */
var getAverageRating = function(ratingsList) {
    var totalRate = 0;

    for (i in ratingsList) {
        totalRate += ratingsList[i].rating;
    }

    return Math.ceil(totalRate / ratingsList.length);
}

/**
 * get questions answered of the class
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassAnswered = function(query, callback) {
    users.getStudentsList(function(err, students) {
        if (err) {
            return callback(err, null);
        }

        if (!students) {
            return  callback('no results', null);
        }

        var classAnswered = 0;
        var classCount = 0;
        var classAnsweredAverage = 0;

        for (i in students) {
            classAnswered += students[i].correctAttemptsCount;
            classCount++;
        }

        classAnsweredAverage =  classCount === 0 ? 0 :Math.ceil(classAnswered / classCount);
        return callback(null, [classAnsweredAverage]);
    });
}

/**
 * get class points per attempt of the class
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassPointsPerAttempt = function(query, callback) {
    users.getStudentsList(function(err, students) {
        if (err) {
            return callback(err, null);
        }

        if (!students) {
            return  callback('no results', null);
        }

        var classPointsPerAttempt = 0;
        var classCount = 0;
        var classPointsPerAttemptAverage = 0;

        for (i in students) {
            classPointsPerAttempt += students[i].totalAttemptsCount === 0 ? 0 : parseFloat((students[i].points / students[i].totalAttemptsCount).toFixed(2));
            classCount++;
        }

        classPointsPerAttemptAverage =  classCount === 0 ? 0 : (classPointsPerAttempt / classCount).toFixed(2);
        return callback(null, [classPointsPerAttemptAverage]);
    });
}

/**
 * get overall value of the class
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassOverall = function(query, callback) {
    users.getStudentsList(function(err, students) {
        if (err) {
            return callback(err, null);
        }

        if (!students) {
            return  callback('no results', null);
        }

        var classOverall = 0;
        var classCount = 0;
        var classOverallAverage = 0;

        for (i in students) {
            classOverall += (students[i].totalAttemptsCount === 0)
            ? 0
            : parseFloat((students[i].points *
            ((students[i].correctAttemptsCount/students[i].totalAttemptsCount) +
            (students[i].points/students[i].totalAttemptsCount))).toFixed(2));
            classCount++;
        }

        classOverallAverage =  classCount === 0 ? 0 : (classOverall / classCount).toFixed(2);
        return callback(null, [classOverallAverage]);
    });
}

/**
 * get questions answered of the class over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassAnsweredOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:classId}, function(err, classObject) {
        if (err) {
            return callback(err, null);
        }

        var dates = [];
        var classData = [];

        for (var i = 0; i < classObject.dates.length; i++) {
            dates.push(classObject.dates[i].date);
            classData.push(classObject.dates[i].info.correctAttemptsCount);
        }

        return callback(null, {
            dates: dates,
            classData: classData
        });
    });
}

/**
 * get overall average of the class over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassOverallOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:classId}, function(err, classObject) {
        if (err) {
            return callback(err, null);
        }

        var dates = [];
        var classData = [];

        for (var i = 0; i < classObject.dates.length; i++) {
            dates.push(classObject.dates[i].date);
            classData.push(classObject.dates[i].info.overallAverage);
        }

        return callback(null, {
            dates: dates,
            classData: classData
        });
    });
}

/**
 * get points per attempt average of the class over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassPointsPerAttemptOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:classId}, function(err, classObject) {
        if (err) {
            return callback(err, null);
        }

        var dates = [];
        var classData = [];

        for (var i = 0; i < classObject.dates.length; i++) {
            dates.push(classObject.dates[i].date);
            classData.push(classObject.dates[i].info.pointsPerAttemptAverage);
        }

        return callback(null, {
            dates: dates,
            classData: classData
        });
    });
}

/**
 * get accuracy of the class over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassAccuracyOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:classId}, function(err, classObject) {
        if (err) {
            return callback(err, null);
        }

        var dates = [];
        var classData = [];

        for (var i = 0; i < classObject.dates.length; i++) {
            dates.push(classObject.dates[i].date);
            classData.push(classObject.dates[i].info.accuracyAverage);
        }

        return callback(null, {
            dates: dates,
            classData: classData
        });
    });
}

/**
 * get points of the class over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassPointsOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:classId}, function(err, classObject) {
        if (err) {
            return callback(err, null);
        }

        var dates = [];
        var classData = [];

        for (var i = 0; i < classObject.dates.length; i++) {
            dates.push(classObject.dates[i].date);
            classData.push(classObject.dates[i].info.pointsAverage);
        }

        return callback(null, {
            dates: dates,
            classData: classData
        });
    });
}

/**
 * get accuracy of the class
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassAccuracy = function(query, callback) {
    users.getStudentsList(function(err, students) {
        if (err) {
            return callback(err, null);
        }

        if (!students) {
            return  callback('no results', null);
        }

        var classAccuracy = 0;
        var classCount = 0;
        var classAverageAccuracy = 0;

        for (i in students) {
            classAccuracy += parseFloat(students[i].totalAttemptsCount ? ((students[i].correctAttemptsCount / students[i].totalAttemptsCount)*100).toFixed(2) : 0);
            classCount++;
        }

        classAverageAccuracy = classCount === 0 ? 0 : parseFloat(((classAccuracy / classCount)).toFixed(2));
        return callback(null, [classAverageAccuracy]);
    });
}

/**
 * get points of the class
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassPoints = function(query, callback) {
    users.getStudentsList(function(err, students) {
        if (err) {
            return callback(err, null);
        }

        if (!students) {
            return  callback('no results', null);
        }

        var classPoints = 0;
        var classCount = 0;
        var classPointsAverage = 0;

        for (i in students) {
            classPoints += students[i].points;
            classCount++;
        }

        classPointsAverage = classCount === 0 ? 0 : Math.ceil(classPoints / classCount);
        return callback(null, [classPointsAverage]);
    });
}

/**
 * get rating of the class
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassRating = function(query, callback) {
    users.getUsersList(function(err, students) {
        if (err) {
            return callback(err, null);
        }

        if (!students) {
            return  callback('no results', null);
        }

        var classRating = 0;
        var classCount = 0;
        var classRatingAverage = 0;

        for (i in students) {
            if (students[i].ratings.length !== 0) {
                classRating += getAverageRating(students[i].ratings);
                classCount++;
            }
        }

        classRatingAverage = classRating ? Math.ceil(classRating / classCount) : 0;
        return callback(null, [classRatingAverage]);
    });
}

/**
 * get number correct attempts of user over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getCorrectAttemptsOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:query.userId}, function(err, studentObject) {
        if (err) {
            return callback(err, null);
        }
        getTimeBasedAnalytics({_id:classId}, function(err, classObject) {
            if (err) {
                return callback(err, null);
            }

            var dates = [];
            var studentData = [];
            var classData = [];

            for (var i = 0; i < classObject.dates.length - studentObject.dates.length; i++) {
                studentData.push(0);
            }

            for (var i = 0; i < studentObject.dates.length; i++) {
                studentData.push(studentObject.dates[i].info.correctAttemptsCount);
            }

            for (var i = 0; i < classObject.dates.length; i++) {
                dates.push(classObject.dates[i].date);
                classData.push(classObject.dates[i].info.correctAttemptsCount);
            }

            return callback(null, {
                dates: dates,
                studentData: studentData,
                classData: classData
            });
        });
    });
}

/**
 * get points per attempts of user and class over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getPointsPerAttemptsOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:query.userId}, function(err, studentObject) {
        if (err) {
            return callback(err, null);
        }
        getTimeBasedAnalytics({_id:classId}, function(err, classObject) {
            if (err) {
                return callback(err, null);
            }

            var dates = [];
            var studentData = [];
            var classData = [];

            for (var i = 0; i < classObject.dates.length - studentObject.dates.length; i++) {
                studentData.push(0);
            }

            for (var i = 0; i < studentObject.dates.length; i++) {
                studentData.push(studentObject.dates[i].info.pointsPerAttemptValue);
            }

            for (var i = 0; i < classObject.dates.length; i++) {
                dates.push(classObject.dates[i].date);
                classData.push(classObject.dates[i].info.pointsPerAttemptAverage);
            }

            return callback(null, {
                dates: dates,
                studentData: studentData,
                classData: classData
            });
        });
    });
}

/**
 * get points per attempts of user and class over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getOverallOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:query.userId}, function(err, studentObject) {
        if (err) {
            return callback(err, null);
        }
        getTimeBasedAnalytics({_id:classId}, function(err, classObject) {
            if (err) {
                return callback(err, null);
            }

            var dates = [];
            var studentData = [];
            var classData = [];

            for (var i = 0; i < classObject.dates.length - studentObject.dates.length; i++) {
                studentData.push(0);
            }

            for (var i = 0; i < studentObject.dates.length; i++) {
                studentData.push(studentObject.dates[i].info.overallValue);
            }

            for (var i = 0; i < classObject.dates.length; i++) {
                dates.push(classObject.dates[i].date);
                classData.push(classObject.dates[i].info.overallAverage);
            }

            return callback(null, {
                dates: dates,
                studentData: studentData,
                classData: classData
            });
        });
    });
}

/**
 * get accuracy of user over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getAccuracyOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:query.userId}, function(err, studentObject) {
        if (err) {
            return callback(err, null);
        }
        getTimeBasedAnalytics({_id:classId}, function(err, classObject) {
            if (err) {
                return callback(err, null);
            }

            var dates = [];
            var studentData = [];
            var classData = [];

            for (var i = 0; i < classObject.dates.length - studentObject.dates.length; i++) {
                studentData.push(0);
            }

            for (var i = 0; i < studentObject.dates.length; i++) {
                studentData.push(studentObject.dates[i].info.accuracy);
            }

            for (var i = 0; i < classObject.dates.length; i++) {
                dates.push(classObject.dates[i].date);
                classData.push(classObject.dates[i].info.accuracy);
            }

            return callback(null, {
                dates: dates,
                studentData: studentData,
                classData: classData
            });
        });
    });
}

/**
 * get points of user over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getPointsOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:query.userId}, function(err, studentObject) {
        if (err) {
            return callback(err, null);
        }
        getTimeBasedAnalytics({_id:classId}, function(err, classObject) {
            if (err) {
                return callback(err, null);
            }

            var dates = [];
            var studentData = [];
            var classData = [];

            for (var i = 0; i < classObject.dates.length - studentObject.dates.length; i++) {
                studentData.push(0);
            }

            for (var i = 0; i < studentObject.dates.length; i++) {
                studentData.push(studentObject.dates[i].info.points);
            }

            for (var i = 0; i < classObject.dates.length; i++) {
                dates.push(classObject.dates[i].date);
                classData.push(classObject.dates[i].info.points);
            }

            return callback(null, {
                dates: dates,
                studentData: studentData,
                classData: classData
            });
        });
    });
}

/**
 * get overall rank over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getOverallRankOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:query.userId}, function(err, studentObject) {
        if (err) {
            return callback(err, null);
        }

        var dates = [];
        var studentData = [];

        for (var i = 0; i < studentObject.dates.length; i++) {
            dates.push(studentObject.dates[i].date);
            studentData.push(studentObject.dates[i].info.overallRank);
        }

        return callback(null, {
            dates: dates,
            studentData: studentData,
            totalStudentsCount: totalStudentsCount
        });
    });
}

/**
 * get points rank over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getPointsRankOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:query.userId}, function(err, studentObject) {
        if (err) {
            return callback(err, null);
        }

        var dates = [];
        var studentData = [];

        for (var i = 0; i < studentObject.dates.length; i++) {
            dates.push(studentObject.dates[i].date);
            studentData.push(studentObject.dates[i].info.pointsRank);
        }

        return callback(null, {
            dates: dates,
            studentData: studentData,
            totalStudentsCount: totalStudentsCount
        });
    });
}

/**
 * get attempt rank over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getAttemptRankOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:query.userId}, function(err, studentObject) {
        if (err) {
            return callback(err, null);
        }

        var dates = [];
        var studentData = [];

        for (var i = 0; i < studentObject.dates.length; i++) {
            dates.push(studentObject.dates[i].date);
            studentData.push(studentObject.dates[i].info.pointsPerAttemptRank);
        }

        return callback(null, {
            dates: dates,
            studentData: studentData,
            totalStudentsCount: totalStudentsCount
        });
    });
}

/**
 * get correct attempt rank over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getCorrectAttemptRankOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:query.userId}, function(err, studentObject) {
        if (err) {
            return callback(err, null);
        }

        var dates = [];
        var studentData = [];

        for (var i = 0; i < studentObject.dates.length; i++) {
            dates.push(studentObject.dates[i].date);
            studentData.push(studentObject.dates[i].info.correctAttemptsRank);
        }

        return callback(null, {
            dates: dates,
            studentData: studentData,
            totalStudentsCount: totalStudentsCount
        });
    });
}

/**
 * get accuracy rank over time
 *
 * @param {object} query
 * @param {function} callback
 */
var getAccuracyRankOverTime = function(query, callback) {
    getTimeBasedAnalytics({_id:query.userId}, function(err, studentObject) {
        if (err) {
            return callback(err, null);
        }

        var dates = [];
        var studentData = [];

        for (var i = 0; i < studentObject.dates.length; i++) {
            dates.push(studentObject.dates[i].date);
            studentData.push(studentObject.dates[i].info.accuracyRank);
        }

        return callback(null, {
            dates: dates,
            studentData: studentData,
            totalStudentsCount: totalStudentsCount
        });
    });
}

/**
 * get points per question topic
 *
 * @param {object} query
 * @param {function} callback
 */
var getPointsPerTopicVsClass = function(query, callback) {
    questions.getAllQuestionsByQuery({}, {topic: 1}, function(err, questionsList) {
        if (err) {
            return callback(err, null);
        }

        if (!questionsList || !questionsList[0]) {
            return callback('no questions available', null);
        }

        var studentId = query.userId;
        var studentData = [];
        var classData = [];
        var labels = [];
        var currentTopic = questionsList[0].topic;
        var studentPoints = 0;
        var classPoints = 0;
        var classCount = 0;

        for (var i = 0; i < questionsList.length; i++) {
            var question = questionsList[i];

            if (question.topic !== currentTopic) {
                labels.push(currentTopic);
                studentData.push(studentPoints);
                classData.push((classCount === 0) ? 0 : (classPoints / classCount).toFixed(0));
                studentPoints = 0;
                classPoints = 0;
                classCount = 0;
                currentTopic = question.topic;
            }

            for (var j = 0; j < question.correctAttempts.length; j++) {
                var attempt = question.correctAttempts[j];
                if (attempt.userId === studentId) {
                    studentPoints = attempt.points;
                } else {
                    classPoints += attempt.points;
                    classCount ++;
                }
            }
        }

        if (questionsList.length > 0) {
            currentTopic = questionsList[questionsList.length-1].topic;
            labels.push(currentTopic);
            studentData.push(studentPoints);
            classData.push((classCount === 0) ? 0 : (classPoints / classCount).toFixed(0));
        }

        return callback(null, {
            studentData: studentData,
            classData: classData,
            labels: labels
        });
    });
}

/**
 * get accuracy per question topic
 *
 * @param {object} query
 * @param {function} callback
 */
var getAccuracyPerTopicVsClass = function(query, callback) {
    questions.getAllQuestionsByQuery({}, {topic: 1}, function(err, questionsList) {
        if (err) {
            return callback(err, null);
        }

        if (!questionsList || !questionsList[0]) {
            return callback('no questions available', null);
        }

        var studentId = query.userId;
        var studentData = [];
        var classData = [];
        var labels = [];
        var currentTopic = questionsList[0].topic;
        var studentCorrect = 0;
        var studentTotal = 0;
        var classCorrect = 0;
        var classTotal = 0;

        for (var i = 0; i < questionsList.length; i++) {
            var question = questionsList[i];

            if (question.topic !== currentTopic) {
                labels.push(currentTopic);
                studentData.push((studentTotal === 0) ? 0 : ((studentCorrect / studentTotal) * 100).toFixed(2));
                classData.push((classTotal === 0) ? 0 : ((classCorrect / classTotal) * 100).toFixed(2));
                studentCorrect = 0;
                studentTotal = 0;
                classCorrect = 0;
                classTotal = 0;
                currentTopic = question.topic;
            }

            for (var j = 0; j < question.correctAttempts.length; j++) {
                var attempt = question.correctAttempts[j];
                if (attempt.userId === studentId) {
                    studentCorrect ++;
                } else {
                    classCorrect ++;
                }
            }

            for (var j = 0; j < question.totalAttempts.length; j++) {
                var attempt = question.totalAttempts[j];
                if (attempt.userId === studentId) {
                    studentTotal ++;
                } else {
                    classTotal ++;
                }
            }
        }

        if (questionsList.length > 0) {
            currentTopic = questionsList[questionsList.length-1].topic;
            labels.push(currentTopic);
            studentData.push((studentTotal === 0) ? 0 : ((studentCorrect / studentTotal) * 100).toFixed(2));
            classData.push((classTotal === 0) ? 0 : ((classCorrect / classTotal) * 100).toFixed(2));
        }

        return callback(null, {
            studentData: studentData,
            classData: classData,
            labels: labels
        });
    });
}

/**
 * get points per question type
 *
 * @param {object} query
 * @param {function} callback
 */
var getPointsPerTypeVsClass = function(query, callback) {
    questions.getAllQuestionsByQuery({}, {type: 1}, function(err, questionsList) {
        if (err) {
            return callback(err, null);
        }

        if (!questionsList || !questionsList[0]) {
            return callback('no questions available', null);
        }

        var studentId = query.userId;
        var studentData = [];
        var classData = [];
        var labels = [];
        var currentType = questionsList[0].type;
        var studentPoints = 0;
        var classPoints = 0;
        var classCount = 0;

        for (var i = 0; i < questionsList.length; i++) {
            var question = questionsList[i];

            if (question.type !== currentType) {
                labels.push(currentType);
                studentData.push(studentPoints);
                classData.push((classCount === 0) ? 0 : (classPoints / classCount).toFixed(0));
                studentPoints = 0;
                classPoints = 0;
                classCount = 0;
                currentType = question.type;
            }

            for (var j = 0; j < question.correctAttempts.length; j++) {
                var attempt = question.correctAttempts[j];
                if (attempt.userId === studentId) {
                    studentPoints = attempt.points;
                } else {
                    classPoints += attempt.points;
                    classCount ++;
                }
            }
        }

        if (questionsList.length > 0) {
            currentType = questionsList[questionsList.length-1].type;
            labels.push(currentType);
            studentData.push(studentPoints);
            classData.push((classCount === 0) ? 0 : (classPoints / classCount).toFixed(0));
        }

        return callback(null, {
            studentData: studentData,
            classData: classData,
            labels: labels
        });
    });
}

/**
 * get accuracy per question type
 *
 * @param {object} query
 * @param {function} callback
 */
var getAccuracyPerTypeVsClass = function(query, callback) {
    questions.getAllQuestionsByQuery({}, {type: 1}, function(err, questionsList) {
        if (err) {
            return callback(err, null);
        }

        if (!questionsList || !questionsList[0]) {
            return callback('no questions available', null);
        }

        var studentId = query.userId;
        var studentData = [];
        var classData = [];
        var labels = [];
        var currentType = questionsList[0].type;
        var studentCorrect = 0;
        var studentTotal = 0;
        var classCorrect = 0;
        var classTotal = 0;

        for (var i = 0; i < questionsList.length; i++) {
            var question = questionsList[i];

            if (question.type !== currentType) {
                labels.push(currentType);
                studentData.push((studentTotal === 0) ? 0 : ((studentCorrect / studentTotal) * 100).toFixed(2));
                classData.push((classTotal === 0) ? 0 : ((classCorrect / classTotal) * 100).toFixed(2));
                studentCorrect = 0;
                studentTotal = 0;
                classCorrect = 0;
                classTotal = 0;
                currentType = question.type;
            }

            for (var j = 0; j < question.correctAttempts.length; j++) {
                var attempt = question.correctAttempts[j];
                if (attempt.userId === studentId) {
                    studentCorrect ++;
                } else {
                    classCorrect ++;
                }
            }

            for (var j = 0; j < question.totalAttempts.length; j++) {
                var attempt = question.totalAttempts[j];
                if (attempt.userId === studentId) {
                    studentTotal ++;
                } else {
                    classTotal ++;
                }
            }
        }

        if (questionsList.length > 0) {
            currentType = questionsList[questionsList.length-1].type;
            labels.push(currentType);
            studentData.push((studentTotal === 0) ? 0 : ((studentCorrect / studentTotal) * 100).toFixed(2));
            classData.push( (classTotal === 0) ? 0 : ((classCorrect / classTotal) * 100).toFixed(2));
        }

        return callback(null, {
            studentData: studentData,
            classData: classData,
            labels: labels
        });
    });
}

/**
 * get class points per question topic
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassPointsPerTopicVsClass = function(query, callback) {
    questions.getAllQuestionsByQuery({}, {topic: 1}, function(err, questionsList) {
        if (err) {
            return callback(err, null);
        }

        if (!questionsList || !questionsList[0]) {
            return callback('no questions available', null);
        }

        var classData = [];
        var labels = [];
        var currentTopic = questionsList[0].topic;
        var classPoints = 0;
        var classCount = 0;

        for (var i = 0; i < questionsList.length; i++) {
            var question = questionsList[i];

            if (question.topic !== currentTopic) {
                labels.push(currentTopic);
                classData.push((classCount === 0) ? 0 : (classPoints / classCount).toFixed(0));
                classPoints = 0;
                classCount = 0;
                currentTopic = question.topic;
            }

            for (var j = 0; j < question.correctAttempts.length; j++) {
                classPoints += question.correctAttempts[j].points;
                classCount ++;
            }
        }

        if (questionsList.length > 0) {
            currentTopic = questionsList[questionsList.length-1].topic;
            labels.push(currentTopic);
            classData.push((classCount === 0) ? 0 : (classPoints / classCount).toFixed(0));
        }

        return callback(null, {
            classData: classData,
            labels: labels
        });
    });
}

/**
 * get class accuracy per question topic
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassAccuracyPerTopicVsClass = function(query, callback) {
    questions.getAllQuestionsByQuery({}, {topic: 1}, function(err, questionsList) {
        if (err) {
            return callback(err, null);
        }

        if (!questionsList || !questionsList[0]) {
            return callback('no questions available', null);
        }

        var classData = [];
        var labels = [];
        var currentTopic = questionsList[0].topic;
        var classCorrect = 0;
        var classTotal = 0;

        for (var i = 0; i < questionsList.length; i++) {
            var question = questionsList[i];

            if (question.topic !== currentTopic) {
                labels.push(currentTopic);
                classData.push((classTotal === 0) ? 0 : ((classCorrect / classTotal) * 100).toFixed(2));
                classCorrect = 0;
                classTotal = 0;
                currentTopic = question.topic;
            }

            classCorrect += question.correctAttemptsCount;
            classTotal += question.totalAttemptsCount;
        }

        if (questionsList.length > 0) {
            currentTopic = questionsList[questionsList.length-1].topic;
            labels.push(currentTopic);
            classData.push((classTotal === 0) ? 0 : ((classCorrect / classTotal) * 100).toFixed(2));
        }

        return callback(null, {
            classData: classData,
            labels: labels
        });
    });
}

/**
 * get class rating per topic vs class
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassRatingPerTopicVsClass = function (query, callback) {
    users.getUsersList(function (err, studentsList) {
        if (err) {
            return callback(err, null);
        }

        var studentsObject = {};
        for (var j = 0; j < studentsList.length; j++) {
            studentsObject[studentsList[j]._id] = studentsList[j].type;
        }

        questions.getAllQuestionsByQuery({}, {topic: 1}, function(err, questionsList) {
            if (err) {
                return callback(err, null);
            }

            if (!questionsList || !questionsList[0]) {
                return callback('no questions available', null);
            }

            var adminsData = [];
            var studentsData = [];
            var labels = [];
            var currentTopic = questionsList[0].topic;
            var adminsRating = 0;
            var studentsRating = 0;
            var adminsCount = 0;
            var studentsCount = 0;

            for (var i = 0; i < questionsList.length; i++) {
                var question = questionsList[i];

                if (question.topic !== currentTopic) {
                    labels.push(currentTopic);
                    adminsData.push((adminsCount === 0) ? 0 : (adminsRating / adminsCount).toFixed(2));
                    studentsData.push((studentsCount === 0) ? 0 : (studentsRating / studentsCount).toFixed(2));
                    adminsCount = 0;
                    studentsCount = 0;
                    adminsRating = 0;
                    studentsRating = 0;
                    currentTopic = question.topic;
                }

                for (var j = 0; j < question.ratings.length; j++) {
                    var userObject = question.ratings[j];

                    if (studentsObject[userObject.userId] === common.userTypes.ADMIN) {
                        adminsRating += userObject.rating;
                        adminsCount ++;
                    } else {
                        studentsRating += userObject.rating;
                        studentsCount ++;
                    }
                }
            }

            if (questionsList.length > 0) {
                currentTopic = questionsList[questionsList.length-1].topic;
                labels.push(currentTopic);
                adminsData.push((adminsCount === 0) ? 0 : (adminsRating / adminsCount).toFixed(2));
                studentsData.push((studentsCount === 0) ? 0 : (studentsRating / studentsCount).toFixed(2));
            }

            return callback(null, {
                adminsData: adminsData,
                studentsData: studentsData,
                labels: labels
            });
        });
    });
}

/**
 * get class rating per type vs class
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassRatingPerTypeVsClass = function (query, callback) {
    users.getUsersList(function (err, studentsList) {
        if (err) {
            return callback(err, null);
        }

        var studentsObject = {};
        for (var j = 0; j < studentsList.length; j++) {
            studentsObject[studentsList[j]._id] = studentsList[j].type;
        }

        questions.getAllQuestionsByQuery({}, {type: 1}, function(err, questionsList) {
            if (err) {
                return callback(err, null);
            }

            if (!questionsList || !questionsList[0]) {
                return callback('no questions available', null);
            }

            var adminsData = [];
            var studentsData = [];
            var labels = [];
            var currentType = questionsList[0].type;
            var adminsRating = 0;
            var studentsRating = 0;
            var adminsCount = 0;
            var studentsCount = 0;

            for (var i = 0; i < questionsList.length; i++) {
                var question = questionsList[i];

                if (question.type !== currentType) {
                    labels.push(currentType);
                    adminsData.push((adminsCount === 0) ? 0 : (adminsRating / adminsCount).toFixed(2));
                    studentsData.push((studentsCount === 0) ? 0 : (studentsRating / studentsCount).toFixed(2));
                    adminsCount = 0;
                    studentsCount = 0;
                    adminsRating = 0;
                    studentsRating = 0;
                    currentType = question.type;
                }

                for (var j = 0; j < question.ratings.length; j++) {
                    var userObject = question.ratings[j];

                    if (studentsObject[userObject.userId] === common.userTypes.ADMIN) {
                        adminsRating += userObject.rating;
                        adminsCount ++;
                    } else {
                        studentsRating += userObject.rating;
                        studentsCount ++;
                    }
                }
            }

            if (questionsList.length > 0) {
                currentType = questionsList[questionsList.length-1].type;
                labels.push(currentType);
                adminsData.push((adminsCount === 0) ? 0 : (adminsRating / adminsCount).toFixed(2));
                studentsData.push((studentsCount === 0) ? 0 : (studentsRating / studentsCount).toFixed(2));
            }

            return callback(null, {
                adminsData: adminsData,
                studentsData: studentsData,
                labels: labels
            });
        });
    });
}

/**
 * get class points per question type
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassPointsPerTypeVsClass = function(query, callback) {
    questions.getAllQuestionsByQuery({}, {type: 1}, function(err, questionsList) {
        if (err) {
            return callback(err, null);
        }

        if (!questionsList || !questionsList[0]) {
            return callback('no questions available', null);
        }

        var classData = [];
        var labels = [];
        var currentType = questionsList[0].type;
        var classPoints = 0;
        var classCount = 0;

        for (var i = 0; i < questionsList.length; i++) {
            var question = questionsList[i];

            if (question.type !== currentType) {
                labels.push(currentType);
                classData.push((classCount === 0) ? 0 : (classPoints / classCount).toFixed(0));
                classPoints = 0;
                classCount = 0;
                currentType = question.type;
            }

            for (var j = 0; j < question.correctAttempts.length; j++) {
                classPoints = question.correctAttempts[j].points;
                classCount ++;
            }
        }

        if (questionsList.length > 0) {
            currentType = questionsList[questionsList.length-1].type;
            labels.push(currentType);
            classData.push((classCount === 0) ? 0 : (classPoints / classCount).toFixed(0));
        }

        return callback(null, {
            classData: classData,
            labels: labels
        });
    });
}

/**
 * get class accuracy per question type
 *
 * @param {object} query
 * @param {function} callback
 */
var getClassAccuracyPerTypeVsClass = function(query, callback) {
    questions.getAllQuestionsByQuery({}, {type: 1}, function(err, questionsList) {
        if (err) {
            return callback(err, null);
        }

        if (!questionsList || !questionsList[0]) {
            return callback('no questions available', null);
        }

        var classData = [];
        var labels = [];
        var currentType = questionsList[0].type;
        var classCorrect = 0;
        var classTotal = 0;

        for (var i = 0; i < questionsList.length; i++) {
            var question = questionsList[i];

            if (question.type !== currentType) {
                labels.push(currentType);
                classData.push((classTotal === 0) ? 0 : ((classCorrect / classTotal) * 100).toFixed(2));
                classCorrect = 0;
                classTotal = 0;
                currentType = question.type;
            }

            classCorrect += question.correctAttemptsCount;
            classTotal += question.totalAttemptsCount;
        }

        if (questionsList.length > 0) {
            currentType = questionsList[questionsList.length-1].type;
            labels.push(currentType);
            classData.push( (classTotal === 0) ? 0 : ((classCorrect / classTotal) * 100).toFixed(2));
        }

        return callback(null, {
            classData: classData,
            labels: labels
        });
    });
}