Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
21627 kshitij.so 1
class Morris.Grid extends Morris.EventEmitter
2
  # A generic pair of axes for line/area/bar charts.
3
  #
4
  # Draws grid lines and axis labels.
5
  #
6
  constructor: (options) ->
7
    # find the container to draw the graph in
8
    if typeof options.element is 'string'
9
      @el = $ document.getElementById(options.element)
10
    else
11
      @el = $ options.element
12
    if not @el? or @el.length == 0
13
      throw new Error("Graph container element not found")
14
 
15
    if @el.css('position') == 'static'
16
      @el.css('position', 'relative')
17
 
18
    @options = $.extend {}, @gridDefaults, (@defaults || {}), options
19
 
20
    # backwards compatibility for units -> postUnits
21
    if typeof @options.units is 'string'
22
      @options.postUnits = options.units
23
 
24
    # the raphael drawing instance
25
    @raphael = new Raphael(@el[0])
26
 
27
    # some redraw stuff
28
    @elementWidth = null
29
    @elementHeight = null
30
    @dirty = false
31
 
32
    # more stuff
33
    @init() if @init
34
 
35
    # load data
36
    @setData @options.data
37
 
38
    # hover
39
    @el.bind 'mousemove', (evt) =>
40
      offset = @el.offset()
41
      @fire 'hovermove', evt.pageX - offset.left, evt.pageY - offset.top
42
 
43
    @el.bind 'mouseout', (evt) =>
44
      @fire 'hoverout'
45
 
46
    @el.bind 'touchstart touchmove touchend', (evt) =>
47
      touch = evt.originalEvent.touches[0] or evt.originalEvent.changedTouches[0]
48
      offset = @el.offset()
49
      @fire 'hover', touch.pageX - offset.left, touch.pageY - offset.top
50
      touch
51
 
52
    @el.bind 'click', (evt) =>
53
      offset = @el.offset()
54
      @fire 'gridclick', evt.pageX - offset.left, evt.pageY - offset.top
55
 
56
    @postInit() if @postInit
57
 
58
  # Default options
59
  #
60
  gridDefaults:
61
    dateFormat: null
62
    axes: true
63
    grid: true
64
    gridLineColor: '#aaa'
65
    gridStrokeWidth: 0.5
66
    gridTextColor: '#888'
67
    gridTextSize: 12
68
    gridTextFamily: 'sans-serif'
69
    gridTextWeight: 'normal'
70
    hideHover: false
71
    yLabelFormat: null
72
    xLabelAngle: 0
73
    numLines: 5
74
    padding: 25
75
    parseTime: true
76
    postUnits: ''
77
    preUnits: ''
78
    ymax: 'auto'
79
    ymin: 'auto 0'
80
    goals: []
81
    goalStrokeWidth: 1.0
82
    goalLineColors: [
83
      '#666633'
84
      '#999966'
85
      '#cc6666'
86
      '#663333'
87
    ]
88
    events: []
89
    eventStrokeWidth: 1.0
90
    eventLineColors: [
91
      '#005a04'
92
      '#ccffbb'
93
      '#3a5f0b'
94
      '#005502'
95
    ]
96
 
97
  # Update the data series and redraw the chart.
98
  #
99
  setData: (data, redraw = true) ->
100
    @options.data = data
101
 
102
    if !data? or data.length == 0
103
      @data = []
104
      @raphael.clear()
105
      @hover.hide() if @hover?
106
      return
107
 
108
    ymax = if @cumulative then 0 else null
109
    ymin = if @cumulative then 0 else null
110
 
111
    if @options.goals.length > 0
112
      minGoal = Math.min.apply(null, @options.goals)
113
      maxGoal = Math.max.apply(null, @options.goals)
114
      ymin = if ymin? then Math.min(ymin, minGoal) else minGoal
115
      ymax = if ymax? then Math.max(ymax, maxGoal) else maxGoal
116
 
117
    @data = for row, index in data
118
      ret = {}
119
 
120
      ret.label = row[@options.xkey]
121
      if @options.parseTime
122
        ret.x = Morris.parseDate(ret.label)
123
        if @options.dateFormat
124
          ret.label = @options.dateFormat ret.x
