WIP Refactor grid drawing into a base class.

This commit is contained in:
Olly Smith 2012-10-21 22:33:49 +01:00
parent 67fbc481aa
commit d5b8071dbb
5 changed files with 733 additions and 686 deletions

View File

@ -25,16 +25,12 @@ var day_data = [
{"period": "2012-09-15", "licensed": 3201, "sorned": 656}, {"period": "2012-09-15", "licensed": 3201, "sorned": 656},
{"period": "2012-09-10", "licensed": 3215, "sorned": 622} {"period": "2012-09-10", "licensed": 3215, "sorned": 622}
]; ];
Morris.Line({ line = Morris.Line({
element: 'graph', element: 'graph',
data: day_data, data: day_data,
xkey: 'period', xkey: 'period',
ykeys: ['licensed', 'sorned'], ykeys: ['licensed', 'sorned'],
labels: ['Licensed', 'SORN'], labels: ['Licensed', 'SORN']
/* custom label formatting with `xLabelFormat` */
xLabelFormat: function(d) { return (d.getMonth()+1)+'/'+d.getDate()+'/'+d.getFullYear(); },
/* setting `xLabels` is recommended when using xLabelFormat */
xLabels: 'day'
}); });
</pre> </pre>
</body> </body>

292
lib/morris.grid.coffee Normal file
View File

@ -0,0 +1,292 @@
class Morris.Grid extends Morris.EventEmitter
# A generic pair of axes for line/area/bar charts.
#
# Draws grid lines and axis labels.
#
constructor: (options) ->
# find the container to draw the graph in
if typeof options.element is 'string'
@el = $ document.getElementById(options.element)
else
@el = $ options.element
if @el is null or @el.length == 0
throw new Error("Graph container element not found")
@options = $.extend {}, @gridDefaults, (@defaults || {}), options
# bail if there's no data
if @options.data is undefined or @options.data.length is 0
return
# backwards compatibility for units -> postUnits
if typeof @options.units is 'string'
@options.postUnits = options.units
# the raphael drawing instance
@r = new Raphael(@el[0])
# some redraw stuff
@elementWidth = null
@elementHeight = null
@dirty = false
# more stuff
@init() if @init
# load data
@setData(@options.data)
# Default options
#
gridDefaults:
dateFormat: null
gridLineColor: '#aaa'
gridStrokeWidth: 0.5
gridTextColor: '#888'
gridTextSize: 12
numLines: 5
padding: 25
parseTime: true
postUnits: ''
preUnits: ''
ymax: 'auto'
ymin: 'auto 0'
# Update the data series and redraw the chart.
#
setData: (data, redraw = true) ->
# shallow copy data
@options.data = data.slice()
if @parseTime
@options.data = @options.data.sort (a, b) =>
(a[@options.xkey] < b[@options.xkey]) - (b[@options.xkey] < a[@options.xkey])
# extract series data
@series = []
for ykey in @options.ykeys
seriesData = []
for d in @options.data
y = d[ykey]
seriesData.push switch typeof y
when 'number' then y
when 'string' then parseFloat y
else null
@series.push seriesData
# extract labels / x values
@columnLabels = $.map @options.data, (d) => d[@options.xkey]
if @options.parseTime
@xvals = $.map @columnLabels, (x) -> Morris.parseDate x
if @options.dateFormat
@columnLabels = $.map @xvals, (d) => @options.dateFormat d
else
@columnLabels = $.map @columnLabels, (d) =>
# default formatter for numeric timestamp labels
if typeof d is 'number' then new Date(d).toString() else d
else
@xvals = [0...@columnLabels.length]
# calculate horizontal range of the graph
@xmin = Math.min.apply null, @xvals
@xmax = Math.max.apply null, @xvals
if @xmin is @xmax
@xmin -= 1
@xmax += 1
# Compute the vertical range of the graph if desired
if typeof @options.ymax is 'string' and @options.ymax[0..3] is 'auto'
# use Array.concat to flatten arrays and find the max y value
ymax = Math.max.apply null, Array.prototype.concat.apply([], @series)
if @options.ymax.length > 5
@ymax = Math.max parseInt(@options.ymax[5..], 10), ymax
else
@ymax = ymax
else if typeof @options.ymax is 'string'
@ymax = parseInt(@options.ymax, 10)
else
@ymax = @options.ymax
if typeof @options.ymin is 'string' and @options.ymin[0..3] is 'auto'
ymin = Math.min.apply null, Array.prototype.concat.apply([], @series)
if @options.ymin.length > 5
@ymin = Math.min parseInt(@options.ymin[5..], 10), ymin
else
@ymin = ymin
else if typeof @options.ymin is 'string'
@ymin = parseInt(@options.ymin, 10)
else
@ymin = @options.ymin
if @ymin is @ymax
if @ymin is not 0 then @ymin -= 1
@ymax += 1
@yInterval = (@ymax - @ymin) / (@options.numLines - 1)
if @yInterval > 0 and @yInterval < 1
@precision = -Math.floor(Math.log(@yInterval) / Math.log(10))
else
@precision = 0
@dirty = true
@redraw() if redraw
_calc: ->
w = @el.width()
h = @el.height()
if @elementWidth != w or @elementHeight != h or @dirty
@elementWidth = w
@elementHeight = h
@dirty = false
# calculate grid dimensions
maxYLabelWidth = Math.max(
@measureText(@yAxisFormat(@ymin), @options.gridTextSize).width,
@measureText(@yAxisFormat(@ymax), @options.gridTextSize).width)
@left = maxYLabelWidth + @options.padding
@right = @elementWidth - @options.padding
@top = @options.padding
@bottom = @elementHeight - @options.padding - 1.5 * @options.gridTextSize
@width = @right - @left
@height = @bottom - @top
@dx = @width / (@xmax - @xmin)
@dy = @height / (@ymax - @ymin)
@calc() if @calc
# Quick translation helpers
#
transY: (y) -> @bottom - (y - @ymin) * @dy
transX: (x) ->
if @xvals.length == 1
(@left + @right) / 2
else
@left + (x - @xmin) * @dx
# Draw it!
#
# If you need to re-size your charts, call this method after changing the
# size of the container element.
redraw: ->
@r.clear()
@_calc()
@drawGrid()
@draw() if @draw
# draw y axis labels, horizontal lines
#
drawGrid: ->
firstY = @ymin
lastY = @ymax
for lineY in [firstY..lastY] by @yInterval
v = parseFloat(lineY.toFixed(@precision))
y = @transY(v)
@r.text(@left - @options.padding / 2, y, @yAxisFormat(v))
.attr('font-size', @options.gridTextSize)
.attr('fill', @options.gridTextColor)
.attr('text-anchor', 'end')
@r.path("M#{@left},#{y}H#{@left + @width}")
.attr('stroke', @options.gridLineColor)
.attr('stroke-width', @options.gridStrokeWidth)
# @private
#
measureText: (text, fontSize = 12) ->
tt = @r.text(100, 100, text).attr('font-size', fontSize)
ret = tt.getBBox()
tt.remove()
ret
# @private
#
yAxisFormat: (label) -> @yLabelFormat(label)
# @private
#
yLabelFormat: (label) ->
"#{@options.preUnits}#{Morris.commas(label)}#{@options.postUnits}"
# Parse a date into a javascript timestamp
#
#
Morris.parseDate = (date) ->
if typeof date is 'number'
return date
m = date.match /^(\d+) Q(\d)$/
n = date.match /^(\d+)-(\d+)$/
o = date.match /^(\d+)-(\d+)-(\d+)$/
p = date.match /^(\d+) W(\d+)$/
q = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+)(Z|([+-])(\d\d):?(\d\d))?$/
r = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+):(\d+(\.\d+)?)(Z|([+-])(\d\d):?(\d\d))?$/
if m
new Date(
parseInt(m[1], 10),
parseInt(m[2], 10) * 3 - 1,
1).getTime()
else if n
new Date(
parseInt(n[1], 10),
parseInt(n[2], 10) - 1,
1).getTime()
else if o
new Date(
parseInt(o[1], 10),
parseInt(o[2], 10) - 1,
parseInt(o[3], 10)).getTime()
else if p
# calculate number of weeks in year given
ret = new Date(parseInt(p[1], 10), 0, 1);
# first thursday in year (ISO 8601 standard)
if ret.getDay() isnt 4
ret.setMonth(0, 1 + ((4 - ret.getDay()) + 7) % 7);
# add weeks
ret.getTime() + parseInt(p[2], 10) * 604800000
else if q
if not q[6]
# no timezone info, use local
new Date(
parseInt(q[1], 10),
parseInt(q[2], 10) - 1,
parseInt(q[3], 10),
parseInt(q[4], 10),
parseInt(q[5], 10)).getTime()
else
# timezone info supplied, use UTC
offsetmins = 0
if q[6] != 'Z'
offsetmins = parseInt(q[8], 10) * 60 + parseInt(q[9], 10)
offsetmins = 0 - offsetmins if q[7] == '+'
Date.UTC(
parseInt(q[1], 10),
parseInt(q[2], 10) - 1,
parseInt(q[3], 10),
parseInt(q[4], 10),
parseInt(q[5], 10) + offsetmins)
else if r
secs = parseFloat(r[6])
isecs = Math.floor(secs)
msecs = Math.round((secs - isecs) * 1000)
if not r[8]
# no timezone info, use local
new Date(
parseInt(r[1], 10),
parseInt(r[2], 10) - 1,
parseInt(r[3], 10),
parseInt(r[4], 10),
parseInt(r[5], 10),
isecs,
msecs).getTime()
else
# timezone info supplied, use UTC
offsetmins = 0
if r[8] != 'Z'
offsetmins = parseInt(r[10], 10) * 60 + parseInt(r[11], 10)
offsetmins = 0 - offsetmins if r[9] == '+'
Date.UTC(
parseInt(r[1], 10),
parseInt(r[2], 10) - 1,
parseInt(r[3], 10),
parseInt(r[4], 10),
parseInt(r[5], 10) + offsetmins,
isecs,
msecs)
else
new Date(parseInt(date, 10), 0, 1).getTime()

