Add 7 day net to admin subs count page

This commit is contained in:
Matt Lott 2015-04-01 20:23:05 -07:00
parent 2213aff909
commit 6c1e7b9560

View file

@ -2,8 +2,8 @@ RootView = require 'views/core/RootView'
template = require 'templates/admin/analytics-subscriptions'
RealTimeCollection = require 'collections/RealTimeCollection'
# TODO: Add last N subscribers table
# TODO: Add revenue line
# TODO: Add LTV line
# TODO: Graphing code copied/mangled from campaign editor level view. OMG, DRY.
require 'vendor/d3'
@ -22,7 +22,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
getRenderData: ->
context = super()
context.analytics = @analytics ? graphs: []
context.subs = @subs ? []
context.subs = _.cloneDeep(@subs ? []).reverse()
context.total = @total ? 0
context.cancelled = @cancelled ? 0
context.monthlyChurn = @monthlyChurn ? 0.0
@ -86,6 +86,8 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
# Currently only one graph
@analytics.graphs = [graphID: 'total-subs', lines: []]
timeframeDays = 60
return unless @subs?.length > 0
# TODO: Where should this metadata live?
@ -93,16 +95,24 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
totalSubsID = 'total-subs'
startedSubsID = 'started-subs'
cancelledSubsID = 'cancelled-subs'
netSubsID = 'net-subs'
lineMetadata = {}
lineMetadata[totalSubsID] =
description: 'Total Active Subscriptions'
color: 'green'
strokeWidth: 1
lineMetadata[startedSubsID] =
description: 'New Subscriptions'
color: 'blue'
strokeWidth: 1
lineMetadata[cancelledSubsID] =
description: 'Cancelled Subscriptions'
color: 'red'
strokeWidth: 1
lineMetadata[netSubsID] =
description: '7-day Average Net Subscriptions'
color: 'black'
strokeWidth: 4
days = (sub.day for sub in @subs)
if days.length > 0
@ -139,7 +149,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
levelPoints[i].x = i
levelPoints[i].pointID = "#{totalSubsID}#{i}"
levelPoints.splice(0, levelPoints.length - 60) if levelPoints.length > 60
levelPoints.splice(0, levelPoints.length - timeframeDays) if levelPoints.length > timeframeDays
@analytics.graphs[0].lines.push
lineID: totalSubsID
@ -173,7 +183,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
levelPoints[i].x = i
levelPoints[i].pointID = "#{startedSubsID}#{i}"
levelPoints.splice(0, levelPoints.length - 60) if levelPoints.length > 60
levelPoints.splice(0, levelPoints.length - timeframeDays) if levelPoints.length > timeframeDays
@analytics.graphs[0].lines.push
lineID: startedSubsID
@ -185,16 +195,21 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
max: d3.max(@subs, (d) -> d.started)
## Cancelled
averageCancelled = 0
# Build line data
levelPoints = []
cancelled = []
for sub, i in @subs
if i >= @subs.length - 30
cancelled.push sub.cancelled
levelPoints.push
x: i
y: sub.cancelled
day: sub.day
pointID: "#{cancelledSubsID}#{i}"
values: []
averageCancelled = cancelled.reduce((a, b) -> a + b) / cancelled.length
# Ensure points for each day
for day, i in days
@ -207,7 +222,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
levelPoints[i].x = i
levelPoints[i].pointID = "#{cancelledSubsID}#{i}"
levelPoints.splice(0, levelPoints.length - 60) if levelPoints.length > 60
levelPoints.splice(0, levelPoints.length - timeframeDays) if levelPoints.length > timeframeDays
@analytics.graphs[0].lines.push
lineID: cancelledSubsID
@ -218,6 +233,52 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
min: 0
max: d3.max(@subs, (d) -> d.started)
## 7-Day Net Subs
# Build line data
levelPoints = []
sevenNets = []
for sub, i in @subs
net = 0
if i >= @subs.length - 30
sevenNets.push sub.started - sub.cancelled
else
sevenNets.push sub.started - averageCancelled
if sevenNets.length > 7
sevenNets.shift()
if sevenNets.length is 7
net = sevenNets.reduce((a, b) -> a + b) / 7
levelPoints.push
x: i
y: net
day: sub.day
pointID: "#{netSubsID}#{i}"
values: []
# Ensure points for each day
for day, i in days
if levelPoints.length <= i or levelPoints[i].day isnt day
prevY = if i > 0 then levelPoints[i - 1].y else 0.0
levelPoints.splice i, 0,
y: prevY
day: day
values: []
levelPoints[i].x = i
levelPoints[i].pointID = "#{netSubsID}#{i}"
levelPoints.splice(0, levelPoints.length - timeframeDays) if levelPoints.length > timeframeDays
@analytics.graphs[0].lines.push
lineID: netSubsID
enabled: true
points: levelPoints
description: lineMetadata[netSubsID].description
lineColor: lineMetadata[netSubsID].color
strokeWidth: lineMetadata[netSubsID].strokeWidth
min: 0
max: d3.max(@subs, (d) -> d.started)
updateAnalyticsGraphs: ->
# Build d3 graphs
return unless @analytics?.graphs?.length > 0
@ -236,7 +297,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
svg = d3.select(containerSelector).append("svg")
.attr("width", containerWidth)
.attr("height", containerHeight)
width = containerWidth - margin * 2 - yAxisWidth * graphLineCount
width = containerWidth - margin * 2 - yAxisWidth * 2
height = containerHeight - margin * 2 - xAxisHeight - keyHeight * graphLineCount
currentLine = 0
for line in graph.lines
@ -258,36 +319,39 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
.call(xAxis)
.selectAll("text")
.attr("dy", ".35em")
.attr("transform", "translate(" + (margin + yAxisWidth * (graphLineCount - 1)) + "," + (height + margin) + ")")
.attr("transform", "translate(" + (margin + yAxisWidth) + "," + (height + margin) + ")")
.style("text-anchor", "start")
if line.lineID is 'started-subs'
# Horizontal guidelines
# svg.selectAll(".line")
# .data([10, 30, 50, 70, 90])
# .enter()
# .append("line")
# .attr("x1", margin + yAxisWidth * graphLineCount)
# .attr("y1", (d) -> margin + yRange(d))
# .attr("x2", margin + yAxisWidth * graphLineCount + width)
# .attr("y2", (d) -> margin + yRange(d))
# .attr("stroke", line.lineColor)
# .style("opacity", "0.5")
marks = (Math.round(i * line.max / 5) for i in [1...5])
svg.selectAll(".line")
.data(marks)
.enter()
.append("line")
.attr("x1", margin + yAxisWidth * 2)
.attr("y1", (d) -> margin + yRange(d))
.attr("x2", margin + yAxisWidth * 2 + width)
.attr("y2", (d) -> margin + yRange(d))
.attr("stroke", line.lineColor)
.style("opacity", "0.5")
# y-Axis
yAxisRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
yAxis = d3.svg.axis()
.scale(yRange)
.orient("left")
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (margin + yAxisWidth * currentLine) + "," + margin + ")")
.style("color", line.lineColor)
.call(yAxis)
.selectAll("text")
.attr("y", 0)
.attr("x", 0)
.attr("fill", line.lineColor)
.style("text-anchor", "start")
if currentLine < 2
# y-Axis
yAxisRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
yAxis = d3.svg.axis()
.scale(yRange)
.orient("left")
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (margin + yAxisWidth * currentLine) + "," + margin + ")")
.style("color", line.lineColor)
.call(yAxis)
.selectAll("text")
.attr("y", 0)
.attr("x", 0)
.attr("fill", line.lineColor)
.style("text-anchor", "start")
# Key
svg.append("line")
@ -309,7 +373,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
.data(line.points)
.enter()
.append("circle")
.attr("transform", "translate(" + (margin + yAxisWidth * graphLineCount) + "," + margin + ")")
.attr("transform", "translate(" + (margin + yAxisWidth * 2) + "," + margin + ")")
.attr("cx", (d) -> xRange(d.x))
.attr("cy", (d) -> yRange(d.y))
.attr("r", 2)
@ -323,8 +387,8 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
.interpolate("linear")
svg.append("path")
.attr("d", d3line(line.points))
.attr("transform", "translate(" + (margin + yAxisWidth * graphLineCount) + "," + margin + ")")
.style("stroke-width", 1)
.attr("transform", "translate(" + (margin + yAxisWidth * 2) + "," + margin + ")")
.style("stroke-width", line.strokeWidth)
.style("stroke", line.lineColor)
.style("fill", "none")
currentLine++