Subversion Repositories Integrator Subversion

Rev

Details | Last modification | View Log | RSS feed

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