View File

@ -1,35 +1,14 @@
class Morris.Line class Morris.Line extends Morris.Grid
# Initialise the graph. # Initialise the graph.
# #
constructor: (options) -> constructor: (options) ->
if not (this instanceof Morris.Line) return new Morris.Line(options) unless (@ instanceof Morris.Line)
return new Morris.Line(options) super(options)
if typeof options.element is 'string'
@el = $ document.getElementById(options.element)
else
@el = $ options.element
if @el == null || @el.length == 0
throw new Error("Graph placeholder not found.")
@options = $.extend {}, @defaults, options
# backwards compatibility for units -> postUnits
if typeof @options.units is 'string'
@options.postUnits = options.units
# bail if there's no data
if @options.data is undefined or @options.data.length is 0
return
@el.addClass 'graph-initialised'
# the raphael drawing instance
@r = new Raphael(@el[0])
init: ->
# Some instance variables for later # Some instance variables for later
@pointGrow = Raphael.animation r: @options.pointSize + 3, 25, 'linear' @pointGrow = Raphael.animation r: @options.pointSize + 3, 25, 'linear'
@pointShrink = Raphael.animation r: @options.pointSize, 25, 'linear' @pointShrink = Raphael.animation r: @options.pointSize, 25, 'linear'
@elementWidth = null
@elementHeight = null
@dirty = false
# column hilight events # column hilight events
@prevHilight = null @prevHilight = null
@el.mousemove (evt) => @el.mousemove (evt) =>
@ -45,9 +24,6 @@ class Morris.Line
@el.bind 'touchmove', touchHandler @el.bind 'touchmove', touchHandler
@el.bind 'touchend', touchHandler @el.bind 'touchend', touchHandler
@seriesLabels = @options.labels
@setData(@options.data)
# Default configuration # Default configuration
# #
defaults: defaults:
@ -65,17 +41,6 @@ class Morris.Line
pointWidths: [1] pointWidths: [1]
pointStrokeColors: ['#ffffff'] pointStrokeColors: ['#ffffff']
pointFillColors: [] pointFillColors: []
ymax: 'auto'
ymin: 'auto 0'
marginTop: 25
marginRight: 25
marginBottom: 30
marginLeft: 25
numLines: 5
gridLineColor: '#aaa'
gridTextColor: '#888'
gridTextSize: 12
gridStrokeWidth: 0.5
hoverPaddingX: 10 hoverPaddingX: 10
hoverPaddingY: 5 hoverPaddingY: 5
hoverMargin: 10 hoverMargin: 10
@ -87,176 +52,41 @@ class Morris.Line
hoverFontSize: 12 hoverFontSize: 12
smooth: true smooth: true
hideHover: false hideHover: false
parseTime: true
preUnits: ''
postUnits: ''
dateFormat: null
xLabels: 'auto' xLabels: 'auto'
xLabelFormat: null xLabelFormat: null
# Update the data series and redraw the chart.
#
setData: (data, redraw = true) ->
# shallow copy & sort data (if required)
@options.data = data.slice(0)
if @options.parseTime
@options.data.sort (a, b) =>
(a[@options.xkey] < b[@options.xkey]) - (b[@options.xkey] < a[@options.xkey])
else
@options.data.reverse()
# extract series data
@series = []
for ykey in @options.ykeys
series_data = []
for d in @options.data
series_data.push switch typeof d[ykey]
when 'number' then d[ykey]
when 'string' then parseFloat(d[ykey])
else null
@series.push(series_data)
# extract labels
@columnLabels = $.map @options.data, (d) => d[@options.xkey]
# translate x labels into nominal dates
if @options.parseTime
@xvals = $.map @columnLabels, (x) -> Morris.parseDate x
else
@xvals = [(@columnLabels.length-1)..0]
# format column labels
if @options.parseTime
if @options.dateFormat
@columnLabels = $.map @xvals, (d) => @options.dateFormat(d)
else
@columnLabels = $.map @columnLabels, (d) =>
# default formatter for numeric timestamp labels
if typeof d is 'number' then new Date(d).toString() else d
# calculate horizontal range of the graph
@xmin = Math.min.apply null, @xvals
@xmax = Math.max.apply null, @xvals
if @xmin is @xmax
@xmin -= 1
@xmax += 1
# Compute the vertical range of the graph if desired
if typeof @options.ymax is 'string' and @options.ymax[0..3] is 'auto'
# use Array.concat to flatten arrays and find the max y value
ymax = Math.max.apply null, Array.prototype.concat.apply([], @series)
if @options.ymax.length > 5
@ymax = Math.max parseInt(@options.ymax[5..], 10), ymax
else
@ymax = ymax
else if typeof @options.ymax is 'string'
@ymax = parseInt(@options.ymax, 10)
else
@ymax = @options.ymax
if typeof @options.ymin is 'string' and @options.ymin[0..3] is 'auto'
ymin = Math.min.apply null, Array.prototype.concat.apply([], @series)
if @options.ymin.length > 5
@ymin = Math.min parseInt(@options.ymin[5..], 10), ymin
else
@ymin = ymin
else if typeof @options.ymin is 'string'
@ymin = parseInt(@options.ymin, 10)
else
@ymin = @options.ymin
if @ymin is @ymax
if @ymin is not 0 then @ymin -= 1
@ymax += 1
@yInterval = (@ymax - @ymin) / (@options.numLines - 1)
if @yInterval > 0 and @yInterval < 1
@precision = -Math.floor(Math.log(@yInterval) / Math.log(10))
else
@precision = 0
@dirty = true
@redraw() if redraw
# Do any size-related calculations # Do any size-related calculations
# #
# @private # @private
calc: -> calc: ->
w = @el.width() # calculate series data point coordinates
h = @el.height() @columns = (@transX(x) for x in @xvals)
@seriesCoords = []
for s in @series
scoords = []
$.each s, (i, y) =>
if y == null
scoords.push(null)
else
scoords.push(x: @columns[i], y: @transY(y))
@seriesCoords.push(scoords)
# calculate hover margins
@hoverMargins = $.map @columns.slice(1), (x, i) => (x + @columns[i]) / 2
if @elementWidth != w or @elementHeight != h or @dirty # Draws the line chart.
@elementWidth = w
@elementHeight = h
@dirty = false
# calculate grid dimensions
@maxYLabelWidth = Math.max(
@measureText(@yAxisFormat(@ymin), @options.gridTextSize).width,
@measureText(@yAxisFormat(@ymax), @options.gridTextSize).width)
@left = @maxYLabelWidth + @options.marginLeft
@width = @el.width() - @left - @options.marginRight
@height = @el.height() - @options.marginTop - @options.marginBottom
@dx = @width / (@xmax - @xmin)
@dy = @height / (@ymax - @ymin)
# calculate series data point coordinates
@columns = (@transX(x) for x in @xvals)
@seriesCoords = []
for s in @series
scoords = []
$.each s, (i, y) =>
if y == null
scoords.push(null)
else
scoords.push(x: @columns[i], y: @transY(y))
@seriesCoords.push(scoords)
# calculate hover margins
@hoverMargins = $.map @columns.slice(1), (x, i) => (x + @columns[i]) / 2
# quick translation helpers
# #
# @private draw: ->
transX: (x) => @drawXAxis()
if @xvals.length is 1
@left + @width / 2
else
@left + (x - @xmin) * @dx
# @private
transY: (y) =>
return @options.marginTop + @height - (y - @ymin) * @dy
# Clear and redraw the chart.
#
# If you need to re-size your charts, call this method after changing the
# size of the container element.
redraw: ->
@r.clear()
@calc()
@drawGrid()
@drawSeries() @drawSeries()
@drawHover() @drawHover()
@hilight(if @options.hideHover then null else 0) @hilight(if @options.hideHover then null else 0)
# draw the grid, and axes labels # draw the x-axis labels
# #
# @private # @private
drawGrid: -> drawXAxis: ->
# draw y axis labels, horizontal lines # draw x axis labels
firstY = @ymin ypos = @bottom + @options.gridTextSize * 1.25
lastY = @ymax
for lineY in [firstY..lastY] by @yInterval
v = parseFloat(lineY.toFixed(@precision))
y = @transY(v)
@r.text(@left - @options.marginLeft/2, y, @yAxisFormat(v))
.attr('font-size', @options.gridTextSize)
.attr('fill', @options.gridTextColor)
.attr('text-anchor', 'end')
@r.path("M#{@left},#{y}H#{@left + @width}")
.attr('stroke', @options.gridLineColor)
.attr('stroke-width', @options.gridStrokeWidth)
## draw x axis labels
ypos = @options.marginTop + @height + @options.marginBottom / 2
xLabelMargin = 50 # make this an option? xLabelMargin = 50 # make this an option?
prevLabelMargin = null prevLabelMargin = null
drawLabel = (labelText, xpos) => drawLabel = (labelText, xpos) =>
@ -290,11 +120,11 @@ class Morris.Line
# @private # @private
drawSeries: -> drawSeries: ->
for i in [@seriesCoords.length-1..0] for i in [@seriesCoords.length-1..0]
coords = $.map(@seriesCoords[i], (c) -> c) coords = @seriesCoords[i]
smooth = @options.smooth is true or smooth = @options.smooth is true or
$.inArray(@options.ykeys[i], @options.smooth) > -1 $.inArray(@options.ykeys[i], @options.smooth) > -1
if coords.length > 1 if coords.length > 1
path = @createPath coords, @options.marginTop + @height, smooth path = @createPath coords, @bottom, smooth
@r.path(path) @r.path(path)
.attr('stroke', @colorForSeries(i)) .attr('stroke', @colorForSeries(i))
.attr('stroke-width', @options.lineWidth) .attr('stroke-width', @options.lineWidth)
@ -313,7 +143,7 @@ class Morris.Line
# create a path for a data series # create a path for a data series
# #
# @private # @private
createPath: (coords, bottom, smooth) -> createPath: (coords, smooth) ->
path = "" path = ""
if smooth if smooth
grads = @gradients coords grads = @gradients coords
@ -327,9 +157,9 @@ class Morris.Line
lg = grads[i - 1] lg = grads[i - 1]
ix = (c.x - lc.x) / 4 ix = (c.x - lc.x) / 4
x1 = lc.x + ix x1 = lc.x + ix
y1 = Math.min(bottom, lc.y + ix * lg) y1 = Math.min(@bottom, lc.y + ix * lg)
x2 = c.x - ix x2 = c.x - ix
y2 = Math.min(bottom, c.y - ix * g) y2 = Math.min(@bottom, c.y - ix * g)
path += "C#{x1},#{y1},#{x2},#{y2},#{c.x},#{c.y}" path += "C#{x1},#{y1},#{x2},#{y2},#{c.x},#{c.y}"
else else
path = "M" + $.map(coords, (c) -> "#{c.x},#{c.y}").join("L") path = "M" + $.map(coords, (c) -> "#{c.x},#{c.y}").join("L")
@ -378,7 +208,7 @@ class Morris.Line
@hoverSet.show() @hoverSet.show()
@xLabel.attr('text', @columnLabels[index]) @xLabel.attr('text', @columnLabels[index])
for i in [0..@series.length-1] for i in [0..@series.length-1]
@yLabels[i].attr('text', "#{@seriesLabels[i]}: #{@yLabelFormat(@series[i][index])}") @yLabels[i].attr('text', "#{@options.labels[i]}: #{@yLabelFormat(@series[i][index])}")
# recalculate hover box width # recalculate hover box width
maxLabelWidth = Math.max.apply null, $.map @yLabels, (l) -> maxLabelWidth = Math.max.apply null, $.map @yLabels, (l) ->
l.getBBox().width l.getBBox().width
@ -388,13 +218,13 @@ class Morris.Line
# move to y pos # move to y pos
yloc = Math.min.apply null, $.map @series, (s) => yloc = Math.min.apply null, $.map @series, (s) =>
@transY s[index] @transY s[index]
if yloc > @hoverHeight + @options.hoverPaddingY * 2 + @options.hoverMargin + @options.marginTop if yloc > @hoverHeight + @options.hoverPaddingY * 2 + @options.hoverMargin + @top
yloc = yloc - @hoverHeight / 2 - @options.hoverPaddingY - @options.hoverMargin yloc = yloc - @hoverHeight / 2 - @options.hoverPaddingY - @options.hoverMargin
else else
yloc = yloc + @hoverHeight / 2 + @options.hoverPaddingY + @options.hoverMargin yloc = yloc + @hoverHeight / 2 + @options.hoverPaddingY + @options.hoverMargin
yloc = Math.max @options.marginTop + @hoverHeight / 2 + @options.hoverPaddingY, yloc yloc = Math.max @top + @hoverHeight / 2 + @options.hoverPaddingY, yloc
yloc = Math.min @options.marginTop + @height - @hoverHeight / 2 - @options.hoverPaddingY, yloc yloc = Math.min @bottom - @hoverHeight / 2 - @options.hoverPaddingY, yloc
xloc = Math.min @left + @width - maxLabelWidth / 2 - @options.hoverPaddingX, @columns[index] xloc = Math.min @right - maxLabelWidth / 2 - @options.hoverPaddingX, @columns[index]
xloc = Math.max @left + maxLabelWidth / 2 + @options.hoverPaddingX, xloc xloc = Math.max @left + maxLabelWidth / 2 + @options.hoverPaddingX, xloc
@hoverSet.attr 'transform', "t#{xloc},#{yloc}" @hoverSet.attr 'transform', "t#{xloc},#{yloc}"
@ -425,21 +255,6 @@ class Morris.Line
@hilight hoverIndex @hilight hoverIndex
break break
# @private
measureText: (text, fontSize = 12) ->
tt = @r.text(100, 100, text).attr('font-size', fontSize)
ret = tt.getBBox()
tt.remove()
return ret
# @private
yAxisFormat: (label) ->
@yLabelFormat(label)
# @private
yLabelFormat: (label) ->
"#{@options.preUnits}#{Morris.commas(label)}#{@options.postUnits}"
# @private # @private
colorForSeries: (index) -> colorForSeries: (index) ->
@options.lineColors[index % @options.lineColors.length] @options.lineColors[index % @options.lineColors.length]
@ -457,93 +272,6 @@ class Morris.Line
@options.pointFillColors[index % @options.pointFillColors.length] @options.pointFillColors[index % @options.pointFillColors.length]
# Parse a date into a javascript timestamp
#
#
Morris.parseDate = (date) ->
if typeof date is 'number'
return date
m = date.match /^(\d+) Q(\d)$/
n = date.match /^(\d+)-(\d+)$/
o = date.match /^(\d+)-(\d+)-(\d+)$/
p = date.match /^(\d+) W(\d+)$/
q = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+)(Z|([+-])(\d\d):?(\d\d))?$/
r = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+):(\d+(\.\d+)?)(Z|([+-])(\d\d):?(\d\d))?$/
if m
new Date(
parseInt(m[1], 10),
parseInt(m[2], 10) * 3 - 1,
1).getTime()
else if n
new Date(
parseInt(n[1], 10),
parseInt(n[2], 10) - 1,
1).getTime()
else if o
new Date(
parseInt(o[1], 10),
parseInt(o[2], 10) - 1,
parseInt(o[3], 10)).getTime()
else if p
# calculate number of weeks in year given
ret = new Date(parseInt(p[1], 10), 0, 1);
# first thursday in year (ISO 8601 standard)
if ret.getDay() isnt 4
ret.setMonth(0, 1 + ((4 - ret.getDay()) + 7) % 7);
# add weeks
ret.getTime() + parseInt(p[2], 10) * 604800000
else if q
if not q[6]
# no timezone info, use local
new Date(
parseInt(q[1], 10),
parseInt(q[2], 10) - 1,
parseInt(q[3], 10),
parseInt(q[4], 10),
parseInt(q[5], 10)).getTime()
else
# timezone info supplied, use UTC
offsetmins = 0
if q[6] != 'Z'
offsetmins = parseInt(q[8], 10) * 60 + parseInt(q[9], 10)
offsetmins = 0 - offsetmins if q[7] == '+'
Date.UTC(
parseInt(q[1], 10),
parseInt(q[2], 10) - 1,
parseInt(q[3], 10),
parseInt(q[4], 10),
parseInt(q[5], 10) + offsetmins)
else if r
secs = parseFloat(r[6])
isecs = Math.floor(secs)
msecs = Math.round((secs - isecs) * 1000)
if not r[8]
# no timezone info, use local
new Date(
parseInt(r[1], 10),
parseInt(r[2], 10) - 1,
parseInt(r[3], 10),
parseInt(r[4], 10),
parseInt(r[5], 10),
isecs,
msecs).getTime()
else
# timezone info supplied, use UTC
offsetmins = 0
if r[8] != 'Z'
offsetmins = parseInt(r[10], 10) * 60 + parseInt(r[11], 10)
offsetmins = 0 - offsetmins if r[9] == '+'
Date.UTC(
parseInt(r[1], 10),
parseInt(r[2], 10) - 1,
parseInt(r[3], 10),
parseInt(r[4], 10),
parseInt(r[5], 10) + offsetmins,
isecs,
msecs)
else
new Date(parseInt(date, 10), 0, 1).getTime()
# generate a series of label, timestamp pairs for x-axis labels # generate a series of label, timestamp pairs for x-axis labels
# #
# @private # @private

