Commit 18921872 by Dennis Jen

Display empty previous data point on trend line when only one data point exists.

parent 140e5294
...@@ -52,24 +52,36 @@ class CourseEngagementPresenter(BasePresenter): ...@@ -52,24 +52,36 @@ class CourseEngagementPresenter(BasePresenter):
return activities 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): def _build_trend(self, api_trends):
""" """
Format activity trends for specified date range and return results with Format activity trends for specified date range and return results with
zeros filled in for all activities. zeros filled in for all activities.
""" """
trend_types = self.get_activity_types() 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 # 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) # the field if no data exists for it, so we fill in the zeros here)
trends = []
for datum in api_trends: for datum in api_trends:
# convert end of interval to ending day of week # convert end of interval to ending day of week
interval_end = self.parse_api_datetime(datum['interval_end']) interval_end = self.parse_api_datetime(datum['interval_end'])
week_ending = interval_end.date() - datetime.timedelta(days=1) week_ending = interval_end.date() - datetime.timedelta(days=1)
trend_week = {'weekEnding': week_ending.isoformat()} trends.append(self._build_trend_week(trend_types, week_ending, datum))
for trend_type in trend_types:
trend_week[trend_type] = datum[trend_type] or 0
trends.append(trend_week)
return trends return trends
...@@ -116,8 +128,18 @@ class CourseEnrollmentPresenter(BasePresenter): ...@@ -116,8 +128,18 @@ class CourseEnrollmentPresenter(BasePresenter):
now = datetime.datetime.utcnow().strftime(Client.DATE_FORMAT) now = datetime.datetime.utcnow().strftime(Client.DATE_FORMAT)
trends = self.course.enrollment(start_date=None, end_date=now) trends = self.course.enrollment(start_date=None, end_date=now)
summary = self._build_summary(trends) 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 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): def get_geography_data(self):
""" """
Returns a list of course geography data and the updated date (ex. 2014-1-31). Returns a list of course geography data and the updated date (ex. 2014-1-31).
......
...@@ -6,7 +6,8 @@ from django.test import TestCase ...@@ -6,7 +6,8 @@ from django.test import TestCase
import analyticsclient.constants.activity_type as AT import analyticsclient.constants.activity_type as AT
from courses.presenters import CourseEngagementPresenter, CourseEnrollmentPresenter, BasePresenter 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, get_mock_presenter_enrollment_geography_data, \
get_mock_api_enrollment_geography_data_limited, get_mock_presenter_enrollment_geography_data_limited, \ get_mock_api_enrollment_geography_data_limited, get_mock_presenter_enrollment_geography_data_limited, \
mock_course_activity, CREATED_DATETIME mock_course_activity, CREATED_DATETIME
...@@ -39,12 +40,24 @@ class CourseEngagementPresenterTests(TestCase): ...@@ -39,12 +40,24 @@ class CourseEngagementPresenterTests(TestCase):
return trends 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, _created = Switch.objects.get_or_create(name='show_engagement_forum_activity')
switch.active = include_forum_activity switch.active = include_forum_activity
switch.save() switch.save()
expected_trends = self.get_expected_trends(include_forum_activity)
summary, trends = self.presenter.get_summary_and_trend_data() summary, trends = self.presenter.get_summary_and_trend_data()
# Validate the trends # Validate the trends
...@@ -66,8 +79,16 @@ class CourseEngagementPresenterTests(TestCase): ...@@ -66,8 +79,16 @@ class CourseEngagementPresenterTests(TestCase):
@mock.patch('analyticsclient.course.Course.activity', mock.Mock(side_effect=mock_course_activity)) @mock.patch('analyticsclient.course.Course.activity', mock.Mock(side_effect=mock_course_activity))
def test_get_summary_and_trend_data(self): def test_get_summary_and_trend_data(self):
self.assertSummaryAndTrendsValid(False) self.assertSummaryAndTrendsValid(False, self.get_expected_trends(False))
self.assertSummaryAndTrendsValid(True) 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): class BasePresenterTests(TestCase):
...@@ -119,6 +140,15 @@ class CourseEnrollmentPresenterTests(TestCase): ...@@ -119,6 +140,15 @@ class CourseEnrollmentPresenterTests(TestCase):
self.assertListEqual(actual_trend, expected_trend) self.assertListEqual(actual_trend, expected_trend)
@mock.patch('analyticsclient.course.Course.enrollment') @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): def test_get_geography_data(self, mock_enrollment):
# test with a full set of countries # test with a full set of countries
mock_data = get_mock_api_enrollment_geography_data(self.course_id) mock_data = get_mock_api_enrollment_geography_data(self.course_id)
......
...@@ -42,6 +42,24 @@ def get_mock_enrollment_summary_and_trend(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) 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): def get_mock_api_enrollment_geography_data(course_id):
data = [] data = []
items = ((u'USA', u'United States', 500), (None, UNKNOWN_COUNTRY_CODE, 300), 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 ...@@ -18,9 +18,9 @@ define(['bootstrap', 'd3', 'jquery', 'moment', 'nvd3', 'underscore', 'views/attr
*/ */
assembleTrendData: function () { assembleTrendData: function () {
var self = this, var self = this,
combinedTrends,
data = self.model.get(self.options.modelAttribute), data = self.model.get(self.options.modelAttribute),
trendOptions = self.options.trends; trendOptions = self.options.trends,
combinedTrends;
// parse and format the data for nvd3 // parse and format the data for nvd3
combinedTrends = _(trendOptions).map(function (trendOption) { combinedTrends = _(trendOptions).map(function (trendOption) {
...@@ -68,9 +68,6 @@ define(['bootstrap', 'd3', 'jquery', 'moment', 'nvd3', 'underscore', 'views/attr ...@@ -68,9 +68,6 @@ define(['bootstrap', 'd3', 'jquery', 'moment', 'nvd3', 'underscore', 'views/attr
// Remove the grid lines // Remove the grid lines
canvas.selectAll('.nvd3 .nv-axis line').remove(); 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. // Get the existing X-axis translation and shift it down a few more pixels.
axisEl = canvas.select('.nvd3 .nv-axis.nv-x'); axisEl = canvas.select('.nvd3 .nv-axis.nv-x');
matches = translateRegex.exec(axisEl.attr('transform')); matches = translateRegex.exec(axisEl.attr('transform'));
...@@ -92,11 +89,14 @@ define(['bootstrap', 'd3', 'jquery', 'moment', 'nvd3', 'underscore', 'views/attr ...@@ -92,11 +89,14 @@ define(['bootstrap', 'd3', 'jquery', 'moment', 'nvd3', 'underscore', 'views/attr
AttributeListenerView.prototype.render.call(this); AttributeListenerView.prototype.render.call(this);
var self = this, var self = this,
canvas = d3.select(self.el), canvas = d3.select(self.el),
assembledData = self.assembleTrendData(),
displayExplicitTicksThreshold = 11,
chart, chart,
$tooltip; $tooltip,
xTicks;
chart = nvd3.models.lineChart() 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. .height(300) // This should be the same as the height set on the chart container in CSS.
.showLegend(false) .showLegend(false)
.useInteractiveGuideline(true) .useInteractiveGuideline(true)
...@@ -110,6 +110,16 @@ define(['bootstrap', 'd3', 'jquery', 'moment', 'nvd3', 'underscore', 'views/attr ...@@ -110,6 +110,16 @@ define(['bootstrap', 'd3', 'jquery', 'moment', 'nvd3', 'underscore', 'views/attr
return d[self.options.y.key]; 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 chart.xAxis
.tickFormat(function (d) { .tickFormat(function (d) {
// date is converted to unix timestamp from our original mm-dd-yyyy format // 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 ...@@ -139,13 +149,13 @@ define(['bootstrap', 'd3', 'jquery', 'moment', 'nvd3', 'underscore', 'views/attr
.append('div') .append('div')
.attr('class', 'line-chart') .attr('class', 'line-chart')
.append('svg') .append('svg')
.datum(self.assembleTrendData()) .datum(assembledData)
.call(chart); .call(chart);
self.styleChart(); self.styleChart();
nvd3.utils.windowResize(chart.update); nvd3.utils.windowResize(chart.update);
nv.utils.windowResize(function () { nvd3.utils.windowResize(function () {
self.styleChart(); 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