Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
21627 kshitij.so 1
class Morris.Line extends Morris.Grid
2
  # Initialise the graph.
3
  #
4
  constructor: (options) ->
5
    return new Morris.Line(options) unless (@ instanceof Morris.Line)
6
    super(options)
7
 
8
  init: ->
9
    # Some instance variables for later
10
    @pointGrow = Raphael.animation r: @options.pointSize + 3, 25, 'linear'
11
    @pointShrink = Raphael.animation r: @options.pointSize, 25, 'linear'
12
 
13
    if @options.hideHover isnt 'always'
14
      @hover = new Morris.Hover(parent: @el)
15
      @on('hovermove', @onHoverMove)
16
      @on('hoverout', @onHoverOut)
17
      @on('gridclick', @onGridClick)
18
 
19
  # Default configuration
20
  #
21
  defaults:
22
    lineWidth: 3
23
    pointSize: 4
24
    lineColors: [
25
      '#0b62a4'
26
      '#7A92A3'
27
      '#4da74d'
28
      '#afd8f8'
29
      '#edc240'
30
      '#cb4b4b'
31
      '#9440ed'
32
    ]
33
    pointWidths: [1]
34
    pointStrokeColors: ['#ffffff']
35
    pointFillColors: []
36
    smooth: true
37
    xLabels: 'auto'
38
    xLabelFormat: null
39
    xLabelMargin: 24
40
    continuousLine: true
41
    hideHover: false
42
 
43
  # Do any size-related calculations
44
  #
45
  # @private
46
  calc: ->
47
    @calcPoints()
48
    @generatePaths()
49
 
50
  # calculate series data point coordinates
51
  #
52
  # @private
53
  calcPoints: ->
54
    for row in @data
55
      row._x = @transX(row.x)
56
      row._y = for y in row.y
57
        if y? then @transY(y) else y
58
      row._ymax = Math.min.apply(null, [@bottom].concat(y for y in row._y when y?))
59
 
60
  # hit test - returns the index of the row beneath the given coordinate
61
  #
62
  hitTest: (x, y) ->
63
    return null if @data.length == 0
64
    # TODO better search algo
65
    for r, index in @data.slice(1)
66
      break if x < (r._x + @data[index]._x) / 2
67
    index
68
 
69
  # click on grid event handler
70
  #
71
  # @private
72
  onGridClick: (x, y) =>
73
    index = @hitTest(x, y)
74
    @fire 'click', index, @options.data[index], x, y
75
 
76
  # hover movement event handler
77
  #
78
  # @private
79
  onHoverMove: (x, y) =>
80
    index = @hitTest(x, y)
81
    @displayHoverForRow(index)
82
 
83
  # hover out event handler
84
  #
85
  # @private
86
  onHoverOut: =>
87
    if @options.hideHover isnt false
88
      @displayHoverForRow(null)
89
 
90
  # display a hover popup over the given row
91
  #
92
  # @private
93
  displayHoverForRow: (index) ->
94
    if index?
95
      @hover.update(@hoverContentForRow(index)...)
96
      @hilight(index)
97
    else
98
      @hover.hide()
99
      @hilight()
100
 
101
  # hover content for a point
102
  #
103
  # @private
104
  hoverContentForRow: (index) ->
105
    row = @data[index]
106
    content = "<div class='morris-hover-row-label'>#{row.label}</div>"
107
    for y, j in row.y
108
      content += """
109
        <div class='morris-hover-point' style='color: #{@colorFor(row, j, 'label')}'>
110
          #{@options.labels[j]}:
111
          #{@yLabelFormat(y)}
112
        </div>
113
      """
114
    if typeof @options.hoverCallback is 'function'
115
      content = @options.hoverCallback(index, @options, content)
116
    [content, row._x, row._ymax]
117
 
118
 
119
  # generate paths for series lines
120
  #
121
  # @private
122
  generatePaths: ->
123
    @paths = for i in [0...@options.ykeys.length]
