diff --git a/app/styles/editor/campaign/campaign-analytics-modal.sass b/app/styles/editor/campaign/campaign-analytics-modal.sass
new file mode 100644
index 000000000..75e0ee6d9
--- /dev/null
+++ b/app/styles/editor/campaign/campaign-analytics-modal.sass
@@ -0,0 +1,3 @@
+#campaign-analytics-modal
+  .modal-dialog
+    width: 75%
diff --git a/app/styles/editor/campaign/campaign-editor-view.sass b/app/styles/editor/campaign/campaign-editor-view.sass
index af81785e5..8ce962a8d 100644
--- a/app/styles/editor/campaign/campaign-editor-view.sass
+++ b/app/styles/editor/campaign/campaign-editor-view.sass
@@ -21,13 +21,3 @@
     bottom: 0
     right: 0
     width: 75%
-
-    #analytics-button
-      position: absolute
-      right: 1%
-      top: 1%
-      padding: 3px 8px
-
-    #analytics-modal 
-      .modal-content
-        background-color: white
diff --git a/app/templates/editor/campaign/campaign-analytics-modal.jade b/app/templates/editor/campaign/campaign-analytics-modal.jade
index a19be9ac9..30b21089c 100644
--- a/app/templates/editor/campaign/campaign-analytics-modal.jade
+++ b/app/templates/editor/campaign/campaign-analytics-modal.jade
@@ -16,7 +16,10 @@ block modal-body-content
           td Level
           td Started
           td Finished
+          td Left Game
+          td LG %
           td Playtime (s)
+          td LG/s
           td Completion %
       tbody
         - for (var i = 0; i < campaignCompletions.levels.length; i++)
@@ -24,10 +27,19 @@ block modal-body-content
             td= campaignCompletions.levels[i].level
             td= campaignCompletions.levels[i].started
             td= campaignCompletions.levels[i].finished
+            td= campaignCompletions.levels[i].dropped
+            if campaignCompletions.top3DropPercentage && campaignCompletions.top3DropPercentage.indexOf(campaignCompletions.levels[i].level) >= 0
+              td(style='background-color:pink;')= td= campaignCompletions.levels[i].dropPercentage
+            else
+              td= campaignCompletions.levels[i].dropPercentage
             td= campaignCompletions.levels[i].averagePlaytime
-            if campaignCompletions.top3.indexOf(campaignCompletions.levels[i].level) >= 0
+            if campaignCompletions.top3DropPerSecond && campaignCompletions.top3DropPerSecond.indexOf(campaignCompletions.levels[i].level) >= 0
+              td(style='background-color:pink;')= td= campaignCompletions.levels[i].droppedPerSecond
+            else
+              td= campaignCompletions.levels[i].droppedPerSecond
+            if campaignCompletions.top3 && 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
+            else if campaignCompletions.bottom3 && campaignCompletions.bottom3.indexOf(campaignCompletions.levels[i].level) >= 0
               td(style='background-color:pink;')= campaignCompletions.levels[i].completionRate
             else
               td= campaignCompletions.levels[i].completionRate
diff --git a/app/views/editor/campaign/CampaignAnalyticsModal.coffee b/app/views/editor/campaign/CampaignAnalyticsModal.coffee
index 0b47bf6b5..f8837b616 100644
--- a/app/views/editor/campaign/CampaignAnalyticsModal.coffee
+++ b/app/views/editor/campaign/CampaignAnalyticsModal.coffee
@@ -31,47 +31,41 @@ module.exports = class CampaignAnalyticsModal extends ModalView
     startDay = $('#input-startday').val()
     endDay = $('#input-endday').val()
     delete @campaignCompletions.levels
+    @campaignCompletions.startDay = startDay
+    @campaignCompletions.endDay = endDay
     @render()
     @getCampaignAnalytics startDay, endDay
 
   getCampaignAnalytics: (startDay, endDay) =>
-    # Fetch campaign analytics, unless dates given
+    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
 
