Skip to content

Commit

Permalink
Color engagement table values based on categories.
Browse files Browse the repository at this point in the history
  • Loading branch information
dsjen committed Mar 22, 2016
1 parent ea9d394 commit 325713b
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
define([
'backbone',
'learners/js/utils'
], function (Backbone, LearnerUtils) {
'learners/js/utils',
'underscore'
], function (Backbone, LearnerUtils, _) {
'use strict';

var CourseMetadataModel = Backbone.Model.extend({
Expand All @@ -14,7 +15,7 @@ define([
problems_attempted: {},
problems_completed: {},
problem_attempts_per_completed: {},
discussions_contributed: {}
discussion_contributions: {}
}
},

Expand All @@ -29,6 +30,36 @@ define([
fetch: function (options) {
return Backbone.Model.prototype.fetch.call(this, options)
.fail(LearnerUtils.handleAjaxFailure.bind(this));
},

/**
* Returns which category ('average', 'above_average', 'below_average'),
* the engagement values falls into. Returns undefined the range isn't
* available.
*
* @param engagementKey Key for engagement range. E.g. problems_attempted.
* @param value Engagement value.
*/
getEngagementCategory: function(engagementKey, value) {
var categories = ['average', 'above_average', 'below_average'],
ranges = this.get('engagement_ranges')[engagementKey],
engagementCategory;

if (ranges) {
_(categories).each(function (category) {
if (_(ranges).has(category)) {
try {
if (LearnerUtils.inRange(value, ranges[category])) {
engagementCategory = category;
}
} catch (Error) {
// min and max are null -- do nothing
}
}
});
}

return engagementCategory;
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,31 @@ require(['learners/js/models/course-metadata'], function (CourseMetadataModel) {
jasmine.clock().tick(2);
expect(courseMetadata.trigger).toHaveBeenCalledWith('networkError', 'timeout');
});

it('categorizes engagement values', function () {
var engagementRanges = {
problems_attempted: {
below_average: [0, 10],
average: [11, 25],
above_average: [26, null]
},
problem_attempts_per_completed: {
below_average: [null, 1],
average: [2, 5.8],
above_average: [null, null]
}
},
courseMetadata = new CourseMetadataModel();

courseMetadata.set('engagement_ranges', engagementRanges);
expect(courseMetadata.getEngagementCategory('problems_attempted', 9)).toBe('below_average');
expect(courseMetadata.getEngagementCategory('problems_attempted', 12)).toBe('average');
expect(courseMetadata.getEngagementCategory('problems_attempted', 100)).toBe('above_average');

expect(courseMetadata.getEngagementCategory('problem_attempts_per_completed', 0)).toBe('below_average');
expect(courseMetadata.getEngagementCategory('problem_attempts_per_completed', 3.3)).toBe('average');
expect(courseMetadata.getEngagementCategory('problem_attempts_per_completed', 100)).toBe(undefined);
});

});
});
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,66 @@ define([
});
});

it('categorizes engagement values', function () {
var learners = [{
name: 'agnes',
username: 'agnes',
engagements: {
discussion_contributions: 10,
problems_attempted: 100,
problems_completed: 32,
videos_viewed: 1,
problem_attempts_per_completed: 0.56
}
}],
engagementRanges = {
problems_attempted: {
below_average: [0, 10],
average: [11, 25],
above_average: [26, null]
},
videos_viewed: {
below_average: [0, 1],
average: [1, 10],
above_average: [10, null]
},
problems_completed: {
below_average: [0, 10],
average: [11, 50],
above_average: [50, null]
},
problem_attempts_per_completed: {
below_average: [null, 1],
average: [2, 25],
above_average: [26, 60]
},
discussion_contributions: {
below_average: [0, 100],
average: [100, 125],
above_average: [125, null]
}
},
rosterView = getRosterView({
collectionResponse: {results: learners},
collectionOptions: {parse: true},
courseMetadata: {
engagement_ranges: engagementRanges
}
}),
$tr = $(rosterView.$('tbody tr'));

expect($tr.children('td.discussion_contributions'))
.toHaveClass('learner-cell-below-average');
expect($tr.find('td.problems_completed'))
.toHaveClass('learner-cell-average');
expect($tr.find('td.problems_attempted'))
.toHaveClass('learner-cell-above-average');
expect($tr.find('td.problem_attempts_per_completed'))
.toHaveClass('learner-cell-below-average');
expect($tr.find('td.videos_viewed'))
.toHaveClass('learner-cell-average');
});

describe('table headers', function() {

it('has tooltips', function () {
Expand Down
25 changes: 25 additions & 0 deletions analytics_dashboard/static/apps/learners/js/spec/utils-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,30 @@ require([
jasmine.clock().tick(2);
});
});

describe('inRange', function () {

it('returns true when value is in range', function () {
expect(LearnerUtils.inRange(1, [0, 2])).toBe(true);
expect(LearnerUtils.inRange(10.5, [5, 11])).toBe(true);
expect(LearnerUtils.inRange(44, [null, 45])).toBe(true);
expect(LearnerUtils.inRange(59.3, [59, null])).toBe(true);
});

it('returns false when value is out of range', function () {
expect(LearnerUtils.inRange(3, [0, 2])).toBe(false);
expect(LearnerUtils.inRange(4.5, [5, 11])).toBe(false);
expect(LearnerUtils.inRange(45, [null, 45])).toBe(false);
expect(LearnerUtils.inRange(58.3, [59, null])).toBe(false);
});

it('throws error when bounds are null', function () {
expect(function () {
LearnerUtils.inRange(1, [null, null]);
}).toThrow(new Error('min and max range values cannot both be null (unbounded)'));
});

});

});
});
26 changes: 26 additions & 0 deletions analytics_dashboard/static/apps/learners/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,32 @@ define([], function () {
// Request incomplete; network error
this.trigger('networkError', textStatus);
}
},

