2016-01-26 14:36:40 -08:00
CocoCollection = require ' collections/CocoCollection '
Course = require ' models/Course '
CourseInstance = require ' models/CourseInstance '
2015-11-10 11:20:35 -08:00
require ' vendor/d3 '
d3Utils = require ' core/d3_utils '
2016-02-23 14:04:35 -08:00
Payment = require ' models/Payment '
2015-11-04 10:54:40 -08:00
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
2016-02-24 06:24:58 -08:00
furthestCourseDayRangeRecent: 60
furthestCourseDayRange: 365
2015-11-10 11:20:35 -08:00
lineColors: [ ' red ' , ' blue ' , ' green ' , ' purple ' , ' goldenrod ' , ' brown ' , ' darkcyan ' ]
2016-02-04 17:31:25 -08:00
minSchoolCount: 20
2015-11-04 10:54:40 -08:00
2016-05-18 17:37:20 +07:00
initialize: ->
@activeClasses = [ ]
@activeClassGroups = { }
@activeUsers = [ ]
@revenue = [ ]
@revenueGroups = { }
@dayEnrollmentsMap = { }
@enrollmentDays = [ ]
2015-11-10 11:20:35 -08:00
@ loadData ( )
2015-11-06 14:11:36 -08:00
2015-11-10 11:20:35 -08:00
afterRender: ->
super ( )
@ createLineCharts ( )
loadData: ->
2016-02-06 14:02:44 -08:00
@ supermodel . addRequestResource ( {
2015-11-06 14:11:36 -08:00
url: ' /db/analytics_perday/-/active_classes '
method: ' POST '
success: (data) =>
2015-11-10 11:20:35 -08:00
# Organize data by day, then group
groupMap = { }
dayGroupMap = { }
2015-11-06 14:11:36 -08:00
for activeClass in data
2015-11-10 11:20:35 -08:00
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 )
2015-11-06 14:11:36 -08:00
@ activeClassGroups . push ' Total '
2015-11-10 11:20:35 -08:00
# Build list of active classes, where each entry is a day of individual group values
2015-11-06 14:11:36 -08:00
@activeClasses = [ ]
2015-11-10 11:20:35 -08:00
for day of dayGroupMap
dashedDay = " #{ day . substring ( 0 , 4 ) } - #{ day . substring ( 4 , 6 ) } - #{ day . substring ( 6 , 8 ) } "
data = day: dashedDay , groups: [ ]
2015-11-06 14:11:36 -08:00
for group in @ activeClassGroups
2015-11-10 11:20:35 -08:00
data . groups . push ( dayGroupMap [ day ] [ group ] ? 0 )
2015-11-06 14:11:36 -08:00
@ activeClasses . push data
@ activeClasses . sort (a, b) -> b . day . localeCompare ( a . day )
2015-11-10 11:20:35 -08:00
@ updateAllKPIChartData ( )
@ updateActiveClassesChartData ( )
2015-11-06 14:11:36 -08:00
@ render ? ( )
} , 0 ) . load ( )
2015-11-04 10:54:40 -08:00
2016-02-06 14:02:44 -08:00
@ supermodel . addRequestResource ( {
2015-11-08 17:00:24 -08:00
url: ' /db/analytics_perday/-/active_users '
method: ' POST '
success: (data) =>
2015-11-10 11:20:35 -08:00
@activeUsers = data . map (a) ->
a.day = " #{ a . day . substring ( 0 , 4 ) } - #{ a . day . substring ( 4 , 6 ) } - #{ a . day . substring ( 6 , 8 ) } "
a
2016-03-08 15:27:54 -08:00
# 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 )
2015-11-08 17:00:24 -08:00
@ activeUsers . sort (a, b) -> b . day . localeCompare ( a . day )
2015-11-10 11:20:35 -08:00
@ updateAllKPIChartData ( )
@ updateActiveUsersChartData ( )
2016-02-19 09:32:17 -08:00
@ updateCampaignVsClassroomActiveUsersChartData ( )
2015-11-08 17:00:24 -08:00
@ render ? ( )
} , 0 ) . load ( )
2016-02-06 14:02:44 -08:00
@ supermodel . addRequestResource ( {
2015-11-08 17:00:24 -08:00
url: ' /db/analytics_perday/-/recurring_revenue '
method: ' POST '
success: (data) =>
2016-02-20 17:27:25 -08:00
2015-11-10 11:20:35 -08:00
# Organize data by day, then group
groupMap = { }
2015-11-08 17:00:24 -08:00
dayGroupCountMap = { }
for dailyRevenue in data
dayGroupCountMap [ dailyRevenue . day ] ? = { }
2016-02-20 17:27:25 -08:00
dayGroupCountMap [ dailyRevenue . day ] [ ' DRR Total ' ] = 0
2015-11-08 17:00:24 -08:00
for group , val of dailyRevenue . groups
2015-11-10 11:20:35 -08:00
groupMap [ group ] = true
2015-11-08 17:00:24 -08:00
dayGroupCountMap [ dailyRevenue . day ] [ group ] = val
2016-02-20 17:27:25 -08:00
dayGroupCountMap [ dailyRevenue . day ] [ ' DRR Total ' ] += val
2015-11-10 11:20:35 -08:00
@revenueGroups = Object . keys ( groupMap )
2016-02-20 17:27:25 -08:00
@ revenueGroups . push ' DRR Total '
2015-11-10 11:20:35 -08:00
# Build list of recurring revenue entries, where each entry is a day of individual group values
2015-11-08 17:00:24 -08:00
@revenue = [ ]
for day of dayGroupCountMap
2015-11-10 11:20:35 -08:00
dashedDay = " #{ day . substring ( 0 , 4 ) } - #{ day . substring ( 4 , 6 ) } - #{ day . substring ( 6 , 8 ) } "
data = day: dashedDay , groups: [ ]
2015-11-08 17:00:24 -08:00
for group in @ revenueGroups
data . groups . push ( dayGroupCountMap [ day ] [ group ] ? 0 )
@ revenue . push data
2016-02-20 17:27:25 -08:00
# Order present to past
2015-11-08 17:00:24 -08:00
@ revenue . sort (a, b) -> b . day . localeCompare ( a . day )
return unless @ revenue . length > 0
2015-11-10 11:20:35 -08:00
# Add monthly recurring revenue values
2016-02-20 17:27:25 -08:00
# 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
2015-11-10 11:20:35 -08:00
@ updateAllKPIChartData ( )
@ updateRevenueChartData ( )
2015-11-08 17:00:24 -08:00
@ render ? ( )
2016-02-20 17:27:25 -08:00
2015-11-08 17:00:24 -08:00
} , 0 ) . load ( )
2016-02-04 17:31:25 -08:00
2016-02-06 14:02:44 -08:00
@ supermodel . addRequestResource ( {
2016-02-04 17:31:25 -08:00
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
2016-02-20 17:27:25 -08:00
@ renderSelectors ? ( ' # school-counts ' )
2016-02-04 17:31:25 -08:00
} , 0 ) . load ( )
2016-02-23 14:04:35 -08:00
@ 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
2016-02-28 15:24:32 -08:00
@ renderSelectors ? ( ' .school-sales ' )
2016-02-23 14:04:35 -08:00
} , 0 ) . load ( )
2016-02-06 14:02:44 -08:00
@ 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 ( )
2016-01-26 14:36:40 -08:00
@courses = new CocoCollection ( [ ] , { url: " /db/course " , model: Course } )
@courses.comparator = " _id "
@ listenToOnce @ courses , ' sync ' , @ onCoursesSync
2016-02-06 14:02:44 -08:00
@ supermodel . loadCollection ( @ courses )
2016-01-26 14:36:40 -08:00
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
2016-02-24 06:24:58 -08:00
options.success = (data) =>
@ onCourseInstancesSync ( data )
@ renderSelectors ? ( ' # furthest-course ' )
2016-02-06 14:02:44 -08:00
@ supermodel . addRequestResource ( options , 0 ) . load ( )
2016-01-26 14:36:40 -08:00
2016-02-24 06:24:58 -08:00
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 ]
2016-05-23 10:26:34 -07:00
if prepaidID = user . coursePrepaidID or user . coursePrepaid ? . _id
2016-02-24 06:24:58 -08:00
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 )
2015-11-08 17:00:24 -08:00
2015-11-10 11:20:35 -08:00
createLineChartPoints: (days, data) ->
points = [ ]
for entry , i in data
points . push
day: entry . day
2015-11-10 17:26:13 -08:00
y: entry . value
# Trim points preceding days
2016-02-16 09:23:38 -08:00
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
2015-11-10 11:20:35 -08:00
# 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
2016-02-16 09:23:38 -08:00
y: prevY
2015-11-10 11:20:35 -08:00
points [ i ] . x = i
points . splice ( 0 , points . length - days . length ) if points . length > days . length
points
createLineCharts: ->
2016-02-20 17:27:25 -08:00
visibleWidth = $ ( ' .kpi-recent-chart ' ) . width ( )
d3Utils . createLineChart ( ' .kpi-recent-chart ' , @ kpiRecentChartLines , visibleWidth )
d3Utils . createLineChart ( ' .kpi-chart ' , @ kpiChartLines , visibleWidth )
2016-02-22 12:05:29 -08:00
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 )
2016-02-20 17:27:25 -08:00
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 )
2015-11-10 11:20:35 -08:00
updateAllKPIChartData: ->
@kpiRecentChartLines = [ ]
@kpiChartLines = [ ]
@ updateKPIChartData ( 60 , @ kpiRecentChartLines )
2016-02-06 14:02:44 -08:00
@ updateKPIChartData ( 365 , @ kpiChartLines )
2015-11-10 11:20:35 -08:00
updateKPIChartData: (timeframeDays, chartLines) ->
days = d3Utils . createContiguousDays ( timeframeDays )
2016-02-16 09:23:38 -08:00
# Build active classes KPI line
2015-11-10 11:20:35 -08:00
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
2016-02-16 09:23:38 -08:00
description: ' Monthly Active Classes '
2015-11-10 11:20:35 -08:00
lineColor: ' blue '
strokeWidth: 1
min: 0
max: _ . max ( points , ' y ' ) . y
showYScale: true
2016-02-16 09:23:38 -08:00
# Build recurring revenue KPI line
2015-11-10 11:20:35 -08:00
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
2016-02-16 09:23:38 -08:00
description: ' Monthly Recurring Revenue (in thousands) '
2015-11-10 11:20:35 -08:00
lineColor: ' green '
strokeWidth: 1
min: 0
max: _ . max ( points , ' y ' ) . y
showYScale: true
2016-03-08 15:27:54 -08:00
# Build campaign MAU KPI line
2015-11-10 11:20:35 -08:00
if @ activeUsers ? . length > 0
2016-02-16 09:23:38 -08:00
eventDayDataMap = { }
2015-11-10 11:20:35 -08:00
for entry in @ activeUsers
2016-02-16 09:23:38 -08:00
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
2016-02-29 13:57:57 -08:00
for day , count of entry
campaignData . push day: day , value: count / 1000
2016-02-16 09:23:38 -08:00
campaignData . reverse ( )
2015-11-10 11:20:35 -08:00
2016-02-16 09:23:38 -08:00
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
2015-11-10 11:20:35 -08:00
updateActiveClassesChartData: ->
2016-02-22 12:05:29 -08:00
@activeClassesChartLines90 = [ ]
@activeClassesChartLines365 = [ ]
2015-11-10 11:20:35 -08:00
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
2016-02-22 12:05:29 -08:00
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 )
2015-11-10 11:20:35 -08:00
updateActiveUsersChartData: ->
2016-02-16 09:23:38 -08:00
# Create chart lines for the active user events returned by active_users in analytics_perday_handler
2016-02-22 12:05:29 -08:00
@campaignDailyActiveUsersChartLines90 = [ ]
@campaignMonthlyActiveUsersChartLines90 = [ ]
@campaignDailyActiveUsersChartLines365 = [ ]
@campaignMonthlyActiveUsersChartLines365 = [ ]
@classroomDailyActiveUsersChartLines90 = [ ]
@classroomMonthlyActiveUsersChartLines90 = [ ]
@classroomDailyActiveUsersChartLines365 = [ ]
@classroomMonthlyActiveUsersChartLines365 = [ ]
2015-11-10 11:20:35 -08:00
return unless @ activeUsers ? . length
2016-02-16 09:23:38 -08:00
# Separate day/value arrays by event
eventDataMap = { }
2015-11-10 11:20:35 -08:00
for entry in @ activeUsers
2016-02-16 09:23:38 -08:00
day = entry . day
for event , count of entry . events
eventDataMap [ event ] ? = [ ]
eventDataMap [ event ] . push
2015-11-10 11:20:35 -08:00
day: entry . day
2016-02-16 09:23:38 -08:00
value: count
2016-02-22 12:05:29 -08:00
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 ' )
2016-02-16 09:23:38 -08:00
2016-02-19 09:32:17 -08:00
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
2016-02-16 09:23:38 -08:00
max = 0
2016-02-19 09:32:17 -08:00
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
2016-02-16 09:23:38 -08:00
for line in @ campaignVsClassroomMonthlyActiveUsersChartLines
line.max = max
2015-11-10 11:20:35 -08:00
2016-02-06 14:02:44 -08:00
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
2015-11-10 11:20:35 -08:00
updateRevenueChartData: ->
2016-02-20 17:27:25 -08:00
@revenueDailyChartLines90Days = [ ]
@revenueMonthlyChartLines90Days = [ ]
@revenueDailyChartLines365Days = [ ]
@revenueMonthlyChartLines365Days = [ ]
2015-11-10 11:20:35 -08:00
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
2016-02-20 17:27:25 -08:00
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 )