125
        else if typeof ret.label is 'number'
126
          ret.label = new Date(ret.label).toString()
127
      else
128
        ret.x = index
129
        if @options.xLabelFormat
130
          ret.label = @options.xLabelFormat ret
131
      total = 0
132
      ret.y = for ykey, idx in @options.ykeys
133
        yval = row[ykey]
134
        yval = parseFloat(yval) if typeof yval is 'string'
135
        yval = null if yval? and typeof yval isnt 'number'
136
        if yval?
137
          if @cumulative
138
            total += yval
139
          else
140
            if ymax?
141
              ymax = Math.max(yval, ymax)
142
              ymin = Math.min(yval, ymin)
143
            else
144
              ymax = ymin = yval
145
        if @cumulative and total?
146
          ymax = Math.max(total, ymax)
147
          ymin = Math.min(total, ymin)
148
        yval
149
      ret
150
 
151
    if @options.parseTime
152
      @data = @data.sort (a, b) -> (a.x > b.x) - (b.x > a.x)
153
 
154
    # calculate horizontal range of the graph
155
    @xmin = @data[0].x
156
    @xmax = @data[@data.length - 1].x
157
 
158
    @events = []
159
    if @options.parseTime and @options.events.length > 0
160
      @events = (Morris.parseDate(e) for e in @options.events)
161
      @xmax = Math.max(@xmax, Math.max.apply(null, @events))
162
      @xmin = Math.min(@xmin, Math.min.apply(null, @events))
163
 
164
    if @xmin is @xmax
165
      @xmin -= 1
166
      @xmax += 1
167
 
168
    @ymin = @yboundary('min', ymin)
169
    @ymax = @yboundary('max', ymax)
170
 
171
    if @ymin is @ymax
172
      @ymin -= 1 if ymin
173
      @ymax += 1
174
 
175
    if @options.axes is true or @options.grid is true
176
      if (@options.ymax == @gridDefaults.ymax and
177
          @options.ymin == @gridDefaults.ymin)
178
        # calculate 'magic' grid placement
179
        @grid = @autoGridLines(@ymin, @ymax, @options.numLines)
180
        @ymin = Math.min(@ymin, @grid[0])
181
        @ymax = Math.max(@ymax, @grid[@grid.length - 1])
182
      else
183
        step = (@ymax - @ymin) / (@options.numLines - 1)
184
        @grid = (y for y in [@ymin..@ymax] by step)
185
 
186
    @dirty = true
187
    @redraw() if redraw
188
 
189
  yboundary: (boundaryType, currentValue) ->
190
    boundaryOption = @options["y#{boundaryType}"]
191
    if typeof boundaryOption is 'string'
192
      if boundaryOption[0..3] is 'auto'
193
        if boundaryOption.length > 5
194
          suggestedValue = parseInt(boundaryOption[5..], 10)
195
          return suggestedValue unless currentValue?
196
          Math[boundaryType](currentValue, suggestedValue)
197
        else
198
          if currentValue? then currentValue else 0
199
      else
200
        parseInt(boundaryOption, 10)
201
    else
202
      boundaryOption
203
 
204
  autoGridLines: (ymin, ymax, nlines) ->
205
    span = ymax - ymin
206
    ymag = Math.floor(Math.log(span) / Math.log(10))
207
    unit = Math.pow(10, ymag)
208
 
209
    # calculate initial grid min and max values
210
    gmin = Math.floor(ymin / unit) * unit
211
    gmax = Math.ceil(ymax / unit) * unit
212
    step = (gmax - gmin) / (nlines - 1)
213
    if unit == 1 and step > 1 and Math.ceil(step) != step
214
      step = Math.ceil(step)
215
      gmax = gmin + step * (nlines - 1)
216
 
217
    # ensure zero is plotted where the range includes zero
218
    if gmin < 0 and gmax > 0
219
      gmin = Math.floor(ymin / step) * step
220
      gmax = Math.ceil(ymax / step) * step
221
 
222
    # special case for decimal numbers
223
    if step < 1
224
      smag = Math.floor(Math.log(step) / Math.log(10))
225
      grid = for y in [gmin..gmax] by step
226
        parseFloat(y.toFixed(1 - smag))