777
morris.js
View File

@ -283,143 +283,84 @@
})(Morris.EventEmitter); })(Morris.EventEmitter);
Morris.Line = (function() { Morris.Grid = (function(_super) {
function Line(options) { __extends(Grid, _super);
this.updateHilight = __bind(this.updateHilight, this);
this.hilight = __bind(this.hilight, this); function Grid(options) {
this.updateHover = __bind(this.updateHover, this);
this.transY = __bind(this.transY, this);
this.transX = __bind(this.transX, this);
var touchHandler,
_this = this;
if (!(this instanceof Morris.Line)) {
return new Morris.Line(options);
}
if (typeof options.element === 'string') { if (typeof options.element === 'string') {
this.el = $(document.getElementById(options.element)); this.el = $(document.getElementById(options.element));
} else { } else {
this.el = $(options.element); this.el = $(options.element);
} }
if (this.el === null || this.el.length === 0) { if (this.el === null || this.el.length === 0) {
throw new Error("Graph placeholder not found."); throw new Error("Graph container element not found");
}
this.options = $.extend({}, this.defaults, options);
if (typeof this.options.units === 'string') {
this.options.postUnits = options.units;
} }
this.options = $.extend({}, this.gridDefaults, this.defaults || {}, options);
if (this.options.data === void 0 || this.options.data.length === 0) { if (this.options.data === void 0 || this.options.data.length === 0) {
return; return;
} }
this.el.addClass('graph-initialised'); if (typeof this.options.units === 'string') {
this.options.postUnits = options.units;
}
this.r = new Raphael(this.el[0]); this.r = new Raphael(this.el[0]);
this.pointGrow = Raphael.animation({
r: this.options.pointSize + 3
}, 25, 'linear');
this.pointShrink = Raphael.animation({
r: this.options.pointSize
}, 25, 'linear');
this.elementWidth = null; this.elementWidth = null;
this.elementHeight = null; this.elementHeight = null;
this.dirty = false; this.dirty = false;
this.prevHilight = null; if (this.init) {
this.el.mousemove(function(evt) { this.init();
return _this.updateHilight(evt.pageX);
});
if (this.options.hideHover) {
this.el.mouseout(function(evt) {
return _this.hilight(null);
});
} }
touchHandler = function(evt) {
var touch;
touch = evt.originalEvent.touches[0] || evt.originalEvent.changedTouches[0];
_this.updateHilight(touch.pageX);
return touch;
};
this.el.bind('touchstart', touchHandler);
this.el.bind('touchmove', touchHandler);
this.el.bind('touchend', touchHandler);
this.seriesLabels = this.options.labels;
this.setData(this.options.data); this.setData(this.options.data);
} }
Line.prototype.defaults = { Grid.prototype.gridDefaults = {
lineWidth: 3, dateFormat: null,
pointSize: 4,
lineColors: ['#0b62a4', '#7A92A3', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed'],
pointWidths: [1],
pointStrokeColors: ['#ffffff'],
pointFillColors: [],
ymax: 'auto',
ymin: 'auto 0',
marginTop: 25,
marginRight: 25,
marginBottom: 30,
marginLeft: 25,
numLines: 5,
gridLineColor: '#aaa', gridLineColor: '#aaa',
gridStrokeWidth: 0.5,
gridTextColor: '#888', gridTextColor: '#888',
gridTextSize: 12, gridTextSize: 12,
gridStrokeWidth: 0.5, numLines: 5,
hoverPaddingX: 10, padding: 25,
hoverPaddingY: 5,
hoverMargin: 10,
hoverFillColor: '#fff',
hoverBorderColor: '#ccc',
hoverBorderWidth: 2,
hoverOpacity: 0.95,
hoverLabelColor: '#444',
hoverFontSize: 12,
smooth: true,
hideHover: false,
parseTime: true, parseTime: true,
preUnits: '',
postUnits: '', postUnits: '',
dateFormat: null, preUnits: '',
xLabels: 'auto', ymax: 'auto',
xLabelFormat: null ymin: 'auto 0'
}; };
Line.prototype.setData = function(data, redraw) { Grid.prototype.setData = function(data, redraw) {
var d, series_data, ykey, ymax, ymin, _i, _j, _k, _len, _len1, _ref, _ref1, _ref2, _results, var d, seriesData, y, ykey, ymax, ymin, _i, _j, _k, _len, _len1, _ref, _ref1, _ref2, _results,
_this = this; _this = this;
if (redraw == null) { if (redraw == null) {
redraw = true; redraw = true;
} }
this.options.data = data.slice(0); this.options.data = data.slice();
if (this.options.parseTime) { if (this.parseTime) {
this.options.data.sort(function(a, b) { this.options.data = this.options.data.sort(function(a, b) {
return (a[_this.options.xkey] < b[_this.options.xkey]) - (b[_this.options.xkey] < a[_this.options.xkey]); return (a[_this.options.xkey] < b[_this.options.xkey]) - (b[_this.options.xkey] < a[_this.options.xkey]);
}); });
} else {
this.options.data.reverse();
} }
this.series = []; this.series = [];
_ref = this.options.ykeys; _ref = this.options.ykeys;
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
ykey = _ref[_i]; ykey = _ref[_i];
series_data = []; seriesData = [];
_ref1 = this.options.data; _ref1 = this.options.data;
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
d = _ref1[_j]; d = _ref1[_j];
series_data.push((function() { y = d[ykey];
switch (typeof d[ykey]) { seriesData.push((function() {
switch (typeof y) {
case 'number': case 'number':
return d[ykey]; return y;
case 'string': case 'string':
return parseFloat(d[ykey]); return parseFloat(y);
default: default:
return null; return null;
} }
})()); })());
} }
this.series.push(series_data); this.series.push(seriesData);
} }
this.columnLabels = $.map(this.options.data, function(d) { this.columnLabels = $.map(this.options.data, function(d) {
return d[_this.options.xkey]; return d[_this.options.xkey];
@ -428,14 +369,6 @@
this.xvals = $.map(this.columnLabels, function(x) { this.xvals = $.map(this.columnLabels, function(x) {
return Morris.parseDate(x); return Morris.parseDate(x);
}); });
} else {
this.xvals = (function() {
_results = [];
for (var _k = _ref2 = this.columnLabels.length - 1; _ref2 <= 0 ? _k <= 0 : _k >= 0; _ref2 <= 0 ? _k++ : _k--){ _results.push(_k); }
return _results;
}).apply(this);
}
if (this.options.parseTime) {
if (this.options.dateFormat) { if (this.options.dateFormat) {
this.columnLabels = $.map(this.xvals, function(d) { this.columnLabels = $.map(this.xvals, function(d) {
return _this.options.dateFormat(d); return _this.options.dateFormat(d);
@ -449,6 +382,12 @@
} }
}); });
} }
} else {
this.xvals = (function() {
_results = [];
for (var _k = 0, _ref2 = this.columnLabels.length; 0 <= _ref2 ? _k < _ref2 : _k > _ref2; 0 <= _ref2 ? _k++ : _k--){ _results.push(_k); }
return _results;
}).apply(this);
} }
this.xmin = Math.min.apply(null, this.xvals); this.xmin = Math.min.apply(null, this.xvals);
this.xmax = Math.max.apply(null, this.xvals); this.xmax = Math.max.apply(null, this.xvals);
@ -498,293 +437,65 @@
} }
}; };
Line.prototype.calc = function() { Grid.prototype._calc = function() {
var h, s, scoords, w, x, _i, _len, _ref, var h, maxYLabelWidth, w;
_this = this;
w = this.el.width(); w = this.el.width();
h = this.el.height(); h = this.el.height();
if (this.elementWidth !== w || this.elementHeight !== h || this.dirty) { if (this.elementWidth !== w || this.elementHeight !== h || this.dirty) {
this.elementWidth = w; this.elementWidth = w;
this.elementHeight = h; this.elementHeight = h;
this.dirty = false; this.dirty = false;
this.maxYLabelWidth = Math.max(this.measureText(this.yAxisFormat(this.ymin), this.options.gridTextSize).width, this.measureText(this.yAxisFormat(this.ymax), this.options.gridTextSize).width); maxYLabelWidth = Math.max(this.measureText(this.yAxisFormat(this.ymin), this.options.gridTextSize).width, this.measureText(this.yAxisFormat(this.ymax), this.options.gridTextSize).width);
this.left = this.maxYLabelWidth + this.options.marginLeft; this.left = maxYLabelWidth + this.options.padding;
this.width = this.el.width() - this.left - this.options.marginRight; this.right = this.elementWidth - this.options.padding;
this.height = this.el.height() - this.options.marginTop - this.options.marginBottom; this.top = this.options.padding;
this.bottom = this.elementHeight - this.options.padding - 1.5 * this.options.gridTextSize;
this.width = this.right - this.left;
this.height = this.bottom - this.top;
this.dx = this.width / (this.xmax - this.xmin); this.dx = this.width / (this.xmax - this.xmin);
this.dy = this.height / (this.ymax - this.ymin); this.dy = this.height / (this.ymax - this.ymin);
this.columns = (function() { if (this.calc) {
var _i, _len, _ref, _results; return this.calc();
_ref = this.xvals;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
x = _ref[_i];
_results.push(this.transX(x));
}
return _results;
}).call(this);
this.seriesCoords = [];
_ref = this.series;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
s = _ref[_i];
scoords = [];
$.each(s, function(i, y) {
if (y === null) {
return scoords.push(null);
} else {
return scoords.push({
x: _this.columns[i],
y: _this.transY(y)
});
}
});
this.seriesCoords.push(scoords);
} }
return this.hoverMargins = $.map(this.columns.slice(1), function(x, i) {
return (x + _this.columns[i]) / 2;
});
} }
}; };
Line.prototype.transX = function(x) { Grid.prototype.transY = function(y) {
return this.bottom - (y - this.ymin) * this.dy;
};
Grid.prototype.transX = function(x) {
if (this.xvals.length === 1) { if (this.xvals.length === 1) {
return this.left + this.width / 2; return (this.left + this.right) / 2;
} else { } else {
return this.left + (x - this.xmin) * this.dx; return this.left + (x - this.xmin) * this.dx;
} }
}; };
Line.prototype.transY = function(y) { Grid.prototype.redraw = function() {
return this.options.marginTop + this.height - (y - this.ymin) * this.dy;
};
Line.prototype.redraw = function() {
this.r.clear(); this.r.clear();
this.calc(); this._calc();
this.drawGrid(); this.drawGrid();
this.drawSeries(); if (this.draw) {
this.drawHover(); return this.draw();
return this.hilight(this.options.hideHover ? null : 0); }
}; };
Line.prototype.drawGrid = function() { Grid.prototype.drawGrid = function() {
var drawLabel, firstY, i, l, labelText, lastY, lineY, prevLabelMargin, v, xLabelMargin, y, ypos, _i, _j, _k, _len, _ref, _ref1, _ref2, _results, _results1, var firstY, lastY, lineY, v, y, _i, _ref, _results;
_this = this;
firstY = this.ymin; firstY = this.ymin;
lastY = this.ymax; lastY = this.ymax;
_results = [];
for (lineY = _i = firstY, _ref = this.yInterval; firstY <= lastY ? _i <= lastY : _i >= lastY; lineY = _i += _ref) { for (lineY = _i = firstY, _ref = this.yInterval; firstY <= lastY ? _i <= lastY : _i >= lastY; lineY = _i += _ref) {
v = parseFloat(lineY.toFixed(this.precision)); v = parseFloat(lineY.toFixed(this.precision));
y = this.transY(v); y = this.transY(v);
this.r.text(this.left - this.options.marginLeft / 2, y, this.yAxisFormat(v)).attr('font-size', this.options.gridTextSize).attr('fill', this.options.gridTextColor).attr('text-anchor', 'end'); this.r.text(this.left - this.options.padding / 2, y, this.yAxisFormat(v)).attr('font-size', this.options.gridTextSize).attr('fill', this.options.gridTextColor).attr('text-anchor', 'end');
this.r.path("M" + this.left + "," + y + "H" + (this.left + this.width)).attr('stroke', this.options.gridLineColor).attr('stroke-width', this.options.gridStrokeWidth); _results.push(this.r.path("M" + this.left + "," + y + "H" + (this.left + this.width)).attr('stroke', this.options.gridLineColor).attr('stroke-width', this.options.gridStrokeWidth));
}
ypos = this.options.marginTop + this.height + this.options.marginBottom / 2;
xLabelMargin = 50;
prevLabelMargin = null;
drawLabel = function(labelText, xpos) {
var label, labelBox;
label = _this.r.text(_this.transX(xpos), ypos, labelText).attr('font-size', _this.options.gridTextSize).attr('fill', _this.options.gridTextColor);
labelBox = label.getBBox();
if ((prevLabelMargin === null || prevLabelMargin <= labelBox.x) && labelBox.x >= 0 && (labelBox.x + labelBox.width) < _this.el.width()) {
return prevLabelMargin = labelBox.x + labelBox.width + xLabelMargin;
} else {
return label.remove();
}
};
if (this.options.parseTime) {
if (this.columnLabels.length === 1 && this.options.xLabels === 'auto') {
return drawLabel(this.columnLabels[0], this.xvals[0]);
} else {
_ref1 = Morris.labelSeries(this.xmin, this.xmax, this.width, this.options.xLabels, this.options.xLabelFormat);
_results = [];
for (_j = 0, _len = _ref1.length; _j < _len; _j++) {
l = _ref1[_j];
_results.push(drawLabel(l[0], l[1]));
}
return _results;
}
} else {
_results1 = [];
for (i = _k = 0, _ref2 = this.columnLabels.length; 0 <= _ref2 ? _k <= _ref2 : _k >= _ref2; i = 0 <= _ref2 ? ++_k : --_k) {
labelText = this.columnLabels[this.columnLabels.length - i - 1];
_results1.push(drawLabel(labelText, i));
}
return _results1;
}
};
Line.prototype.drawSeries = function() {
var c, circle, coords, i, path, smooth, _i, _j, _ref, _ref1, _results;
for (i = _i = _ref = this.seriesCoords.length - 1; _ref <= 0 ? _i <= 0 : _i >= 0; i = _ref <= 0 ? ++_i : --_i) {
coords = $.map(this.seriesCoords[i], function(c) {
return c;
});
smooth = this.options.smooth === true || $.inArray(this.options.ykeys[i], this.options.smooth) > -1;
if (coords.length > 1) {
path = this.createPath(coords, this.options.marginTop + this.height, smooth);
this.r.path(path).attr('stroke', this.colorForSeries(i)).attr('stroke-width', this.options.lineWidth);
}
}
this.seriesPoints = (function() {
var _j, _ref1, _results;
_results = [];
for (i = _j = 0, _ref1 = this.seriesCoords.length - 1; 0 <= _ref1 ? _j <= _ref1 : _j >= _ref1; i = 0 <= _ref1 ? ++_j : --_j) {
_results.push([]);
}
return _results;
}).call(this);
_results = [];
for (i = _j = _ref1 = this.seriesCoords.length - 1; _ref1 <= 0 ? _j <= 0 : _j >= 0; i = _ref1 <= 0 ? ++_j : --_j) {
_results.push((function() {
var _k, _len, _ref2, _results1;
_ref2 = this.seriesCoords[i];
_results1 = [];
for (_k = 0, _len = _ref2.length; _k < _len; _k++) {
c = _ref2[_k];
if (c === null) {
circle = null;
} else {
circle = this.r.circle(c.x, c.y, this.options.pointSize).attr('fill', this.pointFillColorForSeries(i) || this.colorForSeries(i)).attr('stroke-width', this.strokeWidthForSeries(i)).attr('stroke', this.strokeForSeries(i));
}
_results1.push(this.seriesPoints[i].push(circle));
}
return _results1;
}).call(this));
} }
return _results; return _results;
}; };
Line.prototype.createPath = function(coords, bottom, smooth) { Grid.prototype.measureText = function(text, fontSize) {
var c, g, grads, i, ix, lc, lg, path, x1, x2, y1, y2, _i, _ref;
path = "";
if (smooth) {
grads = this.gradients(coords);
for (i = _i = 0, _ref = coords.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) {
c = coords[i];
if (i === 0) {
path += "M" + c.x + "," + c.y;
} else {
g = grads[i];
lc = coords[i - 1];
lg = grads[i - 1];
ix = (c.x - lc.x) / 4;
x1 = lc.x + ix;
y1 = Math.min(bottom, lc.y + ix * lg);
x2 = c.x - ix;
y2 = Math.min(bottom, c.y - ix * g);
path += "C" + x1 + "," + y1 + "," + x2 + "," + y2 + "," + c.x + "," + c.y;
}
}
} else {
path = "M" + $.map(coords, function(c) {
return "" + c.x + "," + c.y;
}).join("L");
}
return path;
};
Line.prototype.gradients = function(coords) {
return $.map(coords, function(c, i) {
if (i === 0) {
return (coords[1].y - c.y) / (coords[1].x - c.x);
} else if (i === (coords.length - 1)) {
return (c.y - coords[i - 1].y) / (c.x - coords[i - 1].x);
} else {
return (coords[i + 1].y - coords[i - 1].y) / (coords[i + 1].x - coords[i - 1].x);
}
});
};
Line.prototype.drawHover = function() {
var i, yLabel, _i, _ref, _results;
this.hoverHeight = this.options.hoverFontSize * 1.5 * (this.series.length + 1);
this.hover = this.r.rect(-10, -this.hoverHeight / 2 - this.options.hoverPaddingY, 20, this.hoverHeight + this.options.hoverPaddingY * 2, 10).attr('fill', this.options.hoverFillColor).attr('stroke', this.options.hoverBorderColor).attr('stroke-width', this.options.hoverBorderWidth).attr('opacity', this.options.hoverOpacity);
this.xLabel = this.r.text(0, (this.options.hoverFontSize * 0.75) - this.hoverHeight / 2, '').attr('fill', this.options.hoverLabelColor).attr('font-weight', 'bold').attr('font-size', this.options.hoverFontSize);
this.hoverSet = this.r.set();
this.hoverSet.push(this.hover);
this.hoverSet.push(this.xLabel);
this.yLabels = [];
_results = [];
for (i = _i = 0, _ref = this.series.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) {
yLabel = this.r.text(0, this.options.hoverFontSize * 1.5 * (i + 1.5) - this.hoverHeight / 2, '').attr('fill', this.colorForSeries(i)).attr('font-size', this.options.hoverFontSize);
this.yLabels.push(yLabel);
_results.push(this.hoverSet.push(yLabel));
}
return _results;
};
Line.prototype.updateHover = function(index) {
var i, maxLabelWidth, xloc, yloc, _i, _ref,
_this = this;
this.hoverSet.show();
this.xLabel.attr('text', this.columnLabels[index]);
for (i = _i = 0, _ref = this.series.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) {
this.yLabels[i].attr('text', "" + this.seriesLabels[i] + ": " + (this.yLabelFormat(this.series[i][index])));
}
maxLabelWidth = Math.max.apply(null, $.map(this.yLabels, function(l) {
return l.getBBox().width;
}));
maxLabelWidth = Math.max(maxLabelWidth, this.xLabel.getBBox().width);
this.hover.attr('width', maxLabelWidth + this.options.hoverPaddingX * 2);
this.hover.attr('x', -this.options.hoverPaddingX - maxLabelWidth / 2);
yloc = Math.min.apply(null, $.map(this.series, function(s) {
return _this.transY(s[index]);
}));
if (yloc > this.hoverHeight + this.options.hoverPaddingY * 2 + this.options.hoverMargin + this.options.marginTop) {
yloc = yloc - this.hoverHeight / 2 - this.options.hoverPaddingY - this.options.hoverMargin;
} else {
yloc = yloc + this.hoverHeight / 2 + this.options.hoverPaddingY + this.options.hoverMargin;
}
yloc = Math.max(this.options.marginTop + this.hoverHeight / 2 + this.options.hoverPaddingY, yloc);
yloc = Math.min(this.options.marginTop + this.height - this.hoverHeight / 2 - this.options.hoverPaddingY, yloc);
xloc = Math.min(this.left + this.width - maxLabelWidth / 2 - this.options.hoverPaddingX, this.columns[index]);
xloc = Math.max(this.left + maxLabelWidth / 2 + this.options.hoverPaddingX, xloc);
return this.hoverSet.attr('transform', "t" + xloc + "," + yloc);
};
Line.prototype.hideHover = function() {
return this.hoverSet.hide();
};
Line.prototype.hilight = function(index) {
var i, _i, _j, _ref, _ref1;
if (this.prevHilight !== null && this.prevHilight !== index) {
for (i = _i = 0, _ref = this.seriesPoints.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) {
if (this.seriesPoints[i][this.prevHilight]) {
this.seriesPoints[i][this.prevHilight].animate(this.pointShrink);
}
}
}
if (index !== null && this.prevHilight !== index) {
for (i = _j = 0, _ref1 = this.seriesPoints.length - 1; 0 <= _ref1 ? _j <= _ref1 : _j >= _ref1; i = 0 <= _ref1 ? ++_j : --_j) {
if (this.seriesPoints[i][index]) {
this.seriesPoints[i][index].animate(this.pointGrow);
}
}
this.updateHover(index);
}
this.prevHilight = index;
if (index === null) {
return this.hideHover();
}
};
Line.prototype.updateHilight = function(x) {
var hoverIndex, _i, _ref, _results;
x -= this.el.offset().left;
_results = [];
for (hoverIndex = _i = _ref = this.hoverMargins.length; _ref <= 0 ? _i <= 0 : _i >= 0; hoverIndex = _ref <= 0 ? ++_i : --_i) {
if (hoverIndex === 0 || this.hoverMargins[hoverIndex - 1] > x) {
this.hilight(hoverIndex);
break;
} else {
_results.push(void 0);
}
}
return _results;
};
Line.prototype.measureText = function(text, fontSize) {
var ret, tt; var ret, tt;
if (fontSize == null) { if (fontSize == null) {
fontSize = 12; fontSize = 12;
@ -795,33 +506,17 @@
return ret; return ret;
}; };
Line.prototype.yAxisFormat = function(label) { Grid.prototype.yAxisFormat = function(label) {
return this.yLabelFormat(label); return this.yLabelFormat(label);
}; };
Line.prototype.yLabelFormat = function(label) { Grid.prototype.yLabelFormat = function(label) {
return "" + this.options.preUnits + (Morris.commas(label)) + this.options.postUnits; return "" + this.options.preUnits + (Morris.commas(label)) + this.options.postUnits;
}; };
Line.prototype.colorForSeries = function(index) { return Grid;
return this.options.lineColors[index % this.options.lineColors.length];
};
Line.prototype.strokeWidthForSeries = function(index) { })(Morris.EventEmitter);
return this.options.pointWidths[index % this.options.pointWidths.length];
};
Line.prototype.strokeForSeries = function(index) {
return this.options.pointStrokeColors[index % this.options.pointStrokeColors.length];
};
Line.prototype.pointFillColorForSeries = function(index) {
return this.options.pointFillColors[index % this.options.pointFillColors.length];
};
return Line;
})();
Morris.parseDate = function(date) { Morris.parseDate = function(date) {
var isecs, m, msecs, n, o, offsetmins, p, q, r, ret, secs; var isecs, m, msecs, n, o, offsetmins, p, q, r, ret, secs;
@ -880,6 +575,342 @@
} }
}; };
Morris.Line = (function(_super) {
__extends(Line, _super);
function Line(options) {
this.updateHilight = __bind(this.updateHilight, this);
this.hilight = __bind(this.hilight, this);
this.updateHover = __bind(this.updateHover, this);
if (!(this instanceof Morris.Line)) {
return new Morris.Line(options);
}
Line.__super__.constructor.call(this, options);
}
Line.prototype.init = function() {
var touchHandler,
_this = this;
this.pointGrow = Raphael.animation({
r: this.options.pointSize + 3
}, 25, 'linear');
this.pointShrink = Raphael.animation({
r: this.options.pointSize
}, 25, 'linear');
this.prevHilight = null;
this.el.mousemove(function(evt) {
return _this.updateHilight(evt.pageX);
});
if (this.options.hideHover) {
this.el.mouseout(function(evt) {
return _this.hilight(null);
});
}
touchHandler = function(evt) {
var touch;
touch = evt.originalEvent.touches[0] || evt.originalEvent.changedTouches[0];
_this.updateHilight(touch.pageX);
return touch;
};
this.el.bind('touchstart', touchHandler);
this.el.bind('touchmove', touchHandler);
return this.el.bind('touchend', touchHandler);
};
Line.prototype.defaults = {
lineWidth: 3,
pointSize: 4,
lineColors: ['#0b62a4', '#7A92A3', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed'],
pointWidths: [1],
pointStrokeColors: ['#ffffff'],
pointFillColors: [],
hoverPaddingX: 10,
hoverPaddingY: 5,
hoverMargin: 10,
hoverFillColor: '#fff',
hoverBorderColor: '#ccc',
hoverBorderWidth: 2,
hoverOpacity: 0.95,
hoverLabelColor: '#444',
hoverFontSize: 12,
smooth: true,
hideHover: false,
xLabels: 'auto',
xLabelFormat: null
};
Line.prototype.calc = function() {
var s, scoords, x, _i, _len, _ref,
_this = this;
this.columns = (function() {
var _i, _len, _ref, _results;
_ref = this.xvals;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
x = _ref[_i];
_results.push(this.transX(x));
}
return _results;
}).call(this);
this.seriesCoords = [];
_ref = this.series;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
s = _ref[_i];
scoords = [];
$.each(s, function(i, y) {
if (y === null) {
return scoords.push(null);
} else {
return scoords.push({
x: _this.columns[i],
y: _this.transY(y)
});
}
});
this.seriesCoords.push(scoords);
}
return this.hoverMargins = $.map(this.columns.slice(1), function(x, i) {
return (x + _this.columns[i]) / 2;
});
};
Line.prototype.draw = function() {
this.drawXAxis();
this.drawSeries();
this.drawHover();
return this.hilight(this.options.hideHover ? null : 0);
};
Line.prototype.drawXAxis = function() {
var drawLabel, i, l, labelText, prevLabelMargin, xLabelMargin, ypos, _i, _j, _len, _ref, _ref1, _results, _results1,
_this = this;
ypos = this.bottom + this.options.gridTextSize * 1.25;
xLabelMargin = 50;
prevLabelMargin = null;
drawLabel = function(labelText, xpos) {
var label, labelBox;
label = _this.r.text(_this.transX(xpos), ypos, labelText).attr('font-size', _this.options.gridTextSize).attr('fill', _this.options.gridTextColor);
labelBox = label.getBBox();
if ((prevLabelMargin === null || prevLabelMargin <= labelBox.x) && labelBox.x >= 0 && (labelBox.x + labelBox.width) < _this.el.width()) {
return prevLabelMargin = labelBox.x + labelBox.width + xLabelMargin;
} else {
return label.remove();
}
};
if (this.options.parseTime) {
if (this.columnLabels.length === 1 && this.options.xLabels === 'auto') {
return drawLabel(this.columnLabels[0], this.xvals[0]);
} else {
_ref = Morris.labelSeries(this.xmin, this.xmax, this.width, this.options.xLabels, this.options.xLabelFormat);
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
l = _ref[_i];
_results.push(drawLabel(l[0], l[1]));
}
return _results;
}
} else {
_results1 = [];
for (i = _j = 0, _ref1 = this.columnLabels.length; 0 <= _ref1 ? _j <= _ref1 : _j >= _ref1; i = 0 <= _ref1 ? ++_j : --_j) {
labelText = this.columnLabels[this.columnLabels.length - i - 1];
_results1.push(drawLabel(labelText, i));
}
return _results1;
}
};
Line.prototype.drawSeries = function() {
var c, circle, coords, i, path, smooth, _i, _j, _ref, _ref1, _results;
for (i = _i = _ref = this.seriesCoords.length - 1; _ref <= 0 ? _i <= 0 : _i >= 0; i = _ref <= 0 ? ++_i : --_i) {
coords = this.seriesCoords[i];
smooth = this.options.smooth === true || $.inArray(this.options.ykeys[i], this.options.smooth) > -1;
if (coords.length > 1) {
path = this.createPath(coords, this.bottom, smooth);
this.r.path(path).attr('stroke', this.colorForSeries(i)).attr('stroke-width', this.options.lineWidth);
}
}
this.seriesPoints = (function() {
var _j, _ref1, _results;
_results = [];
for (i = _j = 0, _ref1 = this.seriesCoords.length - 1; 0 <= _ref1 ? _j <= _ref1 : _j >= _ref1; i = 0 <= _ref1 ? ++_j : --_j) {
_results.push([]);
}
return _results;
}).call(this);
_results = [];
for (i = _j = _ref1 = this.seriesCoords.length - 1; _ref1 <= 0 ? _j <= 0 : _j >= 0; i = _ref1 <= 0 ? ++_j : --_j) {
_results.push((function() {
var _k, _len, _ref2, _results1;
_ref2 = this.seriesCoords[i];
_results1 = [];
for (_k = 0, _len = _ref2.length; _k < _len; _k++) {
c = _ref2[_k];
if (c === null) {
circle = null;
} else {
circle = this.r.circle(c.x, c.y, this.options.pointSize).attr('fill', this.pointFillColorForSeries(i) || this.colorForSeries(i)).attr('stroke-width', this.strokeWidthForSeries(i)).attr('stroke', this.strokeForSeries(i));
}
_results1.push(this.seriesPoints[i].push(circle));
}
return _results1;
}).call(this));
}
return _results;
};
Line.prototype.createPath = function(coords, smooth) {
var c, g, grads, i, ix, lc, lg, path, x1, x2, y1, y2, _i, _ref;
path = "";
if (smooth) {
grads = this.gradients(coords);
for (i = _i = 0, _ref = coords.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) {
c = coords[i];
if (i === 0) {
path += "M" + c.x + "," + c.y;
} else {
g = grads[i];
lc = coords[i - 1];
lg = grads[i - 1];
ix = (c.x - lc.x) / 4;
x1 = lc.x + ix;
y1 = Math.min(this.bottom, lc.y + ix * lg);
x2 = c.x - ix;
y2 = Math.min(this.bottom, c.y - ix * g);
path += "C" + x1 + "," + y1 + "," + x2 + "," + y2 + "," + c.x + "," + c.y;
}
}
} else {
path = "M" + $.map(coords, function(c) {
return "" + c.x + "," + c.y;
}).join("L");
}
return path;
};
Line.prototype.gradients = function(coords) {
return $.map(coords, function(c, i) {
if (i === 0) {
return (coords[1].y - c.y) / (coords[1].x - c.x);
} else if (i === (coords.length - 1)) {
return (c.y - coords[i - 1].y) / (c.x - coords[i - 1].x);
} else {
return (coords[i + 1].y - coords[i - 1].y) / (coords[i + 1].x - coords[i - 1].x);
}
});
};
Line.prototype.drawHover = function() {
var i, yLabel, _i, _ref, _results;
this.hoverHeight = this.options.hoverFontSize * 1.5 * (this.series.length + 1);
this.hover = this.r.rect(-10, -this.hoverHeight / 2 - this.options.hoverPaddingY, 20, this.hoverHeight + this.options.hoverPaddingY * 2, 10).attr('fill', this.options.hoverFillColor).attr('stroke', this.options.hoverBorderColor).attr('stroke-width', this.options.hoverBorderWidth).attr('opacity', this.options.hoverOpacity);
this.xLabel = this.r.text(0, (this.options.hoverFontSize * 0.75) - this.hoverHeight / 2, '').attr('fill', this.options.hoverLabelColor).attr('font-weight', 'bold').attr('font-size', this.options.hoverFontSize);
this.hoverSet = this.r.set();
this.hoverSet.push(this.hover);
this.hoverSet.push(this.xLabel);
this.yLabels = [];
_results = [];
for (i = _i = 0, _ref = this.series.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) {
yLabel = this.r.text(0, this.options.hoverFontSize * 1.5 * (i + 1.5) - this.hoverHeight / 2, '').attr('fill', this.colorForSeries(i)).attr('font-size', this.options.hoverFontSize);
this.yLabels.push(yLabel);
_results.push(this.hoverSet.push(yLabel));
}
return _results;
};
Line.prototype.updateHover = function(index) {
var i, maxLabelWidth, xloc, yloc, _i, _ref,
_this = this;
this.hoverSet.show();
this.xLabel.attr('text', this.columnLabels[index]);
for (i = _i = 0, _ref = this.series.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) {
this.yLabels[i].attr('text', "" + this.options.labels[i] + ": " + (this.yLabelFormat(this.series[i][index])));
}
maxLabelWidth = Math.max.apply(null, $.map(this.yLabels, function(l) {
return l.getBBox().width;
}));
maxLabelWidth = Math.max(maxLabelWidth, this.xLabel.getBBox().width);
this.hover.attr('width', maxLabelWidth + this.options.hoverPaddingX * 2);
this.hover.attr('x', -this.options.hoverPaddingX - maxLabelWidth / 2);
yloc = Math.min.apply(null, $.map(this.series, function(s) {
return _this.transY(s[index]);
}));
if (yloc > this.hoverHeight + this.options.hoverPaddingY * 2 + this.options.hoverMargin + this.top) {
yloc = yloc - this.hoverHeight / 2 - this.options.hoverPaddingY - this.options.hoverMargin;
} else {
yloc = yloc + this.hoverHeight / 2 + this.options.hoverPaddingY + this.options.hoverMargin;
}
yloc = Math.max(this.top + this.hoverHeight / 2 + this.options.hoverPaddingY, yloc);
yloc = Math.min(this.bottom - this.hoverHeight / 2 - this.options.hoverPaddingY, yloc);
xloc = Math.min(this.right - maxLabelWidth / 2 - this.options.hoverPaddingX, this.columns[index]);
xloc = Math.max(this.left + maxLabelWidth / 2 + this.options.hoverPaddingX, xloc);
return this.hoverSet.attr('transform', "t" + xloc + "," + yloc);
};
Line.prototype.hideHover = function() {
return this.hoverSet.hide();
};
Line.prototype.hilight = function(index) {
var i, _i, _j, _ref, _ref1;
if (this.prevHilight !== null && this.prevHilight !== index) {
for (i = _i = 0, _ref = this.seriesPoints.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) {
if (this.seriesPoints[i][this.prevHilight]) {
this.seriesPoints[i][this.prevHilight].animate(this.pointShrink);
}
}
}
if (index !== null && this.prevHilight !== index) {
for (i = _j = 0, _ref1 = this.seriesPoints.length - 1; 0 <= _ref1 ? _j <= _ref1 : _j >= _ref1; i = 0 <= _ref1 ? ++_j : --_j) {
if (this.seriesPoints[i][index]) {
this.seriesPoints[i][index].animate(this.pointGrow);
}
}
this.updateHover(index);
}
this.prevHilight = index;
if (index === null) {
return this.hideHover();
}
};
Line.prototype.updateHilight = function(x) {
var hoverIndex, _i, _ref, _results;
x -= this.el.offset().left;
_results = [];
for (hoverIndex = _i = _ref = this.hoverMargins.length; _ref <= 0 ? _i <= 0 : _i >= 0; hoverIndex = _ref <= 0 ? ++_i : --_i) {
if (hoverIndex === 0 || this.hoverMargins[hoverIndex - 1] > x) {
this.hilight(hoverIndex);
break;
} else {
_results.push(void 0);
}
}
return _results;
};
Line.prototype.colorForSeries = function(index) {
return this.options.lineColors[index % this.options.lineColors.length];
};
Line.prototype.strokeWidthForSeries = function(index) {
return this.options.pointWidths[index % this.options.pointWidths.length];
};
Line.prototype.strokeForSeries = function(index) {
return this.options.pointStrokeColors[index % this.options.pointStrokeColors.length];
};
Line.prototype.pointFillColorForSeries = function(index) {
return this.options.pointFillColors[index % this.options.pointFillColors.length];
};
return Line;
})(Morris.Grid);
Morris.labelSeries = function(dmin, dmax, pxwidth, specName, xLabelFormat) { Morris.labelSeries = function(dmin, dmax, pxwidth, specName, xLabelFormat) {
var d, d0, ddensity, name, ret, s, spec, t, _i, _len, _ref; var d, d0, ddensity, name, ret, s, spec, t, _i, _len, _ref;
ddensity = 200 * (dmax - dmin) / pxwidth; ddensity = 200 * (dmax - dmin) / pxwidth;

2
morris.min.js vendored

File diff suppressed because one or more lines are too long