mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-01-08 05:32:18 -05:00
609884eb51
Adding a second table, setting range to be 60 and 365 days. Updating teacher columns to prioritize student paid status over furthest student course.
766 lines
31 KiB
CoffeeScript
766 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
|
|
|
|
constructor: (options) ->
|
|
super options
|
|
@loadData()
|
|
|
|
getRenderData: ->
|
|
context = super()
|
|
context.activeClasses = @activeClasses ? []
|
|
context.activeClassGroups = @activeClassGroups ? {}
|
|
context.activeUsers = @activeUsers ? []
|
|
context.revenue = @revenue ? []
|
|
context.revenueGroups = @revenueGroups ? {}
|
|
context.dayEnrollmentsMap = @dayEnrollmentsMap ? {}
|
|
context.enrollmentDays = @enrollmentDays ? []
|
|
context
|
|
|
|
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
|
|
@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
|
|
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 and classroom MAU KPI lines
|
|
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
|
|
else if event.indexOf('MAU classroom') >= 0
|
|
eventDayDataMap['MAU classroom'] ?= {}
|
|
eventDayDataMap['MAU classroom'][day] ?= 0
|
|
eventDayDataMap['MAU classroom'][day] += count
|
|
|
|
campaignData = []
|
|
classroomData = []
|
|
for event, entry of eventDayDataMap
|
|
if event is 'MAU campaign'
|
|
for day, count of entry
|
|
campaignData.push day: day, value: count / 1000
|
|
else
|
|
for day, count of entry
|
|
classroomData.push day: day, value: count / 1000
|
|
campaignData.reverse()
|
|
classroomData.reverse()
|
|
|
|
points = @createLineChartPoints(days, classroomData)
|
|
chartLines.push
|
|
points: points
|
|
description: 'Classroom Monthly Active Users (in thousands)'
|
|
lineColor: 'red'
|
|
strokeWidth: 1
|
|
min: 0
|
|
max: _.max(points, 'y').y
|
|
showYScale: true
|
|
|
|
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)
|