2015-01-16 18:44:24 -05:00
|
|
|
template = require 'templates/editor/campaign/campaign-analytics-modal'
|
|
|
|
utils = require 'core/utils'
|
2015-01-20 00:59:25 -05:00
|
|
|
require 'vendor/d3'
|
2015-01-16 18:44:24 -05:00
|
|
|
ModalView = require 'views/core/ModalView'
|
|
|
|
|
|
|
|
# TODO: jquery-ui datepicker doesn't work well in this view
|
|
|
|
# TODO: the date format handling is confusing (yyyy-mm-dd <=> yyyymmdd)
|
|
|
|
|
|
|
|
module.exports = class CampaignAnalyticsModal extends ModalView
|
|
|
|
id: 'campaign-analytics-modal'
|
|
|
|
template: template
|
|
|
|
plain: true
|
|
|
|
|
|
|
|
events:
|
|
|
|
'click #reload-button': 'onClickReloadButton'
|
|
|
|
|
|
|
|
constructor: (options, @campaignHandle, @campaignCompletions) ->
|
|
|
|
super options
|
|
|
|
@getCampaignAnalytics() unless @campaignCompletions?.levels?
|
|
|
|
|
|
|
|
getRenderData: ->
|
|
|
|
c = super()
|
|
|
|
c.campaignCompletions = @campaignCompletions
|
|
|
|
c
|
|
|
|
|
|
|
|
afterRender: ->
|
|
|
|
super()
|
|
|
|
$("#input-startday").datepicker dateFormat: "yy-mm-dd"
|
|
|
|
$("#input-endday").datepicker dateFormat: "yy-mm-dd"
|
2015-01-20 00:59:25 -05:00
|
|
|
@addCompletionLineGraphs()
|
|
|
|
|
|
|
|
addCompletionLineGraphs: ->
|
|
|
|
return unless @campaignCompletions.levels
|
|
|
|
for level in @campaignCompletions.levels
|
|
|
|
days = []
|
|
|
|
for day of level['days']
|
2015-01-20 14:14:50 -05:00
|
|
|
continue unless level['days'][day].started > 0
|
2015-01-20 00:59:25 -05:00
|
|
|
days.push
|
|
|
|
day: day
|
|
|
|
rate: level['days'][day].finished / level['days'][day].started
|
|
|
|
days.sort (a, b) -> a.day - b.day
|
|
|
|
data = []
|
|
|
|
for i in [0...days.length]
|
|
|
|
data.push
|
|
|
|
x: i
|
|
|
|
y: days[i].rate
|
|
|
|
@addLineGraph '#background' + level.level, data
|
|
|
|
|
|
|
|
addLineGraph: (containerSelector, lineData, lineColor='green', min=0, max=1.0) ->
|
|
|
|
# Add a line chart to the given container
|
|
|
|
# TODO: Move this to a utility library
|
|
|
|
vis = d3.select(containerSelector)
|
|
|
|
width = $(containerSelector).width()
|
|
|
|
height = $(containerSelector).height()
|
|
|
|
xRange = d3.scale.linear().range([0, width]).domain([d3.min(lineData, (d) -> d.x), d3.max(lineData, (d) -> d.x)])
|
|
|
|
yRange = d3.scale.linear().range([height, 0]).domain([min, max])
|
|
|
|
xAxis = d3.svg.axis()
|
|
|
|
.scale(xRange)
|
|
|
|
.tickSize(5)
|
|
|
|
.tickSubdivide(true)
|
|
|
|
yAxis = d3.svg.axis()
|
|
|
|
.scale(yRange)
|
|
|
|
.tickSize(5)
|
|
|
|
.orient('left')
|
|
|
|
.tickSubdivide(true)
|
|
|
|
vis.append('svg:g')
|
|
|
|
.attr('class', 'x axis')
|
|
|
|
.attr('transform', 'translate(0,' + height + ')')
|
|
|
|
.call(xAxis)
|
|
|
|
vis.append('svg:g')
|
|
|
|
.attr('class', 'y axis')
|
|
|
|
.attr('transform', 'translate(0,0)')
|
|
|
|
.call(yAxis)
|
|
|
|
lineFunc = d3.svg.line()
|
|
|
|
.x((d) -> xRange(d.x))
|
|
|
|
.y((d) -> yRange(d.y))
|
|
|
|
.interpolate('linear')
|
|
|
|
vis.append('svg:path')
|
|
|
|
.attr('d', lineFunc(lineData))
|
|
|
|
.attr('stroke', lineColor)
|
|
|
|
.attr('stroke-width', 1)
|
|
|
|
.attr('fill', 'none')
|
2015-01-16 18:44:24 -05:00
|
|
|
|
|
|
|
onClickReloadButton: () =>
|
|
|
|
startDay = $('#input-startday').val()
|
|
|
|
endDay = $('#input-endday').val()
|
|
|
|
delete @campaignCompletions.levels
|
2015-01-18 19:29:44 -05:00
|
|
|
@campaignCompletions.startDay = startDay
|
|
|
|
@campaignCompletions.endDay = endDay
|
2015-01-16 18:44:24 -05:00
|
|
|
@render()
|
|
|
|
@getCampaignAnalytics startDay, endDay
|
|
|
|
|
|
|
|
getCampaignAnalytics: (startDay, endDay) =>
|
2015-01-18 19:29:44 -05:00
|
|
|
if startDay?
|
|
|
|
startDayDashed = startDay
|
|
|
|
startDay = startDay.replace(/-/g, '')
|
|
|
|
else
|
|
|
|
startDay = utils.getUTCDay -14
|
|
|
|
startDayDashed = "#{startDay[0..3]}-#{startDay[4..5]}-#{startDay[6..7]}"
|
|
|
|
if endDay?
|
|
|
|
endDayDashed = endDay
|
|
|
|
endDay = endDay.replace(/-/g, '')
|
|
|
|
else
|
|
|
|
endDay = utils.getUTCDay -1
|
|
|
|
endDayDashed = "#{endDay[0..3]}-#{endDay[4..5]}-#{endDay[6..7]}"
|
|
|
|
@campaignCompletions.startDay = startDayDashed
|
|
|
|
@campaignCompletions.endDay = endDayDashed
|
|
|
|
|
|
|
|
# Chain these together so we can calculate relative metrics (e.g. left game per second)
|
|
|
|
@getCampaignLevelCompletions startDay, endDay, () =>
|
|
|
|
@render()
|
|
|
|
@getCompaignLevelDrops startDay, endDay, () =>
|
|
|
|
@render()
|
|
|
|
@getCampaignAveragePlaytimes startDayDashed, endDayDashed, () =>
|
|
|
|
@render()
|
2015-01-16 18:44:24 -05:00
|
|
|
|
2015-01-18 19:29:44 -05:00
|
|
|
getCampaignAveragePlaytimes: (startDay, endDay, doneCallback) =>
|
|
|
|
# Fetch level average playtimes
|
|
|
|
# Needs date format yyyy-mm-dd
|
|
|
|
success = (data) =>
|
|
|
|
return if @destroyed
|
|
|
|
# console.log 'getCampaignAveragePlaytimes success', data
|
|
|
|
levelAverages = {}
|
|
|
|
for item in data
|
|
|
|
levelAverages[item.level] ?= []
|
|
|
|
levelAverages[item.level].push item.average
|
|
|
|
for level in @campaignCompletions.levels
|
|
|
|
if levelAverages[level.level]
|
|
|
|
if levelAverages[level.level].length > 0
|
|
|
|
total = _.reduce levelAverages[level.level], ((sum, num) -> sum + num)
|
|
|
|
level.averagePlaytime = (total / levelAverages[level.level].length).toFixed(2)
|
|
|
|
if level.averagePlaytime > 0 and level.dropped > 0
|
|
|
|
level.droppedPerSecond = (level.dropped / level.averagePlaytime).toFixed(2)
|
|
|
|
else
|
|
|
|
level.averagePlaytime = 0.0
|
|
|
|
|
|
|
|
sortedLevels = _.cloneDeep @campaignCompletions.levels
|
|
|
|
sortedLevels = _.filter sortedLevels, ((a) -> a.droppedPerSecond > 0), @
|
|
|
|
sortedLevels.sort (a, b) -> b.droppedPerSecond - a.droppedPerSecond
|
|
|
|
@campaignCompletions.top3DropPerSecond = _.pluck sortedLevels[0..2], 'level'
|
|
|
|
doneCallback()
|
2015-01-16 18:44:24 -05:00
|
|
|
|
2015-01-18 19:29:44 -05:00
|
|
|
levelSlugs = _.pluck @campaignCompletions.levels, 'level'
|
2015-01-16 18:44:24 -05:00
|
|
|
|
2015-01-18 19:29:44 -05:00
|
|
|
request = @supermodel.addRequestResource 'playtime_averages', {
|
|
|
|
url: '/db/level/-/playtime_averages'
|
|
|
|
data: {startDay: startDay, endDay: endDay, slugs: levelSlugs}
|
|
|
|
method: 'POST'
|
|
|
|
success: success
|
|
|
|
}, 0
|
|
|
|
request.load()
|
|
|
|
|
|
|
|
getCampaignLevelCompletions: (startDay, endDay, doneCallback) =>
|
|
|
|
# Needs date format yyyymmdd
|
2015-01-16 18:44:24 -05:00
|
|
|
success = (data) =>
|
|
|
|
return if @destroyed
|
2015-01-18 19:29:44 -05:00
|
|
|
# console.log 'getCampaignLevelCompletions success', data
|
2015-01-20 00:59:25 -05:00
|
|
|
countCompletions = (item) ->
|
|
|
|
item.started = _.reduce item.days, ((result, current) -> result + current.started), 0
|
|
|
|
item.finished = _.reduce item.days, ((result, current) -> result + current.finished), 0
|
2015-01-18 19:29:44 -05:00
|
|
|
item.completionRate = if item.started > 0 then (item.finished / item.started * 100).toFixed(2) else 0.0
|
2015-01-20 00:59:25 -05:00
|
|
|
item
|
|
|
|
addUserRemaining = (item) ->
|
2015-01-19 18:21:42 -05:00
|
|
|
item.usersRemaining = Math.round(item.started / maxStarted * 100.0) unless maxStarted is 0
|
2015-01-16 18:44:24 -05:00
|
|
|
item
|
2015-01-20 00:59:25 -05:00
|
|
|
|
|
|
|
@campaignCompletions.levels = _.map data, countCompletions, @
|
|
|
|
if @campaignCompletions.levels.length > 0
|
|
|
|
maxStarted = (_.max @campaignCompletions.levels, ((a) -> a.started)).started
|
|
|
|
else
|
|
|
|
maxStarted = 0
|
|
|
|
@campaignCompletions.levels = _.map @campaignCompletions.levels, addUserRemaining, @
|
2015-01-18 19:29:44 -05:00
|
|
|
|
2015-01-16 18:44:24 -05:00
|
|
|
sortedLevels = _.cloneDeep @campaignCompletions.levels
|
|
|
|
sortedLevels = _.filter sortedLevels, ((a) -> a.finished >= 10), @
|
2015-01-18 19:29:44 -05:00
|
|
|
if sortedLevels.length >= 3
|
|
|
|
sortedLevels.sort (a, b) -> b.completionRate - a.completionRate
|
|
|
|
@campaignCompletions.top3 = _.pluck sortedLevels[0..2], 'level'
|
|
|
|
@campaignCompletions.bottom3 = _.pluck sortedLevels[sortedLevels.length - 4...sortedLevels.length - 1], 'level'
|
|
|
|
|
|
|
|
doneCallback()
|
2015-01-16 18:44:24 -05:00
|
|
|
|
|
|
|
# TODO: Why do we need this url dash?
|
|
|
|
request = @supermodel.addRequestResource 'campaign_completions', {
|
|
|
|
url: '/db/analytics_perday/-/campaign_completions'
|
|
|
|
data: {startDay: startDay, endDay: endDay, slug: @campaignHandle}
|
|
|
|
method: 'POST'
|
|
|
|
success: success
|
|
|
|
}, 0
|
|
|
|
request.load()
|
|
|
|
|
2015-01-18 19:29:44 -05:00
|
|
|
getCompaignLevelDrops: (startDay, endDay, doneCallback) =>
|
|
|
|
# Fetch level drops
|
|
|
|
# Needs date format yyyymmdd
|
2015-01-16 18:44:24 -05:00
|
|
|
success = (data) =>
|
|
|
|
return if @destroyed
|
2015-01-18 19:29:44 -05:00
|
|
|
# console.log 'getCompaignLevelDrops success', data
|
|
|
|
levelDrops = {}
|
2015-01-16 18:44:24 -05:00
|
|
|
for item in data
|
2015-01-18 19:29:44 -05:00
|
|
|
levelDrops[item.level] ?= item.dropped
|
2015-01-16 18:44:24 -05:00
|
|
|
for level in @campaignCompletions.levels
|
2015-01-18 19:29:44 -05:00
|
|
|
level.dropped = levelDrops[level.level] ? 0
|
|
|
|
level.dropPercentage = (level.dropped / level.started * 100).toFixed(2) if level.started > 0
|
2015-01-16 18:44:24 -05:00
|
|
|
|
2015-01-18 19:29:44 -05:00
|
|
|
sortedLevels = _.cloneDeep @campaignCompletions.levels
|
|
|
|
sortedLevels = _.filter sortedLevels, ((a) -> a.dropPercentage > 0), @
|
|
|
|
if sortedLevels.length >= 3
|
|
|
|
sortedLevels.sort (a, b) -> b.dropPercentage - a.dropPercentage
|
|
|
|
@campaignCompletions.top3DropPercentage = _.pluck sortedLevels[0..2], 'level'
|
|
|
|
doneCallback()
|
2015-01-16 18:44:24 -05:00
|
|
|
|
2015-01-18 19:29:44 -05:00
|
|
|
return unless @campaignCompletions?.levels?
|
2015-01-16 18:44:24 -05:00
|
|
|
levelSlugs = _.pluck @campaignCompletions.levels, 'level'
|
|
|
|
|
2015-01-18 19:29:44 -05:00
|
|
|
request = @supermodel.addRequestResource 'level_drops', {
|
|
|
|
url: '/db/analytics_perday/-/level_drops'
|
2015-01-16 18:44:24 -05:00
|
|
|
data: {startDay: startDay, endDay: endDay, slugs: levelSlugs}
|
|
|
|
method: 'POST'
|
|
|
|
success: success
|
|
|
|
}, 0
|
|
|
|
request.load()
|