| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499 | class Morris.Grid extends Morris.EventEmitter  # A generic pair of axes for line/area/bar charts.  #  # Draws grid lines and axis labels.  #  constructor: (options) ->    # find the container to draw the graph in    if typeof options.element is 'string'      @el = $ document.getElementById(options.element)    else      @el = $ options.element    if not @el? or @el.length == 0      throw new Error("Graph container element not found")    if @el.css('position') == 'static'      @el.css('position', 'relative')    @options = $.extend {}, @gridDefaults, (@defaults || {}), options    # backwards compatibility for units -> postUnits    if typeof @options.units is 'string'      @options.postUnits = options.units    # the raphael drawing instance    @raphael = new Raphael(@el[0])    # some redraw stuff    @elementWidth = null    @elementHeight = null    @dirty = false    # range selection    @selectFrom = null    # more stuff    @init() if @init    # load data    @setData @options.data    # hover    @el.bind 'mousemove', (evt) =>      offset = @el.offset()      x = evt.pageX - offset.left      if @selectFrom        left = @data[@hitTest(Math.min(x, @selectFrom))]._x        right = @data[@hitTest(Math.max(x, @selectFrom))]._x        width = right - left        @selectionRect.attr({ x: left, width: width })      else        @fire 'hovermove', x, evt.pageY - offset.top    @el.bind 'mouseleave', (evt) =>      if @selectFrom        @selectionRect.hide()        @selectFrom = null      @fire 'hoverout'    @el.bind 'touchstart touchmove touchend', (evt) =>      touch = evt.originalEvent.touches[0] or evt.originalEvent.changedTouches[0]      offset = @el.offset()      @fire 'hovermove', touch.pageX - offset.left, touch.pageY - offset.top    @el.bind 'click', (evt) =>      offset = @el.offset()      @fire 'gridclick', evt.pageX - offset.left, evt.pageY - offset.top    if @options.rangeSelect      @selectionRect = @raphael.rect(0, 0, 0, @el.innerHeight())        .attr({ fill: @options.rangeSelectColor, stroke: false })        .toBack()        .hide()      @el.bind 'mousedown', (evt) =>        offset = @el.offset()        @startRange evt.pageX - offset.left      @el.bind 'mouseup', (evt) =>        offset = @el.offset()        @endRange evt.pageX - offset.left        @fire 'hovermove', evt.pageX - offset.left, evt.pageY - offset.top    if @options.resize      $(window).bind 'resize', (evt) =>        if @timeoutId?          window.clearTimeout @timeoutId        @timeoutId = window.setTimeout @resizeHandler, 100    # Disable tap highlight on iOS.    @el.css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)')    @postInit() if @postInit  # Default options  #  gridDefaults:    dateFormat: null    axes: true    grid: true    gridLineColor: '#aaa'    gridStrokeWidth: 0.5    gridTextColor: '#888'    gridTextSize: 12    gridTextFamily: 'sans-serif'    gridTextWeight: 'normal'    hideHover: false    yLabelFormat: null    xLabelAngle: 0    numLines: 5    padding: 25    parseTime: true    postUnits: ''    preUnits: ''    ymax: 'auto'    ymin: 'auto 0'    goals: []    goalStrokeWidth: 1.0    goalLineColors: [      '#666633'      '#999966'      '#cc6666'      '#663333'    ]    events: []    eventStrokeWidth: 1.0    eventLineColors: [      '#005a04'      '#ccffbb'      '#3a5f0b'      '#005502'    ]    rangeSelect: null    rangeSelectColor: '#eef'    resize: false  # Update the data series and redraw the chart.  #  setData: (data, redraw = true) ->    @options.data = data    if !data? or data.length == 0      @data = []      @raphael.clear()      @hover.hide() if @hover?      return    ymax = if @cumulative then 0 else null    ymin = if @cumulative then 0 else null    if @options.goals.length > 0      minGoal = Math.min @options.goals...      maxGoal = Math.max @options.goals...      ymin = if ymin? then Math.min(ymin, minGoal) else minGoal      ymax = if ymax? then Math.max(ymax, maxGoal) else maxGoal    @data = for row, index in data      ret = {src: row}      ret.label = row[@options.xkey]      if @options.parseTime        ret.x = Morris.parseDate(ret.label)        if @options.dateFormat          ret.label = @options.dateFormat ret.x        else if typeof ret.label is 'number'          ret.label = new Date(ret.label).toString()      else        ret.x = index        if @options.xLabelFormat          ret.label = @options.xLabelFormat ret      total = 0      ret.y = for ykey, idx in @options.ykeys        yval = row[ykey]        yval = parseFloat(yval) if typeof yval is 'string'        yval = null if yval? and typeof yval isnt 'number'        if yval?          if @cumulative            total += yval          else            if ymax?              ymax = Math.max(yval, ymax)              ymin = Math.min(yval, ymin)            else              ymax = ymin = yval        if @cumulative and total?          ymax = Math.max(total, ymax)          ymin = Math.min(total, ymin)        yval      ret    if @options.parseTime      @data = @data.sort (a, b) -> (a.x > b.x) - (b.x > a.x)    # calculate horizontal range of the graph    @xmin = @data[0].x    @xmax = @data[@data.length - 1].x    @events = []    if @options.events.length > 0      if @options.parseTime        @events = (Morris.parseDate(e) for e in @options.events)      else        @events = @options.events      @xmax = Math.max(@xmax, Math.max(@events...))      @xmin = Math.min(@xmin, Math.min(@events...))    if @xmin is @xmax      @xmin -= 1      @xmax += 1    @ymin = @yboundary('min', ymin)    @ymax = @yboundary('max', ymax)    if @ymin is @ymax      @ymin -= 1 if ymin      @ymax += 1    if @options.axes in [true, 'both', 'y'] or @options.grid is true      if (@options.ymax == @gridDefaults.ymax and          @options.ymin == @gridDefaults.ymin)        # calculate 'magic' grid placement        @grid = @autoGridLines(@ymin, @ymax, @options.numLines)        @ymin = Math.min(@ymin, @grid[0])        @ymax = Math.max(@ymax, @grid[@grid.length - 1])      else        step = (@ymax - @ymin) / (@options.numLines - 1)        @grid = (y for y in [@ymin..@ymax] by step)    @dirty = true    @redraw() if redraw  yboundary: (boundaryType, currentValue) ->    boundaryOption = @options["y#{boundaryType}"]    if typeof boundaryOption is 'string'      if boundaryOption[0..3] is 'auto'        if boundaryOption.length > 5          suggestedValue = parseInt(boundaryOption[5..], 10)          return suggestedValue unless currentValue?          Math[boundaryType](currentValue, suggestedValue)        else          if currentValue? then currentValue else 0      else        parseInt(boundaryOption, 10)    else      boundaryOption  autoGridLines: (ymin, ymax, nlines) ->    span = ymax - ymin    ymag = Math.floor(Math.log(span) / Math.log(10))    unit = Math.pow(10, ymag)    # calculate initial grid min and max values    gmin = Math.floor(ymin / unit) * unit    gmax = Math.ceil(ymax / unit) * unit    step = (gmax - gmin) / (nlines - 1)    if unit == 1 and step > 1 and Math.ceil(step) != step      step = Math.ceil(step)      gmax = gmin + step * (nlines - 1)    # ensure zero is plotted where the range includes zero    if gmin < 0 and gmax > 0      gmin = Math.floor(ymin / step) * step      gmax = Math.ceil(ymax / step) * step    # special case for decimal numbers    if step < 1      smag = Math.floor(Math.log(step) / Math.log(10))      grid = for y in [gmin..gmax] by step        parseFloat(y.toFixed(1 - smag))    else      grid = (y for y in [gmin..gmax] by step)    grid  _calc: ->    w = @el.width()    h = @el.height()    if @elementWidth != w or @elementHeight != h or @dirty      @elementWidth = w      @elementHeight = h      @dirty = false      # recalculate grid dimensions      @left = @options.padding      @right = @elementWidth - @options.padding      @top = @options.padding      @bottom = @elementHeight - @options.padding      if @options.axes in [true, 'both', 'y']        yLabelWidths = for gridLine in @grid          @measureText(@yAxisFormat(gridLine)).width        @left += Math.max(yLabelWidths...)      if @options.axes in [true, 'both', 'x']        bottomOffsets = for i in [0...@data.length]          @measureText(@data[i].text, -@options.xLabelAngle).height        @bottom -= Math.max(bottomOffsets...)      @width = Math.max(1, @right - @left)      @height = Math.max(1, @bottom - @top)      @dx = @width / (@xmax - @xmin)      @dy = @height / (@ymax - @ymin)      @calc() if @calc  # Quick translation helpers  #  transY: (y) -> @bottom - (y - @ymin) * @dy  transX: (x) ->    if @data.length == 1      (@left + @right) / 2    else      @left + (x - @xmin) * @dx  # Draw it!  #  # If you need to re-size your charts, call this method after changing the  # size of the container element.  redraw: ->    @raphael.clear()    @_calc()    @drawGrid()    @drawGoals()    @drawEvents()    @draw() if @draw  # @private  #  measureText: (text, angle = 0) ->    tt = @raphael.text(100, 100, text)      .attr('font-size', @options.gridTextSize)      .attr('font-family', @options.gridTextFamily)      .attr('font-weight', @options.gridTextWeight)      .rotate(angle)    ret = tt.getBBox()    tt.remove()    ret  # @private  #  yAxisFormat: (label) -> @yLabelFormat(label)  # @private  #  yLabelFormat: (label) ->    if typeof @options.yLabelFormat is 'function'      @options.yLabelFormat(label)    else      "#{@options.preUnits}#{Morris.commas(label)}#{@options.postUnits}"  # draw y axis labels, horizontal lines  #  drawGrid: ->    return if @options.grid is false and @options.axes not in [true, 'both', 'y']    for lineY in @grid      y = @transY(lineY)      if @options.axes in [true, 'both', 'y']        @drawYAxisLabel(@left - @options.padding / 2, y, @yAxisFormat(lineY))      if @options.grid        @drawGridLine("M#{@left},#{y}H#{@left + @width}")  # draw goals horizontal lines  #  drawGoals: ->    for goal, i in @options.goals      color = @options.goalLineColors[i % @options.goalLineColors.length]      @drawGoal(goal, color)  # draw events vertical lines  drawEvents: ->    for event, i in @events      color = @options.eventLineColors[i % @options.eventLineColors.length]      @drawEvent(event, color)  drawGoal: (goal, color) ->    @raphael.path("M#{@left},#{@transY(goal)}H#{@right}")      .attr('stroke', color)      .attr('stroke-width', @options.goalStrokeWidth)  drawEvent: (event, color) ->    @raphael.path("M#{@transX(event)},#{@bottom}V#{@top}")      .attr('stroke', color)      .attr('stroke-width', @options.eventStrokeWidth)  drawYAxisLabel: (xPos, yPos, text) ->    @raphael.text(xPos, yPos, text)      .attr('font-size', @options.gridTextSize)      .attr('font-family', @options.gridTextFamily)      .attr('font-weight', @options.gridTextWeight)      .attr('fill', @options.gridTextColor)      .attr('text-anchor', 'end')  drawGridLine: (path) ->    @raphael.path(path)      .attr('stroke', @options.gridLineColor)      .attr('stroke-width', @options.gridStrokeWidth)  # Range selection  #  startRange: (x) ->    @hover.hide()    @selectFrom = x    @selectionRect.attr({ x: x, width: 0 }).show()  endRange: (x) ->    if @selectFrom      start = Math.min(@selectFrom, x)      end = Math.max(@selectFrom, x)      @options.rangeSelect.call @el,        start: @data[@hitTest(start)].x        end: @data[@hitTest(end)].x      @selectFrom = null  resizeHandler: =>    @timeoutId = null    @raphael.setSize @el.width(), @el.height()    @redraw()# Parse a date into a javascript timestamp##Morris.parseDate = (date) ->  if typeof date is 'number'    return date  m = date.match /^(\d+) Q(\d)$/  n = date.match /^(\d+)-(\d+)$/  o = date.match /^(\d+)-(\d+)-(\d+)$/  p = date.match /^(\d+) W(\d+)$/  q = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+)(Z|([+-])(\d\d):?(\d\d))?$/  r = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+):(\d+(\.\d+)?)(Z|([+-])(\d\d):?(\d\d))?$/  if m    new Date(      parseInt(m[1], 10),      parseInt(m[2], 10) * 3 - 1,      1).getTime()  else if n    new Date(      parseInt(n[1], 10),      parseInt(n[2], 10) - 1,      1).getTime()  else if o    new Date(      parseInt(o[1], 10),      parseInt(o[2], 10) - 1,      parseInt(o[3], 10)).getTime()  else if p    # calculate number of weeks in year given    ret = new Date(parseInt(p[1], 10), 0, 1);    # first thursday in year (ISO 8601 standard)    if ret.getDay() isnt 4      ret.setMonth(0, 1 + ((4 - ret.getDay()) + 7) % 7);    # add weeks    ret.getTime() + parseInt(p[2], 10) * 604800000  else if q    if not q[6]      # no timezone info, use local      new Date(        parseInt(q[1], 10),        parseInt(q[2], 10) - 1,        parseInt(q[3], 10),        parseInt(q[4], 10),        parseInt(q[5], 10)).getTime()    else      # timezone info supplied, use UTC      offsetmins = 0      if q[6] != 'Z'        offsetmins = parseInt(q[8], 10) * 60 + parseInt(q[9], 10)        offsetmins = 0 - offsetmins if q[7] == '+'      Date.UTC(        parseInt(q[1], 10),        parseInt(q[2], 10) - 1,        parseInt(q[3], 10),        parseInt(q[4], 10),        parseInt(q[5], 10) + offsetmins)  else if r    secs = parseFloat(r[6])    isecs = Math.floor(secs)    msecs = Math.round((secs - isecs) * 1000)    if not r[8]      # no timezone info, use local      new Date(        parseInt(r[1], 10),        parseInt(r[2], 10) - 1,        parseInt(r[3], 10),        parseInt(r[4], 10),        parseInt(r[5], 10),        isecs,        msecs).getTime()    else      # timezone info supplied, use UTC      offsetmins = 0      if r[8] != 'Z'        offsetmins = parseInt(r[10], 10) * 60 + parseInt(r[11], 10)        offsetmins = 0 - offsetmins if r[9] == '+'      Date.UTC(        parseInt(r[1], 10),        parseInt(r[2], 10) - 1,        parseInt(r[3], 10),        parseInt(r[4], 10),        parseInt(r[5], 10) + offsetmins,        isecs,        msecs)  else    new Date(parseInt(date, 10), 0, 1).getTime()
 |