morris-rails 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/Gemfile +3 -0
  2. data/Gemfile.lock +97 -0
  3. data/LICENSE +20 -0
  4. data/README.md +58 -0
  5. data/Rakefile +8 -0
  6. data/VERSION +1 -0
  7. data/app/assets/javascripts/morris.area.coffee +42 -0
  8. data/app/assets/javascripts/morris.bar.coffee +165 -0
  9. data/app/assets/javascripts/morris.coffee +42 -0
  10. data/app/assets/javascripts/morris.donut.coffee +182 -0
  11. data/app/assets/javascripts/morris.grid.coffee +392 -0
  12. data/app/assets/javascripts/morris.hover.coffee +41 -0
  13. data/app/assets/javascripts/morris.line.coffee +354 -0
  14. data/app/assets/stylesheets/morris.core.less +27 -0
  15. data/lib/morris-rails/rails.rb +1 -0
  16. data/morris-rails.gemspec +30 -0
  17. data/spec/dummy/README.rdoc +261 -0
  18. data/spec/dummy/Rakefile +7 -0
  19. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  20. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  21. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  22. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  23. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  24. data/spec/dummy/config.ru +4 -0
  25. data/spec/dummy/config/application.rb +65 -0
  26. data/spec/dummy/config/boot.rb +10 -0
  27. data/spec/dummy/config/database.yml +25 -0
  28. data/spec/dummy/config/environment.rb +5 -0
  29. data/spec/dummy/config/environments/development.rb +37 -0
  30. data/spec/dummy/config/environments/production.rb +67 -0
  31. data/spec/dummy/config/environments/test.rb +37 -0
  32. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  33. data/spec/dummy/config/initializers/inflections.rb +15 -0
  34. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  35. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  36. data/spec/dummy/config/initializers/session_store.rb +8 -0
  37. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  38. data/spec/dummy/config/locales/en.yml +5 -0
  39. data/spec/dummy/config/routes.rb +58 -0
  40. data/spec/dummy/db/test.sqlite3 +0 -0
  41. data/spec/dummy/log/test.log +6 -0
  42. data/spec/dummy/public/404.html +26 -0
  43. data/spec/dummy/public/422.html +26 -0
  44. data/spec/dummy/public/500.html +25 -0
  45. data/spec/dummy/public/favicon.ico +0 -0
  46. data/spec/dummy/script/rails +6 -0
  47. data/spec/dummy/tmp/cache/assets/C87/360/sprockets%2F7f2a79129bb3445603eb704843de5397 +0 -0
  48. data/spec/dummy/tmp/cache/assets/D5B/040/sprockets%2F5d0b7809f616213c43a3ba43cbcda91d +0 -0
  49. data/spec/dummy/tmp/cache/assets/DAE/5D0/sprockets%2Fae427ff9e5b67de97e2ad285c5b623b6 +0 -0
  50. data/spec/morris-rails_spec.rb +12 -0
  51. data/spec/spec_helper.rb +16 -0
  52. metadata +271 -0
@@ -0,0 +1,41 @@
1
+ class Morris.Hover
2
+ # Displays contextual information in a floating HTML div.
3
+
4
+ @defaults:
5
+ class: 'morris-hover morris-default-style'
6
+
7
+ constructor: (options = {}) ->
8
+ @options = $.extend {}, Morris.Hover.defaults, options
9
+ @el = $ "<div class='#{@options.class}'></div>"
10
+ @el.hide()
11
+ @options.parent.append(@el)
12
+
13
+ update: (html, x, y) ->
14
+ @html(html)
15
+ @show()
16
+ @moveTo(x, y)
17
+
18
+ html: (content) ->
19
+ @el.html(content)
20
+
21
+ moveTo: (x, y) ->
22
+ parentWidth = @options.parent.innerWidth()
23
+ parentHeight = @options.parent.innerHeight()
24
+ hoverWidth = @el.outerWidth()
25
+ hoverHeight = @el.outerHeight()
26
+ left = Math.min(Math.max(0, x - hoverWidth / 2), parentWidth - hoverWidth)
27
+ if y?
28
+ top = y - hoverHeight - 10
29
+ if top < 0
30
+ top = y + 10
31
+ if top + hoverHeight > parentHeight
32
+ top = parentHeight / 2 - hoverHeight / 2
33
+ else
34
+ top = parentHeight / 2 - hoverHeight / 2
35
+ @el.css(left: left + "px", top: top + "px")
36
+
37
+ show: ->
38
+ @el.show()
39
+
40
+ hide: ->
41
+ @el.hide()
@@ -0,0 +1,354 @@
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
+
18
+ # Default configuration
19
+ #
20
+ defaults:
21
+ lineWidth: 3
22
+ pointSize: 4
23
+ lineColors: [
24
+ '#0b62a4'
25
+ '#7A92A3'
26
+ '#4da74d'
27
+ '#afd8f8'
28
+ '#edc240'
29
+ '#cb4b4b'
30
+ '#9440ed'
31
+ ]
32
+ pointWidths: [1]
33
+ pointStrokeColors: ['#ffffff']
34
+ pointFillColors: []
35
+ smooth: true
36
+ xLabels: 'auto'
37
+ xLabelFormat: null
38
+ xLabelMargin: 50
39
+ continuousLine: true
40
+ hideHover: false
41
+
42
+ # Do any size-related calculations
43
+ #
44
+ # @private
45
+ calc: ->
46
+ @calcPoints()
47
+ @generatePaths()
48
+
49
+ # calculate series data point coordinates
50
+ #
51
+ # @private
52
+ calcPoints: ->
53
+ for row in @data
54
+ row._x = @transX(row.x)
55
+ row._y = for y in row.y
56
+ if y? then @transY(y) else y
57
+ row._ymax = Math.min.apply(null, [@bottom].concat(y for y in row._y when y?))
58
+
59
+ # hit test - returns the index of the row beneath the given coordinate
60
+ #
61
+ hitTest: (x, y) ->
62
+ return null if @data.length == 0
63
+ # TODO better search algo
64
+ for r, index in @data.slice(1)
65
+ break if x < (r._x + @data[index]._x) / 2
66
+ index
67
+
68
+ # hover movement event handler
69
+ #
70
+ # @private
71
+ onHoverMove: (x, y) =>
72
+ index = @hitTest(x, y)
73
+ @displayHoverForRow(index)
74
+
75
+ # hover out event handler
76
+ #
77
+ # @private
78
+ onHoverOut: =>
79
+ if @options.hideHover is 'auto'
80
+ @displayHoverForRow(null)
81
+
82
+ # display a hover popup over the given row
83
+ #
84
+ # @private
85
+ displayHoverForRow: (index) ->
86
+ if index?
87
+ @hover.update(@hoverContentForRow(index)...)
88
+ @hilight(index)
89
+ else
90
+ @hover.hide()
91
+ @hilight()
92
+
93
+ # hover content for a point
94
+ #
95
+ # @private
96
+ hoverContentForRow: (index) ->
97
+ row = @data[index]
98
+ if typeof @options.hoverCallback is 'function'
99
+ content = @options.hoverCallback(index, @options)
100
+ else
101
+ content = "<div class='morris-hover-row-label'>#{row.label}</div>"
102
+ for y, j in row.y
103
+ content += """
104
+ <div class='morris-hover-point' style='color: #{@colorFor(row, j, 'label')}'>
105
+ #{@options.labels[j]}:
106
+ #{@yLabelFormat(y)}
107
+ </div>
108
+ """
109
+ [content, row._x, row._ymax]
110
+
111
+
112
+ # generate paths for series lines
113
+ #
114
+ # @private
115
+ generatePaths: ->
116
+ @paths = for i in [0...@options.ykeys.length]
117
+ smooth = @options.smooth is true or @options.ykeys[i] in @options.smooth
118
+ coords = ({x: r._x, y: r._y[i]} for r in @data when r._y[i] isnt undefined)
119
+ coords = (c for c in coords when c.y isnt null) if @options.continuousLine
120
+
121
+ if coords.length > 1
122
+ Morris.Line.createPath coords, smooth, @bottom
123
+ else
124
+ null
125
+
126
+ # Draws the line chart.
127
+ #
128
+ draw: ->
129
+ @drawXAxis() if @options.axes
130
+ @drawSeries()
131
+ if @options.hideHover is false
132
+ @displayHoverForRow(@data.length - 1)
133
+
134
+ # draw the x-axis labels
135
+ #
136
+ # @private
137
+ drawXAxis: ->
138
+ # draw x axis labels
139
+ ypos = @bottom + @options.gridTextSize * 1.25
140
+ prevLabelMargin = null
141
+ drawLabel = (labelText, xpos) =>
142
+ label = @drawXAxisLabel(@transX(xpos), ypos, labelText)
143
+ labelBox = label.getBBox()
144
+ # ensure a minimum of `xLabelMargin` pixels between labels, and ensure
145
+ # labels don't overflow the container
146
+ if (not prevLabelMargin? or prevLabelMargin >= labelBox.x + labelBox.width) and
147
+ labelBox.x >= 0 and (labelBox.x + labelBox.width) < @el.width()
148
+ prevLabelMargin = labelBox.x - @options.xLabelMargin
149
+ else
150
+ label.remove()
151
+ if @options.parseTime
152
+ if @data.length == 1 and @options.xLabels == 'auto'
153
+ # where there's only one value in the series, we can't make a
154
+ # sensible guess for an x labelling scheme, so just use the original
155
+ # column label
156
+ labels = [[@data[0].label, @data[0].x]]
157
+ else
158
+ labels = Morris.labelSeries(@xmin, @xmax, @width, @options.xLabels, @options.xLabelFormat)
159
+ else
160
+ labels = ([row.label, row.x] for row in @data)
161
+ labels.reverse()
162
+ for l in labels
163
+ drawLabel(l[0], l[1])
164
+
165
+ # draw the data series
166
+ #
167
+ # @private
168
+ drawSeries: ->
169
+ for i in [@options.ykeys.length-1..0]
170
+ path = @paths[i]
171
+ if path isnt null
172
+ @drawLinePath(path, @colorFor(row, i, 'line')) #row isn't available here?
173
+ @seriesPoints = ([] for i in [0...@options.ykeys.length])
174
+ for i in [@options.ykeys.length-1..0]
175
+ for row in @data
176
+ if row._y[i]?
177
+ circle = @drawLinePoint(row._x, row._y[i], @options.pointSize, @colorFor(row, i, 'point'), i)
178
+ else
179
+ circle = null
180
+ @seriesPoints[i].push(circle)
181
+
182
+ # create a path for a data series
183
+ #
184
+ # @private
185
+ @createPath: (coords, smooth, bottom) ->
186
+ path = ""
187
+ grads = Morris.Line.gradients(coords) if smooth
188
+
189
+ prevCoord = {y: null}
190
+ for coord, i in coords
191
+ if coord.y?
192
+ if prevCoord.y?
193
+ if smooth
194
+ g = grads[i]
195
+ lg = grads[i - 1]
196
+ ix = (coord.x - prevCoord.x) / 4
197
+ x1 = prevCoord.x + ix
198
+ y1 = Math.min(bottom, prevCoord.y + ix * lg)
199
+ x2 = coord.x - ix
200
+ y2 = Math.min(bottom, coord.y - ix * g)
201
+ path += "C#{x1},#{y1},#{x2},#{y2},#{coord.x},#{coord.y}"
202
+ else
203
+ path += "L#{coord.x},#{coord.y}"
204
+ else
205
+ if not smooth or grads[i]?
206
+ path += "M#{coord.x},#{coord.y}"
207
+ prevCoord = coord
208
+ return path
209
+
210
+ # calculate a gradient at each point for a series of points
211
+ #
212
+ # @private
213
+ @gradients: (coords) ->
214
+ grad = (a, b) -> (a.y - b.y) / (a.x - b.x)
215
+ for coord, i in coords
216
+ if coord.y?
217
+ nextCoord = coords[i + 1] or {y: null}
218
+ prevCoord = coords[i - 1] or {y: null}
219
+ if prevCoord.y? and nextCoord.y?
220
+ grad(prevCoord, nextCoord)
221
+ else if prevCoord.y?
222
+ grad(prevCoord, coord)
223
+ else if nextCoord.y?
224
+ grad(coord, nextCoord)
225
+ else
226
+ null
227
+ else
228
+ null
229
+
230
+ # @private
231
+ hilight: (index) =>
232
+ if @prevHilight isnt null and @prevHilight isnt index
233
+ for i in [0..@seriesPoints.length-1]
234
+ if @seriesPoints[i][@prevHilight]
235
+ @seriesPoints[i][@prevHilight].animate @pointShrink
236
+ if index isnt null and @prevHilight isnt index
237
+ for i in [0..@seriesPoints.length-1]
238
+ if @seriesPoints[i][index]
239
+ @seriesPoints[i][index].animate @pointGrow
240
+ @prevHilight = index
241
+
242
+ colorFor: (row, sidx, type) ->
243
+ if typeof @options.lineColors is 'function'
244
+ @options.lineColors.call(@, row, sidx, type)
245
+ else if type is 'point'
246
+ @options.pointFillColors[sidx % @options.pointFillColors.length] || @options.lineColors[sidx % @options.lineColors.length]
247
+ else
248
+ @options.lineColors[sidx % @options.lineColors.length]
249
+
250
+ drawXAxisLabel: (xPos, yPos, text) ->
251
+ @raphael.text(xPos, yPos, text)
252
+ .attr('font-size', @options.gridTextSize)
253
+ .attr('fill', @options.gridTextColor)
254
+
255
+ drawLinePath: (path, lineColor) ->
256
+ @raphael.path(path)
257
+ .attr('stroke', lineColor)
258
+ .attr('stroke-width', @options.lineWidth)
259
+
260
+ drawLinePoint: (xPos, yPos, size, pointColor, lineIndex) ->
261
+ @raphael.circle(xPos, yPos, size)
262
+ .attr('fill', pointColor)
263
+ .attr('stroke-width', @strokeWidthForSeries(lineIndex))
264
+ .attr('stroke', @strokeForSeries(lineIndex))
265
+
266
+ # @private
267
+ strokeWidthForSeries: (index) ->
268
+ @options.pointWidths[index % @options.pointWidths.length]
269
+
270
+ # @private
271
+ strokeForSeries: (index) ->
272
+ @options.pointStrokeColors[index % @options.pointStrokeColors.length]
273
+
274
+ # generate a series of label, timestamp pairs for x-axis labels
275
+ #
276
+ # @private
277
+ Morris.labelSeries = (dmin, dmax, pxwidth, specName, xLabelFormat) ->
278
+ ddensity = 200 * (dmax - dmin) / pxwidth # seconds per `margin` pixels
279
+ d0 = new Date(dmin)
280
+ spec = Morris.LABEL_SPECS[specName]
281
+ # if the spec doesn't exist, search for the closest one in the list
282
+ if spec is undefined
283
+ for name in Morris.AUTO_LABEL_ORDER
284
+ s = Morris.LABEL_SPECS[name]
285
+ if ddensity >= s.span
286
+ spec = s
287
+ break
288
+ # if we run out of options, use second-intervals
289
+ if spec is undefined
290
+ spec = Morris.LABEL_SPECS["second"]
291
+ # check if there's a user-defined formatting function
292
+ if xLabelFormat
293
+ spec = $.extend({}, spec, {fmt: xLabelFormat})
294
+ # calculate labels
295
+ d = spec.start(d0)
296
+ ret = []
297
+ while (t = d.getTime()) <= dmax
298
+ if t >= dmin
299
+ ret.push [spec.fmt(d), t]
300
+ spec.incr(d)
301
+ return ret
302
+
303
+ # @private
304
+ minutesSpecHelper = (interval) ->
305
+ span: interval * 60 * 1000
306
+ start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours())
307
+ fmt: (d) -> "#{Morris.pad2(d.getHours())}:#{Morris.pad2(d.getMinutes())}"
308
+ incr: (d) -> d.setMinutes(d.getMinutes() + interval)
309
+
310
+ # @private
311
+ secondsSpecHelper = (interval) ->
312
+ span: interval * 1000
313
+ start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes())
314
+ fmt: (d) -> "#{Morris.pad2(d.getHours())}:#{Morris.pad2(d.getMinutes())}:#{Morris.pad2(d.getSeconds())}"
315
+ incr: (d) -> d.setSeconds(d.getSeconds() + interval)
316
+
317
+ Morris.LABEL_SPECS =
318
+ "decade":
319
+ span: 172800000000 # 10 * 365 * 24 * 60 * 60 * 1000
320
+ start: (d) -> new Date(d.getFullYear() - d.getFullYear() % 10, 0, 1)
321
+ fmt: (d) -> "#{d.getFullYear()}"
322
+ incr: (d) -> d.setFullYear(d.getFullYear() + 10)
323
+ "year":
324
+ span: 17280000000 # 365 * 24 * 60 * 60 * 1000
325
+ start: (d) -> new Date(d.getFullYear(), 0, 1)
326
+ fmt: (d) -> "#{d.getFullYear()}"
327
+ incr: (d) -> d.setFullYear(d.getFullYear() + 1)
328
+ "month":
329
+ span: 2419200000 # 28 * 24 * 60 * 60 * 1000
330
+ start: (d) -> new Date(d.getFullYear(), d.getMonth(), 1)
331
+ fmt: (d) -> "#{d.getFullYear()}-#{Morris.pad2(d.getMonth() + 1)}"
332
+ incr: (d) -> d.setMonth(d.getMonth() + 1)
333
+ "day":
334
+ span: 86400000 # 24 * 60 * 60 * 1000
335
+ start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate())
336
+ fmt: (d) -> "#{d.getFullYear()}-#{Morris.pad2(d.getMonth() + 1)}-#{Morris.pad2(d.getDate())}"
337
+ incr: (d) -> d.setDate(d.getDate() + 1)
338
+ "hour": minutesSpecHelper(60)
339
+ "30min": minutesSpecHelper(30)
340
+ "15min": minutesSpecHelper(15)
341
+ "10min": minutesSpecHelper(10)
342
+ "5min": minutesSpecHelper(5)
343
+ "minute": minutesSpecHelper(1)
344
+ "30sec": secondsSpecHelper(30)
345
+ "15sec": secondsSpecHelper(15)
346
+ "10sec": secondsSpecHelper(10)
347
+ "5sec": secondsSpecHelper(5)
348
+ "second": secondsSpecHelper(1)
349
+
350
+ Morris.AUTO_LABEL_ORDER = [
351
+ "decade", "year", "month", "day", "hour",
352
+ "30min", "15min", "10min", "5min", "minute",
353
+ "30sec", "15sec", "10sec", "5sec", "second"
354
+ ]
@@ -0,0 +1,27 @@
1
+ .morris-hover {
2
+ position: absolute;
3
+ z-index: 1000;
4
+
5
+ &.morris-default-style {
6
+ border-radius: 10px;
7
+ padding: 6px;
8
+ color: #666;
9
+ background: rgba(255, 255, 255, 0.8);
10
+ border: solid 2px rgba(230, 230, 230, 0.8);
11
+
12
+ font-family: sans-serif;
13
+ font-size: 12px;
14
+ text-align: center;
15
+
16
+ .morris-hover-row-label {
17
+ font-weight: bold;
18
+ margin: 0.25em 0;
19
+ }
20
+
21
+ .morris-hover-point {
22
+ white-space: nowrap;
23
+ margin: 0.1em 0;
24
+ }
25
+ }
26
+
27
+ }
@@ -0,0 +1 @@
1
+ require 'morris-rails/rails/engine'
@@ -0,0 +1,30 @@
1
+ require 'pathname'
2
+
3
+ Gem::Specification.new do |s|
4
+
5
+ # Variables
6
+ s.name = Pathname.new(__FILE__).basename('.gemspec').to_s
7
+ s.author = 'Ryan Scott Lewis'
8
+ s.email = 'ryan@rynet.us'
9
+ s.summary = 'morris.js for the Rails asset pipeline.'
10
+ s.description = s.summary
11
+ s.license = 'MIT'
12
+
13
+ # Dependencies
14
+ s.add_dependency 'version', '~> 1.0'
15
+ s.add_dependency 'railties', '>= 3.2.0', '< 5.0'
16
+ s.add_dependency 'jquery-rails', '~> 2.0'
17
+ s.add_dependency 'raphaeljs-rails', '~> 1.0'
18
+ s.add_development_dependency 'rake', '~> 10.0'
19
+ s.add_development_dependency 'fancy_logger', '~> 0.1'
20
+ s.add_development_dependency 'rspec', '~> 2.13'
21
+ s.add_development_dependency 'fuubar', '~> 1.1'
22
+
23
+ # Pragmatically set variables
24
+ s.homepage = "http://github.com/RyanScottLewis/#{s.name}"
25
+ s.version = Pathname.glob('VERSION*').first.read rescue '0.0.0'
26
+ s.require_paths = ['lib']
27
+ s.files = Dir['{{Rake,Gem}file{.lock,},README*,VERSION,LICENSE,*.gemspec,lib/morris-rails**/*.rb,spec/**/*.rb,app/**/*}']
28
+ s.test_files = Dir['{examples,spec,test}/**/*']
29
+
30
+ end