227
    else
228
      grid = (y for y in [gmin..gmax] by step)
229
    grid
230
 
231
  _calc: ->
232
    w = @el.width()
233
    h = @el.height()
234
 
235
    if @elementWidth != w or @elementHeight != h or @dirty
236
      @elementWidth = w
237
      @elementHeight = h
238
      @dirty = false
239
      # recalculate grid dimensions
240
      @left = @options.padding
241
      @right = @elementWidth - @options.padding
242
      @top = @options.padding
243
      @bottom = @elementHeight - @options.padding
244
      if @options.axes
245
        yLabelWidths = for gridLine in @grid
246
          @measureText(@yAxisFormat(gridLine)).width
247
        @left += Math.max(yLabelWidths...)
248
        bottomOffsets = for i in [0...@data.length]
249
          @measureText(@data[i].text, -@options.xLabelAngle).height
250
        @bottom -= Math.max(bottomOffsets...)
251
      @width = Math.max(1, @right - @left)
252
      @height = Math.max(1, @bottom - @top)
253
      @dx = @width / (@xmax - @xmin)
254
      @dy = @height / (@ymax - @ymin)
255
      @calc() if @calc
256
 
257
  # Quick translation helpers
258
  #
259
  transY: (y) -> @bottom - (y - @ymin) * @dy
260
  transX: (x) ->
261
    if @data.length == 1
262
      (@left + @right) / 2
263
    else
264
      @left + (x - @xmin) * @dx
265
 
266
  # Draw it!
267
  #
268
  # If you need to re-size your charts, call this method after changing the
269
  # size of the container element.
270
  redraw: ->
271
    @raphael.clear()
272
    @_calc()
273
    @drawGrid()
274
    @drawGoals()
275
    @drawEvents()
276
    @draw() if @draw
277
 
278
  # @private
279
  #
280
  measureText: (text, angle = 0) ->
281
    tt = @raphael.text(100, 100, text)
282
      .attr('font-size', @options.gridTextSize)
283
      .attr('font-family', @options.gridTextFamily)
284
      .attr('font-weight', @options.gridTextWeight)
285
      .rotate(angle)
286
    ret = tt.getBBox()
287
    tt.remove()
288
    ret
289
 
290
  # @private
291
  #
292
  yAxisFormat: (label) -> @yLabelFormat(label)
293
 
294
  # @private
295
  #
296
  yLabelFormat: (label) ->
297
    if typeof @options.yLabelFormat is 'function'
298
      @options.yLabelFormat(label)
299
    else
300
      "#{@options.preUnits}#{Morris.commas(label)}#{@options.postUnits}"
301
 
302
  updateHover: (x, y) ->
303
    hit = @hitTest(x, y)
304
    if hit?
305
      @hover.update(hit...)
306
 
307
  # draw y axis labels, horizontal lines
308
  #
309
  drawGrid: ->
310
    return if @options.grid is false and @options.axes is false
311
    for lineY in @grid
312
      y = @transY(lineY)
313
      if @options.axes
314
        @drawYAxisLabel(@left - @options.padding / 2, y, @yAxisFormat(lineY))
315
      if @options.grid
316
        @drawGridLine("M#{@left},#{y}H#{@left + @width}")
317
 
318
  # draw goals horizontal lines
319
  #
320
  drawGoals: ->
321
    for goal, i in @options.goals
322
      color = @options.goalLineColors[i % @options.goalLineColors.length]
323
      @drawGoal(goal, color)
324
 
325
  # draw events vertical lines
326
  drawEvents: ->
327
    for event, i in @events
328
      color = @options.eventLineColors[i % @options.eventLineColors.length]
329
      @drawEvent(event, color)
330
 
331
  drawGoal: (goal, color) ->
332
    @raphael.path("M#{@left},#{@transY(goal)}H#{@right}")
333
      .attr('stroke', color)
334
      .attr('stroke-width', @options.goalStrokeWidth)
335
 
336
  drawEvent: (event, color) ->
337
    @raphael.path("M#{@transX(event)},#{@bottom}V#{@top}")
338
      .attr('stroke', color)
339
      .attr('stroke-width', @options.eventStrokeWidth)
340
 
