diff --git a/morris.coffee b/morris.coffee index 7f9802d..495a810 100644 --- a/morris.coffee +++ b/morris.coffee @@ -1,569 +1,595 @@ -# The original line graph. -# -$ = jQuery - -Morris = {} -class Morris.Line - # Initialise the graph. - # - # @param {Object} options - constructor: (options) -> - if not (this instanceof Morris.Line) - return new Morris.Line(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' - @precalc() - @redraw() - - # Default configuration - # - defaults: - lineWidth: 3 - pointSize: 4 - lineColors: [ - '#0b62a4' - '#7A92A3' - '#4da74d' - '#afd8f8' - '#edc240' - '#cb4b4b' - '#9440ed' - ] - 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 - hoverPaddingY: 5 - hoverMargin: 10 - hoverFillColor: '#fff' - hoverBorderColor: '#ccc' - hoverBorderWidth: 2 - hoverOpacity: 0.95 - hoverLabelColor: '#444' - hoverFontSize: 12 - smooth: true - hideHover: false - parseTime: true - preUnits: '' - postUnits: '' - dateFormat: (x) -> new Date(x).toString() - xLabels: 'auto' - xLabelFormat: null - - # Do any necessary pre-processing for a new dataset - # - precalc: -> - # shallow copy & sort data - @options.data = @options.data.slice(0) - @options.data.sort (a, b) => (a[@options.xkey] < b[@options.xkey]) - (b[@options.xkey] < a[@options.xkey]) - # extract labels - @columnLabels = $.map @options.data, (d) => d[@options.xkey] - @seriesLabels = @options.labels - - # extract series data - @series = [] - for ykey in @options.ykeys - series_data = [] - for d in @options.data - series_data.push(if typeof d[ykey] == 'number' then d[ykey] else null) - @series.push(series_data) - - # translate x labels into nominal dates - # note: currently using decimal years to specify dates - if @options.parseTime - @xvals = $.map @columnLabels, (x) -> Morris.parseDate x - else - @xvals = [(@columnLabels.length-1)..0] - # translate column labels, if they're timestamps - if @options.parseTime - @columnLabels = $.map @columnLabels, (d) => - if typeof d is 'number' - @options.dateFormat(d) - else - d - @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 - @options.ymax = Math.max parseInt(@options.ymax[5..], 10), ymax - else - @options.ymax = 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 - @options.ymin = Math.min parseInt(@options.ymin[5..], 10), ymin - else - @options.ymin = ymin - - # Some instance variables for later - @pointGrow = Raphael.animation r: @options.pointSize + 3, 25, 'linear' - @pointShrink = Raphael.animation r: @options.pointSize, 25, 'linear' - @elementWidth = null - @elementHeight = null - # column hilight events - @prevHilight = null - @el.mousemove (evt) => - @updateHilight evt.pageX - if @options.hideHover - @el.mouseout (evt) => - @hilight null - touchHandler = (evt) => - touch = evt.originalEvent.touches[0] or evt.originalEvent.changedTouches[0] - @updateHilight touch.pageX - return touch - @el.bind 'touchstart', touchHandler - @el.bind 'touchmove', touchHandler - @el.bind 'touchend', touchHandler - - # Do any size-related calculations - # - calc: -> - w = @el.width() - h = @el.height() - if @elementWidth != w or @elementHeight != h - # calculate grid dimensions - @maxYLabelWidth = Math.max( - @measureText(@yLabelFormat(@options.ymin), @options.gridTextSize).width, - @measureText(@yLabelFormat(@options.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 / (@options.ymax - @options.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 - # - transX: (x) => - if @xvals.length is 1 - @left + @width / 2 - else - @left + (x - @xmin) * @dx - - transY: (y) => - return @options.marginTop + @height - (y - @options.ymin) * @dy - - # Clear and redraw the graph - # - redraw: -> - # remove child elements (get rid of old drawings) - @el.empty() - - # the raphael drawing instance - @r = new Raphael(@el[0]) - - @calc() - @drawGrid() - @drawSeries() - @drawHover() - @hilight(if @options.hideHover then null else 0) - - # draw the grid, and axes labels - # - drawGrid: -> - # draw y axis labels, horizontal lines - yInterval = (@options.ymax - @options.ymin) / (@options.numLines - 1) - firstY = Math.ceil(@options.ymin / yInterval) * yInterval - lastY = Math.floor(@options.ymax / yInterval) * yInterval - for lineY in [firstY..lastY] by yInterval - v = Math.floor(lineY) - y = @transY(v) - @r.text(@left - @options.marginLeft/2, y, @yLabelFormat(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? - prevLabelMargin = null - drawLabel = (labelText, xpos) => - label = @r.text(@transX(xpos), ypos, labelText) - .attr('font-size', @options.gridTextSize) - .attr('fill', @options.gridTextColor) - labelBox = label.getBBox() - # ensure a minimum of `xLabelMargin` pixels between labels - if prevLabelMargin is null or prevLabelMargin <= labelBox.x - prevLabelMargin = labelBox.x + labelBox.width + xLabelMargin - else - label.remove() - if @options.parseTime - if @columnLabels.length == 1 and @options.xLabels == 'auto' - # where there's only one value in the series, we can't make a - # sensible guess for an x labelling scheme, so just use the original - # column label - drawLabel(@columnLabels[0], @xvals[0]) - else - for l in Morris.labelSeries(@xmin, @xmax, @width, @options.xLabels, @options.xLabelFormat) - drawLabel(l[0], l[1]) - else - for i in [0..@columnLabels.length] - labelText = @columnLabels[@columnLabels.length - i - 1] - drawLabel(labelText, i) - - # draw the data series - # - drawSeries: -> - for i in [@seriesCoords.length-1..0] - coords = @seriesCoords[i] - if coords.length > 1 - path = @createPath coords, @options.marginTop, @left, @options.marginTop + @height, @left + @width - @r.path(path) - .attr('stroke', @options.lineColors[i]) - .attr('stroke-width', @options.lineWidth) - @seriesPoints = ([] for i in [0..@seriesCoords.length-1]) - for i in [@seriesCoords.length-1..0] - for c in @seriesCoords[i] - if c == null - circle = null - else - circle = @r.circle(c.x, c.y, @options.pointSize) - .attr('fill', @options.lineColors[i]) - .attr('stroke-width', 1) - .attr('stroke', '#ffffff') - @seriesPoints[i].push(circle) - - # create a path for a data series - # - createPath: (all_coords, top, left, bottom, right) -> - path = "" - coords = $.map(all_coords, (c) -> c) - if @options.smooth - grads = @gradients coords - for i in [0..coords.length-1] - c = coords[i] - if i is 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, (c) -> "#{c.x},#{c.y}").join("L") - return path - - # calculate a gradient at each point for a series of points - # - gradients: (coords) -> - $.map coords, (c, i) -> - if i is 0 - (coords[1].y - c.y) / (coords[1].x - c.x) - else if i is (coords.length - 1) - (c.y - coords[i - 1].y) / (c.x - coords[i - 1].x) - else - (coords[i + 1].y - coords[i - 1].y) / (coords[i + 1].x - coords[i - 1].x) - - # draw the hover tooltip - # - drawHover: -> - # hover labels - @hoverHeight = @options.hoverFontSize * 1.5 * (@series.length + 1) - @hover = @r.rect(-10, -@hoverHeight / 2 - @options.hoverPaddingY, 20, @hoverHeight + @options.hoverPaddingY * 2, 10) - .attr('fill', @options.hoverFillColor) - .attr('stroke', @options.hoverBorderColor) - .attr('stroke-width', @options.hoverBorderWidth) - .attr('opacity', @options.hoverOpacity) - @xLabel = @r.text(0, (@options.hoverFontSize * 0.75) - @hoverHeight / 2, '') - .attr('fill', @options.hoverLabelColor) - .attr('font-weight', 'bold') - .attr('font-size', @options.hoverFontSize) - @hoverSet = @r.set() - @hoverSet.push(@hover) - @hoverSet.push(@xLabel) - @yLabels = [] - for i in [0..@series.length-1] - yLabel = @r.text(0, @options.hoverFontSize * 1.5 * (i + 1.5) - @hoverHeight / 2, '') - .attr('fill', @options.lineColors[i]) - .attr('font-size', @options.hoverFontSize) - @yLabels.push(yLabel) - @hoverSet.push(yLabel) - - updateHover: (index) => - @hoverSet.show() - @xLabel.attr('text', @columnLabels[index]) - for i in [0..@series.length-1] - @yLabels[i].attr('text', "#{@seriesLabels[i]}: #{@yLabelFormat(@series[i][index])}") - # recalculate hover box width - maxLabelWidth = Math.max.apply null, $.map @yLabels, (l) -> - l.getBBox().width - maxLabelWidth = Math.max maxLabelWidth, @xLabel.getBBox().width - @hover.attr 'width', maxLabelWidth + @options.hoverPaddingX * 2 - @hover.attr 'x', -@options.hoverPaddingX - maxLabelWidth / 2 - # move to y pos - yloc = Math.min.apply null, $.map @series, (s) => - @transY s[index] - if yloc > @hoverHeight + @options.hoverPaddingY * 2 + @options.hoverMargin + @options.marginTop - yloc = yloc - @hoverHeight / 2 - @options.hoverPaddingY - @options.hoverMargin - else - yloc = yloc + @hoverHeight / 2 + @options.hoverPaddingY + @options.hoverMargin - yloc = Math.max @options.marginTop + @hoverHeight / 2 + @options.hoverPaddingY, yloc - yloc = Math.min @options.marginTop + @height - @hoverHeight / 2 - @options.hoverPaddingY, yloc - xloc = Math.min @left + @width - maxLabelWidth / 2 - @options.hoverPaddingX, @columns[index] - xloc = Math.max @left + maxLabelWidth / 2 + @options.hoverPaddingX, xloc - @hoverSet.attr 'transform', "t#{xloc},#{yloc}" - - hideHover: -> - @hoverSet.hide() - - hilight: (index) => - if @prevHilight isnt null and @prevHilight isnt index - for i in [0..@seriesPoints.length-1] - if @seriesPoints[i][@prevHilight] - @seriesPoints[i][@prevHilight].animate @pointShrink - if index isnt null and @prevHilight isnt index - for i in [0..@seriesPoints.length-1] - if @seriesPoints[i][index] - @seriesPoints[i][index].animate @pointGrow - @updateHover index - @prevHilight = index - if index is null - @hideHover() - - updateHilight: (x) => - x -= @el.offset().left - for hoverIndex in [@hoverMargins.length..0] - if hoverIndex == 0 || @hoverMargins[hoverIndex - 1] > x - @hilight hoverIndex - break - - measureText: (text, fontSize = 12) -> - tt = @r.text(100, 100, text).attr('font-size', fontSize) - ret = tt.getBBox() - tt.remove() - return ret - - 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() - -# make long numbers prettier by inserting commas -# eg: commas(1234567) -> '1,234,567' -# -Morris.commas = (num) -> - if num is null - "n/a" - else - ret = if num < 0 then "-" else "" - absnum = Math.abs(num) - intnum = Math.floor(absnum).toFixed(0) - ret += intnum.replace(/(?=(?:\d{3})+$)(?!^)/g, ',') - strabsnum = absnum.toString() - if strabsnum.length > intnum.length - ret += strabsnum.slice(intnum.length) - ret - -# zero-pad numbers to two characters wide -# -Morris.pad2 = (number) -> (if number < 10 then '0' else '') + number - -# generate a series of label, timestamp pairs for x-axis labels -# -Morris.labelSeries = (dmin, dmax, pxwidth, specName, xLabelFormat) -> - ddensity = 200 * (dmax - dmin) / pxwidth # seconds per `margin` pixels - d0 = new Date(dmin) - spec = Morris.LABEL_SPECS[specName] - # if the spec doesn't exist, search for the closest one in the list - if spec is undefined - for name in Morris.AUTO_LABEL_ORDER - s = Morris.LABEL_SPECS[name] - if ddensity >= s.span - spec = s - break - # if we run out of options, use second-intervals - if spec is undefined - spec = Morris.LABEL_SPECS["second"] - # check if there's a user-defined formatting function - if xLabelFormat - spec = $.extend({}, spec, {fmt: xLabelFormat}) - # calculate labels - d = spec.start(d0) - ret = [] - while (t = d.getTime()) <= dmax - if t >= dmin - ret.push [spec.fmt(d), t] - spec.incr(d) - return ret - -minutesSpecHelper = (interval) -> - span: interval * 60 * 1000 - start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()) - fmt: (d) -> "#{Morris.pad2(d.getHours())}:#{Morris.pad2(d.getMinutes())}" - incr: (d) -> d.setMinutes(d.getMinutes() + interval) - -secondsSpecHelper = (interval) -> - span: interval * 1000 - start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes()) - fmt: (d) -> "#{Morris.pad2(d.getHours())}:#{Morris.pad2(d.getMinutes())}:#{Morris.pad2(d.getSeconds())}" - incr: (d) -> d.setSeconds(d.getSeconds() + interval) - -Morris.LABEL_SPECS = - "year": - span: 17280000000 # 365 * 24 * 60 * 60 * 1000 - start: (d) -> new Date(d.getFullYear(), 0, 1) - fmt: (d) -> "#{d.getFullYear()}" - incr: (d) -> d.setFullYear(d.getFullYear() + 1) - "month": - span: 2419200000 # 28 * 24 * 60 * 60 * 1000 - start: (d) -> new Date(d.getFullYear(), d.getMonth(), 1) - fmt: (d) -> "#{d.getFullYear()}-#{Morris.pad2(d.getMonth() + 1)}" - incr: (d) -> d.setMonth(d.getMonth() + 1) - "day": - span: 86400000 # 24 * 60 * 60 * 1000 - start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate()) - fmt: (d) -> "#{d.getFullYear()}-#{Morris.pad2(d.getMonth() + 1)}-#{Morris.pad2(d.getDate())}" - incr: (d) -> d.setDate(d.getDate() + 1) - "hour": minutesSpecHelper(60) - "30min": minutesSpecHelper(30) - "15min": minutesSpecHelper(15) - "10min": minutesSpecHelper(10) - "5min": minutesSpecHelper(5) - "minute": minutesSpecHelper(1) - "30sec": secondsSpecHelper(30) - "15sec": secondsSpecHelper(15) - "10sec": secondsSpecHelper(10) - "5sec": secondsSpecHelper(5) - "second": secondsSpecHelper(1) - -Morris.AUTO_LABEL_ORDER = [ - "year", "month", "day", "hour", - "30min", "15min", "10min", "5min", "minute", - "30sec", "15sec", "10sec", "5sec", "second" -] - -window.Morris = Morris -# vim: set et ts=2 sw=2 sts=2 +# The original line graph. +# +$ = jQuery + +Morris = {} +class Morris.Line + # Initialise the graph. + # + # @param {Object} options + constructor: (options) -> + if not (this instanceof Morris.Line) + return new Morris.Line(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' + @precalc() + @redraw() + + # Default configuration + # + defaults: + lineWidth: 3 + pointSize: 4 + lineColors: [ + '#0b62a4' + '#7A92A3' + '#4da74d' + '#afd8f8' + '#edc240' + '#cb4b4b' + '#9440ed' + ] + 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 + hoverPaddingY: 5 + hoverMargin: 10 + hoverFillColor: '#fff' + hoverBorderColor: '#ccc' + hoverBorderWidth: 2 + hoverOpacity: 0.95 + hoverLabelColor: '#444' + hoverFontSize: 12 + smooth: true + hideHover: false + parseTime: true + preUnits: '' + postUnits: '' + dateFormat: (x) -> new Date(x).toString() + xLabels: 'auto' + xLabelFormat: null + animate: true + easing: "bounce" + duration: 1500 + + # Do any necessary pre-processing for a new dataset + # + precalc: -> + # shallow copy & sort data + @options.data = @options.data.slice(0) + @options.data.sort (a, b) => (a[@options.xkey] < b[@options.xkey]) - (b[@options.xkey] < a[@options.xkey]) + # extract labels + @columnLabels = $.map @options.data, (d) => d[@options.xkey] + @seriesLabels = @options.labels + + # extract series data + @series = [] + for ykey in @options.ykeys + series_data = [] + for d in @options.data + series_data.push(if typeof d[ykey] == 'number' then d[ykey] else null) + @series.push(series_data) + + # translate x labels into nominal dates + # note: currently using decimal years to specify dates + if @options.parseTime + @xvals = $.map @columnLabels, (x) -> Morris.parseDate x + else + @xvals = [(@columnLabels.length-1)..0] + # translate column labels, if they're timestamps + if @options.parseTime + @columnLabels = $.map @columnLabels, (d) => + if typeof d is 'number' + @options.dateFormat(d) + else + d + @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 + @options.ymax = Math.max parseInt(@options.ymax[5..], 10), ymax + else + @options.ymax = 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 + @options.ymin = Math.min parseInt(@options.ymin[5..], 10), ymin + else + @options.ymin = ymin + + # Some instance variables for later + @pointGrow = Raphael.animation r: @options.pointSize + 3, 25, 'linear' + @pointShrink = Raphael.animation r: @options.pointSize, 25, 'linear' + @elementWidth = null + @elementHeight = null + # column hilight events + @prevHilight = null + @el.mousemove (evt) => + @updateHilight evt.pageX + if @options.hideHover + @el.mouseout (evt) => + @hilight null + touchHandler = (evt) => + touch = evt.originalEvent.touches[0] or evt.originalEvent.changedTouches[0] + @updateHilight touch.pageX + return touch + @el.bind 'touchstart', touchHandler + @el.bind 'touchmove', touchHandler + @el.bind 'touchend', touchHandler + + # Do any size-related calculations + # + calc: -> + w = @el.width() + h = @el.height() + if @elementWidth != w or @elementHeight != h + # calculate grid dimensions + @maxYLabelWidth = Math.max( + @measureText(@yLabelFormat(@options.ymin), @options.gridTextSize).width, + @measureText(@yLabelFormat(@options.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 / (@options.ymax - @options.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 + # + transX: (x) => + if @xvals.length is 1 + @left + @width / 2 + else + @left + (x - @xmin) * @dx + + transY: (y) => + return @options.marginTop + @height - (y - @options.ymin) * @dy + + # Clear and redraw the graph + # + redraw: -> + # remove child elements (get rid of old drawings) + @el.empty() + + # the raphael drawing instance + @r = new Raphael(@el[0]) + + @calc() + @drawGrid() + @drawSeries() + @drawHover() + @hilight(if @options.hideHover then null else 0) + + # draw the grid, and axes labels + # + drawGrid: -> + # draw y axis labels, horizontal lines + yInterval = (@options.ymax - @options.ymin) / (@options.numLines - 1) + firstY = Math.ceil(@options.ymin / yInterval) * yInterval + lastY = Math.floor(@options.ymax / yInterval) * yInterval + for lineY in [firstY..lastY] by yInterval + v = Math.floor(lineY) + y = @transY(v) + @r.text(@left - @options.marginLeft/2, y, @yLabelFormat(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? + prevLabelMargin = null + drawLabel = (labelText, xpos) => + label = @r.text(@transX(xpos), ypos, labelText) + .attr('font-size', @options.gridTextSize) + .attr('fill', @options.gridTextColor) + labelBox = label.getBBox() + # ensure a minimum of `xLabelMargin` pixels between labels + if prevLabelMargin is null or prevLabelMargin <= labelBox.x + prevLabelMargin = labelBox.x + labelBox.width + xLabelMargin + else + label.remove() + if @options.parseTime + if @columnLabels.length == 1 and @options.xLabels == 'auto' + # where there's only one value in the series, we can't make a + # sensible guess for an x labelling scheme, so just use the original + # column label + drawLabel(@columnLabels[0], @xvals[0]) + else + for l in Morris.labelSeries(@xmin, @xmax, @width, @options.xLabels, @options.xLabelFormat) + drawLabel(l[0], l[1]) + else + for i in [0..@columnLabels.length] + labelText = @columnLabels[@columnLabels.length - i - 1] + drawLabel(labelText, i) + + # draw the data series + # + drawSeries: -> + averages = [] + for i in [@seriesCoords.length-1..0] + coords = @seriesCoords[i] + if coords.length > 1 + path = @createPath coords, @options.marginTop, @left, @options.marginTop + @height, @left + @width + if @options.animate + averages[i] = average = Morris.seriesAverage(coords) + straightCoords = ({x:c.x, y:average} for c in coords) + straightPath = @createPath straightCoords, @options.marginTop, @left, @options.marginTop + @height, @left + @width + rPath = @r.path(straightPath) + .attr('stroke', @options.lineColors[i]) + .attr('stroke-width', @options.lineWidth) + do (rPath, path) => + rPath.animate {path}, @options.duration, @options.easing + else + @r.path(path) + .attr('stroke', @options.lineColors[i]) + .attr('stroke-width', @options.lineWidth) + @seriesPoints = ([] for i in [0..@seriesCoords.length-1]) + for i in [@seriesCoords.length-1..0] + for c in @seriesCoords[i] + if c == null + circle = null + else + y = if @options.animate then averages[i] else c.y + circle = @r.circle(c.x, y, @options.pointSize) + .attr('fill', @options.lineColors[i]) + .attr('stroke-width', 1) + .attr('stroke', '#ffffff') + if @options.animate + do (circle, c) => + circle.animate {cy:c.y}, @options.duration, @options.easing + + @seriesPoints[i].push(circle) + + # create a path for a data series + # + createPath: (all_coords, top, left, bottom, right) -> + path = "" + coords = $.map(all_coords, (c) -> c) + if @options.smooth + grads = @gradients coords + for i in [0..coords.length-1] + c = coords[i] + if i is 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, (c) -> "#{c.x},#{c.y}").join("L") + return path + + # calculate a gradient at each point for a series of points + # + gradients: (coords) -> + $.map coords, (c, i) -> + if i is 0 + (coords[1].y - c.y) / (coords[1].x - c.x) + else if i is (coords.length - 1) + (c.y - coords[i - 1].y) / (c.x - coords[i - 1].x) + else + (coords[i + 1].y - coords[i - 1].y) / (coords[i + 1].x - coords[i - 1].x) + + # draw the hover tooltip + # + drawHover: -> + # hover labels + @hoverHeight = @options.hoverFontSize * 1.5 * (@series.length + 1) + @hover = @r.rect(-10, -@hoverHeight / 2 - @options.hoverPaddingY, 20, @hoverHeight + @options.hoverPaddingY * 2, 10) + .attr('fill', @options.hoverFillColor) + .attr('stroke', @options.hoverBorderColor) + .attr('stroke-width', @options.hoverBorderWidth) + .attr('opacity', @options.hoverOpacity) + @xLabel = @r.text(0, (@options.hoverFontSize * 0.75) - @hoverHeight / 2, '') + .attr('fill', @options.hoverLabelColor) + .attr('font-weight', 'bold') + .attr('font-size', @options.hoverFontSize) + @hoverSet = @r.set() + @hoverSet.push(@hover) + @hoverSet.push(@xLabel) + @yLabels = [] + for i in [0..@series.length-1] + yLabel = @r.text(0, @options.hoverFontSize * 1.5 * (i + 1.5) - @hoverHeight / 2, '') + .attr('fill', @options.lineColors[i]) + .attr('font-size', @options.hoverFontSize) + @yLabels.push(yLabel) + @hoverSet.push(yLabel) + + updateHover: (index) => + @hoverSet.show() + @xLabel.attr('text', @columnLabels[index]) + for i in [0..@series.length-1] + @yLabels[i].attr('text', "#{@seriesLabels[i]}: #{@yLabelFormat(@series[i][index])}") + # recalculate hover box width + maxLabelWidth = Math.max.apply null, $.map @yLabels, (l) -> + l.getBBox().width + maxLabelWidth = Math.max maxLabelWidth, @xLabel.getBBox().width + @hover.attr 'width', maxLabelWidth + @options.hoverPaddingX * 2 + @hover.attr 'x', -@options.hoverPaddingX - maxLabelWidth / 2 + # move to y pos + yloc = Math.min.apply null, $.map @series, (s) => + @transY s[index] + if yloc > @hoverHeight + @options.hoverPaddingY * 2 + @options.hoverMargin + @options.marginTop + yloc = yloc - @hoverHeight / 2 - @options.hoverPaddingY - @options.hoverMargin + else + yloc = yloc + @hoverHeight / 2 + @options.hoverPaddingY + @options.hoverMargin + yloc = Math.max @options.marginTop + @hoverHeight / 2 + @options.hoverPaddingY, yloc + yloc = Math.min @options.marginTop + @height - @hoverHeight / 2 - @options.hoverPaddingY, yloc + xloc = Math.min @left + @width - maxLabelWidth / 2 - @options.hoverPaddingX, @columns[index] + xloc = Math.max @left + maxLabelWidth / 2 + @options.hoverPaddingX, xloc + @hoverSet.attr 'transform', "t#{xloc},#{yloc}" + + hideHover: -> + @hoverSet.hide() + + hilight: (index) => + if @prevHilight isnt null and @prevHilight isnt index + for i in [0..@seriesPoints.length-1] + if @seriesPoints[i][@prevHilight] + @seriesPoints[i][@prevHilight].animate @pointShrink + if index isnt null and @prevHilight isnt index + for i in [0..@seriesPoints.length-1] + if @seriesPoints[i][index] + @seriesPoints[i][index].animate @pointGrow + @updateHover index + @prevHilight = index + if index is null + @hideHover() + + updateHilight: (x) => + x -= @el.offset().left + for hoverIndex in [@hoverMargins.length..0] + if hoverIndex == 0 || @hoverMargins[hoverIndex - 1] > x + @hilight hoverIndex + break + + measureText: (text, fontSize = 12) -> + tt = @r.text(100, 100, text).attr('font-size', fontSize) + ret = tt.getBBox() + tt.remove() + return ret + + 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() + +# make long numbers prettier by inserting commas +# eg: commas(1234567) -> '1,234,567' +# +Morris.commas = (num) -> + if num is null + "n/a" + else + ret = if num < 0 then "-" else "" + absnum = Math.abs(num) + intnum = Math.floor(absnum).toFixed(0) + ret += intnum.replace(/(?=(?:\d{3})+$)(?!^)/g, ',') + strabsnum = absnum.toString() + if strabsnum.length > intnum.length + ret += strabsnum.slice(intnum.length) + ret + +# zero-pad numbers to two characters wide +# +Morris.pad2 = (number) -> (if number < 10 then '0' else '') + number + +# generate a series of label, timestamp pairs for x-axis labels +# +Morris.labelSeries = (dmin, dmax, pxwidth, specName, xLabelFormat) -> + ddensity = 200 * (dmax - dmin) / pxwidth # seconds per `margin` pixels + d0 = new Date(dmin) + spec = Morris.LABEL_SPECS[specName] + # if the spec doesn't exist, search for the closest one in the list + if spec is undefined + for name in Morris.AUTO_LABEL_ORDER + s = Morris.LABEL_SPECS[name] + if ddensity >= s.span + spec = s + break + # if we run out of options, use second-intervals + if spec is undefined + spec = Morris.LABEL_SPECS["second"] + # check if there's a user-defined formatting function + if xLabelFormat + spec = $.extend({}, spec, {fmt: xLabelFormat}) + # calculate labels + d = spec.start(d0) + ret = [] + while (t = d.getTime()) <= dmax + if t >= dmin + ret.push [spec.fmt(d), t] + spec.incr(d) + return ret + +minutesSpecHelper = (interval) -> + span: interval * 60 * 1000 + start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()) + fmt: (d) -> "#{Morris.pad2(d.getHours())}:#{Morris.pad2(d.getMinutes())}" + incr: (d) -> d.setMinutes(d.getMinutes() + interval) + +secondsSpecHelper = (interval) -> + span: interval * 1000 + start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes()) + fmt: (d) -> "#{Morris.pad2(d.getHours())}:#{Morris.pad2(d.getMinutes())}:#{Morris.pad2(d.getSeconds())}" + incr: (d) -> d.setSeconds(d.getSeconds() + interval) + +Morris.LABEL_SPECS = + "year": + span: 17280000000 # 365 * 24 * 60 * 60 * 1000 + start: (d) -> new Date(d.getFullYear(), 0, 1) + fmt: (d) -> "#{d.getFullYear()}" + incr: (d) -> d.setFullYear(d.getFullYear() + 1) + "month": + span: 2419200000 # 28 * 24 * 60 * 60 * 1000 + start: (d) -> new Date(d.getFullYear(), d.getMonth(), 1) + fmt: (d) -> "#{d.getFullYear()}-#{Morris.pad2(d.getMonth() + 1)}" + incr: (d) -> d.setMonth(d.getMonth() + 1) + "day": + span: 86400000 # 24 * 60 * 60 * 1000 + start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate()) + fmt: (d) -> "#{d.getFullYear()}-#{Morris.pad2(d.getMonth() + 1)}-#{Morris.pad2(d.getDate())}" + incr: (d) -> d.setDate(d.getDate() + 1) + "hour": minutesSpecHelper(60) + "30min": minutesSpecHelper(30) + "15min": minutesSpecHelper(15) + "10min": minutesSpecHelper(10) + "5min": minutesSpecHelper(5) + "minute": minutesSpecHelper(1) + "30sec": secondsSpecHelper(30) + "15sec": secondsSpecHelper(15) + "10sec": secondsSpecHelper(10) + "5sec": secondsSpecHelper(5) + "second": secondsSpecHelper(1) + +Morris.AUTO_LABEL_ORDER = [ + "year", "month", "day", "hour", + "30min", "15min", "10min", "5min", "minute", + "30sec", "15sec", "10sec", "5sec", "second" +] + +Morris.seriesAverage = (series) -> + total = 0 + for point in series + total += point.y ? 0 + (total / series.length) / 2 + + +window.Morris = Morris +# vim: set et ts=2 sw=2 sts=2 diff --git a/morris.js b/morris.js index 09a5648..2acc79c 100644 --- a/morris.js +++ b/morris.js @@ -1,675 +1,724 @@ -// Generated by CoffeeScript 1.3.3 -(function() { - var $, Morris, minutesSpecHelper, secondsSpecHelper, - __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - $ = jQuery; - - Morris = {}; - - Morris.Line = (function() { - - function Line(options) { - this.updateHilight = __bind(this.updateHilight, this); - - this.hilight = __bind(this.hilight, this); - - this.updateHover = __bind(this.updateHover, this); - - this.transY = __bind(this.transY, this); - - this.transX = __bind(this.transX, this); - if (!(this instanceof Morris.Line)) { - return new Morris.Line(options); - } - if (typeof options.element === 'string') { - this.el = $(document.getElementById(options.element)); - } else { - this.el = $(options.element); - } - if (this.el === null || this.el.length === 0) { - throw new Error("Graph placeholder not found."); - } - this.options = $.extend({}, this.defaults, options); - if (typeof this.options.units === 'string') { - this.options.postUnits = options.units; - } - if (this.options.data === void 0 || this.options.data.length === 0) { - return; - } - this.el.addClass('graph-initialised'); - this.precalc(); - this.redraw(); - } - - Line.prototype.defaults = { - lineWidth: 3, - pointSize: 4, - lineColors: ['#0b62a4', '#7A92A3', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed'], - 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, - hoverPaddingY: 5, - hoverMargin: 10, - hoverFillColor: '#fff', - hoverBorderColor: '#ccc', - hoverBorderWidth: 2, - hoverOpacity: 0.95, - hoverLabelColor: '#444', - hoverFontSize: 12, - smooth: true, - hideHover: false, - parseTime: true, - preUnits: '', - postUnits: '', - dateFormat: function(x) { - return new Date(x).toString(); - }, - xLabels: 'auto', - xLabelFormat: null - }; - - Line.prototype.precalc = function() { - var d, series_data, touchHandler, ykey, ymax, ymin, _i, _j, _k, _len, _len1, _ref, _ref1, _ref2, _results, - _this = this; - this.options.data = this.options.data.slice(0); - this.options.data.sort(function(a, b) { - return (a[_this.options.xkey] < b[_this.options.xkey]) - (b[_this.options.xkey] < a[_this.options.xkey]); - }); - this.columnLabels = $.map(this.options.data, function(d) { - return d[_this.options.xkey]; - }); - this.seriesLabels = this.options.labels; - this.series = []; - _ref = this.options.ykeys; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - ykey = _ref[_i]; - series_data = []; - _ref1 = this.options.data; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - d = _ref1[_j]; - series_data.push(typeof d[ykey] === 'number' ? d[ykey] : null); - } - this.series.push(series_data); - } - if (this.options.parseTime) { - this.xvals = $.map(this.columnLabels, function(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) { - this.columnLabels = $.map(this.columnLabels, function(d) { - if (typeof d === 'number') { - return _this.options.dateFormat(d); - } else { - return d; - } - }); - } - this.xmin = Math.min.apply(null, this.xvals); - this.xmax = Math.max.apply(null, this.xvals); - if (this.xmin === this.xmax) { - this.xmin -= 1; - this.xmax += 1; - } - if (typeof this.options.ymax === 'string' && this.options.ymax.slice(0, 4) === 'auto') { - ymax = Math.max.apply(null, Array.prototype.concat.apply([], this.series)); - if (this.options.ymax.length > 5) { - this.options.ymax = Math.max(parseInt(this.options.ymax.slice(5), 10), ymax); - } else { - this.options.ymax = ymax; - } - } - if (typeof this.options.ymin === 'string' && this.options.ymin.slice(0, 4) === 'auto') { - ymin = Math.min.apply(null, Array.prototype.concat.apply([], this.series)); - if (this.options.ymin.length > 5) { - this.options.ymin = Math.min(parseInt(this.options.ymin.slice(5), 10), ymin); - } else { - this.options.ymin = ymin; - } - } - 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.elementHeight = null; - 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.calc = function() { - var h, s, scoords, w, x, _i, _len, _ref, - _this = this; - w = this.el.width(); - h = this.el.height(); - if (this.elementWidth !== w || this.elementHeight !== h) { - this.maxYLabelWidth = Math.max(this.measureText(this.yLabelFormat(this.options.ymin), this.options.gridTextSize).width, this.measureText(this.yLabelFormat(this.options.ymax), this.options.gridTextSize).width); - this.left = this.maxYLabelWidth + this.options.marginLeft; - this.width = this.el.width() - this.left - this.options.marginRight; - this.height = this.el.height() - this.options.marginTop - this.options.marginBottom; - this.dx = this.width / (this.xmax - this.xmin); - this.dy = this.height / (this.options.ymax - this.options.ymin); - 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.transX = function(x) { - if (this.xvals.length === 1) { - return this.left + this.width / 2; - } else { - return this.left + (x - this.xmin) * this.dx; - } - }; - - Line.prototype.transY = function(y) { - return this.options.marginTop + this.height - (y - this.options.ymin) * this.dy; - }; - - Line.prototype.redraw = function() { - this.el.empty(); - this.r = new Raphael(this.el[0]); - this.calc(); - this.drawGrid(); - this.drawSeries(); - this.drawHover(); - return this.hilight(this.options.hideHover ? null : 0); - }; - - Line.prototype.drawGrid = function() { - var drawLabel, firstY, i, l, labelText, lastY, lineY, prevLabelMargin, v, xLabelMargin, y, yInterval, ypos, _i, _j, _k, _len, _ref, _ref1, _results, _results1, - _this = this; - yInterval = (this.options.ymax - this.options.ymin) / (this.options.numLines - 1); - firstY = Math.ceil(this.options.ymin / yInterval) * yInterval; - lastY = Math.floor(this.options.ymax / yInterval) * yInterval; - for (lineY = _i = firstY; firstY <= lastY ? _i <= lastY : _i >= lastY; lineY = _i += yInterval) { - v = Math.floor(lineY); - y = this.transY(v); - this.r.text(this.left - this.options.marginLeft / 2, y, this.yLabelFormat(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); - } - 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) { - 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 (_j = 0, _len = _ref.length; _j < _len; _j++) { - l = _ref[_j]; - _results.push(drawLabel(l[0], l[1])); - } - return _results; - } - } else { - _results1 = []; - for (i = _k = 0, _ref1 = this.columnLabels.length; 0 <= _ref1 ? _k <= _ref1 : _k >= _ref1; i = 0 <= _ref1 ? ++_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, _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]; - if (coords.length > 1) { - path = this.createPath(coords, this.options.marginTop, this.left, this.options.marginTop + this.height, this.left + this.width); - this.r.path(path).attr('stroke', this.options.lineColors[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.options.lineColors[i]).attr('stroke-width', 1).attr('stroke', '#ffffff'); - } - _results1.push(this.seriesPoints[i].push(circle)); - } - return _results1; - }).call(this)); - } - return _results; - }; - - Line.prototype.createPath = function(all_coords, top, left, bottom, right) { - var c, coords, g, grads, i, ix, lc, lg, path, x1, x2, y1, y2, _i, _ref; - path = ""; - coords = $.map(all_coords, function(c) { - return c; - }); - if (this.options.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.options.lineColors[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; - if (fontSize == null) { - fontSize = 12; - } - tt = this.r.text(100, 100, text).attr('font-size', fontSize); - ret = tt.getBBox(); - tt.remove(); - return ret; - }; - - Line.prototype.yLabelFormat = function(label) { - return "" + this.options.preUnits + (Morris.commas(label)) + this.options.postUnits; - }; - - return Line; - - })(); - - Morris.parseDate = function(date) { - var isecs, m, msecs, n, o, offsetmins, p, q, r, ret, secs; - if (typeof date === '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) { - return new Date(parseInt(m[1], 10), parseInt(m[2], 10) * 3 - 1, 1).getTime(); - } else if (n) { - return new Date(parseInt(n[1], 10), parseInt(n[2], 10) - 1, 1).getTime(); - } else if (o) { - return new Date(parseInt(o[1], 10), parseInt(o[2], 10) - 1, parseInt(o[3], 10)).getTime(); - } else if (p) { - ret = new Date(parseInt(p[1], 10), 0, 1); - if (ret.getDay() !== 4) { - ret.setMonth(0, 1 + ((4 - ret.getDay()) + 7) % 7); - } - return ret.getTime() + parseInt(p[2], 10) * 604800000; - } else if (q) { - if (!q[6]) { - return 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 { - offsetmins = 0; - if (q[6] !== 'Z') { - offsetmins = parseInt(q[8], 10) * 60 + parseInt(q[9], 10); - if (q[7] === '+') { - offsetmins = 0 - offsetmins; - } - } - return 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 (!r[8]) { - return 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 { - offsetmins = 0; - if (r[8] !== 'Z') { - offsetmins = parseInt(r[10], 10) * 60 + parseInt(r[11], 10); - if (r[9] === '+') { - offsetmins = 0 - offsetmins; - } - } - return 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 { - return new Date(parseInt(date, 10), 0, 1).getTime(); - } - }; - - Morris.commas = function(num) { - var absnum, intnum, ret, strabsnum; - if (num === null) { - return "n/a"; - } else { - ret = num < 0 ? "-" : ""; - absnum = Math.abs(num); - intnum = Math.floor(absnum).toFixed(0); - ret += intnum.replace(/(?=(?:\d{3})+$)(?!^)/g, ','); - strabsnum = absnum.toString(); - if (strabsnum.length > intnum.length) { - ret += strabsnum.slice(intnum.length); - } - return ret; - } - }; - - Morris.pad2 = function(number) { - return (number < 10 ? '0' : '') + number; - }; - - Morris.labelSeries = function(dmin, dmax, pxwidth, specName, xLabelFormat) { - var d, d0, ddensity, name, ret, s, spec, t, _i, _len, _ref; - ddensity = 200 * (dmax - dmin) / pxwidth; - d0 = new Date(dmin); - spec = Morris.LABEL_SPECS[specName]; - if (spec === void 0) { - _ref = Morris.AUTO_LABEL_ORDER; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - name = _ref[_i]; - s = Morris.LABEL_SPECS[name]; - if (ddensity >= s.span) { - spec = s; - break; - } - } - } - if (spec === void 0) { - spec = Morris.LABEL_SPECS["second"]; - } - if (xLabelFormat) { - spec = $.extend({}, spec, { - fmt: xLabelFormat - }); - } - d = spec.start(d0); - ret = []; - while ((t = d.getTime()) <= dmax) { - if (t >= dmin) { - ret.push([spec.fmt(d), t]); - } - spec.incr(d); - } - return ret; - }; - - minutesSpecHelper = function(interval) { - return { - span: interval * 60 * 1000, - start: function(d) { - return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()); - }, - fmt: function(d) { - return "" + (Morris.pad2(d.getHours())) + ":" + (Morris.pad2(d.getMinutes())); - }, - incr: function(d) { - return d.setMinutes(d.getMinutes() + interval); - } - }; - }; - - secondsSpecHelper = function(interval) { - return { - span: interval * 1000, - start: function(d) { - return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes()); - }, - fmt: function(d) { - return "" + (Morris.pad2(d.getHours())) + ":" + (Morris.pad2(d.getMinutes())) + ":" + (Morris.pad2(d.getSeconds())); - }, - incr: function(d) { - return d.setSeconds(d.getSeconds() + interval); - } - }; - }; - - Morris.LABEL_SPECS = { - "year": { - span: 17280000000, - start: function(d) { - return new Date(d.getFullYear(), 0, 1); - }, - fmt: function(d) { - return "" + (d.getFullYear()); - }, - incr: function(d) { - return d.setFullYear(d.getFullYear() + 1); - } - }, - "month": { - span: 2419200000, - start: function(d) { - return new Date(d.getFullYear(), d.getMonth(), 1); - }, - fmt: function(d) { - return "" + (d.getFullYear()) + "-" + (Morris.pad2(d.getMonth() + 1)); - }, - incr: function(d) { - return d.setMonth(d.getMonth() + 1); - } - }, - "day": { - span: 86400000, - start: function(d) { - return new Date(d.getFullYear(), d.getMonth(), d.getDate()); - }, - fmt: function(d) { - return "" + (d.getFullYear()) + "-" + (Morris.pad2(d.getMonth() + 1)) + "-" + (Morris.pad2(d.getDate())); - }, - incr: function(d) { - return d.setDate(d.getDate() + 1); - } - }, - "hour": minutesSpecHelper(60), - "30min": minutesSpecHelper(30), - "15min": minutesSpecHelper(15), - "10min": minutesSpecHelper(10), - "5min": minutesSpecHelper(5), - "minute": minutesSpecHelper(1), - "30sec": secondsSpecHelper(30), - "15sec": secondsSpecHelper(15), - "10sec": secondsSpecHelper(10), - "5sec": secondsSpecHelper(5), - "second": secondsSpecHelper(1) - }; - - Morris.AUTO_LABEL_ORDER = ["year", "month", "day", "hour", "30min", "15min", "10min", "5min", "minute", "30sec", "15sec", "10sec", "5sec", "second"]; - - window.Morris = Morris; - -}).call(this); +// Generated by CoffeeScript 1.3.1 +(function() { + var $, Morris, minutesSpecHelper, secondsSpecHelper, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + $ = jQuery; + + Morris = {}; + + Morris.Line = (function() { + + Line.name = 'Line'; + + function Line(options) { + this.updateHilight = __bind(this.updateHilight, this); + + this.hilight = __bind(this.hilight, this); + + this.updateHover = __bind(this.updateHover, this); + + this.transY = __bind(this.transY, this); + + this.transX = __bind(this.transX, this); + if (!(this instanceof Morris.Line)) { + return new Morris.Line(options); + } + if (typeof options.element === 'string') { + this.el = $(document.getElementById(options.element)); + } else { + this.el = $(options.element); + } + if (this.el === null || this.el.length === 0) { + throw new Error("Graph placeholder not found."); + } + this.options = $.extend({}, this.defaults, options); + if (typeof this.options.units === 'string') { + this.options.postUnits = options.units; + } + if (this.options.data === void 0 || this.options.data.length === 0) { + return; + } + this.el.addClass('graph-initialised'); + this.precalc(); + this.redraw(); + } + + Line.prototype.defaults = { + lineWidth: 3, + pointSize: 4, + lineColors: ['#0b62a4', '#7A92A3', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed'], + 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, + hoverPaddingY: 5, + hoverMargin: 10, + hoverFillColor: '#fff', + hoverBorderColor: '#ccc', + hoverBorderWidth: 2, + hoverOpacity: 0.95, + hoverLabelColor: '#444', + hoverFontSize: 12, + smooth: true, + hideHover: false, + parseTime: true, + preUnits: '', + postUnits: '', + dateFormat: function(x) { + return new Date(x).toString(); + }, + xLabels: 'auto', + xLabelFormat: null, + animate: true, + easing: "bounce", + duration: 1500 + }; + + Line.prototype.precalc = function() { + var d, series_data, touchHandler, ykey, ymax, ymin, _i, _j, _k, _len, _len1, _ref, _ref1, _ref2, _results, + _this = this; + this.options.data = this.options.data.slice(0); + this.options.data.sort(function(a, b) { + return (a[_this.options.xkey] < b[_this.options.xkey]) - (b[_this.options.xkey] < a[_this.options.xkey]); + }); + this.columnLabels = $.map(this.options.data, function(d) { + return d[_this.options.xkey]; + }); + this.seriesLabels = this.options.labels; + this.series = []; + _ref = this.options.ykeys; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + ykey = _ref[_i]; + series_data = []; + _ref1 = this.options.data; + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + d = _ref1[_j]; + series_data.push(typeof d[ykey] === 'number' ? d[ykey] : null); + } + this.series.push(series_data); + } + if (this.options.parseTime) { + this.xvals = $.map(this.columnLabels, function(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) { + this.columnLabels = $.map(this.columnLabels, function(d) { + if (typeof d === 'number') { + return _this.options.dateFormat(d); + } else { + return d; + } + }); + } + this.xmin = Math.min.apply(null, this.xvals); + this.xmax = Math.max.apply(null, this.xvals); + if (this.xmin === this.xmax) { + this.xmin -= 1; + this.xmax += 1; + } + if (typeof this.options.ymax === 'string' && this.options.ymax.slice(0, 4) === 'auto') { + ymax = Math.max.apply(null, Array.prototype.concat.apply([], this.series)); + if (this.options.ymax.length > 5) { + this.options.ymax = Math.max(parseInt(this.options.ymax.slice(5), 10), ymax); + } else { + this.options.ymax = ymax; + } + } + if (typeof this.options.ymin === 'string' && this.options.ymin.slice(0, 4) === 'auto') { + ymin = Math.min.apply(null, Array.prototype.concat.apply([], this.series)); + if (this.options.ymin.length > 5) { + this.options.ymin = Math.min(parseInt(this.options.ymin.slice(5), 10), ymin); + } else { + this.options.ymin = ymin; + } + } + 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.elementHeight = null; + 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.calc = function() { + var h, s, scoords, w, x, _i, _len, _ref, + _this = this; + w = this.el.width(); + h = this.el.height(); + if (this.elementWidth !== w || this.elementHeight !== h) { + this.maxYLabelWidth = Math.max(this.measureText(this.yLabelFormat(this.options.ymin), this.options.gridTextSize).width, this.measureText(this.yLabelFormat(this.options.ymax), this.options.gridTextSize).width); + this.left = this.maxYLabelWidth + this.options.marginLeft; + this.width = this.el.width() - this.left - this.options.marginRight; + this.height = this.el.height() - this.options.marginTop - this.options.marginBottom; + this.dx = this.width / (this.xmax - this.xmin); + this.dy = this.height / (this.options.ymax - this.options.ymin); + 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.transX = function(x) { + if (this.xvals.length === 1) { + return this.left + this.width / 2; + } else { + return this.left + (x - this.xmin) * this.dx; + } + }; + + Line.prototype.transY = function(y) { + return this.options.marginTop + this.height - (y - this.options.ymin) * this.dy; + }; + + Line.prototype.redraw = function() { + this.el.empty(); + this.r = new Raphael(this.el[0]); + this.calc(); + this.drawGrid(); + this.drawSeries(); + this.drawHover(); + return this.hilight(this.options.hideHover ? null : 0); + }; + + Line.prototype.drawGrid = function() { + var drawLabel, firstY, i, l, labelText, lastY, lineY, prevLabelMargin, v, xLabelMargin, y, yInterval, ypos, _i, _j, _k, _len, _ref, _ref1, _results, _results1, + _this = this; + yInterval = (this.options.ymax - this.options.ymin) / (this.options.numLines - 1); + firstY = Math.ceil(this.options.ymin / yInterval) * yInterval; + lastY = Math.floor(this.options.ymax / yInterval) * yInterval; + for (lineY = _i = firstY; firstY <= lastY ? _i <= lastY : _i >= lastY; lineY = _i += yInterval) { + v = Math.floor(lineY); + y = this.transY(v); + this.r.text(this.left - this.options.marginLeft / 2, y, this.yLabelFormat(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); + } + 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) { + 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 (_j = 0, _len = _ref.length; _j < _len; _j++) { + l = _ref[_j]; + _results.push(drawLabel(l[0], l[1])); + } + return _results; + } + } else { + _results1 = []; + for (i = _k = 0, _ref1 = this.columnLabels.length; 0 <= _ref1 ? _k <= _ref1 : _k >= _ref1; i = 0 <= _ref1 ? ++_k : --_k) { + labelText = this.columnLabels[this.columnLabels.length - i - 1]; + _results1.push(drawLabel(labelText, i)); + } + return _results1; + } + }; + + Line.prototype.drawSeries = function() { + var average, averages, c, circle, coords, i, path, rPath, straightCoords, straightPath, y, _i, _j, _ref, _ref1, _results, + _this = this; + averages = []; + for (i = _i = _ref = this.seriesCoords.length - 1; _ref <= 0 ? _i <= 0 : _i >= 0; i = _ref <= 0 ? ++_i : --_i) { + coords = this.seriesCoords[i]; + if (coords.length > 1) { + path = this.createPath(coords, this.options.marginTop, this.left, this.options.marginTop + this.height, this.left + this.width); + if (this.options.animate) { + averages[i] = average = Morris.seriesAverage(coords); + straightCoords = (function() { + var _j, _len, _results; + _results = []; + for (_j = 0, _len = coords.length; _j < _len; _j++) { + c = coords[_j]; + _results.push({ + x: c.x, + y: average + }); + } + return _results; + })(); + straightPath = this.createPath(straightCoords, this.options.marginTop, this.left, this.options.marginTop + this.height, this.left + this.width); + rPath = this.r.path(straightPath).attr('stroke', this.options.lineColors[i]).attr('stroke-width', this.options.lineWidth); + (function(rPath, path) { + return rPath.animate({ + path: path + }, _this.options.duration, _this.options.easing); + })(rPath, path); + } else { + this.r.path(path).attr('stroke', this.options.lineColors[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, + _this = this; + _ref2 = this.seriesCoords[i]; + _results1 = []; + for (_k = 0, _len = _ref2.length; _k < _len; _k++) { + c = _ref2[_k]; + if (c === null) { + circle = null; + } else { + y = this.options.animate ? averages[i] : c.y; + circle = this.r.circle(c.x, y, this.options.pointSize).attr('fill', this.options.lineColors[i]).attr('stroke-width', 1).attr('stroke', '#ffffff'); + if (this.options.animate) { + (function(circle, c) { + return circle.animate({ + cy: c.y + }, _this.options.duration, _this.options.easing); + })(circle, c); + } + } + _results1.push(this.seriesPoints[i].push(circle)); + } + return _results1; + }).call(this)); + } + return _results; + }; + + Line.prototype.createPath = function(all_coords, top, left, bottom, right) { + var c, coords, g, grads, i, ix, lc, lg, path, x1, x2, y1, y2, _i, _ref; + path = ""; + coords = $.map(all_coords, function(c) { + return c; + }); + if (this.options.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.options.lineColors[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; + if (fontSize == null) { + fontSize = 12; + } + tt = this.r.text(100, 100, text).attr('font-size', fontSize); + ret = tt.getBBox(); + tt.remove(); + return ret; + }; + + Line.prototype.yLabelFormat = function(label) { + return "" + this.options.preUnits + (Morris.commas(label)) + this.options.postUnits; + }; + + return Line; + + })(); + + Morris.parseDate = function(date) { + var isecs, m, msecs, n, o, offsetmins, p, q, r, ret, secs; + if (typeof date === '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) { + return new Date(parseInt(m[1], 10), parseInt(m[2], 10) * 3 - 1, 1).getTime(); + } else if (n) { + return new Date(parseInt(n[1], 10), parseInt(n[2], 10) - 1, 1).getTime(); + } else if (o) { + return new Date(parseInt(o[1], 10), parseInt(o[2], 10) - 1, parseInt(o[3], 10)).getTime(); + } else if (p) { + ret = new Date(parseInt(p[1], 10), 0, 1); + if (ret.getDay() !== 4) { + ret.setMonth(0, 1 + ((4 - ret.getDay()) + 7) % 7); + } + return ret.getTime() + parseInt(p[2], 10) * 604800000; + } else if (q) { + if (!q[6]) { + return 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 { + offsetmins = 0; + if (q[6] !== 'Z') { + offsetmins = parseInt(q[8], 10) * 60 + parseInt(q[9], 10); + if (q[7] === '+') { + offsetmins = 0 - offsetmins; + } + } + return 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 (!r[8]) { + return 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 { + offsetmins = 0; + if (r[8] !== 'Z') { + offsetmins = parseInt(r[10], 10) * 60 + parseInt(r[11], 10); + if (r[9] === '+') { + offsetmins = 0 - offsetmins; + } + } + return 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 { + return new Date(parseInt(date, 10), 0, 1).getTime(); + } + }; + + Morris.commas = function(num) { + var absnum, intnum, ret, strabsnum; + if (num === null) { + return "n/a"; + } else { + ret = num < 0 ? "-" : ""; + absnum = Math.abs(num); + intnum = Math.floor(absnum).toFixed(0); + ret += intnum.replace(/(?=(?:\d{3})+$)(?!^)/g, ','); + strabsnum = absnum.toString(); + if (strabsnum.length > intnum.length) { + ret += strabsnum.slice(intnum.length); + } + return ret; + } + }; + + Morris.pad2 = function(number) { + return (number < 10 ? '0' : '') + number; + }; + + Morris.labelSeries = function(dmin, dmax, pxwidth, specName, xLabelFormat) { + var d, d0, ddensity, name, ret, s, spec, t, _i, _len, _ref; + ddensity = 200 * (dmax - dmin) / pxwidth; + d0 = new Date(dmin); + spec = Morris.LABEL_SPECS[specName]; + if (spec === void 0) { + _ref = Morris.AUTO_LABEL_ORDER; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + name = _ref[_i]; + s = Morris.LABEL_SPECS[name]; + if (ddensity >= s.span) { + spec = s; + break; + } + } + } + if (spec === void 0) { + spec = Morris.LABEL_SPECS["second"]; + } + if (xLabelFormat) { + spec = $.extend({}, spec, { + fmt: xLabelFormat + }); + } + d = spec.start(d0); + ret = []; + while ((t = d.getTime()) <= dmax) { + if (t >= dmin) { + ret.push([spec.fmt(d), t]); + } + spec.incr(d); + } + return ret; + }; + + minutesSpecHelper = function(interval) { + return { + span: interval * 60 * 1000, + start: function(d) { + return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()); + }, + fmt: function(d) { + return "" + (Morris.pad2(d.getHours())) + ":" + (Morris.pad2(d.getMinutes())); + }, + incr: function(d) { + return d.setMinutes(d.getMinutes() + interval); + } + }; + }; + + secondsSpecHelper = function(interval) { + return { + span: interval * 1000, + start: function(d) { + return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes()); + }, + fmt: function(d) { + return "" + (Morris.pad2(d.getHours())) + ":" + (Morris.pad2(d.getMinutes())) + ":" + (Morris.pad2(d.getSeconds())); + }, + incr: function(d) { + return d.setSeconds(d.getSeconds() + interval); + } + }; + }; + + Morris.LABEL_SPECS = { + "year": { + span: 17280000000, + start: function(d) { + return new Date(d.getFullYear(), 0, 1); + }, + fmt: function(d) { + return "" + (d.getFullYear()); + }, + incr: function(d) { + return d.setFullYear(d.getFullYear() + 1); + } + }, + "month": { + span: 2419200000, + start: function(d) { + return new Date(d.getFullYear(), d.getMonth(), 1); + }, + fmt: function(d) { + return "" + (d.getFullYear()) + "-" + (Morris.pad2(d.getMonth() + 1)); + }, + incr: function(d) { + return d.setMonth(d.getMonth() + 1); + } + }, + "day": { + span: 86400000, + start: function(d) { + return new Date(d.getFullYear(), d.getMonth(), d.getDate()); + }, + fmt: function(d) { + return "" + (d.getFullYear()) + "-" + (Morris.pad2(d.getMonth() + 1)) + "-" + (Morris.pad2(d.getDate())); + }, + incr: function(d) { + return d.setDate(d.getDate() + 1); + } + }, + "hour": minutesSpecHelper(60), + "30min": minutesSpecHelper(30), + "15min": minutesSpecHelper(15), + "10min": minutesSpecHelper(10), + "5min": minutesSpecHelper(5), + "minute": minutesSpecHelper(1), + "30sec": secondsSpecHelper(30), + "15sec": secondsSpecHelper(15), + "10sec": secondsSpecHelper(10), + "5sec": secondsSpecHelper(5), + "second": secondsSpecHelper(1) + }; + + Morris.AUTO_LABEL_ORDER = ["year", "month", "day", "hour", "30min", "15min", "10min", "5min", "minute", "30sec", "15sec", "10sec", "5sec", "second"]; + + Morris.seriesAverage = function(series) { + var point, total, _i, _len, _ref; + total = 0; + for (_i = 0, _len = series.length; _i < _len; _i++) { + point = series[_i]; + total += (_ref = point.y) != null ? _ref : 0; + } + return (total / series.length) / 2; + }; + + window.Morris = Morris; + +}).call(this);