mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-25 16:47:58 -05:00
8dbc86ca04
Hide TeachersContactModal after sending message Fix GET /db/level/:handle/session, more extensively test Fix EnrollmentView number of students input to stop losing focus on input Fix EnrollmentsView syntax Fix ActivateLicensesModal "Get Enrollments" button when already in the enrollments page Update EnrollmentsView with new credit numbers when ActivateLicensesModal closes Hide search box in TeacherClassView "Enrollment Status" tab Tweak EnrollmentsView styling Fix EnrollmentsView tests Fix AnalyticsView Make EnrollmentsView more explicitly handle undefined and empty array prepaid groups Remove log CoursesView handles JoinClassModal cancel Re-enable EditStudentModal set password button when the form changes Fix course instance tests, next level endpoint bug Fix EditStudentModal tests
765 lines
31 KiB
CoffeeScript
765 lines
31 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 = []
|
|
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
|
|
entry.events['DAU campaign total'] = campaignDauTotal
|
|
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)
|
|
entry.events['DAU classroom total'] = classroomDauTotal
|
|
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)
|
|
|
|
@activeUsers.sort (a, b) -> b.day.localeCompare(a.day)
|
|
|
|
@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: {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
|
|
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})
|
|
@courses.comparator = "_id"
|
|
@listenToOnce @courses, 'sync', @onCoursesSync
|
|
@supermodel.loadCollection(@courses)
|
|
|
|
onCoursesSync: ->
|
|
# Assumes courses retrieved in order
|
|
@courseOrderMap = {}
|
|
@courseOrderMap[@courses.models[i].get('_id')] = i for i in [0...@courses.models.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
|
|
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.coursePrepaidID or 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
|
|
if not teacherPaidStatusMap[teacher]
|
|
teacherPaidStatusMap[teacher] = studentPaidStatusMap[student]
|
|
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
|
|
else if teacherPaidStatusMap[teacher] is 'trial' and studentPaidStatusMap[student] is 'paid'
|
|
teacherPaidStatusMap[teacher] = studentPaidStatusMap[student]
|
|
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
|
|
else if teacherPaidStatusMap[teacher] is 'free' and studentPaidStatusMap[student] in ['paid', 'trial']
|
|
teacherPaidStatusMap[teacher] = studentPaidStatusMap[student]
|
|
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
|
|
else if 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('Introduction') >= 0 and b.courseName.indexOf('Introduction') < 0 then return -1
|
|
else if b.courseName.indexOf('Introduction') >= 0 and a.courseName.indexOf('Introduction') < 0 then return 1
|
|
else 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
|
|
a.courseName.localeCompare(b.courseName)
|
|
|
|
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
|
|
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 = []
|
|
total = 0
|
|
for entry in @paidCourseTotalEnrollments
|
|
total += entry.count
|
|
data.push
|
|
day: entry.day
|
|
value: total
|
|
points = @createLineChartPoints(days, data)
|
|
@enrollmentsChartLines.push
|
|
points: points
|
|
description: 'Total 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 = []
|
|
total = 0
|
|
for entry in @paidCourseRedeemedEnrollments
|
|
total += entry.count
|
|
data.push
|
|
day: entry.day
|
|
value: total
|
|
points = @createLineChartPoints(days, data)
|
|
@enrollmentsChartLines.push
|
|
points: points
|
|
description: 'Total 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 = []
|
|
total = 0
|
|
for entry in @trialCourseTotalEnrollments
|
|
total += entry.count
|
|
data.push
|
|
day: entry.day
|
|
value: total
|
|
points = @createLineChartPoints(days, data)
|
|
@enrollmentsChartLines.push
|
|
points: points
|
|
description: 'Total 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 = []
|
|
total = 0
|
|
for entry in @trialCourseRedeemedEnrollments
|
|
total += entry.count
|
|
data.push
|
|
day: entry.day
|
|
value: total
|
|
points = @createLineChartPoints(days, data)
|
|
@enrollmentsChartLines.push
|
|
points: points
|
|
description: 'Total 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)
|