codecombat/app/views/admin/AnalyticsView.coffee
Matt Lott 951db5a721 🐛Fix course ordering and furthest logic in dashboard
Also speeding up server APIs via lean() db calls
2016-09-19 13:17:02 -07:00

786 lines
32 KiB
CoffeeScript

CocoCollection = require 'collections/CocoCollection'
Course = require 'models/Course'
CourseInstance = require 'models/CourseInstance'
require 'vendor/d3'
d3Utils = require 'core/d3_utils'
Payment = require 'models/Payment'
RootView = require 'views/core/RootView'
template = require 'templates/admin/analytics'
utils = require 'core/utils'
module.exports = class AnalyticsView extends RootView
id: 'admin-analytics-view'
template: template
furthestCourseDayRangeRecent: 60
furthestCourseDayRange: 365
lineColors: ['red', 'blue', 'green', 'purple', 'goldenrod', 'brown', 'darkcyan']
minSchoolCount: 20
initialize: ->
@activeClasses = []
@activeClassGroups = {}
@activeUsers = []
@revenue = []
@revenueGroups = {}
@dayEnrollmentsMap = {}
@enrollmentDays = []
@loadData()
afterRender: ->
super()
@createLineCharts()
loadData: ->
@supermodel.addRequestResource({
url: '/db/analytics_perday/-/active_classes'
method: 'POST'
success: (data) =>
# Organize data by day, then group
groupMap = {}
dayGroupMap = {}
for activeClass in data
dayGroupMap[activeClass.day] ?= {}
dayGroupMap[activeClass.day]['Total'] = 0
for group, val of activeClass.classes
groupMap[group] = true
dayGroupMap[activeClass.day][group] = val
dayGroupMap[activeClass.day]['Total'] += val
@activeClassGroups = Object.keys(groupMap)
@activeClassGroups.push 'Total'
# Build list of active classes, where each entry is a day of individual group values
@activeClasses = []
for day of dayGroupMap
dashedDay = "#{day.substring(0, 4)}-#{day.substring(4, 6)}-#{day.substring(6, 8)}"
data = day: dashedDay, groups: []
for group in @activeClassGroups
data.groups.push(dayGroupMap[day][group] ? 0)
@activeClasses.push data
@activeClasses.sort (a, b) -> b.day.localeCompare(a.day)
@updateAllKPIChartData()
@updateActiveClassesChartData()
@render?()
}, 0).load()
@supermodel.addRequestResource({
url: '/db/analytics_perday/-/active_users'
method: 'POST'
success: (data) =>
@activeUsers = data.map (a) ->
a.day = "#{a.day.substring(0, 4)}-#{a.day.substring(4, 6)}-#{a.day.substring(6, 8)}"
a
# Add campaign/classroom DAU 30-day averages and daily totals
campaignDauTotals = []
classroomDauTotals = []
eventMap = {}
for entry in @activeUsers
day = entry.day
campaignDauTotal = 0
classroomDauTotal = 0
for event, count of entry.events
if event.indexOf('DAU campaign') >= 0
campaignDauTotal += count
else if event.indexOf('DAU classroom') >= 0
classroomDauTotal += count
eventMap[event] = true
entry.events['DAU campaign total'] = campaignDauTotal
eventMap['DAU campaign total'] = true
campaignDauTotals.unshift(campaignDauTotal)
campaignDauTotals.pop() while campaignDauTotals.length > 30
if campaignDauTotals.length is 30
entry.events['DAU campaign 30-day average'] = Math.round(_.reduce(campaignDauTotals, (a, b) -> a + b) / 30)
eventMap['DAU campaign 30-day average'] = true
entry.events['DAU classroom total'] = classroomDauTotal
eventMap['DAU classroom total'] = true
classroomDauTotals.unshift(classroomDauTotal)
classroomDauTotals.pop() while classroomDauTotals.length > 30
if classroomDauTotals.length is 30
entry.events['DAU classroom 30-day average'] = Math.round(_.reduce(classroomDauTotals, (a, b) -> a + b) / 30)
eventMap['DAU classroom 30-day average'] = true
@activeUsers.sort (a, b) -> b.day.localeCompare(a.day)
@activeUserEventNames = Object.keys(eventMap)
@activeUserEventNames.sort (a, b) ->
if a.indexOf('campaign') is b.indexOf('campaign') or a.indexOf('classroom') is b.indexOf('classroom')
a.localeCompare(b)
else if a.indexOf('campaign') > b.indexOf('campaign')
1
else
-1
@updateAllKPIChartData()
@updateActiveUsersChartData()
@updateCampaignVsClassroomActiveUsersChartData()
@render?()
}, 0).load()
@supermodel.addRequestResource({
url: '/db/analytics_perday/-/recurring_revenue'
method: 'POST'
success: (data) =>
# Organize data by day, then group
groupMap = {}
dayGroupCountMap = {}
for dailyRevenue in data
dayGroupCountMap[dailyRevenue.day] ?= {}
dayGroupCountMap[dailyRevenue.day]['DRR Total'] = 0
for group, val of dailyRevenue.groups
groupMap[group] = true
dayGroupCountMap[dailyRevenue.day][group] = val
dayGroupCountMap[dailyRevenue.day]['DRR Total'] += val
@revenueGroups = Object.keys(groupMap)
@revenueGroups.push 'DRR Total'
# Build list of recurring revenue entries, where each entry is a day of individual group values
@revenue = []
for day of dayGroupCountMap
dashedDay = "#{day.substring(0, 4)}-#{day.substring(4, 6)}-#{day.substring(6, 8)}"
data = day: dashedDay, groups: []
for group in @revenueGroups
data.groups.push(dayGroupCountMap[day][group] ? 0)
@revenue.push data
# Order present to past
@revenue.sort (a, b) -> b.day.localeCompare(a.day)
return unless @revenue.length > 0
# Add monthly recurring revenue values
# For each daily group, add up monthly values walking forward through time, and add to revenue groups
monthlyDailyGroupMap = {}
dailyGroupIndexMap = {}
for group, i in @revenueGroups
monthlyDailyGroupMap[group.replace('DRR', 'MRR')] = group
dailyGroupIndexMap[group] = i
for monthlyGroup, dailyGroup of monthlyDailyGroupMap
monthlyValues = []
for i in [@revenue.length-1..0]
dailyTotal = @revenue[i].groups[dailyGroupIndexMap[dailyGroup]]
monthlyValues.push(dailyTotal)
monthlyValues.shift() while monthlyValues.length > 30
if monthlyValues.length is 30
@revenue[i].groups.push(_.reduce(monthlyValues, (s, num) -> s + num))
for monthlyGroup, dailyGroup of monthlyDailyGroupMap
@revenueGroups.push monthlyGroup
@updateAllKPIChartData()
@updateRevenueChartData()
@render?()
}, 0).load()
@supermodel.addRequestResource({
url: '/db/user/-/school_counts'
method: 'POST'
data: {minCount: @minSchoolCount}
success: (@schoolCounts) =>
@schoolCounts?.sort (a, b) ->
return -1 if a.count > b.count
return 0 if a.count is b.count
1
@renderSelectors?('#school-counts')
}, 0).load()
@supermodel.addRequestResource({
url: '/db/payment/-/school_sales'
success: (@schoolSales) =>
@schoolSales?.sort (a, b) ->
return -1 if a.created > b.created
return 0 if a.created is b.created
1
@renderSelectors?('.school-sales')
}, 0).load()
@supermodel.addRequestResource({
url: '/db/prepaid/-/courses'
method: 'POST'
data: {project: {endDate: 1, maxRedeemers: 1, properties: 1, redeemers: 1}}
success: (prepaids) =>
paidDayMaxMap = {}
paidDayRedeemedMap = {}
trialDayMaxMap = {}
trialDayRedeemedMap = {}
for prepaid in prepaids
day = utils.objectIdToDate(prepaid._id).toISOString().substring(0, 10)
if prepaid.properties?.trialRequestID? or prepaid.properties?.endDate?
trialDayMaxMap[day] ?= 0
if prepaid.properties?.endDate?
trialDayMaxMap[day] += prepaid.redeemers?.length ? 0
else
trialDayMaxMap[day] += prepaid.maxRedeemers
for redeemer in (prepaid.redeemers ? [])
redeemDay = redeemer.date.substring(0, 10)
trialDayRedeemedMap[redeemDay] ?= 0
trialDayRedeemedMap[redeemDay]++
else if not prepaid.endDate? or new Date(prepaid.endDate) > new Date()
paidDayMaxMap[day] ?= 0
paidDayMaxMap[day] += prepaid.maxRedeemers
for redeemer in prepaid.redeemers
redeemDay = redeemer.date.substring(0, 10)
paidDayRedeemedMap[redeemDay] ?= 0
paidDayRedeemedMap[redeemDay]++
@dayEnrollmentsMap = {}
@paidCourseTotalEnrollments = []
for day, count of paidDayMaxMap
@paidCourseTotalEnrollments.push({day: day, count: count})
@dayEnrollmentsMap[day] ?= {paidIssued: 0, paidRedeemed: 0, trialIssued: 0, trialRedeemed: 0}
@dayEnrollmentsMap[day].paidIssued += count
@paidCourseTotalEnrollments.sort (a, b) -> a.day.localeCompare(b.day)
@paidCourseRedeemedEnrollments = []
for day, count of paidDayRedeemedMap
@paidCourseRedeemedEnrollments.push({day: day, count: count})
@dayEnrollmentsMap[day] ?= {paidIssued: 0, paidRedeemed: 0, trialIssued: 0, trialRedeemed: 0}
@dayEnrollmentsMap[day].paidRedeemed += count
@paidCourseRedeemedEnrollments.sort (a, b) -> a.day.localeCompare(b.day)
@trialCourseTotalEnrollments = []
for day, count of trialDayMaxMap
@trialCourseTotalEnrollments.push({day: day, count: count})
@dayEnrollmentsMap[day] ?= {paidIssued: 0, paidRedeemed: 0, trialIssued: 0, trialRedeemed: 0}
@dayEnrollmentsMap[day].trialIssued += count
@trialCourseTotalEnrollments.sort (a, b) -> a.day.localeCompare(b.day)
@trialCourseRedeemedEnrollments = []
for day, count of trialDayRedeemedMap
@trialCourseRedeemedEnrollments.push({day: day, count: count})
@dayEnrollmentsMap[day] ?= {paidIssued: 0, paidRedeemed: 0, trialIssued: 0, trialRedeemed: 0}
@dayEnrollmentsMap[day].trialRedeemed += count
@trialCourseRedeemedEnrollments.sort (a, b) -> a.day.localeCompare(b.day)
@updateEnrollmentsChartData()
@render?()
}, 0).load()
@courses = new CocoCollection([], { url: "/db/course", model: Course})
@listenToOnce @courses, 'sync', @onCoursesSync
@supermodel.loadCollection(@courses)
onCoursesSync: ->
@courses.remove(@courses.findWhere({releasePhase: 'beta'}))
sortedCourses = utils.sortCourses(@courses.models ? [])
@courseOrderMap = {}
@courseOrderMap[sortedCourses[i].get('_id')] = i for i in [0...sortedCourses.length]
startDay = new Date()
startDay.setUTCDate(startDay.getUTCDate() - @furthestCourseDayRange)
startDay = startDay.toISOString().substring(0, 10)
options =
url: '/db/course_instance/-/recent'
method: 'POST'
data: {startDay: startDay}
options.error = (models, response, options) =>
return if @destroyed
console.error 'Failed to get recent course instances', response
options.success = (data) =>
@onCourseInstancesSync(data)
@renderSelectors?('#furthest-course')
@supermodel.addRequestResource(options, 0).load()
onCourseInstancesSync: (data) ->
@courseDistributionsRecent = []
@courseDistributions = []
return unless data.courseInstances and data.students and data.prepaids
createCourseDistributions = (numDays) =>
# Find student furthest course
startDate = new Date()
startDate.setUTCDate(startDate.getUTCDate() - numDays)
teacherStudentsMap = {}
studentFurthestCourseMap = {}
studentPaidStatusMap = {}
for courseInstance in data.courseInstances
continue if utils.objectIdToDate(courseInstance._id) < startDate
courseID = courseInstance.courseID
unless @courseOrderMap[courseID]?
console.error "ERROR: no course order for courseID=#{courseID}"
continue
teacherID = courseInstance.ownerID
for studentID in courseInstance.members
studentPaidStatusMap[studentID] = 'free'
if not studentFurthestCourseMap[studentID] or studentFurthestCourseMap[studentID] < @courseOrderMap[courseID]
studentFurthestCourseMap[studentID] = @courseOrderMap[courseID]
teacherStudentsMap[teacherID] ?= []
teacherStudentsMap[teacherID].push(studentID)
# Find paid students
prepaidUserMap = {}
for user in data.students
continue unless studentPaidStatusMap[user._id]
if prepaidID = user.coursePrepaid?._id
studentPaidStatusMap[user._id] = 'paid'
prepaidUserMap[prepaidID] ?= []
prepaidUserMap[prepaidID].push(user._id)
# Find trial students
for prepaid in data.prepaids
continue unless prepaidUserMap[prepaid._id]
if prepaid.properties?.trialRequestID
for userID in prepaidUserMap[prepaid._id]
studentPaidStatusMap[userID] = 'trial'
# Find teacher furthest course and paid status based on their students
# Paid teacher: at least one paid student
# Trial teacher: at least one trial student in course instance, and no paid students
# Free teacher: no paid students, no trial students
# Teacher furthest course is furthest course of highest paid status student
teacherFurthestCourseMap = {}
teacherPaidStatusMap = {}
for teacher, students of teacherStudentsMap
for student in students
unless studentFurthestCourseMap[student]?
console.error "ERROR: no student furthest map for teacher=#{teacher} student=#{student}"
continue
if not teacherPaidStatusMap[teacher]
teacherPaidStatusMap[teacher] = studentPaidStatusMap[student]
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
else if teacherPaidStatusMap[teacher] is 'paid'
if studentPaidStatusMap[student] is 'paid' and teacherFurthestCourseMap[teacher] < studentFurthestCourseMap[student]
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
else if teacherPaidStatusMap[teacher] is 'trial'
if studentPaidStatusMap[student] is 'paid'
teacherPaidStatusMap[teacher] = studentPaidStatusMap[student]
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
else if studentPaidStatusMap[student] is 'trial' and teacherFurthestCourseMap[teacher] < studentFurthestCourseMap[student]
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
else # free teacher
if studentPaidStatusMap[student] in ['paid', 'trial']
teacherPaidStatusMap[teacher] = studentPaidStatusMap[student]
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
else if studentPaidStatusMap[student] is 'free' and teacherFurthestCourseMap[teacher] < studentFurthestCourseMap[student]
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
# Build table of student/teacher paid/trial/free totals
updateCourseTotalsMap = (courseTotalsMap, furthestCourseMap, paidStatusMap, columnSuffix) =>
for user, courseIndex of furthestCourseMap
courseName = @courses.models[courseIndex].get('name')
courseTotalsMap[courseName] ?= {}
columnName = switch paidStatusMap[user]
when 'paid' then 'Paid ' + columnSuffix
when 'trial' then 'Trial ' + columnSuffix
when 'free' then 'Free ' + columnSuffix
courseTotalsMap[courseName][columnName] ?= 0
courseTotalsMap[courseName][columnName]++
courseTotalsMap[courseName]['Total ' + columnSuffix] ?= 0
courseTotalsMap[courseName]['Total ' + columnSuffix]++
courseTotalsMap['All Courses']['Total ' + columnSuffix] ?= 0
courseTotalsMap['All Courses']['Total ' + columnSuffix]++
courseTotalsMap['All Courses'][columnName] ?= 0
courseTotalsMap['All Courses'][columnName]++
courseTotalsMap = {'All Courses': {}}
updateCourseTotalsMap(courseTotalsMap, teacherFurthestCourseMap, teacherPaidStatusMap, 'Teachers')
updateCourseTotalsMap(courseTotalsMap, studentFurthestCourseMap, studentPaidStatusMap, 'Students')
courseDistributions = []
for courseName, totals of courseTotalsMap
courseDistributions.push({courseName: courseName, totals: totals})
courseDistributions.sort (a, b) =>
if a.courseName.indexOf('All Courses') >= 0 and b.courseName.indexOf('All Courses') < 0 then return 1
else if b.courseName.indexOf('All Courses') >= 0 and a.courseName.indexOf('All Courses') < 0 then return -1
aID = @courses.findWhere({name: a.courseName}).id
bID = @courses.findWhere({name: b.courseName}).id
@courseOrderMap[aID] - @courseOrderMap[bID]
courseDistributions
@courseDistributionsRecent = createCourseDistributions(@furthestCourseDayRangeRecent)
@courseDistributions = createCourseDistributions(@furthestCourseDayRange)
createLineChartPoints: (days, data) ->
points = []
for entry, i in data
points.push
day: entry.day
y: entry.value
# Trim points preceding days
if points.length and days.length and points[0].day.localeCompare(days[0]) < 0
if points[points.length - 1].day.localeCompare(days[0]) < 0
points = []
else
for point, i in points
if point.day.localeCompare(days[0]) >= 0
points.splice(0, i)
break
# Ensure points for each day
for day, i in days
if points.length <= i or points[i]?.day isnt day
prevY = if i > 0 then points[i - 1].y else 0.0
points.splice i, 0,
day: day
y: prevY
points[i].x = i
points.splice(0, points.length - days.length) if points.length > days.length
points
createLineCharts: ->
visibleWidth = $('.kpi-recent-chart').width()
d3Utils.createLineChart('.kpi-recent-chart', @kpiRecentChartLines, visibleWidth)
d3Utils.createLineChart('.kpi-chart', @kpiChartLines, visibleWidth)
d3Utils.createLineChart('.active-classes-chart-90', @activeClassesChartLines90, visibleWidth)
d3Utils.createLineChart('.active-classes-chart-365', @activeClassesChartLines365, visibleWidth)
d3Utils.createLineChart('.classroom-daily-active-users-chart-90', @classroomDailyActiveUsersChartLines90, visibleWidth)
d3Utils.createLineChart('.classroom-monthly-active-users-chart-90', @classroomMonthlyActiveUsersChartLines90, visibleWidth)
d3Utils.createLineChart('.classroom-daily-active-users-chart-365', @classroomDailyActiveUsersChartLines365, visibleWidth)
d3Utils.createLineChart('.classroom-monthly-active-users-chart-365', @classroomMonthlyActiveUsersChartLines365, visibleWidth)
d3Utils.createLineChart('.campaign-daily-active-users-chart-90', @campaignDailyActiveUsersChartLines90, visibleWidth)
d3Utils.createLineChart('.campaign-monthly-active-users-chart-90', @campaignMonthlyActiveUsersChartLines90, visibleWidth)
d3Utils.createLineChart('.campaign-daily-active-users-chart-365', @campaignDailyActiveUsersChartLines365, visibleWidth)
d3Utils.createLineChart('.campaign-monthly-active-users-chart-365', @campaignMonthlyActiveUsersChartLines365, visibleWidth)
d3Utils.createLineChart('.campaign-vs-classroom-monthly-active-users-recent-chart.line-chart-container', @campaignVsClassroomMonthlyActiveUsersRecentChartLines, visibleWidth)
d3Utils.createLineChart('.campaign-vs-classroom-monthly-active-users-chart.line-chart-container', @campaignVsClassroomMonthlyActiveUsersChartLines, visibleWidth)
d3Utils.createLineChart('.paid-courses-chart', @enrollmentsChartLines, visibleWidth)
d3Utils.createLineChart('.recurring-daily-revenue-chart-90', @revenueDailyChartLines90Days, visibleWidth)
d3Utils.createLineChart('.recurring-monthly-revenue-chart-90', @revenueMonthlyChartLines90Days, visibleWidth)
d3Utils.createLineChart('.recurring-daily-revenue-chart-365', @revenueDailyChartLines365Days, visibleWidth)
d3Utils.createLineChart('.recurring-monthly-revenue-chart-365', @revenueMonthlyChartLines365Days, visibleWidth)
updateAllKPIChartData: ->
@kpiRecentChartLines = []
@kpiChartLines = []
@updateKPIChartData(60, @kpiRecentChartLines)
@updateKPIChartData(365, @kpiChartLines)
updateKPIChartData: (timeframeDays, chartLines) ->
days = d3Utils.createContiguousDays(timeframeDays)
# Build active classes KPI line
if @activeClasses?.length > 0
data = []
for entry in @activeClasses
data.push
day: entry.day
value: entry.groups[entry.groups.length - 1]
data.reverse()
points = @createLineChartPoints(days, data)
chartLines.push
points: points
description: 'Monthly Active Classes'
lineColor: 'blue'
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: true
# Build recurring revenue KPI line
if @revenue?.length > 0
data = []
for entry in @revenue
data.push
day: entry.day
value: entry.groups[entry.groups.length - 1] / 100000
data.reverse()
points = @createLineChartPoints(days, data)
chartLines.push
points: points
description: 'Monthly Recurring Revenue (in thousands)'
lineColor: 'green'
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: true
# Build campaign MAU KPI line
if @activeUsers?.length > 0
eventDayDataMap = {}
for entry in @activeUsers
day = entry.day
for event, count of entry.events
if event.indexOf('MAU campaign') >= 0
eventDayDataMap['MAU campaign'] ?= {}
eventDayDataMap['MAU campaign'][day] ?= 0
eventDayDataMap['MAU campaign'][day] += count
campaignData = []
for event, entry of eventDayDataMap
for day, count of entry
campaignData.push day: day, value: count / 1000
campaignData.reverse()
points = @createLineChartPoints(days, campaignData)
chartLines.push
points: points
description: 'Campaign Monthly Active Users (in thousands)'
lineColor: 'purple'
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: true
updateActiveClassesChartData: ->
@activeClassesChartLines90 = []
@activeClassesChartLines365 = []
return unless @activeClasses?.length
groupDayMap = {}
for entry in @activeClasses
for count, i in entry.groups
groupDayMap[@activeClassGroups[i]] ?= {}
groupDayMap[@activeClassGroups[i]][entry.day] ?= 0
groupDayMap[@activeClassGroups[i]][entry.day] += count
createActiveClassesChartLines = (lines, numDays) =>
days = d3Utils.createContiguousDays(numDays)
colorIndex = 0
totalMax = 0
for group, entries of groupDayMap
data = []
for day, count of entries
data.push
day: day
value: count
data.reverse()
points = @createLineChartPoints(days, data)
lines.push
points: points
description: group.replace('Active classes ', '')
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: group is 'Total'
totalMax = _.max(points, 'y').y if group is 'Total'
line.max = totalMax for line in lines
createActiveClassesChartLines(@activeClassesChartLines90, 90)
createActiveClassesChartLines(@activeClassesChartLines365, 365)
updateActiveUsersChartData: ->
# Create chart lines for the active user events returned by active_users in analytics_perday_handler
@campaignDailyActiveUsersChartLines90 = []
@campaignMonthlyActiveUsersChartLines90 = []
@campaignDailyActiveUsersChartLines365 = []
@campaignMonthlyActiveUsersChartLines365 = []
@classroomDailyActiveUsersChartLines90 = []
@classroomMonthlyActiveUsersChartLines90 = []
@classroomDailyActiveUsersChartLines365 = []
@classroomMonthlyActiveUsersChartLines365 = []
return unless @activeUsers?.length
# Separate day/value arrays by event
eventDataMap = {}
for entry in @activeUsers
day = entry.day
for event, count of entry.events
eventDataMap[event] ?= []
eventDataMap[event].push
day: entry.day
value: count
createActiveUsersChartLines = (lines, numDays, eventPrefix) =>
days = d3Utils.createContiguousDays(numDays)
colorIndex = 0
lineMax = 0
showYScale = true
for event, data of eventDataMap
continue unless event.indexOf(eventPrefix) >= 0
points = @createLineChartPoints(days, _.cloneDeep(data).reverse())
lineMax = Math.max(_.max(points, 'y').y, lineMax)
lines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: showYScale
showYScale = false
line.max = lineMax for line in lines
createActiveUsersChartLines(@campaignDailyActiveUsersChartLines90, 90, 'DAU campaign')
createActiveUsersChartLines(@campaignMonthlyActiveUsersChartLines90, 90, 'MAU campaign')
createActiveUsersChartLines(@classroomDailyActiveUsersChartLines90, 90, 'DAU classroom')
createActiveUsersChartLines(@classroomMonthlyActiveUsersChartLines90, 90, 'MAU classroom')
createActiveUsersChartLines(@campaignDailyActiveUsersChartLines365, 365, 'DAU campaign')
createActiveUsersChartLines(@campaignMonthlyActiveUsersChartLines365, 365, 'MAU campaign')
createActiveUsersChartLines(@classroomDailyActiveUsersChartLines365, 365, 'DAU classroom')
createActiveUsersChartLines(@classroomMonthlyActiveUsersChartLines365, 365, 'MAU classroom')
updateCampaignVsClassroomActiveUsersChartData: ->
@campaignVsClassroomMonthlyActiveUsersRecentChartLines = []
@campaignVsClassroomMonthlyActiveUsersChartLines = []
return unless @activeUsers?.length
# Separate day/value arrays by event
eventDataMap = {}
for entry in @activeUsers
day = entry.day
for event, count of entry.events
eventDataMap[event] ?= []
eventDataMap[event].push
day: entry.day
value: count
days = d3Utils.createContiguousDays(90)
colorIndex = 0
max = 0
for event, data of eventDataMap
if event is 'MAU campaign paid'
points = @createLineChartPoints(days, _.cloneDeep(data).reverse())
max = Math.max(max, _.max(points, 'y').y)
@campaignVsClassroomMonthlyActiveUsersRecentChartLines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: true
else if event is 'MAU classroom paid'
points = @createLineChartPoints(days, _.cloneDeep(data).reverse())
max = Math.max(max, _.max(points, 'y').y)
@campaignVsClassroomMonthlyActiveUsersRecentChartLines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: false
for line in @campaignVsClassroomMonthlyActiveUsersRecentChartLines
line.max = max
days = d3Utils.createContiguousDays(365)
colorIndex = 0
max = 0
for event, data of eventDataMap
if event is 'MAU campaign paid'
points = @createLineChartPoints(days, _.cloneDeep(data).reverse())
max = Math.max(max, _.max(points, 'y').y)
@campaignVsClassroomMonthlyActiveUsersChartLines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: true
else if event is 'MAU classroom paid'
points = @createLineChartPoints(days, _.cloneDeep(data).reverse())
max = Math.max(max, _.max(points, 'y').y)
@campaignVsClassroomMonthlyActiveUsersChartLines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: false
for line in @campaignVsClassroomMonthlyActiveUsersChartLines
line.max = max
updateEnrollmentsChartData: ->
@enrollmentsChartLines = []
return unless @paidCourseTotalEnrollments?.length and @trialCourseTotalEnrollments?.length
days = d3Utils.createContiguousDays(90, false)
@enrollmentDays = _.cloneDeep(days)
@enrollmentDays.reverse()
colorIndex = 0
dailyMax = 0
data = []
for entry in @paidCourseTotalEnrollments
data.push
day: entry.day
value: entry.count
points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push
points: points
description: 'Paid enrollments issued'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: true
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = []
for entry in @paidCourseRedeemedEnrollments
data.push
day: entry.day
value: entry.count
points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push
points: points
description: 'Paid enrollments redeemed'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: false
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = []
for entry in @trialCourseTotalEnrollments
data.push
day: entry.day
value: entry.count
points = @createLineChartPoints(days, data, true)
@enrollmentsChartLines.push
points: points
description: 'Trial enrollments issued'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: false
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = []
for entry in @trialCourseRedeemedEnrollments
data.push
day: entry.day
value: entry.count
points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push
points: points
description: 'Trial enrollments redeemed'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: false
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
line.max = dailyMax for line in @enrollmentsChartLines
updateRevenueChartData: ->
@revenueDailyChartLines90Days = []
@revenueMonthlyChartLines90Days = []
@revenueDailyChartLines365Days = []
@revenueMonthlyChartLines365Days = []
return unless @revenue?.length
groupDayMap = {}
for entry in @revenue
for count, i in entry.groups
groupDayMap[@revenueGroups[i]] ?= {}
groupDayMap[@revenueGroups[i]][entry.day] ?= 0
groupDayMap[@revenueGroups[i]][entry.day] += count
addRevenueChartLine = (days, eventPrefix, lines) =>
colorIndex = 0
dailyMax = 0
for group, entries of groupDayMap
continue unless group.indexOf(eventPrefix) >= 0
data = []
for day, count of entries
data.push
day: day
value: count / 100
data.reverse()
points = @createLineChartPoints(days, data)
lines.push
points: points
description: group.replace(eventPrefix + ' ', 'Daily ')
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: group is eventPrefix + ' Total'
dailyMax = _.max(points, 'y').y if group is eventPrefix + ' Total'
for line in lines
line.max = dailyMax
addRevenueChartLine(d3Utils.createContiguousDays(90), 'DRR', @revenueDailyChartLines90Days)
addRevenueChartLine(d3Utils.createContiguousDays(90), 'MRR', @revenueMonthlyChartLines90Days)
addRevenueChartLine(d3Utils.createContiguousDays(365), 'DRR', @revenueDailyChartLines365Days)
addRevenueChartLine(d3Utils.createContiguousDays(365), 'MRR', @revenueMonthlyChartLines365Days)