From d532d9fe6e536d65255a35e87e073668c924e6f9 Mon Sep 17 00:00:00 2001 From: Matt Lott <mattlott@live.com> Date: Fri, 16 Jan 2015 15:44:24 -0800 Subject: [PATCH] Add date picker to campaign editor analytics Only for overall campaign editor analytics. Still need to add date pickers to individual level views. Moving campaign editor analytics to its own modal to support reloading with new dates. The date picker widget works poorly, but you can type in dates if necessary. Also moving analytics button up into the top right header, with a little stats glyph icon. --- .../campaign/campaign-analytics-modal.jade | 37 +++++++ .../editor/campaign/campaign-editor-view.jade | 41 +------ .../campaign/CampaignAnalyticsModal.coffee | 101 ++++++++++++++++++ .../editor/campaign/CampaignEditorView.coffee | 72 ++----------- 4 files changed, 150 insertions(+), 101 deletions(-) create mode 100644 app/templates/editor/campaign/campaign-analytics-modal.jade create mode 100644 app/views/editor/campaign/CampaignAnalyticsModal.coffee diff --git a/app/templates/editor/campaign/campaign-analytics-modal.jade b/app/templates/editor/campaign/campaign-analytics-modal.jade new file mode 100644 index 000000000..a19be9ac9 --- /dev/null +++ b/app/templates/editor/campaign/campaign-analytics-modal.jade @@ -0,0 +1,37 @@ +extends /templates/core/modal-base + +block modal-header-content + h3 Campaign Analytics + if campaignCompletions.startDay && campaignCompletions.endDay + .input-group.input-group-sm + input.form-control#input-startday(type='text', style='width:100px;', value=campaignCompletions.startDay) + input.form-control#input-endday(type='text', style='width:100px;', value=campaignCompletions.endDay) + button.btn.btn-default.btn-sm#reload-button(style='margin-left:10px;') Reload + +block modal-body-content + if campaignCompletions && campaignCompletions.levels + table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt') + thead + tr + td Level + td Started + td Finished + td Playtime (s) + td Completion % + tbody + - for (var i = 0; i < campaignCompletions.levels.length; i++) + tr + td= campaignCompletions.levels[i].level + td= campaignCompletions.levels[i].started + td= campaignCompletions.levels[i].finished + td= campaignCompletions.levels[i].averagePlaytime + if campaignCompletions.top3.indexOf(campaignCompletions.levels[i].level) >= 0 + td(style='background-color:lightblue;')= campaignCompletions.levels[i].completionRate + else if campaignCompletions.bottom3.indexOf(campaignCompletions.levels[i].level) >= 0 + td(style='background-color:pink;')= campaignCompletions.levels[i].completionRate + else + td= campaignCompletions.levels[i].completionRate + else + div Loading... + +block modal-footer \ No newline at end of file diff --git a/app/templates/editor/campaign/campaign-editor-view.jade b/app/templates/editor/campaign/campaign-editor-view.jade index 0e873ddf7..b97154825 100644 --- a/app/templates/editor/campaign/campaign-editor-view.jade +++ b/app/templates/editor/campaign/campaign-editor-view.jade @@ -16,6 +16,9 @@ block header span.glyphicon-home.glyphicon ul.nav.navbar-nav.navbar-right + li#analytics-button + a + span.glyphicon-stats.glyphicon if me.isAdmin() li#save-button a @@ -40,43 +43,5 @@ block outer_content #right-column #campaign-view #campaign-level-view.hidden - if campaignCompletions - button.btn.btn-default#analytics-button(title="Analytics", data-toggle="modal" data-target="#analytics-modal") Analytics - .modal.fade#analytics-modal(tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true") - .modal-dialog.modal-lg - .modal-content - .modal-header - button.close(type="button", data-dismiss="modal", aria-label="Close") - span(aria-hidden="true") × - h4.modal-title Analytics - .modal-body - if campaignCompletions.startDay - if campaignCompletions.endDay - div #{campaignCompletions.startDay} to #{campaignCompletions.endDay} - else - div #{campaignCompletions.startDay} to yesterday - table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt') - thead - tr - td Level - td Started - td Finished - td Playtime (s) - td Completion % - tbody - - for (var i = 0; i < campaignCompletions.levels.length; i++) - tr - td= campaignCompletions.levels[i].level - td= campaignCompletions.levels[i].started - td= campaignCompletions.levels[i].finished - td= campaignCompletions.levels[i].averagePlaytime - if campaignCompletions.top3.indexOf(campaignCompletions.levels[i].level) >= 0 - td(style='background-color:lightblue;')= campaignCompletions.levels[i].completionRate - else if campaignCompletions.bottom3.indexOf(campaignCompletions.levels[i].level) >= 0 - td(style='background-color:pink;')= campaignCompletions.levels[i].completionRate - else - td= campaignCompletions.levels[i].completionRate - else - button.btn.btn-default.disabled#analytics-button Analytics Loading... block footer diff --git a/app/views/editor/campaign/CampaignAnalyticsModal.coffee b/app/views/editor/campaign/CampaignAnalyticsModal.coffee new file mode 100644 index 000000000..0b47bf6b5 --- /dev/null +++ b/app/views/editor/campaign/CampaignAnalyticsModal.coffee @@ -0,0 +1,101 @@ +template = require 'templates/editor/campaign/campaign-analytics-modal' +utils = require 'core/utils' +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" + + onClickReloadButton: () => + startDay = $('#input-startday').val() + endDay = $('#input-endday').val() + delete @campaignCompletions.levels + @render() + @getCampaignAnalytics startDay, endDay + + getCampaignAnalytics: (startDay, endDay) => + # Fetch campaign analytics, unless dates given + + startDay = startDay.replace(/-/g, '') if startDay? + endDay = endDay.replace(/-/g, '') if endDay? + + startDay ?= utils.getUTCDay -14 + endDay ?= utils.getUTCDay -1 + + success = (data) => + return if @destroyed + mapFn = (item) -> + item.completionRate = (item.finished / item.started * 100).toFixed(2) + item + @campaignCompletions.levels = _.map data, mapFn, @ + sortedLevels = _.cloneDeep @campaignCompletions.levels + sortedLevels = _.filter sortedLevels, ((a) -> a.finished >= 10), @ + sortedLevels.sort (a, b) -> b.completionRate - a.completionRate + @campaignCompletions.top3 = _.pluck sortedLevels[0..2], 'level' + sortedLevels.sort (a, b) -> a.completionRate - b.completionRate + @campaignCompletions.bottom3 = _.pluck sortedLevels[0..2], 'level' + @campaignCompletions.startDay = "#{startDay[0..3]}-#{startDay[4..5]}-#{startDay[6..7]}" + @campaignCompletions.endDay = "#{endDay[0..3]}-#{endDay[4..5]}-#{endDay[6..7]}" + @getCampaignAveragePlaytimes startDay, endDay + + # 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() + + getCampaignAveragePlaytimes: (startDay, endDay) => + # Fetch level average playtimes + success = (data) => + return if @destroyed + 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) + else + level.averagePlaytime = 0.0 + @render() + + startDay ?= utils.getUTCDay -14 + startDay = "#{startDay[0..3]}-#{startDay[4..5]}-#{startDay[6..7]}" + endDay ?= utils.getUTCDay -1 + endDay = "#{endDay[0..3]}-#{endDay[4..5]}-#{endDay[6..7]}" + + levelSlugs = _.pluck @campaignCompletions.levels, 'level' + + request = @supermodel.addRequestResource 'playtime_averages', { + url: '/db/level/-/playtime_averages' + data: {startDay: startDay, endDay: endDay, slugs: levelSlugs} + method: 'POST' + success: success + }, 0 + request.load() diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index 7f9fe1969..db3cb140e 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -7,9 +7,10 @@ CampaignView = require 'views/play/CampaignView' CocoCollection = require 'collections/CocoCollection' treemaExt = require 'core/treema-ext' utils = require 'core/utils' -SaveCampaignModal = require './SaveCampaignModal' RelatedAchievementsCollection = require 'collections/RelatedAchievementsCollection' +CampaignAnalyticsModal = require './CampaignAnalyticsModal' CampaignLevelView = require './CampaignLevelView' +SaveCampaignModal = require './SaveCampaignModal' achievementProject = ['related', 'rewards', 'name', 'slug'] thangTypeProject = ['name', 'original'] @@ -20,6 +21,7 @@ module.exports = class CampaignEditorView extends RootView className: 'editor' events: + 'click #analytics-button': 'onClickAnalyticsButton' 'click #save-button': 'onClickSaveButton' constructor: (options, @campaignHandle) -> @@ -27,6 +29,9 @@ module.exports = class CampaignEditorView extends RootView @campaign = new Campaign({_id:@campaignHandle}) @supermodel.loadModel(@campaign, 'campaign') + # Save reference to data used by anlytics modal so it persists across modal open/closes. + @campaignAnalytics = {} + @levels = new CocoCollection([], { model: Level url: "/db/campaign/#{@campaignHandle}/levels" @@ -47,8 +52,6 @@ module.exports = class CampaignEditorView extends RootView @listenToOnce @levels, 'sync', @onFundamentalLoaded @listenToOnce @achievements, 'sync', @onFundamentalLoaded - _.delay @getCampaignAnalytics, 500 - loadThangTypeNames: -> # Load the names of the ThangTypes that this level's Treema nodes might want to display. originals = [] @@ -132,9 +135,11 @@ module.exports = class CampaignEditorView extends RootView getRenderData: -> c = super() c.campaign = @campaign - c.campaignCompletions = @campaignCompletions c + onClickAnalyticsButton: -> + @openModalView new CampaignAnalyticsModal {}, @campaignHandle, @campaignAnalytics + onClickSaveButton: -> @toSave.set @toSave.filter (m) -> m.hasLocalChanges() @openModalView new SaveCampaignModal({}, @toSave) @@ -240,65 +245,6 @@ module.exports = class CampaignEditorView extends RootView if achievement.hasLocalChanges() @toSave.add achievement - getCampaignAnalytics: => - # Fetch last 14 days of campaign analytics - - startDay = utils.getUTCDay -14 - endDay = utils.getUTCDay -1 - - success = (data) => - return if @destroyed - mapFn = (item) -> - item.completionRate = (item.finished / item.started * 100).toFixed(2) - item - @campaignCompletions = levels: _.map data, mapFn, @ - sortedLevels = _.cloneDeep @campaignCompletions.levels - sortedLevels = _.filter sortedLevels, ((a) -> a.completionRate > 1.0), @ - sortedLevels.sort (a, b) -> b.completionRate - a.completionRate - @campaignCompletions.top3 = _.pluck sortedLevels[0..2], 'level' - sortedLevels.sort (a, b) -> a.completionRate - b.completionRate - @campaignCompletions.bottom3 = _.pluck sortedLevels[0..2], 'level' - @campaignCompletions.startDay = "#{startDay[0..3]}-#{startDay[4..5]}-#{startDay[6..7]}" - @campaignCompletions.endDay = "#{endDay[0..3]}-#{endDay[4..5]}-#{endDay[6..7]}" - @getCampaignAveragePlaytimes() - - # TODO: Why do we need this url dash? - request = @supermodel.addRequestResource 'campaign_completions', { - url: '/db/analytics_perday/-/campaign_completions' - data: {startDay: startDay, slug: @campaignHandle} - method: 'POST' - success: success - }, 0 - request.load() - - getCampaignAveragePlaytimes: => - # Fetch last 14 days of level average playtimes - success = (data) => - return if @destroyed - 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) - else - level.averagePlaytime = 0.0 - @render() - - levelSlugs = _.pluck @campaignCompletions.levels, 'level' - startDay = utils.getUTCDay -14 - startDay = "#{startDay[0..3]}-#{startDay[4..5]}-#{startDay[6..7]}" - request = @supermodel.addRequestResource 'playtime_averages', { - url: '/db/level/-/playtime_averages' - data: {startDay: startDay, slugs: levelSlugs} - method: 'POST' - success: success - }, 0 - request.load() - class LevelsNode extends TreemaObjectNode valueClass: 'treema-levels'