124
      smooth = @options.smooth is true or @options.ykeys[i] in @options.smooth
125
      coords = ({x: r._x, y: r._y[i]} for r in @data when r._y[i] isnt undefined)
126
      coords = (c for c in coords when c.y isnt null) if @options.continuousLine
127
 
128
      if coords.length > 1
129
        Morris.Line.createPath coords, smooth, @bottom
130
      else
131
        null
132
 
133
  # Draws the line chart.
134
  #
135
  draw: ->
136
    @drawXAxis() if @options.axes
137
    @drawSeries()
138
    if @options.hideHover is false
139
      @displayHoverForRow(@data.length - 1)
140
 
141
  # draw the x-axis labels
142
  #
143
  # @private
144
  drawXAxis: ->
145
    # draw x axis labels
146
    ypos = @bottom + @options.padding / 2
147
    prevLabelMargin = null
148
    prevAngleMargin = null
149
    drawLabel = (labelText, xpos) =>
150
      label = @drawXAxisLabel(@transX(xpos), ypos, labelText)
151
      textBox = label.getBBox()
152
      label.transform("r#{-@options.xLabelAngle}")
153
      labelBox = label.getBBox()
154
      label.transform("t0,#{labelBox.height / 2}...")
155
      if @options.xLabelAngle != 0
156
        offset = -0.5 * textBox.width *
157
          Math.cos(@options.xLabelAngle * Math.PI / 180.0)
158
        label.transform("t#{offset},0...")
159
      # try to avoid overlaps
160
      labelBox = label.getBBox()
161
      if (not prevLabelMargin? or
162
          prevLabelMargin >= labelBox.x + labelBox.width or
163
          prevAngleMargin? and prevAngleMargin >= labelBox.x) and
164
         labelBox.x >= 0 and (labelBox.x + labelBox.width) < @el.width()
165
        if @options.xLabelAngle != 0
166
          margin = 1.25 * @options.gridTextSize /
167
            Math.sin(@options.xLabelAngle * Math.PI / 180.0)
168
          prevAngleMargin = labelBox.x - margin
169
        prevLabelMargin = labelBox.x - @options.xLabelMargin
170
      else
171
        label.remove()
172
    if @options.parseTime
173
      if @data.length == 1 and @options.xLabels == 'auto'
174
        # where there's only one value in the series, we can't make a
175
        # sensible guess for an x labelling scheme, so just use the original
176
        # column label
177
        labels = [[@data[0].label, @data[0].x]]
178
      else
179
        labels = Morris.labelSeries(@xmin, @xmax, @width, @options.xLabels, @options.xLabelFormat)
180
    else
181
      labels = ([row.label, row.x] for row in @data)
182
    labels.reverse()
183
    for l in labels
184
      drawLabel(l[0], l[1])
185
 
186
  # draw the data series
187
  #
188
  # @private
189
  drawSeries: ->
190
    @seriesPoints = []
191
    for i in [@options.ykeys.length-1..0]
192
      @_drawLineFor i
193
    for i in [@options.ykeys.length-1..0]
194
      @_drawPointFor i
195
 
196
  _drawPointFor: (index) ->
197
    @seriesPoints[index] = []
198
    for row in @data
199
      circle = null
200
      if row._y[index]?
201
        circle = @drawLinePoint(row._x, row._y[index], @options.pointSize, @colorFor(row, index, 'point'), index)
202
      @seriesPoints[index].push(circle)
203
 
204
  _drawLineFor: (index) ->
205
    path = @paths[index]
206
    if path isnt null
207
      @drawLinePath path, @colorFor(null, index, 'line')
208
 
209
  # create a path for a data series
210
  #
211
  # @private
212
  @createPath: (coords, smooth, bottom) ->
213
    path = ""
214
    grads = Morris.Line.gradients(coords) if smooth
215
 
216
    prevCoord = {y: null}
217
    for coord, i in coords
218
      if coord.y?
