RootView = require 'views/core/RootView'
template = require 'templates/admin/analytics-subscriptions'
ThangType = require 'models/ThangType'
User = require 'models/User'

# TODO: Graphing code copied/mangled from campaign editor level view.  OMG, DRY.

require 'vendor/d3'

module.exports = class AnalyticsSubscriptionsView extends RootView
  id: 'admin-analytics-subscriptions-view'
  template: template

  events:
    'click .btn-show-more-cancellations': 'onClickShowMoreCancellations'

  constructor: (options) ->
    super options
    @showMoreCancellations = false
    @resetSubscriptionsData()
    @refreshData() if me.isAdmin()

  getRenderData: ->
    context = super()
    context.analytics = @analytics ? graphs: []
    context.cancellations = if @showMoreCancellations then @cancellations else (@cancellations ? []).slice(0, 40)
    context.showMoreCancellations = @showMoreCancellations
    context.subs = _.cloneDeep(@subs ? []).reverse()
    context.subscribers = @subscribers ? []
    context.subscriberCancelled = _.find context.subscribers, (subscriber) -> subscriber.cancel
    context.subscriberSponsored = _.find context.subscribers, (subscriber) -> subscriber.user?.stripe?.sponsorID
    context.total = @total ? 0
    context.monthlyChurn = @monthlyChurn ? 0.0
    context.monthlyGrowth = @monthlyGrowth ? 0.0
    context.outstandingCancels = @outstandingCancels ? []
    context.refreshDataState = @refreshDataState
    context

  afterRender: ->
    super()
    @updateAnalyticsGraphs()

  onClickShowMoreCancellations: (e) ->
    @showMoreCancellations = true
    @render?()

  resetSubscriptionsData: ->
    @analytics = graphs: []
    @subs = []
    @total = 0
    @monthlyChurn = 0.0
    @monthlyGrowth = 0.0
    @refreshDataState = 'Fetching dashboard data...'

  refreshData: ->
    return unless me.isAdmin()
    @resetSubscriptionsData()
    @getCancellations (cancellations) =>
      @cancellations = cancellations
      @render?()
      @getOutstandingCancelledSubscriptions cancellations, (outstandingCancels) =>
        @outstandingCancels = outstandingCancels
        @getSubscriptions cancellations, (subscriptions) =>
          @updateAnalyticsGraphData()
          @render?()
          @getSubscribers subscriptions, =>
            @render?()

  updateFetchDataState: (msg) ->
    @refreshDataState = msg
    @render?()

  getCancellations: (done) ->
    cancellations = []
    @getCancellationEvents (cancelledSubscriptions) =>
      # Get user objects for cancelled subscriptions
      userIDs = _.map cancelledSubscriptions, (a) -> a.userID
      options =
        url: '/db/user/-/users'
        method: 'POST'
        data: {ids: userIDs}
      options.error = (model, response, options) =>
        return if @destroyed
        console.error 'Failed to get cancelled users', response
      options.success = (cancelledUsers, response, options) =>
        return if @destroyed
        userMap = {}
        userMap[user._id] = user for user in cancelledUsers
        for cancellation in cancelledSubscriptions when cancellation.userID of userMap
          cancellation.user = userMap[cancellation.userID]
          cancellation.level = User.levelFromExp(cancellation.user.points)
        cancelledSubscriptions.sort (a, b) -> if a.cancel > b.cancel then -1 else 1
        done(cancelledSubscriptions)
      @updateFetchDataState 'Fetching cancellations...'
      @supermodel.addRequestResource('get_cancelled_users', options, 0).load()

  getCancellationEvents: (done) ->
    cancellationEvents = []
    earliestEventDate = new Date()
    earliestEventDate.setUTCMonth(earliestEventDate.getUTCMonth() - 2)
    earliestEventDate.setUTCDate(earliestEventDate.getUTCDate() - 8)
    nextBatch = (starting_after, done) =>
      @updateFetchDataState "Fetching cancellations #{cancellationEvents.length}..."
      options =
        url: '/db/subscription/-/stripe_events'
        method: 'POST'
        data: {options: {limit: 100}}
      options.data.options.starting_after = starting_after if starting_after
      options.data.options.type = 'customer.subscription.updated'
      options.data.options.created = gte: Math.floor(earliestEventDate.getTime() / 1000)
      options.error = (model, response, options) =>
        return if @destroyed
        console.error 'Failed to get cancelled events', response
      options.success = (events, response, options) =>
        return if @destroyed
        for event in events.data
          continue unless event.data?.object?.cancel_at_period_end is true and event.data?.previous_attributes.cancel_at_period_end is false
          continue unless event.data?.object?.plan?.id is 'basic'
          continue unless event.data?.object?.id?
          cancellationEvents.push
            cancel: new Date(event.created * 1000)
            customerID: event.data.object.customer
            start: new Date(event.data.object.start * 1000)
            subscriptionID: event.data.object.id
            userID: event.data.object.metadata?.id

        if events.has_more
          return nextBatch(events.data[events.data.length - 1].id, done)
        done(cancellationEvents)
      @supermodel.addRequestResource('get_cancellation_events', options, 0).load()
    nextBatch null, done

  getOutstandingCancelledSubscriptions: (cancellations, done) ->
    @updateFetchDataState "Fetching oustanding cancellations..."
    options =
      url: '/db/subscription/-/stripe_subscriptions'
      method: 'POST'
      data: {subscriptions: cancellations}
    options.error = (model, response, options) =>
      return if @destroyed
      console.error 'Failed to get outstanding cancellations', response
    options.success = (subscriptions, response, options) =>
      return if @destroyed
      outstandingCancelledSubscriptions = []
      for subscription in subscriptions
        continue unless subscription?.cancel_at_period_end
        outstandingCancelledSubscriptions.push
          cancel: new Date(subscription.canceled_at * 1000)
          customerID: subscription.customerID
          start: new Date(subscription.start * 1000)
          subscriptionID: subscription.id
          userID: subscription.metadata?.id
      done(outstandingCancelledSubscriptions)
    @supermodel.addRequestResource('get_outstanding_cancelled_subscriptions', options, 0).load()

  getSubscribers: (subscriptions, done) ->
    # console.log 'getSubscribers', subscriptions.length
    @updateFetchDataState "Fetching recent subscribers..."
    @render?()
    maxSubscribers = 40

    subscribers = _.filter subscriptions, (a) -> a.userID?
    subscribers.sort (a, b) -> if a.start > b.start then -1 else 1
    subscribers = subscribers.slice(0, maxSubscribers)
    subscriberUserIDs = _.map subscribers, (a) -> a.userID

    options =
      url: '/db/subscription/-/subscribers'
      method: 'POST'
      data: {ids: subscriberUserIDs}
    options.error = (model, response, options) =>
      return if @destroyed
      console.error 'Failed to get subscribers', response
    options.success = (userMap, response, options) =>
      return if @destroyed
      for subscriber in subscribers
        continue unless subscriber.userID of userMap
        subscriber.user = userMap[subscriber.userID]
        subscriber.level = User.levelFromExp subscriber.user.points
        if hero = subscriber.user.heroConfig?.thangType
          subscriber.hero = _.invert(ThangType.heroes)[hero]
      @subscribers = subscribers
      done()
    @supermodel.addRequestResource('get_subscribers', options, 0).load()

  getSubscriptions: (cancellations=[], done) ->
    @getInvoices (invoices) =>
      subMap = {}
      for invoice in invoices
        subID = invoice.subscriptionID
        if subID of subMap
          subMap[subID].first = new Date(invoice.date)
        else
          subMap[subID] =
            first: new Date(invoice.date)
            last: new Date(invoice.date)
            customerID: invoice.customerID
        subMap[subID].userID = invoice.userID if invoice.userID

      @getSponsors (sponsors) =>
        @getRecipientSubscriptions sponsors, (recipientSubscriptions) =>
          for subscription in recipientSubscriptions
            subMap[subscription.id] =
              first: new Date(subscription.start * 1000)
            subMap[subscription.id].userID = subscription.metadata.id if subscription.metadata?.id?
            if subscription.cancel_at_period_end
              subMap[subscription.id].cancel = new Date(subscription.canceled_at * 1000)
              subMap[subscription.id].end = new Date(subscription.current_period_end * 1000)

          subs = []
          for subID of subMap
            sub =
              customerID: subMap[subID].customerID
              start: subMap[subID].first
              subscriptionID: subID
            sub.cancel = subMap[subID].cancel if subMap[subID].cancel
            oneMonthAgo = new Date()
            oneMonthAgo.setUTCMonth(oneMonthAgo.getUTCMonth() - 1)
            if subMap[subID].end?
              sub.end = subMap[subID].end
            else if subMap[subID].last < oneMonthAgo
              sub.end = subMap[subID].last
              sub.end.setUTCMonth(sub.end.getUTCMonth() + 1)
            sub.userID = subMap[subID].userID if subMap[subID].userID
            subs.push sub

          subDayMap = {}
          for sub in subs
            startDay = sub.start.toISOString().substring(0, 10)
            subDayMap[startDay] ?= {}
            subDayMap[startDay]['start'] ?= 0
            subDayMap[startDay]['start']++
            if endDay = sub?.end?.toISOString().substring(0, 10)
              subDayMap[endDay] ?= {}
              subDayMap[endDay]['end'] ?= 0
              subDayMap[endDay]['end']++
            for cancellation in cancellations
              if cancellation.subscriptionID is sub.subscriptionID
                sub.cancel = cancellation.cancel
                cancelDay = cancellation.cancel.toISOString().substring(0, 10)
                subDayMap[cancelDay] ?= {}
                subDayMap[cancelDay]['cancel'] ?= 0
                subDayMap[cancelDay]['cancel']++
                break

          today = new Date().toISOString().substring(0, 10)
          for day of subDayMap
            continue if day > today
            @subs.push
              day: day
              started: subDayMap[day]['start'] or 0
              cancelled: subDayMap[day]['cancel'] or 0
              ended: subDayMap[day]['end'] or 0

          @subs.sort (a, b) -> a.day.localeCompare(b.day)
          cancelledThisMonth = 0
          totalLastMonth = 0
          for sub, i in @subs
            @total += sub.started
            @total -= sub.ended
            sub.total = @total
            cancelledThisMonth += sub.cancelled if @subs.length - i < 31
            totalLastMonth = @total if @subs.length - i is 31
          @monthlyChurn = cancelledThisMonth / totalLastMonth * 100.0 if totalLastMonth > 0
          if @subs.length > 30 and @subs[@subs.length - 31].total > 0
            startMonthTotal = @subs[@subs.length - 31].total
            endMonthTotal = @subs[@subs.length - 1].total
            @monthlyGrowth = (endMonthTotal / startMonthTotal - 1) * 100
          done(subs)

  getInvoices: (done) ->
    invoices = {}

    addInvoice = (invoice) =>
      return unless invoice.paid
      return unless invoice.subscription
      return unless invoice.total > 0
      return unless invoice.lines?.data?[0]?.plan?.id is 'basic'
      invoices[invoice.id] =
        customerID: invoice.customer
        subscriptionID: invoice.subscription
        date: new Date(invoice.date * 1000)
      invoices[invoice.id].userID = invoice.lines.data[0].metadata.id if invoice.lines?.data?[0]?.metadata?.id

    getLiveInvoices = (ending_before, done) =>

      nextBatch = (ending_before, done) =>
        @updateFetchDataState "Fetching invoices #{Object.keys(invoices).length}..."
        options =
          url: '/db/subscription/-/stripe_invoices'
          method: 'POST'
          data: {options: {ending_before: ending_before, limit: 100}}
        options.error = (model, response, options) =>
          return if @destroyed
          console.error 'Failed to get live invoices', response
        options.success = (invoiceData, response, options) =>
          return if @destroyed
          addInvoice(invoice) for invoice in invoiceData.data
          if invoiceData.has_more
            return nextBatch(invoiceData.data[0].id, done)
          else
            invoices = (invoice for invoiceID, invoice of invoices)
            invoices.sort (a, b) -> if a.date > b.date then -1 else 1
            return done(invoices)
        @supermodel.addRequestResource('get_live_invoices', options, 0).load()

      nextBatch ending_before, done

    getAnalyticsInvoices = (done) =>
      @updateFetchDataState "Fetching invoices #{Object.keys(invoices).length}..."
      options =
        url: '/db/analytics.stripe.invoice/-/all'
        method: 'GET'
      options.error = (model, response, options) =>
        return if @destroyed
        console.error 'Failed to get analytics stripe invoices', response
      options.success = (docs, response, options) =>
        return if @destroyed
        docs.sort (a, b) -> b.date - a.date
        addInvoice(doc.properties) for doc in docs
        getLiveInvoices(docs[0]._id, done)
      @supermodel.addRequestResource('get_analytics_invoices', options, 0).load()

    getAnalyticsInvoices(done)

  getRecipientSubscriptions: (sponsors, done) ->
    @updateFetchDataState "Fetching recipient subscriptions..."
    subscriptionsToFetch = []
    for user in sponsors
      for recipient in user.stripe?.recipients
        subscriptionsToFetch.push
          customerID: user.stripe.customerID
          subscriptionID: recipient.subscriptionID
    return done([]) if _.isEmpty subscriptionsToFetch
    options =
      url: '/db/subscription/-/stripe_subscriptions'
      method: 'POST'
      data: {subscriptions: subscriptionsToFetch}
    options.error = (model, response, options) =>
      return if @destroyed
      console.error 'Failed to get recipient subscriptions', response
    options.success = (subscriptions, response, options) =>
      return if @destroyed
      done(subscriptions)
    @supermodel.addRequestResource('get_recipient_subscriptions', options, 0).load()

  getSponsors: (done) ->
    @updateFetchDataState "Fetching sponsors..."
    options =
      url: '/db/user/-/sub_sponsors'
      method: 'POST'
    options.error = (model, response, options) =>
      return if @destroyed
      console.error 'Failed to get sponsors', response
    options.success = (sponsors, response, options) =>
      return if @destroyed
      done(sponsors)
    @supermodel.addRequestResource('get_sponsors', options, 0).load()

  updateAnalyticsGraphData: ->
    # console.log 'updateAnalyticsGraphData'
    # Build graphs based on available @analytics data
    # Currently only one graph
    @analytics.graphs = []

    return unless @subs?.length > 0

    @addGraphData(60)
    @addGraphData(180, true)

  addGraphData: (timeframeDays, skipCancelled=false) ->
    graph = {graphID: 'total-subs', lines: []}

    # TODO: Where should this metadata live?
    # TODO: lineIDs assumed to be unique across graphs
    totalSubsID = 'total-subs'
    startedSubsID = 'started-subs'
    cancelledSubsID = 'cancelled-subs'
    netSubsID = 'net-subs'
    averageNewID = 'average-new'
    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 (started - cancelled)'
      color: 'black'
      strokeWidth: 4
    lineMetadata[averageNewID] =
      description: '7-day Average New Subscriptions'
      color: 'black'
      strokeWidth: 4

    days = (sub.day for sub in @subs)
    if days.length > 0
      currentIndex = 0
      currentDay = days[currentIndex]
      currentDate = new Date(currentDay + "T00:00:00.000Z")
      lastDay = days[days.length - 1]
      while currentDay isnt lastDay
        days.splice currentIndex, 0, currentDay if days[currentIndex] isnt currentDay
        currentIndex++
        currentDate.setUTCDate(currentDate.getUTCDate() + 1)
        currentDay = currentDate.toISOString().substr(0, 10)

    ## Totals

    # Build line data
    levelPoints = []
    for sub, i in @subs
      levelPoints.push
        x: i
        y: sub.total
        day: sub.day
        pointID: "#{totalSubsID}#{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 = "#{totalSubsID}#{i}"

    levelPoints.splice(0, levelPoints.length - timeframeDays) if levelPoints.length > timeframeDays

    graph.lines.push
      lineID: totalSubsID
      enabled: true
      points: levelPoints
      description: lineMetadata[totalSubsID].description
      lineColor: lineMetadata[totalSubsID].color
      strokeWidth: lineMetadata[totalSubsID].strokeWidth
      min: 0
      max: d3.max(@subs, (d) -> d.total)

    ## Started

    # Build line data
    levelPoints = []
    for sub, i in @subs
      levelPoints.push
        x: i
        y: sub.started
        day: sub.day
        pointID: "#{startedSubsID}#{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 = "#{startedSubsID}#{i}"

    levelPoints.splice(0, levelPoints.length - timeframeDays) if levelPoints.length > timeframeDays

    graph.lines.push
      lineID: startedSubsID
      enabled: true
      points: levelPoints
      description: lineMetadata[startedSubsID].description
      lineColor: lineMetadata[startedSubsID].color
      strokeWidth: lineMetadata[startedSubsID].strokeWidth
      min: 0
      max: d3.max(@subs[-timeframeDays..], (d) -> d.started + 2)

    if skipCancelled

      ## 7-Day average started

      # Build line data
      levelPoints = []
      sevenStarts = []
      for sub, i in @subs
        average = 0
        sevenStarts.push sub.started
        if sevenStarts.length > 7
          sevenStarts.shift()
        if sevenStarts.length is 7
          average = sevenStarts.reduce((a, b) -> a + b) / sevenStarts.length
        levelPoints.push
          x: i
          y: average
          day: sub.day
          pointID: "#{averageNewID}#{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 = "#{averageNewID}#{i}"

      levelPoints.splice(0, levelPoints.length - timeframeDays) if levelPoints.length > timeframeDays

      graph.lines.push
        lineID: averageNewID
        enabled: true
        points: levelPoints
        description: lineMetadata[averageNewID].description
        lineColor: lineMetadata[averageNewID].color
        strokeWidth: lineMetadata[averageNewID].strokeWidth
        min: 0
        max: d3.max(@subs[-timeframeDays..], (d) -> d.started + 2)

    else

      ## Cancelled

      # Build line data
      levelPoints = []
      for sub, i in @subs
        levelPoints.push
          x: @subs.length - 30 + i
          y: sub.cancelled
          day: sub.day
          pointID: "#{cancelledSubsID}#{@subs.length - 30 + 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 = "#{cancelledSubsID}#{i}"

      levelPoints.splice(0, levelPoints.length - timeframeDays) if levelPoints.length > timeframeDays

      graph.lines.push
        lineID: cancelledSubsID
        enabled: true
        points: levelPoints
        description: lineMetadata[cancelledSubsID].description
        lineColor: lineMetadata[cancelledSubsID].color
        strokeWidth: lineMetadata[cancelledSubsID].strokeWidth
        min: 0
        max: d3.max(@subs[-timeframeDays..], (d) -> d.started + 2)

      ## 7-Day Net Subs

      # Build line data
      levelPoints = []
      sevenNets = []
      for sub, i in @subs
        net = 0
        sevenNets.push sub.started - sub.cancelled
        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

      graph.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[-timeframeDays..], (d) -> d.started + 2)

    @analytics.graphs.push(graph)

  updateAnalyticsGraphs: ->
    # Build d3 graphs
    return unless @analytics?.graphs?.length > 0
    containerSelector = '.line-graph-container'
    # console.log 'updateAnalyticsGraphs', containerSelector, @analytics.graphs

    margin = 20
    keyHeight = 20
    xAxisHeight = 20
    yAxisWidth = 40
    containerWidth = $(containerSelector).width()
    containerHeight = $(containerSelector).height()

    for graph in @analytics.graphs
      graphLineCount = _.reduce graph.lines, ((sum, item) -> if item.enabled then sum + 1 else sum), 0
      svg = d3.select(containerSelector).append("svg")
        .attr("width", containerWidth)
        .attr("height", containerHeight)
      width = containerWidth - margin * 2 - yAxisWidth * 2
      height = containerHeight - margin * 2 - xAxisHeight - keyHeight * graphLineCount
      currentLine = 0
      for line in graph.lines
        continue unless line.enabled
        xRange = d3.scale.linear().range([0, width]).domain([d3.min(line.points, (d) -> d.x), d3.max(line.points, (d) -> d.x)])
        yRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])

        # x-Axis
        if currentLine is 0
          startDay = new Date(line.points[0].day)
          endDay = new Date(line.points[line.points.length - 1].day)
          xAxisRange = d3.time.scale()
            .domain([startDay, endDay])
            .range([0, width])
          xAxis = d3.svg.axis()
            .scale(xAxisRange)
          svg.append("g")
            .attr("class", "x axis")
            .call(xAxis)
            .selectAll("text")
            .attr("dy", ".35em")
            .attr("transform", "translate(" + (margin + yAxisWidth) + "," + (height + margin) + ")")
            .style("text-anchor", "start")

        if line.lineID is 'started-subs'
          # Horizontal guidelines
          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")

        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")
          .attr("x1", margin)
          .attr("y1", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
          .attr("x2", margin + 40)
          .attr("y2", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
          .attr("stroke", line.lineColor)
          .attr("class", "key-line")
        svg.append("text")
          .attr("x", margin + 40 + 10)
          .attr("y", margin + height + xAxisHeight + keyHeight * currentLine + (keyHeight + 10) / 2)
          .attr("fill", if line.lineColor is 'gold' then 'orange' else line.lineColor)
          .attr("class", "key-text")
          .text(line.description)

        # Path and points
        svg.selectAll(".circle")
          .data(line.points)
          .enter()
          .append("circle")
          .attr("transform", "translate(" + (margin + yAxisWidth * 2) + "," + margin + ")")
          .attr("cx", (d) -> xRange(d.x))
          .attr("cy", (d) -> yRange(d.y))
          .attr("r", 2)
          .attr("fill", line.lineColor)
          .attr("stroke-width", 1)
          .attr("class", "graph-point")
          .attr("data-pointid", (d) -> "#{line.lineID}#{d.x}")
        d3line = d3.svg.line()
          .x((d) -> xRange(d.x))
          .y((d) -> yRange(d.y))
          .interpolate("linear")
        svg.append("path")
          .attr("d", d3line(line.points))
          .attr("transform", "translate(" + (margin + yAxisWidth * 2) + "," + margin + ")")
          .style("stroke-width", line.strokeWidth)
          .style("stroke", line.lineColor)
          .style("fill", "none")
        currentLine++