341
  drawYAxisLabel: (xPos, yPos, text) ->
342
    @raphael.text(xPos, yPos, text)
343
      .attr('font-size', @options.gridTextSize)
344
      .attr('font-family', @options.gridTextFamily)
345
      .attr('font-weight', @options.gridTextWeight)
346
      .attr('fill', @options.gridTextColor)
347
      .attr('text-anchor', 'end')
348
 
349
  drawGridLine: (path) ->
350
    @raphael.path(path)
351
      .attr('stroke', @options.gridLineColor)
352
      .attr('stroke-width', @options.gridStrokeWidth)
353
 
354
# Parse a date into a javascript timestamp
355
#
356
#
357
Morris.parseDate = (date) ->
358
  if typeof date is 'number'
359
    return date
360
  m = date.match /^(\d+) Q(\d)$/
361
  n = date.match /^(\d+)-(\d+)$/
362
  o = date.match /^(\d+)-(\d+)-(\d+)$/
363
  p = date.match /^(\d+) W(\d+)$/
364
  q = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+)(Z|([+-])(\d\d):?(\d\d))?$/
365
  r = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+):(\d+(\.\d+)?)(Z|([+-])(\d\d):?(\d\d))?$/
366
  if m
367
    new Date(
368
      parseInt(m[1], 10),
369
      parseInt(m[2], 10) * 3 - 1,
370
      1).getTime()
371
  else if n
372
    new Date(
373
      parseInt(n[1], 10),
374
      parseInt(n[2], 10) - 1,
375
      1).getTime()
376
  else if o
377
    new Date(
378
      parseInt(o[1], 10),
379
      parseInt(o[2], 10) - 1,
380
      parseInt(o[3], 10)).getTime()
381
  else if p
382
    # calculate number of weeks in year given
383
    ret = new Date(parseInt(p[1], 10), 0, 1);
384
    # first thursday in year (ISO 8601 standard)
385
    if ret.getDay() isnt 4
386
      ret.setMonth(0, 1 + ((4 - ret.getDay()) + 7) % 7);
387
    # add weeks
388
    ret.getTime() + parseInt(p[2], 10) * 604800000
389
  else if q
390
    if not q[6]
391
      # no timezone info, use local
392
      new Date(
393
        parseInt(q[1], 10),
394
        parseInt(q[2], 10) - 1,
395
        parseInt(q[3], 10),
396
        parseInt(q[4], 10),
397
        parseInt(q[5], 10)).getTime()
398
    else
399
      # timezone info supplied, use UTC
400
      offsetmins = 0
401
      if q[6] != 'Z'
402
        offsetmins = parseInt(q[8], 10) * 60 + parseInt(q[9], 10)
403
        offsetmins = 0 - offsetmins if q[7] == '+'
404
      Date.UTC(
405
        parseInt(q[1], 10),
406
        parseInt(q[2], 10) - 1,
407
        parseInt(q[3], 10),
408
        parseInt(q[4], 10),
409
        parseInt(q[5], 10) + offsetmins)
410
  else if r
411
    secs = parseFloat(r[6])
412
    isecs = Math.floor(secs)
413
    msecs = Math.round((secs - isecs) * 1000)
414
    if not r[8]
415
      # no timezone info, use local
416
      new Date(
417
        parseInt(r[1], 10),
418
        parseInt(r[2], 10) - 1,
419
        parseInt(r[3], 10),
420
        parseInt(r[4], 10),
421
        parseInt(r[5], 10),
422
        isecs,
423
        msecs).getTime()
424
    else
425
      # timezone info supplied, use UTC
426
      offsetmins = 0
427
      if r[8] != 'Z'
428
        offsetmins = parseInt(r[10], 10) * 60 + parseInt(r[11], 10)
429
        offsetmins = 0 - offsetmins if r[9] == '+'
430
      Date.UTC(
431
        parseInt(r[1], 10),
432
        parseInt(r[2], 10) - 1,
433
        parseInt(r[3], 10),
434
        parseInt(r[4], 10),
435
        parseInt(r[5], 10) + offsetmins,
436
        isecs,
437
        msecs)
438
  else
439
    new Date(parseInt(date, 10), 0, 1).getTime()
440