219
        if prevCoord.y?
220
          if smooth
221
            g = grads[i]
222
            lg = grads[i - 1]
223
            ix = (coord.x - prevCoord.x) / 4
224
            x1 = prevCoord.x + ix
225
            y1 = Math.min(bottom, prevCoord.y + ix * lg)
226
            x2 = coord.x - ix
227
            y2 = Math.min(bottom, coord.y - ix * g)
228
            path += "C#{x1},#{y1},#{x2},#{y2},#{coord.x},#{coord.y}"
229
          else
230
            path += "L#{coord.x},#{coord.y}"
231
        else
232
          if not smooth or grads[i]?
233
            path += "M#{coord.x},#{coord.y}"
234
      prevCoord = coord
235
    return path
236
 
237
  # calculate a gradient at each point for a series of points
238
  #
239
  # @private
240
  @gradients: (coords) ->
241
    grad = (a, b) -> (a.y - b.y) / (a.x - b.x)
242
    for coord, i in coords
243
      if coord.y?
244
        nextCoord = coords[i + 1] or {y: null}
245
        prevCoord = coords[i - 1] or {y: null}
246
        if prevCoord.y? and nextCoord.y?
247
          grad(prevCoord, nextCoord)
248
        else if prevCoord.y?
249
          grad(prevCoord, coord)
250
        else if nextCoord.y?
251
          grad(coord, nextCoord)
252
        else
253
          null
254
      else
255
        null
256
 
257
  # @private
258
  hilight: (index) =>
259
    if @prevHilight isnt null and @prevHilight isnt index
260
      for i in [0..@seriesPoints.length-1]
261
        if @seriesPoints[i][@prevHilight]
262
          @seriesPoints[i][@prevHilight].animate @pointShrink
263
    if index isnt null and @prevHilight isnt index
264
      for i in [0..@seriesPoints.length-1]
265
        if @seriesPoints[i][index]
266
          @seriesPoints[i][index].animate @pointGrow
267
    @prevHilight = index
268
 
269
  colorFor: (row, sidx, type) ->
270
    if typeof @options.lineColors is 'function'
271
      @options.lineColors.call(@, row, sidx, type)
272
    else if type is 'point'
273
      @options.pointFillColors[sidx % @options.pointFillColors.length] || @options.lineColors[sidx % @options.lineColors.length]
274
    else
275
      @options.lineColors[sidx % @options.lineColors.length]
276
 
277
  drawXAxisLabel: (xPos, yPos, text) ->
278
    @raphael.text(xPos, yPos, text)
279
      .attr('font-size', @options.gridTextSize)
280
      .attr('font-family', @options.gridTextFamily)
281
      .attr('font-weight', @options.gridTextWeight)
282
      .attr('fill', @options.gridTextColor)
283
 
284
  drawLinePath: (path, lineColor) ->
285
    @raphael.path(path)
286
      .attr('stroke', lineColor)
287
      .attr('stroke-width', @options.lineWidth)
288
 
289
  drawLinePoint: (xPos, yPos, size, pointColor, lineIndex) ->
290
    @raphael.circle(xPos, yPos, size)
291
      .attr('fill', pointColor)
292
      .attr('stroke-width', @strokeWidthForSeries(lineIndex))
293
      .attr('stroke', @strokeForSeries(lineIndex))
294
 
295
  # @private
296
  strokeWidthForSeries: (index) ->
297
    @options.pointWidths[index % @options.pointWidths.length]
298
 
299
  # @private
300
  strokeForSeries: (index) ->
301
    @options.pointStrokeColors[index % @options.pointStrokeColors.length]
302
 
303
# generate a series of label, timestamp pairs for x-axis labels
304
#
305
# @private
306
Morris.labelSeries = (dmin, dmax, pxwidth, specName, xLabelFormat) ->
307
  ddensity = 200 * (dmax - dmin) / pxwidth # seconds per `margin` pixels
308
  d0 = new Date(dmin)