-    startDay = startDay.replace(/-/g, '') if startDay?
-    endDay = endDay.replace(/-/g, '') if endDay?
+    # 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()
 
-    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) =>
+  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] ?= []
@@ -81,14 +75,16 @@ module.exports = class CampaignAnalyticsModal extends ModalView
           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
-      @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]}"
+            
+      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()
 
     levelSlugs = _.pluck @campaignCompletions.levels, 'level'
 
@@ -99,3 +95,62 @@ module.exports = class CampaignAnalyticsModal extends ModalView
       success: success
     }, 0
     request.load()
+
+  getCampaignLevelCompletions: (startDay, endDay, doneCallback) =>
+    # Needs date format yyyymmdd
+    success = (data) =>
+      return if @destroyed
+      # console.log 'getCampaignLevelCompletions success', data
+      mapFn = (item) ->
+        item.completionRate = if item.started > 0 then (item.finished / item.started * 100).toFixed(2) else 0.0
+        item
+      @campaignCompletions.levels = _.map data, mapFn, @
+
+      sortedLevels = _.cloneDeep @campaignCompletions.levels
+      sortedLevels = _.filter sortedLevels, ((a) -> a.finished >= 10), @
+      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()
+
+    # 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()
+
+  getCompaignLevelDrops: (startDay, endDay, doneCallback) =>
+    # Fetch level drops
+    # Needs date format yyyymmdd
+    success = (data) =>
+      return if @destroyed
+      # console.log 'getCompaignLevelDrops success', data
+      levelDrops = {}
+      for item in data
+        levelDrops[item.level] ?= item.dropped
+      for level in @campaignCompletions.levels
+        level.dropped = levelDrops[level.level] ? 0
+        level.dropPercentage = (level.dropped / level.started * 100).toFixed(2) if level.started > 0
+
+      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()
+
+    return unless @campaignCompletions?.levels?
+    levelSlugs = _.pluck @campaignCompletions.levels, 'level'
+
+    request = @supermodel.addRequestResource 'level_drops', {
+      url: '/db/analytics_perday/-/level_drops'
+      data: {startDay: startDay, endDay: endDay, slugs: levelSlugs}
+      method: 'POST'
+      success: success
+    }, 0
+    request.load()
diff --git a/scripts/analytics/mongodb/queries/insertPerDayAnalytics.js b/scripts/analytics/mongodb/queries/insertPerDayAnalytics.js
index 86518d90d..fa0128471 100644
--- a/scripts/analytics/mongodb/queries/insertPerDayAnalytics.js
+++ b/scripts/analytics/mongodb/queries/insertPerDayAnalytics.js
@@ -8,6 +8,8 @@
 // Finish count for the same start date is how many unique users finished the remaining steps in the following ~30 days
 // https://mixpanel.com/help/questions/articles/how-are-funnels-calculated
 
+// Drop count: last started or finished level event for a given unique user
+
 // TODO: Why are Mixpanel level finish events significantly lower?
 // TODO: dungeons-of-kithgard completion rate is 62% vs. 77%
 // TODO: Similar start events, finish events off by 20% (5334 vs 6486)
