Commit 01b70e48 by Dennis Jen

Merge pull request #157 from edx/dsjen/update-sparse-trend-display

Display empty previous data point on trend line when only one data point exists.
parents 140e5294 18921872
......@@ -52,24 +52,36 @@ class CourseEngagementPresenter(BasePresenter):
return activities
def _build_trend_week(self, trend_types, week_ending, api_trend):
trend_week = {'weekEnding': week_ending.isoformat()}
for trend_type in trend_types:
if trend_type in api_trend:
trend_week[trend_type] = api_trend[trend_type] or 0
else:
trend_week[trend_type] = 0
return trend_week
def _build_trend(self, api_trends):
"""
Format activity trends for specified date range and return results with
zeros filled in for all activities.
"""
trend_types = self.get_activity_types()
trends = []
# add zeros for the week prior if we only have a single point (prevents just a single point in the chart)
if len(api_trends) == 1:
interval_end = self.parse_api_datetime(api_trends[0]['interval_end'])
week_ending = interval_end.date() - datetime.timedelta(days=8)
trends.append(self._build_trend_week(trend_types, week_ending, {}))
# fill in gaps in activity with zero for display (api doesn't return
# the field if no data exists for it, so we fill in the zeros here)
trends = []
for datum in api_trends:
# convert end of interval to ending day of week
interval_end = self.parse_api_datetime(datum['interval_end'])
week_ending = interval_end.date() - datetime.timedelta(days=1)
trend_week = {'weekEnding': week_ending.isoformat()}
for trend_type in trend_types:
trend_week[trend_type] = datum[trend_type] or 0
trends.append(trend_week)
trends.append(self._build_trend_week(trend_types, week_ending, datum))
return trends
......@@ -116,8 +128,18 @@ class CourseEnrollmentPresenter(BasePresenter):
now = datetime.datetime.utcnow().strftime(Client.DATE_FORMAT)
trends = self.course.enrollment(start_date=None, end_date=now)
summary = self._build_summary(trends)
# add zero for the day prior (prevents just a single point in the chart)
if len(trends) == 1:
trends.insert(0, self._build_empty_trend(self.parse_api_date(trends[0]['date'])))
return summary, trends
def _build_empty_trend(self, day):
day = day - datetime.timedelta(days=1)
trend = {'date': day.isoformat(), 'count': 0}
return trend
def get_geography_data(self):
"""
Returns a list of course geography data and the updated date (ex. 2014-1-31).
......
......@@ -6,7 +6,8 @@ from django.test import TestCase
import analyticsclient.constants.activity_type as AT
from courses.presenters import CourseEngagementPresenter, CourseEnrollmentPresenter, BasePresenter
from courses.tests.utils import get_mock_enrollment_data, get_mock_enrollment_summary, \
from courses.tests.utils import get_mock_enrollment_data, get_mock_presenter_enrollment_data_small, \
get_mock_enrollment_summary, get_mock_presenter_enrollment_summary_small, \
get_mock_api_enrollment_geography_data, get_mock_presenter_enrollment_geography_data, \
get_mock_api_enrollment_geography_data_limited, get_mock_presenter_enrollment_geography_data_limited, \
mock_course_activity, CREATED_DATETIME
......@@ -39,12 +40,24 @@ class CourseEngagementPresenterTests(TestCase):
return trends
def assertSummaryAndTrendsValid(self, include_forum_activity):
def get_expected_trends_small(self, include_forum_data):
trends = self.get_expected_trends(include_forum_data)
trends[0].update({
AT.ANY: 0,
AT.ATTEMPTED_PROBLEM: 0,
AT.PLAYED_VIDEO: 0
})
if include_forum_data:
trends[0][AT.POSTED_FORUM] = 0
return trends
def assertSummaryAndTrendsValid(self, include_forum_activity, expected_trends):
switch, _created = Switch.objects.get_or_create(name='show_engagement_forum_activity')
switch.active = include_forum_activity
switch.save()
expected_trends = self.get_expected_trends(include_forum_activity)
summary, trends = self.presenter.get_summary_and_trend_data()
# Validate the trends
......@@ -66,8 +79,16 @@ class CourseEngagementPresenterTests(TestCase):
@mock.patch('analyticsclient.course.Course.activity', mock.Mock(side_effect=mock_course_activity))
def test_get_summary_and_trend_data(self):
self.assertSummaryAndTrendsValid(False)
self.assertSummaryAndTrendsValid(True)
self.assertSummaryAndTrendsValid(False, self.get_expected_trends(False))
self.assertSummaryAndTrendsValid(True, self.get_expected_trends(True))
@mock.patch('analyticsclient.course.Course.activity')
def test_get_summary_and_trend_data_small(self, mock_activity):
api_trend = [mock_course_activity()[-1]]
mock_activity.return_value = api_trend
self.assertSummaryAndTrendsValid(False, self.get_expected_trends_small(False))
self.assertSummaryAndTrendsValid(True, self.get_expected_trends_small(True))
class BasePresenterTests(TestCase):
......@@ -119,6 +140,15 @@ class CourseEnrollmentPresenterTests(TestCase):
self.assertListEqual(actual_trend, expected_trend)
@mock.patch('analyticsclient.course.Course.enrollment')
def test_get_summary_and_trend_data_small(self, mock_enrollment):
api_trend = [get_mock_enrollment_data(self.course_id)[-1]]
mock_enrollment.return_value = api_trend
actual_summary, actual_trend = self.presenter.get_summary_and_trend_data()
self.assertDictEqual(actual_summary, get_mock_presenter_enrollment_summary_small())
self.assertListEqual(actual_trend, get_mock_presenter_enrollment_data_small(self.course_id))
@mock.patch('analyticsclient.course.Course.enrollment')
def test_get_geography_data(self, mock_enrollment):
# test with a full set of countries
mock_data = get_mock_api_enrollment_geography_data(self.course_id)
......
......@@ -42,6 +42,24 @@ def get_mock_enrollment_summary_and_trend(course_id):
return get_mock_enrollment_summary(), get_mock_enrollment_data(course_id)
def get_mock_presenter_enrollment_data_small(course_id):
single_enrollment = get_mock_enrollment_data(course_id)[-1]
empty_enrollment = {
'count': 0,
'date': '2014-01-30'
}
return [empty_enrollment, single_enrollment]
def get_mock_presenter_enrollment_summary_small():
return {
'last_updated': CREATED_DATETIME,
'current_enrollment': 30,
'enrollment_change_last_7_days': None,
}
def get_mock_api_enrollment_geography_data(course_id):
data = []
items = ((u'USA', u'United States', 500), (None, UNKNOWN_COUNTRY_CODE, 300),
......
......@@ -18,9 +18,9 @@ define(['bootstrap', 'd3', 'jquery', 'moment', 'nvd3', 'underscore', 'views/attr
*/
assembleTrendData: function () {
var self = this,
combinedTrends,
data = self.model.get(self.options.modelAttribute),
trendOptions = self.options.trends;
trendOptions = self.options.trends,
combinedTrends;
// parse and format the data for nvd3
combinedTrends = _(trendOptions).map(function (trendOption) {
......@@ -68,9 +68,6 @@ define(['bootstrap', 'd3', 'jquery', 'moment', 'nvd3', 'underscore', 'views/attr
// Remove the grid lines
canvas.selectAll('.nvd3 .nv-axis line').remove();
// Remove max value from the X-axis
canvas.select('.nvd3 .nv-axis.nv-x .nv-axisMaxMin:nth-child(3)').remove();
// Get the existing X-axis translation and shift it down a few more pixels.
axisEl = canvas.select('.nvd3 .nv-axis.nv-x');
matches = translateRegex.exec(axisEl.attr('transform'));
......@@ -92,11 +89,14 @@ define(['bootstrap', 'd3', 'jquery', 'moment', 'nvd3', 'underscore', 'views/attr
AttributeListenerView.prototype.render.call(this);
var self = this,
canvas = d3.select(self.el),
assembledData = self.assembleTrendData(),
displayExplicitTicksThreshold = 11,
chart,
$tooltip;
$tooltip,
xTicks;
chart = nvd3.models.lineChart()
.margin({top: 1})
.margin({top: 6})// minimize the spacing, but leave enough for point at the top to be shown w/o being clipped
.height(300) // This should be the same as the height set on the chart container in CSS.
.showLegend(false)
.useInteractiveGuideline(true)
......@@ -110,6 +110,16 @@ define(['bootstrap', 'd3', 'jquery', 'moment', 'nvd3', 'underscore', 'views/attr
return d[self.options.y.key];
});
// explicitly display tick marks for small numbers of points, otherwise
// ticks will be interpolated and dates look to be repeated on the x-axis
if(self.model.get(self.options.modelAttribute).length < displayExplicitTicksThreshold) {
// get dates for the explicit ticks -- assuming data isn't sparse
xTicks = _(self.assembleTrendData()[0].values).map(function (data) {
return Date.parse(data[self.options.x.key]);
});
chart.xAxis.tickValues(xTicks);
}
chart.xAxis
.tickFormat(function (d) {
// date is converted to unix timestamp from our original mm-dd-yyyy format
......@@ -139,13 +149,13 @@ define(['bootstrap', 'd3', 'jquery', 'moment', 'nvd3', 'underscore', 'views/attr
.append('div')
.attr('class', 'line-chart')
.append('svg')
.datum(self.assembleTrendData())
.datum(assembledData)
.call(chart);
self.styleChart();
nvd3.utils.windowResize(chart.update);
nv.utils.windowResize(function () {
nvd3.utils.windowResize(function () {
self.styleChart();
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment