mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-05-01 00:15:00 -04:00
Dashboard monthly revenue counts, and tabs
This commit is contained in:
parent
16969e1d5a
commit
cac9a7463f
4 changed files with 317 additions and 236 deletions
app
core
styles/admin
templates/admin
views/admin
|
@ -12,7 +12,7 @@ module.exports.createContiguousDays = (timeframeDays, skipToday=true) ->
|
||||||
currentDate.setUTCDate(currentDate.getUTCDate() + 1)
|
currentDate.setUTCDate(currentDate.getUTCDate() + 1)
|
||||||
days
|
days
|
||||||
|
|
||||||
module.exports.createLineChart = (containerSelector, chartLines) ->
|
module.exports.createLineChart = (containerSelector, chartLines, containerWidth) ->
|
||||||
# Creates a line chart within 'containerSelector' based on chartLines
|
# Creates a line chart within 'containerSelector' based on chartLines
|
||||||
return unless chartLines?.length > 0 and containerSelector
|
return unless chartLines?.length > 0 and containerSelector
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ module.exports.createLineChart = (containerSelector, chartLines) ->
|
||||||
keyHeight = 20
|
keyHeight = 20
|
||||||
xAxisHeight = 20
|
xAxisHeight = 20
|
||||||
yAxisWidth = 40
|
yAxisWidth = 40
|
||||||
containerWidth = $(containerSelector).width()
|
containerWidth = $(containerSelector).width() unless containerWidth
|
||||||
containerHeight = $(containerSelector).height()
|
containerHeight = $(containerSelector).height()
|
||||||
|
|
||||||
yScaleCount = 0
|
yScaleCount = 0
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
#admin-analytics-view
|
#admin-analytics-view
|
||||||
|
|
||||||
|
// Force compact top site chrome
|
||||||
|
background-position: center -226px
|
||||||
|
padding-top: 50px
|
||||||
|
#site-nav
|
||||||
|
top: -80px
|
||||||
|
#nav-logo
|
||||||
|
display: none
|
||||||
|
#small-nav-logo
|
||||||
|
display: inline-block
|
||||||
|
height: 30px
|
||||||
|
|
||||||
#site-content-area
|
#site-content-area
|
||||||
width: 100%
|
width: 100%
|
||||||
.big-stat
|
.big-stat
|
||||||
|
|
|
@ -32,201 +32,246 @@ block content
|
||||||
div.description Campaign Monthly Active Users
|
div.description Campaign Monthly Active Users
|
||||||
div.count= campaignBigMAU
|
div.count= campaignBigMAU
|
||||||
|
|
||||||
h3 KPI 60 days
|
ul.nav.nav-tabs
|
||||||
.kpi-recent-chart.line-chart-container
|
li.active
|
||||||
|
a(data-target="#tab_kpis", data-toggle="tab") KPIs
|
||||||
|
li
|
||||||
|
a(data-target="#tab_active_classes", data-toggle="tab") Active Classes
|
||||||
|
li
|
||||||
|
a(data-target="#tab_revenue", data-toggle="tab") Revenue
|
||||||
|
li
|
||||||
|
a(data-target="#tab_classroom", data-toggle="tab") Classroom
|
||||||
|
li
|
||||||
|
a(data-target="#tab_campaign", data-toggle="tab") Campaign
|
||||||
|
li
|
||||||
|
a(data-target="#tab_campaign_vs_classroom", data-toggle="tab") Campaign vs Classroom
|
||||||
|
|
||||||
h3 KPI 365 days
|
.tab-content
|
||||||
.kpi-chart.line-chart-container
|
.tab-pane.active#tab_kpis
|
||||||
|
h3 KPI 60 days
|
||||||
|
.kpi-recent-chart.line-chart-container
|
||||||
|
|
||||||
h1 Table of Contents
|
h3 KPI 365 days
|
||||||
b Graphs
|
.kpi-chart.line-chart-container
|
||||||
div
|
|
||||||
a(href='#active-classes-graph') Active Classes
|
|
||||||
div
|
|
||||||
a(href='#recurring-revenue-graph') Recurring Revenue
|
|
||||||
div
|
|
||||||
a(href='#classroom-daus-graph') Classroom Daily Active Users
|
|
||||||
div
|
|
||||||
a(href='#classroom-maus-graph') Classroom Monthly Active Users
|
|
||||||
div
|
|
||||||
a(href='#campaign-daus-graph') Campaign Daily Active Users
|
|
||||||
div
|
|
||||||
a(href='#campaign-maus-graph') Campaign Monthly Active Users
|
|
||||||
div
|
|
||||||
a(href='#campaign-vs-classroom-paid-maus-recent-graph') Campaign vs Classroom Paid Monthly Active Users (90 days)
|
|
||||||
div
|
|
||||||
a(href='#campaign-vs-classroom-paid-maus-graph') Campaign vs Classroom Paid Monthly Active Users (365 days)
|
|
||||||
div
|
|
||||||
a(href='#enrollments-graph') Enrollments Issued and Redeemed
|
|
||||||
b Tables
|
|
||||||
div
|
|
||||||
a(href='#furthest-courses-table') Furthest Course
|
|
||||||
div
|
|
||||||
a(href='#school-counts-table') School Counts
|
|
||||||
div
|
|
||||||
a(href='#active-classes-table') Active Classes
|
|
||||||
div
|
|
||||||
a(href='#recurring-revenue-table') Recurring Revenue
|
|
||||||
div
|
|
||||||
a(href='#active-users-table') Active Users
|
|
||||||
div
|
|
||||||
a(href='#enrollments-table') Enrollments
|
|
||||||
|
|
||||||
h3#active-classes-graph Active Classes 90 days
|
.tab-pane#tab_active_classes
|
||||||
.small Active class: 12+ students in a classroom, with 6+ who played in last 30 days. Played == 'Started Level' analytics event.
|
h3 Active Classes 90 days
|
||||||
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set
|
.small Active class: 12+ students in a classroom, with 6+ who played in last 30 days. Played == 'Started Level' analytics event.
|
||||||
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set
|
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set
|
||||||
.small Paid class: at least one paid student in the classroom
|
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set
|
||||||
.small Trial class: not paid, at least one trial student in classroom
|
.small Paid class: at least one paid student in the classroom
|
||||||
.small Free class: not paid, not trial
|
.small Trial class: not paid, at least one trial student in classroom
|
||||||
.active-classes-chart.line-chart-container
|
.small Free class: not paid, not trial
|
||||||
|
.active-classes-chart.line-chart-container
|
||||||
|
|
||||||
h3#recurring-revenue-graph Recurring Revenue 90 days
|
h1#active-classes-table Active Classes
|
||||||
.recurring-revenue-chart.line-chart-container
|
table.table.table-striped.table-condensed
|
||||||
|
|
||||||
h3#classroom-daus-graph Classroom Daily Active Users 90 days
|
|
||||||
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set
|
|
||||||
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set
|
|
||||||
.small Free student: not paid, not trial
|
|
||||||
.classroom-daily-active-users-chart.line-chart-container
|
|
||||||
|
|
||||||
h3#classroom-maus-graph Classroom Monthly Active Users 90 days
|
|
||||||
.classroom-monthly-active-users-chart.line-chart-container
|
|
||||||
|
|
||||||
h3#campaign-daus-graph Campaign Daily Active Users 90 days
|
|
||||||
.small Paid user: had monthly or yearly sub on given day
|
|
||||||
.small Free user: not paid
|
|
||||||
.campaign-daily-active-users-chart.line-chart-container
|
|
||||||
|
|
||||||
h3#campaign-maus-graph Campaign Monthly Active Users 90 days
|
|
||||||
.campaign-monthly-active-users-chart.line-chart-container
|
|
||||||
|
|
||||||
h3#campaign-vs-classroom-paid-maus-recent-graph Campaign vs Classroom Paid Monthly Active Users 90 days
|
|
||||||
.campaign-vs-classroom-monthly-active-users-recent-chart.line-chart-container
|
|
||||||
|
|
||||||
h3#campaign-vs-classroom-paid-maus-graph Campaign vs Classroom Paid Monthly Active Users 365 days
|
|
||||||
.small TODO: aggregate active user data from last year
|
|
||||||
.campaign-vs-classroom-monthly-active-users-chart.line-chart-container
|
|
||||||
|
|
||||||
h3#enrollments-graph Enrollments Issued and Redeemed 90 days
|
|
||||||
.paid-courses-chart.line-chart-container
|
|
||||||
|
|
||||||
h3#furthest-courses-table Furthest Course
|
|
||||||
.small Teacher: owner of a course instance
|
|
||||||
.small Student: member of a course instance (assigned to course)
|
|
||||||
.small For course instances created in last #{view.furthestCourseDayRange} days, not Single Player, hourOfCode != true
|
|
||||||
.small Counts are not summed. I.e. a student or teacher only contributes to the count of one course.
|
|
||||||
if view.teacherCourseDistribution
|
|
||||||
table.table.table-striped.table-condensed
|
|
||||||
tr
|
|
||||||
th Course
|
|
||||||
th Teachers
|
|
||||||
th Students
|
|
||||||
th Avg students per teacher
|
|
||||||
each count, courseIndex in view.teacherCourseDistribution
|
|
||||||
tr
|
tr
|
||||||
td= view.courses.models[courseIndex].get('name')
|
th Day
|
||||||
td= count
|
for group in activeClassGroups
|
||||||
td= view.studentCourseDistribution[courseIndex] || 0
|
th= group.replace('Active classes', '')
|
||||||
td= Math.round((view.studentCourseDistribution[courseIndex] || 0) / count)
|
each activeClass in activeClasses
|
||||||
else
|
tr
|
||||||
div Loading ...
|
td= activeClass.day
|
||||||
|
each val in activeClass.groups
|
||||||
|
td= val
|
||||||
|
|
||||||
h3#school-counts-table School Counts
|
|
||||||
.small Only including schools with #{view.minSchoolCount}+ counts
|
.tab-pane#tab_revenue
|
||||||
if view.schoolCounts
|
h3 Daily Recurring Revenue 90 days
|
||||||
table.table.table-striped.table-condensed
|
.recurring-daily-revenue-chart-90.line-chart-container
|
||||||
tr
|
|
||||||
th
|
h3 Monthly Recurring Revenue 90 days
|
||||||
th School Name
|
.recurring-monthly-revenue-chart-90.line-chart-container
|
||||||
th User Count
|
|
||||||
each val, i in view.schoolCounts
|
h3 Daily Recurring Revenue 365 days
|
||||||
|
.recurring-daily-revenue-chart-365.line-chart-container
|
||||||
|
|
||||||
|
h3 Monthly Recurring Revenue 365 days
|
||||||
|
.recurring-monthly-revenue-chart-365.line-chart-container
|
||||||
|
|
||||||
|
h1#recurring-revenue-table Recurring Revenue
|
||||||
|
table.table.table-striped.table-condensed
|
||||||
tr
|
tr
|
||||||
td= i + 1
|
th Day
|
||||||
td= val.schoolName
|
for group in revenueGroups
|
||||||
td= val.count
|
th= group.replace('DRR ', 'Daily ').replace('MRR ', 'Monthly ')
|
||||||
else
|
each entry in revenue
|
||||||
div Loading ...
|
tr
|
||||||
|
td= entry.day
|
||||||
|
each val in entry.groups
|
||||||
|
td $#{(val / 100).toFixed(2)}
|
||||||
|
|
||||||
h1#active-classes-table Active Classes
|
.tab-pane#tab_classroom
|
||||||
table.table.table-striped.table-condensed
|
h3#classroom-daus-graph Classroom Daily Active Users 90 days
|
||||||
tr
|
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set
|
||||||
th Day
|
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set
|
||||||
for group in activeClassGroups
|
.small Free student: not paid, not trial
|
||||||
th= group.replace('Active classes', '')
|
.classroom-daily-active-users-chart.line-chart-container
|
||||||
each activeClass in activeClasses
|
|
||||||
tr
|
|
||||||
td= activeClass.day
|
|
||||||
each val in activeClass.groups
|
|
||||||
td= val
|
|
||||||
|
|
||||||
h1#recurring-revenue-table Recurring Revenue
|
h3#classroom-maus-graph Classroom Monthly Active Users 90 days
|
||||||
table.table.table-striped.table-condensed
|
.classroom-monthly-active-users-chart.line-chart-container
|
||||||
tr
|
|
||||||
th Day
|
|
||||||
for group in revenueGroups
|
|
||||||
th= group.replace('DRR ', 'Daily ')
|
|
||||||
each entry in revenue
|
|
||||||
tr
|
|
||||||
td= entry.day
|
|
||||||
each val in entry.groups
|
|
||||||
td $#{(val / 100).toFixed(2)}
|
|
||||||
|
|
||||||
h1#active-users-table Active Users
|
h3#enrollments-graph Enrollments Issued and Redeemed 90 days
|
||||||
if activeUsers.length > 0
|
.paid-courses-chart.line-chart-container
|
||||||
- var eventNames = [];
|
|
||||||
each count, event in activeUsers[0].events
|
|
||||||
- eventNames.push(event)
|
|
||||||
- eventNames.sort(function (a, b) {
|
|
||||||
- if (a.indexOf('campaign') == b.indexOf('campaign') || a.indexOf('classroom') == b.indexOf('classroom')) {
|
|
||||||
- return a.localeCompare(b);
|
|
||||||
- }
|
|
||||||
- else if (a.indexOf('campaign') > b.indexOf('campaign')) {
|
|
||||||
- return 1;
|
|
||||||
- }
|
|
||||||
- else {
|
|
||||||
- return -1;
|
|
||||||
- }
|
|
||||||
- });
|
|
||||||
table.table.table-striped.table-condensed
|
|
||||||
tr
|
|
||||||
th(style='min-width:85px;') Day
|
|
||||||
each eventName in eventNames
|
|
||||||
th= eventName
|
|
||||||
th DAU Campaign Total
|
|
||||||
th DAU Classroom Total
|
|
||||||
each activeUser in activeUsers
|
|
||||||
tr
|
|
||||||
td= activeUser.day
|
|
||||||
- var dauCampaignTotal = 0
|
|
||||||
- var dauClassroomTotal = 0
|
|
||||||
each eventName in eventNames
|
|
||||||
td= activeUser.events[eventName] || 0
|
|
||||||
if eventName.indexOf('DAU campaign') >= 0
|
|
||||||
- dauCampaignTotal += (activeUser.events[eventName] || 0);
|
|
||||||
else if eventName.indexOf('DAU classroom') >= 0
|
|
||||||
- dauClassroomTotal += (activeUser.events[eventName] || 0);
|
|
||||||
td= dauCampaignTotal
|
|
||||||
td= dauClassroomTotal
|
|
||||||
|
|
||||||
h1#enrollments-table Enrollments
|
h3#furthest-courses-table Furthest Course
|
||||||
table.table.table-striped.table-condensed
|
.small Teacher: owner of a course instance
|
||||||
tr
|
.small Student: member of a course instance (assigned to course)
|
||||||
th Day
|
.small For course instances created in last #{view.furthestCourseDayRange} days, not Single Player, hourOfCode != true
|
||||||
th Paid Enrollments Issued
|
.small Counts are not summed. I.e. a student or teacher only contributes to the count of one course.
|
||||||
th Paid Enrollments Redeemed
|
if view.teacherCourseDistribution
|
||||||
th Trial Enrollments Issued
|
table.table.table-striped.table-condensed
|
||||||
th Trial Enrollments Redeemed
|
tr
|
||||||
each day in enrollmentDays
|
th Course
|
||||||
tr
|
th Teachers
|
||||||
td= day
|
th Students
|
||||||
if dayEnrollmentsMap[day]
|
th Avg students per teacher
|
||||||
td= dayEnrollmentsMap[day].paidIssued || 0
|
each count, courseIndex in view.teacherCourseDistribution
|
||||||
td= dayEnrollmentsMap[day].paidRedeemed || 0
|
tr
|
||||||
td= dayEnrollmentsMap[day].trialIssued || 0
|
td= view.courses.models[courseIndex].get('name')
|
||||||
td= dayEnrollmentsMap[day].trialRedeemed || 0
|
td= count
|
||||||
|
td= view.studentCourseDistribution[courseIndex] || 0
|
||||||
|
td= Math.round((view.studentCourseDistribution[courseIndex] || 0) / count)
|
||||||
|
else
|
||||||
|
div Loading ...
|
||||||
|
|
||||||
|
#school-counts
|
||||||
|
h3 School Counts
|
||||||
|
.small Only including schools with #{view.minSchoolCount}+ counts
|
||||||
|
if view.schoolCounts
|
||||||
|
table.table.table-striped.table-condensed
|
||||||
|
tr
|
||||||
|
th
|
||||||
|
th School Name
|
||||||
|
th User Count
|
||||||
|
each val, i in view.schoolCounts
|
||||||
|
tr
|
||||||
|
td= i + 1
|
||||||
|
td= val.schoolName
|
||||||
|
td= val.count
|
||||||
else
|
else
|
||||||
td 0
|
div Loading ...
|
||||||
td 0
|
|
||||||
td 0
|
h1#active-users-table Active Users
|
||||||
td 0
|
if activeUsers.length > 0
|
||||||
|
- var eventNames = [];
|
||||||
|
each count, event in activeUsers[0].events
|
||||||
|
if event.indexOf('classroom') >= 0
|
||||||
|
- eventNames.push(event)
|
||||||
|
- eventNames.sort(function (a, b) {return a.localeCompare(b);});
|
||||||
|
table.table.table-striped.table-condensed
|
||||||
|
tr
|
||||||
|
th(style='min-width:85px;') Day
|
||||||
|
each eventName in eventNames
|
||||||
|
th= eventName
|
||||||
|
th DAU Classroom Total
|
||||||
|
each activeUser in activeUsers
|
||||||
|
tr
|
||||||
|
td= activeUser.day
|
||||||
|
- var dauClassroomTotal = 0
|
||||||
|
each eventName in eventNames
|
||||||
|
if eventName.indexOf('DAU') >= 0
|
||||||
|
- dauClassroomTotal += (activeUser.events[eventName] || 0);
|
||||||
|
td= activeUser.events[eventName] || 0
|
||||||
|
td= dauClassroomTotal
|
||||||
|
|
||||||
|
h1#enrollments-table Enrollments
|
||||||
|
table.table.table-striped.table-condensed
|
||||||
|
tr
|
||||||
|
th Day
|
||||||
|
th Paid Enrollments Issued
|
||||||
|
th Paid Enrollments Redeemed
|
||||||
|
th Trial Enrollments Issued
|
||||||
|
th Trial Enrollments Redeemed
|
||||||
|
each day in enrollmentDays
|
||||||
|
tr
|
||||||
|
td= day
|
||||||
|
if dayEnrollmentsMap[day]
|
||||||
|
td= dayEnrollmentsMap[day].paidIssued || 0
|
||||||
|
td= dayEnrollmentsMap[day].paidRedeemed || 0
|
||||||
|
td= dayEnrollmentsMap[day].trialIssued || 0
|
||||||
|
td= dayEnrollmentsMap[day].trialRedeemed || 0
|
||||||
|
else
|
||||||
|
td 0
|
||||||
|
td 0
|
||||||
|
td 0
|
||||||
|
td 0
|
||||||
|
|
||||||
|
.tab-pane#tab_campaign
|
||||||
|
h3#campaign-daus-graph Campaign Daily Active Users 90 days
|
||||||
|
.small Paid user: had monthly or yearly sub on given day
|
||||||
|
.small Free user: not paid
|
||||||
|
.campaign-daily-active-users-chart.line-chart-container
|
||||||
|
|
||||||
|
h3#campaign-maus-graph Campaign Monthly Active Users 90 days
|
||||||
|
.campaign-monthly-active-users-chart.line-chart-container
|
||||||
|
|
||||||
|
h1#active-users-table Active Users
|
||||||
|
if activeUsers.length > 0
|
||||||
|
- var eventNames = [];
|
||||||
|
each count, event in activeUsers[0].events
|
||||||
|
if event.indexOf('campaign') >= 0
|
||||||
|
- eventNames.push(event)
|
||||||
|
- eventNames.sort(function (a, b) {return a.localeCompare(b);});
|
||||||
|
table.table.table-striped.table-condensed
|
||||||
|
tr
|
||||||
|
th(style='min-width:85px;') Day
|
||||||
|
each eventName in eventNames
|
||||||
|
th= eventName
|
||||||
|
th DAU Total
|
||||||
|
each activeUser in activeUsers
|
||||||
|
tr
|
||||||
|
td= activeUser.day
|
||||||
|
- var dauCampaignTotal = 0
|
||||||
|
each eventName in eventNames
|
||||||
|
if eventName.indexOf('DAU') >= 0
|
||||||
|
- dauCampaignTotal += (activeUser.events[eventName] || 0);
|
||||||
|
td= activeUser.events[eventName] || 0
|
||||||
|
td= dauCampaignTotal
|
||||||
|
|
||||||
|
|
||||||
|
.tab-pane#tab_campaign_vs_classroom
|
||||||
|
h3#campaign-vs-classroom-paid-maus-recent-graph Campaign vs Classroom Paid Monthly Active Users 90 days
|
||||||
|
.campaign-vs-classroom-monthly-active-users-recent-chart.line-chart-container
|
||||||
|
|
||||||
|
h3#campaign-vs-classroom-paid-maus-graph Campaign vs Classroom Paid Monthly Active Users 365 days
|
||||||
|
.small TODO: aggregate active user data from last year
|
||||||
|
.campaign-vs-classroom-monthly-active-users-chart.line-chart-container
|
||||||
|
|
||||||
|
h1#active-users-table Active Users
|
||||||
|
if activeUsers.length > 0
|
||||||
|
- var eventNames = [];
|
||||||
|
each count, event in activeUsers[0].events
|
||||||
|
- eventNames.push(event)
|
||||||
|
- eventNames.sort(function (a, b) {
|
||||||
|
- if (a.indexOf('campaign') == b.indexOf('campaign') || a.indexOf('classroom') == b.indexOf('classroom')) {
|
||||||
|
- return a.localeCompare(b);
|
||||||
|
- }
|
||||||
|
- else if (a.indexOf('campaign') > b.indexOf('campaign')) {
|
||||||
|
- return 1;
|
||||||
|
- }
|
||||||
|
- else {
|
||||||
|
- return -1;
|
||||||
|
- }
|
||||||
|
- });
|
||||||
|
table.table.table-striped.table-condensed
|
||||||
|
tr
|
||||||
|
th(style='min-width:85px;') Day
|
||||||
|
each eventName in eventNames
|
||||||
|
th= eventName
|
||||||
|
th DAU Campaign Total
|
||||||
|
th DAU Classroom Total
|
||||||
|
each activeUser in activeUsers
|
||||||
|
tr
|
||||||
|
td= activeUser.day
|
||||||
|
- var dauCampaignTotal = 0
|
||||||
|
- var dauClassroomTotal = 0
|
||||||
|
each eventName in eventNames
|
||||||
|
td= activeUser.events[eventName] || 0
|
||||||
|
if eventName.indexOf('DAU campaign') >= 0
|
||||||
|
- dauCampaignTotal += (activeUser.events[eventName] || 0);
|
||||||
|
else if eventName.indexOf('DAU classroom') >= 0
|
||||||
|
- dauClassroomTotal += (activeUser.events[eventName] || 0);
|
||||||
|
td= dauCampaignTotal
|
||||||
|
td= dauClassroomTotal
|
||||||
|
|
|
@ -7,8 +7,6 @@ RootView = require 'views/core/RootView'
|
||||||
template = require 'templates/admin/analytics'
|
template = require 'templates/admin/analytics'
|
||||||
utils = require 'core/utils'
|
utils = require 'core/utils'
|
||||||
|
|
||||||
# TODO: switch page to tabs instead of table of contents
|
|
||||||
|
|
||||||
module.exports = class AnalyticsView extends RootView
|
module.exports = class AnalyticsView extends RootView
|
||||||
id: 'admin-analytics-view'
|
id: 'admin-analytics-view'
|
||||||
template: template
|
template: template
|
||||||
|
@ -86,18 +84,20 @@ module.exports = class AnalyticsView extends RootView
|
||||||
url: '/db/analytics_perday/-/recurring_revenue'
|
url: '/db/analytics_perday/-/recurring_revenue'
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
success: (data) =>
|
success: (data) =>
|
||||||
|
|
||||||
# Organize data by day, then group
|
# Organize data by day, then group
|
||||||
groupMap = {}
|
groupMap = {}
|
||||||
dayGroupCountMap = {}
|
dayGroupCountMap = {}
|
||||||
for dailyRevenue in data
|
for dailyRevenue in data
|
||||||
dayGroupCountMap[dailyRevenue.day] ?= {}
|
dayGroupCountMap[dailyRevenue.day] ?= {}
|
||||||
dayGroupCountMap[dailyRevenue.day]['Daily Total'] = 0
|
dayGroupCountMap[dailyRevenue.day]['DRR Total'] = 0
|
||||||
for group, val of dailyRevenue.groups
|
for group, val of dailyRevenue.groups
|
||||||
groupMap[group] = true
|
groupMap[group] = true
|
||||||
dayGroupCountMap[dailyRevenue.day][group] = val
|
dayGroupCountMap[dailyRevenue.day][group] = val
|
||||||
dayGroupCountMap[dailyRevenue.day]['Daily Total'] += val
|
dayGroupCountMap[dailyRevenue.day]['DRR Total'] += val
|
||||||
@revenueGroups = Object.keys(groupMap)
|
@revenueGroups = Object.keys(groupMap)
|
||||||
@revenueGroups.push 'Daily Total'
|
@revenueGroups.push 'DRR Total'
|
||||||
|
|
||||||
# Build list of recurring revenue entries, where each entry is a day of individual group values
|
# Build list of recurring revenue entries, where each entry is a day of individual group values
|
||||||
@revenue = []
|
@revenue = []
|
||||||
for day of dayGroupCountMap
|
for day of dayGroupCountMap
|
||||||
|
@ -106,23 +106,35 @@ module.exports = class AnalyticsView extends RootView
|
||||||
for group in @revenueGroups
|
for group in @revenueGroups
|
||||||
data.groups.push(dayGroupCountMap[day][group] ? 0)
|
data.groups.push(dayGroupCountMap[day][group] ? 0)
|
||||||
@revenue.push data
|
@revenue.push data
|
||||||
|
|
||||||
|
# Order present to past
|
||||||
@revenue.sort (a, b) -> b.day.localeCompare(a.day)
|
@revenue.sort (a, b) -> b.day.localeCompare(a.day)
|
||||||
|
|
||||||
return unless @revenue.length > 0
|
return unless @revenue.length > 0
|
||||||
|
|
||||||
# Add monthly recurring revenue values
|
# Add monthly recurring revenue values
|
||||||
@revenueGroups.push 'Monthly'
|
|
||||||
monthlyValues = []
|
# For each daily group, add up monthly values walking forward through time, and add to revenue groups
|
||||||
for i in [@revenue.length-1..0]
|
monthlyDailyGroupMap = {}
|
||||||
dailyTotal = @revenue[i].groups[@revenue[i].groups.length - 1]
|
dailyGroupIndexMap = {}
|
||||||
monthlyValues.push(dailyTotal)
|
for group, i in @revenueGroups
|
||||||
monthlyValues.shift() while monthlyValues.length > 30
|
monthlyDailyGroupMap[group.replace('DRR', 'MRR')] = group
|
||||||
if monthlyValues.length is 30
|
dailyGroupIndexMap[group] = i
|
||||||
@revenue[i].groups.push(_.reduce(monthlyValues, (s, num) -> s + num))
|
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()
|
@updateAllKPIChartData()
|
||||||
@updateRevenueChartData()
|
@updateRevenueChartData()
|
||||||
@render?()
|
@render?()
|
||||||
|
|
||||||
}, 0).load()
|
}, 0).load()
|
||||||
|
|
||||||
@supermodel.addRequestResource({
|
@supermodel.addRequestResource({
|
||||||
|
@ -134,7 +146,7 @@ module.exports = class AnalyticsView extends RootView
|
||||||
return -1 if a.count > b.count
|
return -1 if a.count > b.count
|
||||||
return 0 if a.count is b.count
|
return 0 if a.count is b.count
|
||||||
1
|
1
|
||||||
@render?()
|
@renderSelectors?('#school-counts')
|
||||||
}, 0).load()
|
}, 0).load()
|
||||||
|
|
||||||
@supermodel.addRequestResource({
|
@supermodel.addRequestResource({
|
||||||
|
@ -272,17 +284,21 @@ module.exports = class AnalyticsView extends RootView
|
||||||
points
|
points
|
||||||
|
|
||||||
createLineCharts: ->
|
createLineCharts: ->
|
||||||
d3Utils.createLineChart('.kpi-recent-chart', @kpiRecentChartLines)
|
visibleWidth = $('.kpi-recent-chart').width()
|
||||||
d3Utils.createLineChart('.kpi-chart', @kpiChartLines)
|
d3Utils.createLineChart('.kpi-recent-chart', @kpiRecentChartLines, visibleWidth)
|
||||||
d3Utils.createLineChart('.active-classes-chart', @activeClassesChartLines)
|
d3Utils.createLineChart('.kpi-chart', @kpiChartLines, visibleWidth)
|
||||||
d3Utils.createLineChart('.classroom-daily-active-users-chart', @classroomDailyActiveUsersChartLines)
|
d3Utils.createLineChart('.active-classes-chart', @activeClassesChartLines, visibleWidth)
|
||||||
d3Utils.createLineChart('.classroom-monthly-active-users-chart', @classroomMonthlyActiveUsersChartLines)
|
d3Utils.createLineChart('.classroom-daily-active-users-chart', @classroomDailyActiveUsersChartLines, visibleWidth)
|
||||||
d3Utils.createLineChart('.campaign-daily-active-users-chart', @campaignDailyActiveUsersChartLines)
|
d3Utils.createLineChart('.classroom-monthly-active-users-chart', @classroomMonthlyActiveUsersChartLines, visibleWidth)
|
||||||
d3Utils.createLineChart('.campaign-monthly-active-users-chart', @campaignMonthlyActiveUsersChartLines)
|
d3Utils.createLineChart('.campaign-daily-active-users-chart', @campaignDailyActiveUsersChartLines, visibleWidth)
|
||||||
d3Utils.createLineChart('.campaign-vs-classroom-monthly-active-users-recent-chart.line-chart-container', @campaignVsClassroomMonthlyActiveUsersRecentChartLines)
|
d3Utils.createLineChart('.campaign-monthly-active-users-chart', @campaignMonthlyActiveUsersChartLines, visibleWidth)
|
||||||
d3Utils.createLineChart('.campaign-vs-classroom-monthly-active-users-chart.line-chart-container', @campaignVsClassroomMonthlyActiveUsersChartLines)
|
d3Utils.createLineChart('.campaign-vs-classroom-monthly-active-users-recent-chart.line-chart-container', @campaignVsClassroomMonthlyActiveUsersRecentChartLines, visibleWidth)
|
||||||
d3Utils.createLineChart('.paid-courses-chart', @enrollmentsChartLines)
|
d3Utils.createLineChart('.campaign-vs-classroom-monthly-active-users-chart.line-chart-container', @campaignVsClassroomMonthlyActiveUsersChartLines, visibleWidth)
|
||||||
d3Utils.createLineChart('.recurring-revenue-chart', @revenueChartLines)
|
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: ->
|
updateAllKPIChartData: ->
|
||||||
@kpiRecentChartLines = []
|
@kpiRecentChartLines = []
|
||||||
|
@ -644,9 +660,11 @@ module.exports = class AnalyticsView extends RootView
|
||||||
line.max = dailyMax for line in @enrollmentsChartLines
|
line.max = dailyMax for line in @enrollmentsChartLines
|
||||||
|
|
||||||
updateRevenueChartData: ->
|
updateRevenueChartData: ->
|
||||||
@revenueChartLines = []
|
@revenueDailyChartLines90Days = []
|
||||||
|
@revenueMonthlyChartLines90Days = []
|
||||||
|
@revenueDailyChartLines365Days = []
|
||||||
|
@revenueMonthlyChartLines365Days = []
|
||||||
return unless @revenue?.length
|
return unless @revenue?.length
|
||||||
days = d3Utils.createContiguousDays(90)
|
|
||||||
|
|
||||||
groupDayMap = {}
|
groupDayMap = {}
|
||||||
for entry in @revenue
|
for entry in @revenue
|
||||||
|
@ -655,24 +673,31 @@ module.exports = class AnalyticsView extends RootView
|
||||||
groupDayMap[@revenueGroups[i]][entry.day] ?= 0
|
groupDayMap[@revenueGroups[i]][entry.day] ?= 0
|
||||||
groupDayMap[@revenueGroups[i]][entry.day] += count
|
groupDayMap[@revenueGroups[i]][entry.day] += count
|
||||||
|
|
||||||
colorIndex = 0
|
addRevenueChartLine = (days, eventPrefix, lines) =>
|
||||||
dailyMax = 0
|
colorIndex = 0
|
||||||
for group, entries of groupDayMap
|
dailyMax = 0
|
||||||
data = []
|
for group, entries of groupDayMap
|
||||||
for day, count of entries
|
continue unless group.indexOf(eventPrefix) >= 0
|
||||||
data.push
|
data = []
|
||||||
day: day
|
for day, count of entries
|
||||||
value: count / 100
|
data.push
|
||||||
data.reverse()
|
day: day
|
||||||
points = @createLineChartPoints(days, data)
|
value: count / 100
|
||||||
@revenueChartLines.push
|
data.reverse()
|
||||||
points: points
|
points = @createLineChartPoints(days, data)
|
||||||
description: group.replace('DRR ', 'Daily ')
|
lines.push
|
||||||
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
points: points
|
||||||
strokeWidth: 1
|
description: group.replace(eventPrefix + ' ', 'Daily ')
|
||||||
min: 0
|
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
||||||
max: _.max(points, 'y').y
|
strokeWidth: 1
|
||||||
showYScale: group in ['Daily Total', 'Monthly']
|
min: 0
|
||||||
dailyMax = _.max(points, 'y').y if group is 'Daily Total'
|
max: _.max(points, 'y').y
|
||||||
for line in @revenueChartLines when line.description isnt 'Monthly'
|
showYScale: group is eventPrefix + ' Total'
|
||||||
line.max = dailyMax
|
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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue