morris-rails 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +3 -0
- data/Gemfile.lock +97 -0
- data/LICENSE +20 -0
- data/README.md +58 -0
- data/Rakefile +8 -0
- data/VERSION +1 -0
- data/app/assets/javascripts/morris.area.coffee +42 -0
- data/app/assets/javascripts/morris.bar.coffee +165 -0
- data/app/assets/javascripts/morris.coffee +42 -0
- data/app/assets/javascripts/morris.donut.coffee +182 -0
- data/app/assets/javascripts/morris.grid.coffee +392 -0
- data/app/assets/javascripts/morris.hover.coffee +41 -0
- data/app/assets/javascripts/morris.line.coffee +354 -0
- data/app/assets/stylesheets/morris.core.less +27 -0
- data/lib/morris-rails/rails.rb +1 -0
- data/morris-rails.gemspec +30 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +65 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/test.log +6 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/dummy/tmp/cache/assets/C87/360/sprockets%2F7f2a79129bb3445603eb704843de5397 +0 -0
- data/spec/dummy/tmp/cache/assets/D5B/040/sprockets%2F5d0b7809f616213c43a3ba43cbcda91d +0 -0
- data/spec/dummy/tmp/cache/assets/DAE/5D0/sprockets%2Fae427ff9e5b67de97e2ad285c5b623b6 +0 -0
- data/spec/morris-rails_spec.rb +12 -0
- data/spec/spec_helper.rb +16 -0
- metadata +271 -0
@@ -0,0 +1,182 @@
|
|
1
|
+
# Donut charts.
|
2
|
+
#
|
3
|
+
# @example
|
4
|
+
# Morris.Donut({
|
5
|
+
# el: $('#donut-container'),
|
6
|
+
# data: [
|
7
|
+
# { label: 'yin', value: 50 },
|
8
|
+
# { label: 'yang', value: 50 }
|
9
|
+
# ]
|
10
|
+
# });
|
11
|
+
class Morris.Donut
|
12
|
+
defaults:
|
13
|
+
colors: [
|
14
|
+
'#0B62A4'
|
15
|
+
'#3980B5'
|
16
|
+
'#679DC6'
|
17
|
+
'#95BBD7'
|
18
|
+
'#B0CCE1'
|
19
|
+
'#095791'
|
20
|
+
'#095085'
|
21
|
+
'#083E67'
|
22
|
+
'#052C48'
|
23
|
+
'#042135'
|
24
|
+
],
|
25
|
+
backgroundColor: '#FFFFFF',
|
26
|
+
labelColor: '#000000',
|
27
|
+
formatter: Morris.commas
|
28
|
+
|
29
|
+
# Create and render a donut chart.
|
30
|
+
#
|
31
|
+
constructor: (options) ->
|
32
|
+
if not (this instanceof Morris.Donut)
|
33
|
+
return new Morris.Donut(options)
|
34
|
+
|
35
|
+
if typeof options.element is 'string'
|
36
|
+
@el = $ document.getElementById(options.element)
|
37
|
+
else
|
38
|
+
@el = $ options.element
|
39
|
+
|
40
|
+
@options = $.extend {}, @defaults, options
|
41
|
+
|
42
|
+
if @el == null || @el.length == 0
|
43
|
+
throw new Error("Graph placeholder not found.")
|
44
|
+
|
45
|
+
# bail if there's no data
|
46
|
+
if options.data is undefined or options.data.length is 0
|
47
|
+
return
|
48
|
+
@data = options.data
|
49
|
+
|
50
|
+
@redraw()
|
51
|
+
|
52
|
+
# Clear and redraw the chart.
|
53
|
+
#
|
54
|
+
# If you need to re-size your charts, call this method after changing the
|
55
|
+
# size of the container element.
|
56
|
+
redraw: ->
|
57
|
+
@el.empty()
|
58
|
+
|
59
|
+
@raphael = new Raphael(@el[0])
|
60
|
+
|
61
|
+
cx = @el.width() / 2
|
62
|
+
cy = @el.height() / 2
|
63
|
+
w = (Math.min(cx, cy) - 10) / 3
|
64
|
+
|
65
|
+
total = 0
|
66
|
+
total += x.value for x in @data
|
67
|
+
|
68
|
+
min = 5 / (2 * w)
|
69
|
+
C = 1.9999 * Math.PI - min * @data.length
|
70
|
+
|
71
|
+
last = 0
|
72
|
+
idx = 0
|
73
|
+
@segments = []
|
74
|
+
for d in @data
|
75
|
+
next = last + min + C * (d.value / total)
|
76
|
+
seg = new Morris.DonutSegment(cx, cy, w*2, w, last, next, @options.colors[idx % @options.colors.length], @options.backgroundColor, d, @raphael)
|
77
|
+
seg.render()
|
78
|
+
@segments.push seg
|
79
|
+
seg.on 'hover', @select
|
80
|
+
last = next
|
81
|
+
idx += 1
|
82
|
+
@text1 = @drawEmptyDonutLabel(cx, cy - 10, @options.labelColor, 15, 800)
|
83
|
+
@text2 = @drawEmptyDonutLabel(cx, cy + 10, @options.labelColor, 14)
|
84
|
+
max_value = Math.max.apply(null, d.value for d in @data)
|
85
|
+
idx = 0
|
86
|
+
for d in @data
|
87
|
+
if d.value == max_value
|
88
|
+
@select idx
|
89
|
+
break
|
90
|
+
idx += 1
|
91
|
+
|
92
|
+
# Select the segment at the given index.
|
93
|
+
select: (idx) =>
|
94
|
+
s.deselect() for s in @segments
|
95
|
+
if typeof idx is 'number' then segment = @segments[idx] else segment = idx
|
96
|
+
segment.select()
|
97
|
+
@setLabels segment.data.label, @options.formatter(segment.data.value, segment.data)
|
98
|
+
|
99
|
+
# @private
|
100
|
+
setLabels: (label1, label2) ->
|
101
|
+
inner = (Math.min(@el.width() / 2, @el.height() / 2) - 10) * 2 / 3
|
102
|
+
maxWidth = 1.8 * inner
|
103
|
+
maxHeightTop = inner / 2
|
104
|
+
maxHeightBottom = inner / 3
|
105
|
+
@text1.attr(text: label1, transform: '')
|
106
|
+
text1bbox = @text1.getBBox()
|
107
|
+
text1scale = Math.min(maxWidth / text1bbox.width, maxHeightTop / text1bbox.height)
|
108
|
+
@text1.attr(transform: "S#{text1scale},#{text1scale},#{text1bbox.x + text1bbox.width / 2},#{text1bbox.y + text1bbox.height}")
|
109
|
+
@text2.attr(text: label2, transform: '')
|
110
|
+
text2bbox = @text2.getBBox()
|
111
|
+
text2scale = Math.min(maxWidth / text2bbox.width, maxHeightBottom / text2bbox.height)
|
112
|
+
@text2.attr(transform: "S#{text2scale},#{text2scale},#{text2bbox.x + text2bbox.width / 2},#{text2bbox.y}")
|
113
|
+
|
114
|
+
drawEmptyDonutLabel: (xPos, yPos, color, fontSize, fontWeight) ->
|
115
|
+
text = @raphael.text(xPos, yPos, '')
|
116
|
+
.attr('font-size', fontSize)
|
117
|
+
.attr('fill', color)
|
118
|
+
text.attr('font-weight', fontWeight) if fontWeight?
|
119
|
+
return text
|
120
|
+
|
121
|
+
|
122
|
+
# A segment within a donut chart.
|
123
|
+
#
|
124
|
+
# @private
|
125
|
+
class Morris.DonutSegment extends Morris.EventEmitter
|
126
|
+
constructor: (@cx, @cy, @inner, @outer, p0, p1, @color, @backgroundColor, @data, @raphael) ->
|
127
|
+
@sin_p0 = Math.sin(p0)
|
128
|
+
@cos_p0 = Math.cos(p0)
|
129
|
+
@sin_p1 = Math.sin(p1)
|
130
|
+
@cos_p1 = Math.cos(p1)
|
131
|
+
@is_long = if (p1 - p0) > Math.PI then 1 else 0
|
132
|
+
@path = @calcSegment(@inner + 3, @inner + @outer - 5)
|
133
|
+
@selectedPath = @calcSegment(@inner + 3, @inner + @outer)
|
134
|
+
@hilight = @calcArc(@inner)
|
135
|
+
|
136
|
+
calcArcPoints: (r) ->
|
137
|
+
return [
|
138
|
+
@cx + r * @sin_p0,
|
139
|
+
@cy + r * @cos_p0,
|
140
|
+
@cx + r * @sin_p1,
|
141
|
+
@cy + r * @cos_p1]
|
142
|
+
|
143
|
+
calcSegment: (r1, r2) ->
|
144
|
+
[ix0, iy0, ix1, iy1] = @calcArcPoints(r1)
|
145
|
+
[ox0, oy0, ox1, oy1] = @calcArcPoints(r2)
|
146
|
+
return (
|
147
|
+
"M#{ix0},#{iy0}" +
|
148
|
+
"A#{r1},#{r1},0,#{@is_long},0,#{ix1},#{iy1}" +
|
149
|
+
"L#{ox1},#{oy1}" +
|
150
|
+
"A#{r2},#{r2},0,#{@is_long},1,#{ox0},#{oy0}" +
|
151
|
+
"Z")
|
152
|
+
|
153
|
+
calcArc: (r) ->
|
154
|
+
[ix0, iy0, ix1, iy1] = @calcArcPoints(r)
|
155
|
+
return (
|
156
|
+
"M#{ix0},#{iy0}" +
|
157
|
+
"A#{r},#{r},0,#{@is_long},0,#{ix1},#{iy1}")
|
158
|
+
|
159
|
+
render: ->
|
160
|
+
@arc = @drawDonutArc(@hilight, @color)
|
161
|
+
@seg = @drawDonutSegment(@path, @color, @backgroundColor, => @fire('hover', @))
|
162
|
+
|
163
|
+
drawDonutArc: (path, color) ->
|
164
|
+
@raphael.path(path)
|
165
|
+
.attr(stroke: color, 'stroke-width': 2, opacity: 0)
|
166
|
+
|
167
|
+
drawDonutSegment: (path, fillColor, strokeColor, hoverFunction) ->
|
168
|
+
@raphael.path(path)
|
169
|
+
.attr(fill: fillColor, stroke: strokeColor, 'stroke-width': 3)
|
170
|
+
.hover(hoverFunction)
|
171
|
+
|
172
|
+
select: =>
|
173
|
+
unless @selected
|
174
|
+
@seg.animate(path: @selectedPath, 150, '<>')
|
175
|
+
@arc.animate(opacity: 1, 150, '<>')
|
176
|
+
@selected = true
|
177
|
+
|
178
|
+
deselect: =>
|
179
|
+
if @selected
|
180
|
+
@seg.animate(path: @path, 150, '<>')
|
181
|
+
@arc.animate(opacity: 0, 150, '<>')
|
182
|
+
@selected = false
|
@@ -0,0 +1,392 @@
|
|
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
|
+
@postInit() if @postInit
|
53
|
+
|
54
|
+
# Default options
|
55
|
+
#
|
56
|
+
gridDefaults:
|
57
|
+
dateFormat: null
|
58
|
+
axes: true
|
59
|
+
grid: true
|
60
|
+
gridLineColor: '#aaa'
|
61
|
+
gridStrokeWidth: 0.5
|
62
|
+
gridTextColor: '#888'
|
63
|
+
gridTextSize: 12
|
64
|
+
hideHover: false
|
65
|
+
yLabelFormat: null
|
66
|
+
numLines: 5
|
67
|
+
padding: 25
|
68
|
+
parseTime: true
|
69
|
+
postUnits: ''
|
70
|
+
preUnits: ''
|
71
|
+
ymax: 'auto'
|
72
|
+
ymin: 'auto 0'
|
73
|
+
goals: []
|
74
|
+
goalStrokeWidth: 1.0
|
75
|
+
goalLineColors: [
|
76
|
+
'#666633'
|
77
|
+
'#999966'
|
78
|
+
'#cc6666'
|
79
|
+
'#663333'
|
80
|
+
]
|
81
|
+
events: []
|
82
|
+
eventStrokeWidth: 1.0
|
83
|
+
eventLineColors: [
|
84
|
+
'#005a04'
|
85
|
+
'#ccffbb'
|
86
|
+
'#3a5f0b'
|
87
|
+
'#005502'
|
88
|
+
]
|
89
|
+
|
90
|
+
# Update the data series and redraw the chart.
|
91
|
+
#
|
92
|
+
setData: (data, redraw = true) ->
|
93
|
+
if !data? or data.length == 0
|
94
|
+
@data = []
|
95
|
+
@raphael.clear()
|
96
|
+
@hover.hide() if @hover?
|
97
|
+
return
|
98
|
+
|
99
|
+
ymax = if @cumulative then 0 else null
|
100
|
+
ymin = if @cumulative then 0 else null
|
101
|
+
|
102
|
+
if @options.goals.length > 0
|
103
|
+
minGoal = Math.min.apply(null, @options.goals)
|
104
|
+
maxGoal = Math.max.apply(null, @options.goals)
|
105
|
+
ymin = if ymin? then Math.min(ymin, minGoal) else minGoal
|
106
|
+
ymax = if ymax? then Math.max(ymax, maxGoal) else maxGoal
|
107
|
+
|
108
|
+
@data = for row, index in data
|
109
|
+
ret = {}
|
110
|
+
ret.label = row[@options.xkey]
|
111
|
+
if @options.parseTime
|
112
|
+
ret.x = Morris.parseDate(ret.label)
|
113
|
+
if @options.dateFormat
|
114
|
+
ret.label = @options.dateFormat ret.x
|
115
|
+
else if typeof ret.label is 'number'
|
116
|
+
ret.label = new Date(ret.label).toString()
|
117
|
+
else
|
118
|
+
ret.x = index
|
119
|
+
total = 0
|
120
|
+
ret.y = for ykey, idx in @options.ykeys
|
121
|
+
yval = row[ykey]
|
122
|
+
yval = parseFloat(yval) if typeof yval is 'string'
|
123
|
+
yval = null if yval? and typeof yval isnt 'number'
|
124
|
+
if yval?
|
125
|
+
if @cumulative
|
126
|
+
total += yval
|
127
|
+
else
|
128
|
+
if ymax?
|
129
|
+
ymax = Math.max(yval, ymax)
|
130
|
+
ymin = Math.min(yval, ymin)
|
131
|
+
else
|
132
|
+
ymax = ymin = yval
|
133
|
+
if @cumulative and total?
|
134
|
+
ymax = Math.max(total, ymax)
|
135
|
+
ymin = Math.min(total, ymin)
|
136
|
+
yval
|
137
|
+
ret
|
138
|
+
|
139
|
+
if @options.parseTime
|
140
|
+
@data = @data.sort (a, b) -> (a.x > b.x) - (b.x > a.x)
|
141
|
+
|
142
|
+
# calculate horizontal range of the graph
|
143
|
+
@xmin = @data[0].x
|
144
|
+
@xmax = @data[@data.length - 1].x
|
145
|
+
|
146
|
+
@events = []
|
147
|
+
if @options.parseTime and @options.events.length > 0
|
148
|
+
@events = (Morris.parseDate(e) for e in @options.events)
|
149
|
+
@xmax = Math.max(@xmax, Math.max.apply(null, @events))
|
150
|
+
@xmin = Math.min(@xmin, Math.min.apply(null, @events))
|
151
|
+
|
152
|
+
if @xmin is @xmax
|
153
|
+
@xmin -= 1
|
154
|
+
@xmax += 1
|
155
|
+
|
156
|
+
@ymin = @yboundary('min', ymin)
|
157
|
+
@ymax = @yboundary('max', ymax)
|
158
|
+
|
159
|
+
if @ymin is @ymax
|
160
|
+
@ymin -= 1 if ymin
|
161
|
+
@ymax += 1
|
162
|
+
|
163
|
+
@yInterval = (@ymax - @ymin) / (@options.numLines - 1)
|
164
|
+
if @yInterval > 0 and @yInterval < 1
|
165
|
+
@precision = -Math.floor(Math.log(@yInterval) / Math.log(10))
|
166
|
+
else
|
167
|
+
@precision = 0
|
168
|
+
|
169
|
+
@dirty = true
|
170
|
+
@redraw() if redraw
|
171
|
+
|
172
|
+
yboundary: (boundaryType, currentValue) ->
|
173
|
+
boundaryOption = @options["y#{boundaryType}"]
|
174
|
+
if typeof boundaryOption is 'string'
|
175
|
+
if boundaryOption[0..3] is 'auto'
|
176
|
+
if boundaryOption.length > 5
|
177
|
+
suggestedValue = parseInt(boundaryOption[5..], 10)
|
178
|
+
return suggestedValue unless currentValue?
|
179
|
+
Math[boundaryType](currentValue, suggestedValue)
|
180
|
+
else
|
181
|
+
if currentValue? then currentValue else 0
|
182
|
+
else
|
183
|
+
parseInt(boundaryOption, 10)
|
184
|
+
else
|
185
|
+
boundaryOption
|
186
|
+
|
187
|
+
_calc: ->
|
188
|
+
w = @el.width()
|
189
|
+
h = @el.height()
|
190
|
+
|
191
|
+
if @elementWidth != w or @elementHeight != h or @dirty
|
192
|
+
@elementWidth = w
|
193
|
+
@elementHeight = h
|
194
|
+
@dirty = false
|
195
|
+
# recalculate grid dimensions
|
196
|
+
@left = @options.padding
|
197
|
+
@right = @elementWidth - @options.padding
|
198
|
+
@top = @options.padding
|
199
|
+
@bottom = @elementHeight - @options.padding
|
200
|
+
if @options.axes
|
201
|
+
maxYLabelWidth = Math.max(
|
202
|
+
@measureText(@yAxisFormat(@ymin), @options.gridTextSize).width,
|
203
|
+
@measureText(@yAxisFormat(@ymax), @options.gridTextSize).width)
|
204
|
+
@left += maxYLabelWidth
|
205
|
+
@bottom -= 1.5 * @options.gridTextSize
|
206
|
+
@width = @right - @left
|
207
|
+
@height = @bottom - @top
|
208
|
+
@dx = @width / (@xmax - @xmin)
|
209
|
+
@dy = @height / (@ymax - @ymin)
|
210
|
+
@calc() if @calc
|
211
|
+
|
212
|
+
# Quick translation helpers
|
213
|
+
#
|
214
|
+
transY: (y) -> @bottom - (y - @ymin) * @dy
|
215
|
+
transX: (x) ->
|
216
|
+
if @data.length == 1
|
217
|
+
(@left + @right) / 2
|
218
|
+
else
|
219
|
+
@left + (x - @xmin) * @dx
|
220
|
+
|
221
|
+
# Draw it!
|
222
|
+
#
|
223
|
+
# If you need to re-size your charts, call this method after changing the
|
224
|
+
# size of the container element.
|
225
|
+
redraw: ->
|
226
|
+
@raphael.clear()
|
227
|
+
@_calc()
|
228
|
+
@drawGrid()
|
229
|
+
@drawGoals()
|
230
|
+
@drawEvents()
|
231
|
+
@draw() if @draw
|
232
|
+
|
233
|
+
# @private
|
234
|
+
#
|
235
|
+
measureText: (text, fontSize = 12) ->
|
236
|
+
tt = @raphael.text(100, 100, text).attr('font-size', fontSize)
|
237
|
+
ret = tt.getBBox()
|
238
|
+
tt.remove()
|
239
|
+
ret
|
240
|
+
|
241
|
+
# @private
|
242
|
+
#
|
243
|
+
yAxisFormat: (label) -> @yLabelFormat(label)
|
244
|
+
|
245
|
+
# @private
|
246
|
+
#
|
247
|
+
yLabelFormat: (label) ->
|
248
|
+
if typeof @options.yLabelFormat is 'function'
|
249
|
+
@options.yLabelFormat(label)
|
250
|
+
else
|
251
|
+
"#{@options.preUnits}#{Morris.commas(label)}#{@options.postUnits}"
|
252
|
+
|
253
|
+
updateHover: (x, y) ->
|
254
|
+
hit = @hitTest(x, y)
|
255
|
+
if hit?
|
256
|
+
@hover.update(hit...)
|
257
|
+
|
258
|
+
# draw y axis labels, horizontal lines
|
259
|
+
#
|
260
|
+
drawGrid: ->
|
261
|
+
return if @options.grid is false and @options.axes is false
|
262
|
+
firstY = @ymin
|
263
|
+
lastY = @ymax
|
264
|
+
for lineY in [firstY..lastY] by @yInterval
|
265
|
+
v = parseFloat(lineY.toFixed(@precision))
|
266
|
+
y = @transY(v)
|
267
|
+
if @options.axes
|
268
|
+
@drawYAxisLabel(@left - @options.padding / 2, y, @yAxisFormat(v))
|
269
|
+
if @options.grid
|
270
|
+
@drawGridLine("M#{@left},#{y}H#{@left + @width}")
|
271
|
+
|
272
|
+
# draw goals horizontal lines
|
273
|
+
#
|
274
|
+
drawGoals: ->
|
275
|
+
for goal, i in @options.goals
|
276
|
+
color = @options.goalLineColors[i % @options.goalLineColors.length]
|
277
|
+
@drawGoal(goal, color)
|
278
|
+
|
279
|
+
# draw events vertical lines
|
280
|
+
drawEvents: ->
|
281
|
+
for event, i in @events
|
282
|
+
color = @options.eventLineColors[i % @options.eventLineColors.length]
|
283
|
+
@drawEvent(event, color)
|
284
|
+
|
285
|
+
drawGoal: (goal, color) ->
|
286
|
+
@raphael.path("M#{@left},#{@transY(goal)}H#{@right}")
|
287
|
+
.attr('stroke', color)
|
288
|
+
.attr('stroke-width', @options.goalStrokeWidth)
|
289
|
+
|
290
|
+
drawEvent: (event, color) ->
|
291
|
+
@raphael.path("M#{@transX(event)},#{@bottom}V#{@top}")
|
292
|
+
.attr('stroke', color)
|
293
|
+
.attr('stroke-width', @options.eventStrokeWidth)
|
294
|
+
|
295
|
+
drawYAxisLabel: (xPos, yPos, text) ->
|
296
|
+
@raphael.text(xPos, yPos, text)
|
297
|
+
.attr('font-size', @options.gridTextSize)
|
298
|
+
.attr('fill', @options.gridTextColor)
|
299
|
+
.attr('text-anchor', 'end')
|
300
|
+
|
301
|
+
drawGridLine: (path) ->
|
302
|
+
@raphael.path(path)
|
303
|
+
.attr('stroke', @options.gridLineColor)
|
304
|
+
.attr('stroke-width', @options.gridStrokeWidth)
|
305
|
+
|
306
|
+
# Parse a date into a javascript timestamp
|
307
|
+
#
|
308
|
+
#
|
309
|
+
Morris.parseDate = (date) ->
|
310
|
+
if typeof date is 'number'
|
311
|
+
return date
|
312
|
+
m = date.match /^(\d+) Q(\d)$/
|
313
|
+
n = date.match /^(\d+)-(\d+)$/
|
314
|
+
o = date.match /^(\d+)-(\d+)-(\d+)$/
|
315
|
+
p = date.match /^(\d+) W(\d+)$/
|
316
|
+
q = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+)(Z|([+-])(\d\d):?(\d\d))?$/
|
317
|
+
r = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+):(\d+(\.\d+)?)(Z|([+-])(\d\d):?(\d\d))?$/
|
318
|
+
if m
|
319
|
+
new Date(
|
320
|
+
parseInt(m[1], 10),
|
321
|
+
parseInt(m[2], 10) * 3 - 1,
|
322
|
+
1).getTime()
|
323
|
+
else if n
|
324
|
+
new Date(
|
325
|
+
parseInt(n[1], 10),
|
326
|
+
parseInt(n[2], 10) - 1,
|
327
|
+
1).getTime()
|
328
|
+
else if o
|
329
|
+
new Date(
|
330
|
+
parseInt(o[1], 10),
|
331
|
+
parseInt(o[2], 10) - 1,
|
332
|
+
parseInt(o[3], 10)).getTime()
|
333
|
+
else if p
|
334
|
+
# calculate number of weeks in year given
|
335
|
+
ret = new Date(parseInt(p[1], 10), 0, 1);
|
336
|
+
# first thursday in year (ISO 8601 standard)
|
337
|
+
if ret.getDay() isnt 4
|
338
|
+
ret.setMonth(0, 1 + ((4 - ret.getDay()) + 7) % 7);
|
339
|
+
# add weeks
|
340
|
+
ret.getTime() + parseInt(p[2], 10) * 604800000
|
341
|
+
else if q
|
342
|
+
if not q[6]
|
343
|
+
# no timezone info, use local
|
344
|
+
new Date(
|
345
|
+
parseInt(q[1], 10),
|
346
|
+
parseInt(q[2], 10) - 1,
|
347
|
+
parseInt(q[3], 10),
|
348
|
+
parseInt(q[4], 10),
|
349
|
+
parseInt(q[5], 10)).getTime()
|
350
|
+
else
|
351
|
+
# timezone info supplied, use UTC
|
352
|
+
offsetmins = 0
|
353
|
+
if q[6] != 'Z'
|
354
|
+
offsetmins = parseInt(q[8], 10) * 60 + parseInt(q[9], 10)
|
355
|
+
offsetmins = 0 - offsetmins if q[7] == '+'
|
356
|
+
Date.UTC(
|
357
|
+
parseInt(q[1], 10),
|
358
|
+
parseInt(q[2], 10) - 1,
|
359
|
+
parseInt(q[3], 10),
|
360
|
+
parseInt(q[4], 10),
|
361
|
+
parseInt(q[5], 10) + offsetmins)
|
362
|
+
else if r
|
363
|
+
secs = parseFloat(r[6])
|
364
|
+
isecs = Math.floor(secs)
|
365
|
+
msecs = Math.round((secs - isecs) * 1000)
|
366
|
+
if not r[8]
|
367
|
+
# no timezone info, use local
|
368
|
+
new Date(
|
369
|
+
parseInt(r[1], 10),
|
370
|
+
parseInt(r[2], 10) - 1,
|
371
|
+
parseInt(r[3], 10),
|
372
|
+
parseInt(r[4], 10),
|
373
|
+
parseInt(r[5], 10),
|
374
|
+
isecs,
|
375
|
+
msecs).getTime()
|
376
|
+
else
|
377
|
+
# timezone info supplied, use UTC
|
378
|
+
offsetmins = 0
|
379
|
+
if r[8] != 'Z'
|
380
|
+
offsetmins = parseInt(r[10], 10) * 60 + parseInt(r[11], 10)
|
381
|
+
offsetmins = 0 - offsetmins if r[9] == '+'
|
382
|
+
Date.UTC(
|
383
|
+
parseInt(r[1], 10),
|
384
|
+
parseInt(r[2], 10) - 1,
|
385
|
+
parseInt(r[3], 10),
|
386
|
+
parseInt(r[4], 10),
|
387
|
+
parseInt(r[5], 10) + offsetmins,
|
388
|
+
isecs,
|
389
|
+
msecs)
|
390
|
+
else
|
391
|
+
new Date(parseInt(date, 10), 0, 1).getTime()
|
392
|
+
|