@@ -74,11 +76,12 @@ function getLevelFunnelData(startDay, eventFunnel) {
   var queryParams = {$and: [{_id: {$gte: startObj}},{"event": {$in: eventFunnel}}]};
   var cursor = db['analytics.log.events'].find(queryParams);
 
-  // Map ordering: level, user, event, created
+  // Map ordering: level, user, event, day
   var userDataMap = {};
   while (cursor.hasNext()) {
     var doc = cursor.next();
-    var created = doc._id.getTimestamp().toISOString().substring(0, 10);
+    var created = doc._id.getTimestamp().toISOString();
+    var day = created.substring(0, 10);
     var event = doc.event;
     var properties = doc.properties;
     var user = doc.user;
@@ -91,13 +94,13 @@ function getLevelFunnelData(startDay, eventFunnel) {
 
     if (!userDataMap[level]) userDataMap[level] = {};
     if (!userDataMap[level][user]) userDataMap[level][user] = {};
-    if (!userDataMap[level][user][event] || userDataMap[level][user][event].localeCompare(created) > 0) {
-      // if (userDataMap[level][user][event]) log("Found earlier date " + level + " " + event + " " + user + " " + userDataMap[level][user][event] + " " + created);
-      userDataMap[level][user][event] = created;
+    if (!userDataMap[level][user][event] || userDataMap[level][user][event].localeCompare(day) > 0) {
+      // if (userDataMap[level][user][event]) log("Found earlier date " + level + " " + event + " " + user + " " + userDataMap[level][user][event] + " " + day);
+      userDataMap[level][user][event] = day;
     }
   }
 
-  // Data: level, created, event
+  // Data: level, day, event
   var levelFunnelData = {};
   for (level in userDataMap) {
     for (user in userDataMap[level]) {
@@ -105,14 +108,14 @@ function getLevelFunnelData(startDay, eventFunnel) {
       // Find first event date
       var funnelStartDay = null;
       for (event in userDataMap[level][user]) {
-        var created = userDataMap[level][user][event];
+        var day = userDataMap[level][user][event];
         if (!levelFunnelData[level]) levelFunnelData[level] = {};
-        if (!levelFunnelData[level][created]) levelFunnelData[level][created] = {};
-        if (!levelFunnelData[level][created][event]) levelFunnelData[level][created][event] = 0;
+        if (!levelFunnelData[level][day]) levelFunnelData[level][day] = {};
+        if (!levelFunnelData[level][day][event]) levelFunnelData[level][day][event] = 0;
         if (eventFunnel[0] === event) {
           // First event gets attributed to current date
-          levelFunnelData[level][created][event]++;
-          funnelStartDay = created;
+          levelFunnelData[level][day][event]++;
+          funnelStartDay = day;
           break;
         }
       }
@@ -135,6 +138,51 @@ function getLevelFunnelData(startDay, eventFunnel) {
   return levelFunnelData;
 }
 
+function getLevelDropCounts(startDay, events) {
+  // How many unique users did one of these events last?
+  // Return level/day breakdown
+
+  if (!startDay || !events || events.length === 0) return {};
+
+  var startObj = objectIdWithTimestamp(ISODate(startDay + "T00:00:00.000Z"));
+  var queryParams = {$and: [{_id: {$gte: startObj}},{"event": {$in: events}}]};
+  var cursor = db['analytics.log.events'].find(queryParams);
+
+  var userProgression = {};
+  while (cursor.hasNext()) {
+    var doc = cursor.next();
+    var created = doc._id.getTimestamp().toISOString();
+    var event = doc.event;
+    var properties = doc.properties;
+    var user = doc.user;
+    var level;
+
+    // TODO: Switch to properties.levelID for 'Saw Victory'
+    if (event === 'Saw Victory' && properties.level) level = properties.level.toLowerCase().replace(/ /g, '-');
+    else if (properties.levelID) level = properties.levelID
+    else continue
+
+    if (!userProgression[user]) userProgression[user] = [];
+    userProgression[user].push({
+      created: created,
+      event: event,
+      level: level
+    });
+  }
+
+  var levelDropCounts = {};
+  for (user in userProgression) {
+    userProgression[user].sort(function (a,b) {return a.created < b.created ? -1 : 1});
+    var lastEvent = userProgression[user][userProgression[user].length - 1];
+    var level = lastEvent.level;
+    var day = lastEvent.created.substring(0, 10);
+    if (!levelDropCounts[level]) levelDropCounts[level] = {};
+    if (!levelDropCounts[level][day]) levelDropCounts[level][day] = 0
+      levelDropCounts[level][day]++;
+  }
+  return levelDropCounts;
+}
+
 function insertEventCount(event, level, day, count) {
   // analytics.perdays schema in server/analytics/AnalyticsPeryDay.coffee
   day = day.replace(/-/g, '');
@@ -153,7 +201,7 @@ function insertEventCount(event, level, day, count) {
     // log("Updating count in db for " + day + " " + event + " " + level + " " + doc.c + " => " + count);
     var results = db['analytics.perdays'].update(queryParams, {$set: {c: count}});
     if (results.nMatched !== 1 && results.nModified !== 1) {
-      log("ERROR: update count failed");
+      log("ERROR: update event count failed");
       printjson(results);
     }
   }
@@ -191,13 +239,25 @@ try {
   
   log("Inserting aggregated level completion data...");
   for (level in levelCompletionData) {
-    for (created in levelCompletionData[level]) {
-      if (today === created) continue; // Never save data for today because it's incomplete
-      for (event in levelCompletionData[level][created]) {
-        insertEventCount(event, level, created, levelCompletionData[level][created][event]);
+    for (day in levelCompletionData[level]) {
+      if (today === day) continue; // Never save data for today because it's incomplete
+      for (event in levelCompletionData[level][day]) {
+        insertEventCount(event, level, day, levelCompletionData[level][day][event]);
       }
     }
   }
+
+  log("Getting level drop counts...");
+  var levelDropCounts = getLevelDropCounts(startDay, levelCompletionFunnel)
+
+  log("Inserting level drop counts...");
+  var levelDropCounts = getLevelDropCounts(startDay, levelCompletionFunnel)
+  for (level in levelDropCounts) {
+    for (day in levelDropCounts[level]) {
+      if (today === day) continue; // Never save data for today because it's incomplete
+      insertEventCount('User Dropped', level, day, levelDropCounts[level][day]);
+    }
+  }
 } 
 catch(err) {
   log("ERROR: " + err);
diff --git a/server/analytics/analytics_perday_handler.coffee b/server/analytics/analytics_perday_handler.coffee
index c5cbde6b8..3f5537e2c 100644
--- a/server/analytics/analytics_perday_handler.coffee
+++ b/server/analytics/analytics_perday_handler.coffee
@@ -15,10 +15,11 @@ class AnalyticsPerDayHandler extends Handler
   getByRelationship: (req, res, args...) ->
     return @getCampaignCompletionsBySlug(req, res) if args[1] is 'campaign_completions'
     return @getLevelCompletionsBySlug(req, res) if args[1] is 'level_completions'
+    return @getLevelDropsBySlugs(req, res) if args[1] is 'level_drops'
     super(arguments...)
 
   getCampaignCompletionsBySlug: (req, res) ->
-    # Send back an array of level starts and finishes
+    # Send back an ordered array of level starts and finishes
     # Parameters:
     # slug - campaign slug
     # startDay - Inclusive, optional, YYYYMMDD e.g. '20141214'
@@ -41,7 +42,7 @@ class AnalyticsPerDayHandler extends Handler
     cacheKey = campaignSlug
     cacheKey += 's' + startDay if startDay?
     cacheKey += 'e' + endDay if endDay?
-    return @sendSuccess res, campaignDropOffs if campaignDropOffs = @campaignCompletionsCache[cacheKey]
+    return @sendSuccess res, completions if completions = @campaignCompletionsCache[cacheKey]
 
     getCompletions = (orderedLevelSlugs, levelStringIDSlugMap) =>
       # 3. Send back an array of level starts and finishes
@@ -80,8 +81,8 @@ class AnalyticsPerDayHandler extends Handler
           for levelID of levelEventCounts
             completions.push
               level: levelStringIDSlugMap[levelID]
-              started: levelEventCounts[levelID][startEventID]
-              finished: levelEventCounts[levelID][finishEventID]
+              started: levelEventCounts[levelID][startEventID] ? 0
+              finished: levelEventCounts[levelID][finishEventID] ? 0
           completions.sort (a, b) -> orderedLevelSlugs.indexOf(a.level) - orderedLevelSlugs.indexOf(b.level)
 
           @campaignCompletionsCache[cacheKey] = completions
@@ -124,6 +125,69 @@ class AnalyticsPerDayHandler extends Handler
       campaignLevels.push level for level of doc.get('levels') for doc in documents
       getLevelData campaignLevels
 
+  getLevelDropsBySlugs: (req, res) ->
+    # Send back an array of level/drops
+    # Drops - Number of unique users for which this was the last level they played
+    # Parameters:
+    # slugs - level slugs
+    # startDay - Inclusive, optional, YYYYMMDD e.g. '20141214'
+    # endDay - Exclusive, optional, YYYYMMDD e.g. '20141216'
+
+    levelSlugs = req.query.slugs or req.body.slugs
+    startDay = req.query.startDay or req.body.startDay
+    endDay = req.query.endDay or req.body.endDay
+
+    # log.warn "level_drops levelSlugs='#{levelSlugs}' startDay=#{startDay} endDay=#{endDay}"
+
+    return @sendSuccess res, [] unless levelSlugs?
+
+    # Cache results in app server memory for 1 day
+    @levelDropsCache ?= {}
+    @levelDropsCachedSince ?= new Date()
+    if (new Date()) - @levelDropsCachedSince > 86400 * 1000
+      @levelDropsCache = {}
+      @levelDropsCachedSince = new Date()
+    cacheKey = levelSlugs.join ''
+    cacheKey += 's' + startDay if startDay?
+    cacheKey += 'e' + endDay if endDay?
+    return @sendSuccess res, drops if drops = @levelDropsCache[cacheKey]
+
+    AnalyticsString.find({v: {$in: ['User Dropped', 'all'].concat(levelSlugs)}}).exec (err, documents) =>
+      if err? then return @sendDatabaseError res, err
+
+      levelStringIDSlugMap = {}
+      for doc in documents
+        droppedEventID = doc._id if doc.v is 'User Dropped'
+        filterEventID =  doc._id if doc.v is 'all'
+        levelStringIDSlugMap[doc._id] = doc.v if doc.v in levelSlugs
+
+      return @sendSuccess res, [] unless droppedEventID? and filterEventID?
+
+      queryParams = {$and: [
+        {e: droppedEventID},
+        {f: filterEventID},
+        {l: {$in: Object.keys(levelStringIDSlugMap)}}
+      ]}
+      queryParams["$and"].push {d: {$gte: startDay}} if startDay?
+      queryParams["$and"].push {d: {$lt: endDay}} if endDay?
+      AnalyticsPerDay.find(queryParams).exec (err, documents) =>
+        if err? then return @sendDatabaseError res, err
+
+        levelEventCounts = {}
+        for doc in documents
+          levelEventCounts[doc.l] ?= {}
+          levelEventCounts[doc.l][doc.e] ?= 0
+          levelEventCounts[doc.l][doc.e] += doc.c
+
+        drops = []
+        for levelID of levelEventCounts
+          drops.push
+            level: levelStringIDSlugMap[levelID]
+            dropped: levelEventCounts[levelID][droppedEventID] ? 0
+
+        @levelDropsCache[cacheKey] = drops
+        @sendSuccess res, drops
+
   getLevelCompletionsBySlug: (req, res) ->
     # Returns an array of per-day starts and finishes for given level
     # Parameters:
diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee
index 4e86301f5..1831b5b35 100644
--- a/server/levels/level_handler.coffee
+++ b/server/levels/level_handler.coffee
@@ -7,6 +7,7 @@ Handler = require '../commons/Handler'
 mongoose = require 'mongoose'
 async = require 'async'
 utils = require '../lib/utils'
+log = require 'winston'
 
 LevelHandler = class LevelHandler extends Handler
   modelClass: Level
@@ -363,6 +364,8 @@ LevelHandler = class LevelHandler extends Handler
 
     return @sendSuccess res, [] unless levelSlugs?
 
+    # log.warn "playtime_averages levelSlugs='#{levelSlugs}' startDay=#{startDay} endDay=#{endDay}"
+
     # Cache results for 1 day
     @levelPlaytimesCache ?= {}
     @levelPlaytimesCachedSince ?= new Date()