309
  spec = Morris.LABEL_SPECS[specName]
310
  # if the spec doesn't exist, search for the closest one in the list
311
  if spec is undefined
312
    for name in Morris.AUTO_LABEL_ORDER
313
      s = Morris.LABEL_SPECS[name]
314
      if ddensity >= s.span
315
        spec = s
316
        break
317
  # if we run out of options, use second-intervals
318
  if spec is undefined
319
    spec = Morris.LABEL_SPECS["second"]
320
  # check if there's a user-defined formatting function
321
  if xLabelFormat
322
    spec = $.extend({}, spec, {fmt: xLabelFormat})
323
  # calculate labels
324
  d = spec.start(d0)
325
  ret = []
326
  while  (t = d.getTime()) <= dmax
327
    if t >= dmin
328
      ret.push [spec.fmt(d), t]
329
    spec.incr(d)
330
  return ret
331
 
332
# @private
333
minutesSpecHelper = (interval) ->
334
  span: interval * 60 * 1000
335
  start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours())
336
  fmt: (d) -> "#{Morris.pad2(d.getHours())}:#{Morris.pad2(d.getMinutes())}"
337
  incr: (d) -> d.setUTCMinutes(d.getUTCMinutes() + interval)
338
 
339
# @private
340
secondsSpecHelper = (interval) ->
341
  span: interval * 1000
342
  start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes())
343
  fmt: (d) -> "#{Morris.pad2(d.getHours())}:#{Morris.pad2(d.getMinutes())}:#{Morris.pad2(d.getSeconds())}"
344
  incr: (d) -> d.setUTCSeconds(d.getUTCSeconds() + interval)
345
 
346
Morris.LABEL_SPECS =
347
  "decade":
348
    span: 172800000000 # 10 * 365 * 24 * 60 * 60 * 1000
349
    start: (d) -> new Date(d.getFullYear() - d.getFullYear() % 10, 0, 1)
350
    fmt: (d) -> "#{d.getFullYear()}"
351
    incr: (d) -> d.setFullYear(d.getFullYear() + 10)
352
  "year":
353
    span: 17280000000 # 365 * 24 * 60 * 60 * 1000
354
    start: (d) -> new Date(d.getFullYear(), 0, 1)
355
    fmt: (d) -> "#{d.getFullYear()}"
356
    incr: (d) -> d.setFullYear(d.getFullYear() + 1)
357
  "month":
358
    span: 2419200000 # 28 * 24 * 60 * 60 * 1000
359
    start: (d) -> new Date(d.getFullYear(), d.getMonth(), 1)
360
    fmt: (d) -> "#{d.getFullYear()}-#{Morris.pad2(d.getMonth() + 1)}"
361
    incr: (d) -> d.setMonth(d.getMonth() + 1)
362
  "day":
363
    span: 86400000 # 24 * 60 * 60 * 1000
364
    start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate())
365
    fmt: (d) -> "#{d.getFullYear()}-#{Morris.pad2(d.getMonth() + 1)}-#{Morris.pad2(d.getDate())}"
366
    incr: (d) -> d.setDate(d.getDate() + 1)
367
  "hour": minutesSpecHelper(60)
368
  "30min": minutesSpecHelper(30)
369
  "15min": minutesSpecHelper(15)
370
  "10min": minutesSpecHelper(10)
371
  "5min": minutesSpecHelper(5)
372
  "minute": minutesSpecHelper(1)
373
  "30sec": secondsSpecHelper(30)
374
  "15sec": secondsSpecHelper(15)
375
  "10sec": secondsSpecHelper(10)
376
  "5sec": secondsSpecHelper(5)
377
  "second": secondsSpecHelper(1)
378
 
379
Morris.AUTO_LABEL_ORDER = [
380
  "decade", "year", "month", "day", "hour",
381
  "30min", "15min", "10min", "5min", "minute",
382
  "30sec", "15sec", "10sec", "5sec", "second"
383
]