/**
* Returns true if the value falls within the range (inclusive of min
* and exclusive of max).
*
* @param value Value in question.
* @param range Array of min and max.
*/
inRange: function(value, range) {
var min = range[0],
max = range[1],
minIsUnbounded = _.isNull(min),
maxIsUnbounded = _.isNull(max);

if (minIsUnbounded && maxIsUnbounded) {
throw new Error('min and max range values cannot both be null (unbounded)');
}

if (minIsUnbounded) {
return value < max;
} else if (maxIsUnbounded) {
return value >= min;
}

return value >= min && value < max;
}
};

Expand Down
34 changes: 30 additions & 4 deletions analytics_dashboard/static/apps/learners/js/views/roster-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,34 @@ define([
* Factory for creating a Backgrid cell class that renders a key
* from the learner model's engagement attribute.
*/
createEngagementCell = function (key) {
createEngagementCell = function (key, options) {
return Backgrid.Cell.extend({

className: 'learner-engagement-cell ' + key,

options: options,

formatter: {
fromRaw: function (rawData, model) {
var value = model.get('engagements')[key];
// Translators: 'N/A' is an abbreviation of "Not Applicable". Please translate accordingly.
return value === null ? gettext('N/A') : value;
}
},

enagementCategoryToClass: {
below_average: 'learner-cell-below-average',
average: 'learner-cell-average',
above_average: 'learner-cell-above-average'
},

render: function() {
var value = this.model.get('engagements')[key],
engagementCategory = this.options.courseMetadata.getEngagementCategory(key, value);
if (engagementCategory) {
this.$el.addClass(this.enagementCategoryToClass[engagementCategory]);
}
return Backgrid.Cell.prototype.render.apply(this, arguments);
}
});
};
Expand Down Expand Up @@ -352,6 +371,7 @@ define([
this.options = options || {};
},
onBeforeShow: function () {
var options = this.options;
this.showChildView('table', new Backgrid.Grid({
className: 'table table-striped', // Use bootstrap styling
collection: this.options.collection,
Expand All @@ -368,7 +388,7 @@ define([
column.cell = NameAndUsernameCell;
column.headerCell = BaseHeaderCell;
} else {
column.cell = createEngagementCell(key);
column.cell = createEngagementCell(key, options);
column.headerCell = createEngagementHeaderCell(key);
}

Expand Down Expand Up @@ -403,7 +423,10 @@ define([
if (collection.length) {
// Don't re-render the learner table view if one already exists.
if (!(this.getRegion('main').currentView instanceof LearnerTableView)) {
this.showChildView('main', new LearnerTableView({collection: collection}));
this.showChildView('main', new LearnerTableView({
collection: collection,
courseMetadata: this.options.courseMetadata
}));
}
} else {
this.showChildView('main', this.createAlertView(collection));
Expand Down Expand Up @@ -499,7 +522,10 @@ define([
collection: this.options.collection,
courseMetadata: this.options.courseMetadata
}));
this.showChildView('results', new LearnerResultsView({collection: this.options.collection}));
this.showChildView('results', new LearnerResultsView({
collection: this.options.collection,
courseMetadata: this.options.courseMetadata
}));
}
});

Expand Down
17 changes: 17 additions & 0 deletions analytics_dashboard/static/sass/_developer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,23 @@ table.dataTable thead th.sorting_desc:after {
}
}

.learner-engagement-cell {
// gives the numbers in the engagement table a bit more weight
font-weight: 600;
}

// color-bind safe colors for differentiating engagement categories
$below-average-color: rgb(230,121,80);
$above-average-color: rgb(82,184,135);
.learner-cell-below-average {
color: $below-average-color;
}

.learner-cell-above-average {

color: $above-average-color;
}

.backgrid-paginator {

ul {
Expand Down

0 comments on commit 325713b

Please sign in to comment.