diff options
author | Marvin Borner | 2018-07-13 19:06:45 +0200 |
---|---|---|
committer | Marvin Borner | 2018-07-13 19:06:45 +0200 |
commit | 6fcfb7c04d32e1c8b26a312295bf7ac3ec2d2ad7 (patch) | |
tree | dbc87ef16fa01d5d99116de283592b8fe5e02944 /public/bower_components/morris.js/lib | |
parent | dfd839f27146df0ad0494e11734fc7d310c70ebf (diff) |
Fixed many permissions and began admin interface
Diffstat (limited to 'public/bower_components/morris.js/lib')
7 files changed, 1478 insertions, 0 deletions
diff --git a/public/bower_components/morris.js/lib/morris.area.coffee b/public/bower_components/morris.js/lib/morris.area.coffee new file mode 100644 index 0000000..86f8595 --- /dev/null +++ b/public/bower_components/morris.js/lib/morris.area.coffee @@ -0,0 +1,66 @@ +class Morris.Area extends Morris.Line + # Initialise + # + areaDefaults = + fillOpacity: 'auto' + behaveLikeLine: false + + constructor: (options) -> + return new Morris.Area(options) unless (@ instanceof Morris.Area) + areaOptions = $.extend {}, areaDefaults, options + + @cumulative = not areaOptions.behaveLikeLine + + if areaOptions.fillOpacity is 'auto' + areaOptions.fillOpacity = if areaOptions.behaveLikeLine then .8 else 1 + + super(areaOptions) + + # calculate series data point coordinates + # + # @private + calcPoints: -> + for row in @data + row._x = @transX(row.x) + total = 0 + row._y = for y in row.y + if @options.behaveLikeLine + @transY(y) + else + total += (y || 0) + @transY(total) + row._ymax = Math.max row._y... + + # draw the data series + # + # @private + drawSeries: -> + @seriesPoints = [] + if @options.behaveLikeLine + range = [0..@options.ykeys.length-1] + else + range = [@options.ykeys.length-1..0] + + for i in range + @_drawFillFor i + @_drawLineFor i + @_drawPointFor i + + _drawFillFor: (index) -> + path = @paths[index] + if path isnt null + path = path + "L#{@transX(@xmax)},#{@bottom}L#{@transX(@xmin)},#{@bottom}Z" + @drawFilledPath path, @fillForSeries(index) + + fillForSeries: (i) -> + color = Raphael.rgb2hsl @colorFor(@data[i], i, 'line') + Raphael.hsl( + color.h, + if @options.behaveLikeLine then color.s * 0.9 else color.s * 0.75, + Math.min(0.98, if @options.behaveLikeLine then color.l * 1.2 else color.l * 1.25)) + + drawFilledPath: (path, fill) -> + @raphael.path(path) + .attr('fill', fill) + .attr('fill-opacity', @options.fillOpacity) + .attr('stroke', 'none') diff --git a/public/bower_components/morris.js/lib/morris.bar.coffee b/public/bower_components/morris.js/lib/morris.bar.coffee new file mode 100644 index 0000000..86fb32b --- /dev/null +++ b/public/bower_components/morris.js/lib/morris.bar.coffee @@ -0,0 +1,208 @@ +class Morris.Bar extends Morris.Grid + constructor: (options) -> + return new Morris.Bar(options) unless (@ instanceof Morris.Bar) + super($.extend {}, options, parseTime: false) + + init: -> + @cumulative = @options.stacked + + if @options.hideHover isnt 'always' + @hover = new Morris.Hover(parent: @el) + @on('hovermove', @onHoverMove) + @on('hoverout', @onHoverOut) + @on('gridclick', @onGridClick) + + # Default configuration + # + defaults: + barSizeRatio: 0.75 + barGap: 3 + barColors: [ + '#0b62a4' + '#7a92a3' + '#4da74d' + '#afd8f8' + '#edc240' + '#cb4b4b' + '#9440ed' + ], + barOpacity: 1.0 + barRadius: [0, 0, 0, 0] + xLabelMargin: 50 + + # Do any size-related calculations + # + # @private + calc: -> + @calcBars() + if @options.hideHover is false + @hover.update(@hoverContentForRow(@data.length - 1)...) + + # calculate series data bars coordinates and sizes + # + # @private + calcBars: -> + for row, idx in @data + row._x = @left + @width * (idx + 0.5) / @data.length + row._y = for y in row.y + if y? then @transY(y) else null + + # Draws the bar chart. + # + draw: -> + @drawXAxis() if @options.axes in [true, 'both', 'x'] + @drawSeries() + + # draw the x-axis labels + # + # @private + drawXAxis: -> + # draw x axis labels + ypos = @bottom + (@options.xAxisLabelTopPadding || @options.padding / 2) + prevLabelMargin = null + prevAngleMargin = null + for i in [0...@data.length] + row = @data[@data.length - 1 - i] + label = @drawXAxisLabel(row._x, ypos, row.label) + textBox = label.getBBox() + label.transform("r#{-@options.xLabelAngle}") + labelBox = label.getBBox() + label.transform("t0,#{labelBox.height / 2}...") + if @options.xLabelAngle != 0 + offset = -0.5 * textBox.width * + Math.cos(@options.xLabelAngle * Math.PI / 180.0) + label.transform("t#{offset},0...") + # try to avoid overlaps + if (not prevLabelMargin? or + prevLabelMargin >= labelBox.x + labelBox.width or + prevAngleMargin? and prevAngleMargin >= labelBox.x) and + labelBox.x >= 0 and (labelBox.x + labelBox.width) < @el.width() + if @options.xLabelAngle != 0 + margin = 1.25 * @options.gridTextSize / + Math.sin(@options.xLabelAngle * Math.PI / 180.0) + prevAngleMargin = labelBox.x - margin + prevLabelMargin = labelBox.x - @options.xLabelMargin + else + label.remove() + + # draw the data series + # + # @private + drawSeries: -> + groupWidth = @width / @options.data.length + numBars = if @options.stacked then 1 else @options.ykeys.length + barWidth = (groupWidth * @options.barSizeRatio - @options.barGap * (numBars - 1)) / numBars + barWidth = Math.min(barWidth, @options.barSize) if @options.barSize + spaceLeft = groupWidth - barWidth * numBars - @options.barGap * (numBars - 1) + leftPadding = spaceLeft / 2 + zeroPos = if @ymin <= 0 and @ymax >= 0 then @transY(0) else null + @bars = for row, idx in @data + lastTop = 0 + for ypos, sidx in row._y + if ypos != null + if zeroPos + top = Math.min(ypos, zeroPos) + bottom = Math.max(ypos, zeroPos) + else + top = ypos + bottom = @bottom + + left = @left + idx * groupWidth + leftPadding + left += sidx * (barWidth + @options.barGap) unless @options.stacked + size = bottom - top + + if @options.verticalGridCondition and @options.verticalGridCondition(row.x) + @drawBar(@left + idx * groupWidth, @top, groupWidth, Math.abs(@top - @bottom), @options.verticalGridColor, @options.verticalGridOpacity, @options.barRadius) + + top -= lastTop if @options.stacked + @drawBar(left, top, barWidth, size, @colorFor(row, sidx, 'bar'), + @options.barOpacity, @options.barRadius) + + lastTop += size + else + null + + # @private + # + # @param row [Object] row data + # @param sidx [Number] series index + # @param type [String] "bar", "hover" or "label" + colorFor: (row, sidx, type) -> + if typeof @options.barColors is 'function' + r = { x: row.x, y: row.y[sidx], label: row.label } + s = { index: sidx, key: @options.ykeys[sidx], label: @options.labels[sidx] } + @options.barColors.call(@, r, s, type) + else + @options.barColors[sidx % @options.barColors.length] + + # hit test - returns the index of the row at the given x-coordinate + # + hitTest: (x) -> + return null if @data.length == 0 + x = Math.max(Math.min(x, @right), @left) + Math.min(@data.length - 1, + Math.floor((x - @left) / (@width / @data.length))) + + # click on grid event handler + # + # @private + onGridClick: (x, y) => + index = @hitTest(x) + @fire 'click', index, @data[index].src, x, y + + # hover movement event handler + # + # @private + onHoverMove: (x, y) => + index = @hitTest(x) + @hover.update(@hoverContentForRow(index)...) + + # hover out event handler + # + # @private + onHoverOut: => + if @options.hideHover isnt false + @hover.hide() + + # hover content for a point + # + # @private + hoverContentForRow: (index) -> + row = @data[index] + content = "<div class='morris-hover-row-label'>#{row.label}</div>" + for y, j in row.y + content += """ + <div class='morris-hover-point' style='color: #{@colorFor(row, j, 'label')}'> + #{@options.labels[j]}: + #{@yLabelFormat(y)} + </div> + """ + if typeof @options.hoverCallback is 'function' + content = @options.hoverCallback(index, @options, content, row.src) + x = @left + (index + 0.5) * @width / @data.length + [content, x] + + drawXAxisLabel: (xPos, yPos, text) -> + label = @raphael.text(xPos, yPos, text) + .attr('font-size', @options.gridTextSize) + .attr('font-family', @options.gridTextFamily) + .attr('font-weight', @options.gridTextWeight) + .attr('fill', @options.gridTextColor) + + drawBar: (xPos, yPos, width, height, barColor, opacity, radiusArray) -> + maxRadius = Math.max(radiusArray...) + if maxRadius == 0 or maxRadius > height + path = @raphael.rect(xPos, yPos, width, height) + else + path = @raphael.path @roundedRect(xPos, yPos, width, height, radiusArray) + path + .attr('fill', barColor) + .attr('fill-opacity', opacity) + .attr('stroke', 'none') + + roundedRect: (x, y, w, h, r = [0,0,0,0]) -> + [ "M", x, r[0] + y, "Q", x, y, x + r[0], y, + "L", x + w - r[1], y, "Q", x + w, y, x + w, y + r[1], + "L", x + w, y + h - r[2], "Q", x + w, y + h, x + w - r[2], y + h, + "L", x + r[3], y + h, "Q", x, y + h, x, y + h - r[3], "Z" ] + diff --git a/public/bower_components/morris.js/lib/morris.coffee b/public/bower_components/morris.js/lib/morris.coffee new file mode 100644 index 0000000..f2cd2df --- /dev/null +++ b/public/bower_components/morris.js/lib/morris.coffee @@ -0,0 +1,43 @@ +Morris = window.Morris = {} + +$ = jQuery + +# Very simple event-emitter class. +# +# @private +class Morris.EventEmitter + on: (name, handler) -> + unless @handlers? + @handlers = {} + unless @handlers[name]? + @handlers[name] = [] + @handlers[name].push(handler) + @ + + fire: (name, args...) -> + if @handlers? and @handlers[name]? + for handler in @handlers[name] + handler(args...) + +# Make long numbers prettier by inserting commas. +# +# @example +# Morris.commas(1234567) -> '1,234,567' +Morris.commas = (num) -> + if num? + 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 + else + '-' + +# Zero-pad numbers to two characters wide. +# +# @example +# Morris.pad2(1) -> '01' +Morris.pad2 = (number) -> (if number < 10 then '0' else '') + number diff --git a/public/bower_components/morris.js/lib/morris.donut.coffee b/public/bower_components/morris.js/lib/morris.donut.coffee new file mode 100644 index 0000000..e40314d --- /dev/null +++ b/public/bower_components/morris.js/lib/morris.donut.coffee @@ -0,0 +1,213 @@ +# Donut charts. +# +# @example +# Morris.Donut({ +# el: $('#donut-container'), +# data: [ +# { label: 'yin', value: 50 }, +# { label: 'yang', value: 50 } +# ] +# }); +class Morris.Donut extends Morris.EventEmitter + defaults: + colors: [ + '#0B62A4' + '#3980B5' + '#679DC6' + '#95BBD7' + '#B0CCE1' + '#095791' + '#095085' + '#083E67' + '#052C48' + '#042135' + ], + backgroundColor: '#FFFFFF', + labelColor: '#000000', + formatter: Morris.commas + resize: false + + # Create and render a donut chart. + # + constructor: (options) -> + return new Morris.Donut(options) unless (@ instanceof Morris.Donut) + @options = $.extend {}, @defaults, 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.") + + # bail if there's no data + if options.data is undefined or options.data.length is 0 + return + + @raphael = new Raphael(@el[0]) + + if @options.resize + $(window).bind 'resize', (evt) => + if @timeoutId? + window.clearTimeout @timeoutId + @timeoutId = window.setTimeout @resizeHandler, 100 + + @setData options.data + + # Clear and redraw the chart. + redraw: -> + @raphael.clear() + + cx = @el.width() / 2 + cy = @el.height() / 2 + w = (Math.min(cx, cy) - 10) / 3 + + total = 0 + total += value for value in @values + + min = 5 / (2 * w) + C = 1.9999 * Math.PI - min * @data.length + + last = 0 + idx = 0 + @segments = [] + for value, i in @values + next = last + min + C * (value / total) + seg = new Morris.DonutSegment( + cx, cy, w*2, w, last, next, + @data[i].color || @options.colors[idx % @options.colors.length], + @options.backgroundColor, idx, @raphael) + seg.render() + @segments.push seg + seg.on 'hover', @select + seg.on 'click', @click + last = next + idx += 1 + + @text1 = @drawEmptyDonutLabel(cx, cy - 10, @options.labelColor, 15, 800) + @text2 = @drawEmptyDonutLabel(cx, cy + 10, @options.labelColor, 14) + + max_value = Math.max @values... + idx = 0 + for value in @values + if value == max_value + @select idx + break + idx += 1 + + setData: (data) -> + @data = data + @values = (parseFloat(row.value) for row in @data) + @redraw() + + # @private + click: (idx) => + @fire 'click', idx, @data[idx] + + # Select the segment at the given index. + select: (idx) => + s.deselect() for s in @segments + segment = @segments[idx] + segment.select() + row = @data[idx] + @setLabels(row.label, @options.formatter(row.value, row)) + + + + # @private + setLabels: (label1, label2) -> + inner = (Math.min(@el.width() / 2, @el.height() / 2) - 10) * 2 / 3 + maxWidth = 1.8 * inner + maxHeightTop = inner / 2 + maxHeightBottom = inner / 3 + @text1.attr(text: label1, transform: '') + text1bbox = @text1.getBBox() + text1scale = Math.min(maxWidth / text1bbox.width, maxHeightTop / text1bbox.height) + @text1.attr(transform: "S#{text1scale},#{text1scale},#{text1bbox.x + text1bbox.width / 2},#{text1bbox.y + text1bbox.height}") + @text2.attr(text: label2, transform: '') + text2bbox = @text2.getBBox() + text2scale = Math.min(maxWidth / text2bbox.width, maxHeightBottom / text2bbox.height) + @text2.attr(transform: "S#{text2scale},#{text2scale},#{text2bbox.x + text2bbox.width / 2},#{text2bbox.y}") + + drawEmptyDonutLabel: (xPos, yPos, color, fontSize, fontWeight) -> + text = @raphael.text(xPos, yPos, '') + .attr('font-size', fontSize) + .attr('fill', color) + text.attr('font-weight', fontWeight) if fontWeight? + return text + + resizeHandler: => + @timeoutId = null + @raphael.setSize @el.width(), @el.height() + @redraw() + + +# A segment within a donut chart. +# +# @private +class Morris.DonutSegment extends Morris.EventEmitter + constructor: (@cx, @cy, @inner, @outer, p0, p1, @color, @backgroundColor, @index, @raphael) -> + @sin_p0 = Math.sin(p0) + @cos_p0 = Math.cos(p0) + @sin_p1 = Math.sin(p1) + @cos_p1 = Math.cos(p1) + @is_long = if (p1 - p0) > Math.PI then 1 else 0 + @path = @calcSegment(@inner + 3, @inner + @outer - 5) + @selectedPath = @calcSegment(@inner + 3, @inner + @outer) + @hilight = @calcArc(@inner) + + calcArcPoints: (r) -> + return [ + @cx + r * @sin_p0, + @cy + r * @cos_p0, + @cx + r * @sin_p1, + @cy + r * @cos_p1] + + calcSegment: (r1, r2) -> + [ix0, iy0, ix1, iy1] = @calcArcPoints(r1) + [ox0, oy0, ox1, oy1] = @calcArcPoints(r2) + return ( + "M#{ix0},#{iy0}" + + "A#{r1},#{r1},0,#{@is_long},0,#{ix1},#{iy1}" + + "L#{ox1},#{oy1}" + + "A#{r2},#{r2},0,#{@is_long},1,#{ox0},#{oy0}" + + "Z") + + calcArc: (r) -> + [ix0, iy0, ix1, iy1] = @calcArcPoints(r) + return ( + "M#{ix0},#{iy0}" + + "A#{r},#{r},0,#{@is_long},0,#{ix1},#{iy1}") + + render: -> + @arc = @drawDonutArc(@hilight, @color) + @seg = @drawDonutSegment( + @path, + @color, + @backgroundColor, + => @fire('hover', @index), + => @fire('click', @index) + ) + + drawDonutArc: (path, color) -> + @raphael.path(path) + .attr(stroke: color, 'stroke-width': 2, opacity: 0) + + drawDonutSegment: (path, fillColor, strokeColor, hoverFunction, clickFunction) -> + @raphael.path(path) + .attr(fill: fillColor, stroke: strokeColor, 'stroke-width': 3) + .hover(hoverFunction) + .click(clickFunction) + + select: => + unless @selected + @seg.animate(path: @selectedPath, 150, '<>') + @arc.animate(opacity: 1, 150, '<>') + @selected = true + + deselect: => + if @selected + @seg.animate(path: @path, 150, '<>') + @arc.animate(opacity: 0, 150, '<>') + @selected = false diff --git a/public/bower_components/morris.js/lib/morris.grid.coffee b/public/bower_components/morris.js/lib/morris.grid.coffee new file mode 100644 index 0000000..db0882c --- /dev/null +++ b/public/bower_components/morris.js/lib/morris.grid.coffee @@ -0,0 +1,499 @@ +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 not @el? or @el.length == 0 + throw new Error("Graph container element not found") + + if @el.css('position') == 'static' + @el.css('position', 'relative') + + @options = $.extend {}, @gridDefaults, (@defaults || {}), options + + # backwards compatibility for units -> postUnits + if typeof @options.units is 'string' + @options.postUnits = options.units + + # the raphael drawing instance + @raphael = new Raphael(@el[0]) + + # some redraw stuff + @elementWidth = null + @elementHeight = null + @dirty = false + + # range selection + @selectFrom = null + + # more stuff + @init() if @init + + # load data + @setData @options.data + + # hover + @el.bind 'mousemove', (evt) => + offset = @el.offset() + x = evt.pageX - offset.left + if @selectFrom + left = @data[@hitTest(Math.min(x, @selectFrom))]._x + right = @data[@hitTest(Math.max(x, @selectFrom))]._x + width = right - left + @selectionRect.attr({ x: left, width: width }) + else + @fire 'hovermove', x, evt.pageY - offset.top + + @el.bind 'mouseleave', (evt) => + if @selectFrom + @selectionRect.hide() + @selectFrom = null + @fire 'hoverout' + + @el.bind 'touchstart touchmove touchend', (evt) => + touch = evt.originalEvent.touches[0] or evt.originalEvent.changedTouches[0] + offset = @el.offset() + @fire 'hovermove', touch.pageX - offset.left, touch.pageY - offset.top + + @el.bind 'click', (evt) => + offset = @el.offset() + @fire 'gridclick', evt.pageX - offset.left, evt.pageY - offset.top + + if @options.rangeSelect + @selectionRect = @raphael.rect(0, 0, 0, @el.innerHeight()) + .attr({ fill: @options.rangeSelectColor, stroke: false }) + .toBack() + .hide() + + @el.bind 'mousedown', (evt) => + offset = @el.offset() + @startRange evt.pageX - offset.left + + @el.bind 'mouseup', (evt) => + offset = @el.offset() + @endRange evt.pageX - offset.left + @fire 'hovermove', evt.pageX - offset.left, evt.pageY - offset.top + + if @options.resize + $(window).bind 'resize', (evt) => + if @timeoutId? + window.clearTimeout @timeoutId + @timeoutId = window.setTimeout @resizeHandler, 100 + + # Disable tap highlight on iOS. + @el.css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)') + + @postInit() if @postInit + + # Default options + # + gridDefaults: + dateFormat: null + axes: true + grid: true + gridLineColor: '#aaa' + gridStrokeWidth: 0.5 + gridTextColor: '#888' + gridTextSize: 12 + gridTextFamily: 'sans-serif' + gridTextWeight: 'normal' + hideHover: false + yLabelFormat: null + xLabelAngle: 0 + numLines: 5 + padding: 25 + parseTime: true + postUnits: '' + preUnits: '' + ymax: 'auto' + ymin: 'auto 0' + goals: [] + goalStrokeWidth: 1.0 + goalLineColors: [ + '#666633' + '#999966' + '#cc6666' + '#663333' + ] + events: [] + eventStrokeWidth: 1.0 + eventLineColors: [ + '#005a04' + '#ccffbb' + '#3a5f0b' + '#005502' + ] + rangeSelect: null + rangeSelectColor: '#eef' + resize: false + + # Update the data series and redraw the chart. + # + setData: (data, redraw = true) -> + @options.data = data + + if !data? or data.length == 0 + @data = [] + @raphael.clear() + @hover.hide() if @hover? + return + + ymax = if @cumulative then 0 else null + ymin = if @cumulative then 0 else null + + if @options.goals.length > 0 + minGoal = Math.min @options.goals... + maxGoal = Math.max @options.goals... + ymin = if ymin? then Math.min(ymin, minGoal) else minGoal + ymax = if ymax? then Math.max(ymax, maxGoal) else maxGoal + + @data = for row, index in data + ret = {src: row} + + ret.label = row[@options.xkey] + if @options.parseTime + ret.x = Morris.parseDate(ret.label) + if @options.dateFormat + ret.label = @options.dateFormat ret.x + else if typeof ret.label is 'number' + ret.label = new Date(ret.label).toString() + else + ret.x = index + if @options.xLabelFormat + ret.label = @options.xLabelFormat ret + total = 0 + ret.y = for ykey, idx in @options.ykeys + yval = row[ykey] + yval = parseFloat(yval) if typeof yval is 'string' + yval = null if yval? and typeof yval isnt 'number' + if yval? + if @cumulative + total += yval + else + if ymax? + ymax = Math.max(yval, ymax) + ymin = Math.min(yval, ymin) + else + ymax = ymin = yval + if @cumulative and total? + ymax = Math.max(total, ymax) + ymin = Math.min(total, ymin) + yval + ret + + if @options.parseTime + @data = @data.sort (a, b) -> (a.x > b.x) - (b.x > a.x) + + # calculate horizontal range of the graph + @xmin = @data[0].x + @xmax = @data[@data.length - 1].x + + @events = [] + if @options.events.length > 0 + if @options.parseTime + @events = (Morris.parseDate(e) for e in @options.events) + else + @events = @options.events + @xmax = Math.max(@xmax, Math.max(@events...)) + @xmin = Math.min(@xmin, Math.min(@events...)) + + if @xmin is @xmax + @xmin -= 1 + @xmax += 1 + + @ymin = @yboundary('min', ymin) + @ymax = @yboundary('max', ymax) + + if @ymin is @ymax + @ymin -= 1 if ymin + @ymax += 1 + + if @options.axes in [true, 'both', 'y'] or @options.grid is true + if (@options.ymax == @gridDefaults.ymax and + @options.ymin == @gridDefaults.ymin) + # calculate 'magic' grid placement + @grid = @autoGridLines(@ymin, @ymax, @options.numLines) + @ymin = Math.min(@ymin, @grid[0]) + @ymax = Math.max(@ymax, @grid[@grid.length - 1]) + else + step = (@ymax - @ymin) / (@options.numLines - 1) + @grid = (y for y in [@ymin..@ymax] by step) + + @dirty = true + @redraw() if redraw + + yboundary: (boundaryType, currentValue) -> + boundaryOption = @options["y#{boundaryType}"] + if typeof boundaryOption is 'string' + if boundaryOption[0..3] is 'auto' + if boundaryOption.length > 5 + suggestedValue = parseInt(boundaryOption[5..], 10) + return suggestedValue unless currentValue? + Math[boundaryType](currentValue, suggestedValue) + else + if currentValue? then currentValue else 0 + else + parseInt(boundaryOption, 10) + else + boundaryOption + + autoGridLines: (ymin, ymax, nlines) -> + span = ymax - ymin + ymag = Math.floor(Math.log(span) / Math.log(10)) + unit = Math.pow(10, ymag) + + # calculate initial grid min and max values + gmin = Math.floor(ymin / unit) * unit + gmax = Math.ceil(ymax / unit) * unit + step = (gmax - gmin) / (nlines - 1) + if unit == 1 and step > 1 and Math.ceil(step) != step + step = Math.ceil(step) + gmax = gmin + step * (nlines - 1) + + # ensure zero is plotted where the range includes zero + if gmin < 0 and gmax > 0 + gmin = Math.floor(ymin / step) * step + gmax = Math.ceil(ymax / step) * step + + # special case for decimal numbers + if step < 1 + smag = Math.floor(Math.log(step) / Math.log(10)) + grid = for y in [gmin..gmax] by step + parseFloat(y.toFixed(1 - smag)) + else + grid = (y for y in [gmin..gmax] by step) + grid + + _calc: -> + w = @el.width() + h = @el.height() + + if @elementWidth != w or @elementHeight != h or @dirty + @elementWidth = w + @elementHeight = h + @dirty = false + # recalculate grid dimensions + @left = @options.padding + @right = @elementWidth - @options.padding + @top = @options.padding + @bottom = @elementHeight - @options.padding + if @options.axes in [true, 'both', 'y'] + yLabelWidths = for gridLine in @grid + @measureText(@yAxisFormat(gridLine)).width + @left += Math.max(yLabelWidths...) + if @options.axes in [true, 'both', 'x'] + bottomOffsets = for i in [0...@data.length] + @measureText(@data[i].text, -@options.xLabelAngle).height + @bottom -= Math.max(bottomOffsets...) + @width = Math.max(1, @right - @left) + @height = Math.max(1, @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 @data.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: -> + @raphael.clear() + @_calc() + @drawGrid() + @drawGoals() + @drawEvents() + @draw() if @draw + + # @private + # + measureText: (text, angle = 0) -> + tt = @raphael.text(100, 100, text) + .attr('font-size', @options.gridTextSize) + .attr('font-family', @options.gridTextFamily) + .attr('font-weight', @options.gridTextWeight) + .rotate(angle) + ret = tt.getBBox() + tt.remove() + ret + + # @private + # + yAxisFormat: (label) -> @yLabelFormat(label) + + # @private + # + yLabelFormat: (label) -> + if typeof @options.yLabelFormat is 'function' + @options.yLabelFormat(label) + else + "#{@options.preUnits}#{Morris.commas(label)}#{@options.postUnits}" + + # draw y axis labels, horizontal lines + # + drawGrid: -> + return if @options.grid is false and @options.axes not in [true, 'both', 'y'] + for lineY in @grid + y = @transY(lineY) + if @options.axes in [true, 'both', 'y'] + @drawYAxisLabel(@left - @options.padding / 2, y, @yAxisFormat(lineY)) + if @options.grid + @drawGridLine("M#{@left},#{y}H#{@left + @width}") + + # draw goals horizontal lines + # + drawGoals: -> + for goal, i in @options.goals + color = @options.goalLineColors[i % @options.goalLineColors.length] + @drawGoal(goal, color) + + # draw events vertical lines + drawEvents: -> + for event, i in @events + color = @options.eventLineColors[i % @options.eventLineColors.length] + @drawEvent(event, color) + + drawGoal: (goal, color) -> + @raphael.path("M#{@left},#{@transY(goal)}H#{@right}") + .attr('stroke', color) + .attr('stroke-width', @options.goalStrokeWidth) + + drawEvent: (event, color) -> + @raphael.path("M#{@transX(event)},#{@bottom}V#{@top}") + .attr('stroke', color) + .attr('stroke-width', @options.eventStrokeWidth) + + drawYAxisLabel: (xPos, yPos, text) -> + @raphael.text(xPos, yPos, text) + .attr('font-size', @options.gridTextSize) + .attr('font-family', @options.gridTextFamily) + .attr('font-weight', @options.gridTextWeight) + .attr('fill', @options.gridTextColor) + .attr('text-anchor', 'end') + + drawGridLine: (path) -> + @raphael.path(path) + .attr('stroke', @options.gridLineColor) + .attr('stroke-width', @options.gridStrokeWidth) + + # Range selection + # + startRange: (x) -> + @hover.hide() + @selectFrom = x + @selectionRect.attr({ x: x, width: 0 }).show() + + endRange: (x) -> + if @selectFrom + start = Math.min(@selectFrom, x) + end = Math.max(@selectFrom, x) + @options.rangeSelect.call @el, + start: @data[@hitTest(start)].x + end: @data[@hitTest(end)].x + @selectFrom = null + + resizeHandler: => + @timeoutId = null + @raphael.setSize @el.width(), @el.height() + @redraw() + +# 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() + diff --git a/public/bower_components/morris.js/lib/morris.hover.coffee b/public/bower_components/morris.js/lib/morris.hover.coffee new file mode 100644 index 0000000..530cb08 --- /dev/null +++ b/public/bower_components/morris.js/lib/morris.hover.coffee @@ -0,0 +1,44 @@ +class Morris.Hover + # Displays contextual information in a floating HTML div. + + @defaults: + class: 'morris-hover morris-default-style' + + constructor: (options = {}) -> + @options = $.extend {}, Morris.Hover.defaults, options + @el = $ "<div class='#{@options.class}'></div>" + @el.hide() + @options.parent.append(@el) + + update: (html, x, y) -> + if not html + @hide() + else + @html(html) + @show() + @moveTo(x, y) + + html: (content) -> + @el.html(content) + + moveTo: (x, y) -> + parentWidth = @options.parent.innerWidth() + parentHeight = @options.parent.innerHeight() + hoverWidth = @el.outerWidth() + hoverHeight = @el.outerHeight() + left = Math.min(Math.max(0, x - hoverWidth / 2), parentWidth - hoverWidth) + if y? + top = y - hoverHeight - 10 + if top < 0 + top = y + 10 + if top + hoverHeight > parentHeight + top = parentHeight / 2 - hoverHeight / 2 + else + top = parentHeight / 2 - hoverHeight / 2 + @el.css(left: left + "px", top: parseInt(top) + "px") + + show: -> + @el.show() + + hide: -> + @el.hide() diff --git a/public/bower_components/morris.js/lib/morris.line.coffee b/public/bower_components/morris.js/lib/morris.line.coffee new file mode 100644 index 0000000..ea5e7dd --- /dev/null +++ b/public/bower_components/morris.js/lib/morris.line.coffee @@ -0,0 +1,405 @@ +class Morris.Line extends Morris.Grid + # Initialise the graph. + # + constructor: (options) -> + return new Morris.Line(options) unless (@ instanceof Morris.Line) + super(options) + + init: -> + # Some instance variables for later + if @options.hideHover isnt 'always' + @hover = new Morris.Hover(parent: @el) + @on('hovermove', @onHoverMove) + @on('hoverout', @onHoverOut) + @on('gridclick', @onGridClick) + + # Default configuration + # + defaults: + lineWidth: 3 + pointSize: 4 + lineColors: [ + '#0b62a4' + '#7A92A3' + '#4da74d' + '#afd8f8' + '#edc240' + '#cb4b4b' + '#9440ed' + ] + pointStrokeWidths: [1] + pointStrokeColors: ['#ffffff'] + pointFillColors: [] + smooth: true + xLabels: 'auto' + xLabelFormat: null + xLabelMargin: 24 + hideHover: false + + # Do any size-related calculations + # + # @private + calc: -> + @calcPoints() + @generatePaths() + + # calculate series data point coordinates + # + # @private + calcPoints: -> + for row in @data + row._x = @transX(row.x) + row._y = for y in row.y + if y? then @transY(y) else y + row._ymax = Math.min [@bottom].concat(y for y in row._y when y?)... + + # hit test - returns the index of the row at the given x-coordinate + # + hitTest: (x) -> + return null if @data.length == 0 + # TODO better search algo + for r, index in @data.slice(1) + break if x < (r._x + @data[index]._x) / 2 + index + + # click on grid event handler + # + # @private + onGridClick: (x, y) => + index = @hitTest(x) + @fire 'click', index, @data[index].src, x, y + + # hover movement event handler + # + # @private + onHoverMove: (x, y) => + index = @hitTest(x) + @displayHoverForRow(index) + + # hover out event handler + # + # @private + onHoverOut: => + if @options.hideHover isnt false + @displayHoverForRow(null) + + # display a hover popup over the given row + # + # @private + displayHoverForRow: (index) -> + if index? + @hover.update(@hoverContentForRow(index)...) + @hilight(index) + else + @hover.hide() + @hilight() + + # hover content for a point + # + # @private + hoverContentForRow: (index) -> + row = @data[index] + content = "<div class='morris-hover-row-label'>#{row.label}</div>" + for y, j in row.y + content += """ + <div class='morris-hover-point' style='color: #{@colorFor(row, j, 'label')}'> + #{@options.labels[j]}: + #{@yLabelFormat(y)} + </div> + """ + if typeof @options.hoverCallback is 'function' + content = @options.hoverCallback(index, @options, content, row.src) + [content, row._x, row._ymax] + + + # generate paths for series lines + # + # @private + generatePaths: -> + @paths = for i in [0...@options.ykeys.length] + smooth = if typeof @options.smooth is "boolean" then @options.smooth else @options.ykeys[i] in @options.smooth + coords = ({x: r._x, y: r._y[i]} for r in @data when r._y[i] isnt undefined) + + if coords.length > 1 + Morris.Line.createPath coords, smooth, @bottom + else + null + + # Draws the line chart. + # + draw: -> + @drawXAxis() if @options.axes in [true, 'both', 'x'] + @drawSeries() + if @options.hideHover is false + @displayHoverForRow(@data.length - 1) + + # draw the x-axis labels + # + # @private + drawXAxis: -> + # draw x axis labels + ypos = @bottom + @options.padding / 2 + prevLabelMargin = null + prevAngleMargin = null + drawLabel = (labelText, xpos) => + label = @drawXAxisLabel(@transX(xpos), ypos, labelText) + textBox = label.getBBox() + label.transform("r#{-@options.xLabelAngle}") + labelBox = label.getBBox() + label.transform("t0,#{labelBox.height / 2}...") + if @options.xLabelAngle != 0 + offset = -0.5 * textBox.width * + Math.cos(@options.xLabelAngle * Math.PI / 180.0) + label.transform("t#{offset},0...") + # try to avoid overlaps + labelBox = label.getBBox() + if (not prevLabelMargin? or + prevLabelMargin >= labelBox.x + labelBox.width or + prevAngleMargin? and prevAngleMargin >= labelBox.x) and + labelBox.x >= 0 and (labelBox.x + labelBox.width) < @el.width() + if @options.xLabelAngle != 0 + margin = 1.25 * @options.gridTextSize / + Math.sin(@options.xLabelAngle * Math.PI / 180.0) + prevAngleMargin = labelBox.x - margin + prevLabelMargin = labelBox.x - @options.xLabelMargin + else + label.remove() + if @options.parseTime + if @data.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 + labels = [[@data[0].label, @data[0].x]] + else + labels = Morris.labelSeries(@xmin, @xmax, @width, @options.xLabels, @options.xLabelFormat) + else + labels = ([row.label, row.x] for row in @data) + labels.reverse() + for l in labels + drawLabel(l[0], l[1]) + + # draw the data series + # + # @private + drawSeries: -> + @seriesPoints = [] + for i in [@options.ykeys.length-1..0] + @_drawLineFor i + for i in [@options.ykeys.length-1..0] + @_drawPointFor i + + _drawPointFor: (index) -> + @seriesPoints[index] = [] + for row in @data + circle = null + if row._y[index]? + circle = @drawLinePoint(row._x, row._y[index], @colorFor(row, index, 'point'), index) + @seriesPoints[index].push(circle) + + _drawLineFor: (index) -> + path = @paths[index] + if path isnt null + @drawLinePath path, @colorFor(null, index, 'line'), index + + # create a path for a data series + # + # @private + @createPath: (coords, smooth, bottom) -> + path = "" + grads = Morris.Line.gradients(coords) if smooth + + prevCoord = {y: null} + for coord, i in coords + if coord.y? + if prevCoord.y? + if smooth + g = grads[i] + lg = grads[i - 1] + ix = (coord.x - prevCoord.x) / 4 + x1 = prevCoord.x + ix + y1 = Math.min(bottom, prevCoord.y + ix * lg) + x2 = coord.x - ix + y2 = Math.min(bottom, coord.y - ix * g) + path += "C#{x1},#{y1},#{x2},#{y2},#{coord.x},#{coord.y}" + else + path += "L#{coord.x},#{coord.y}" + else + if not smooth or grads[i]? + path += "M#{coord.x},#{coord.y}" + prevCoord = coord + return path + + # calculate a gradient at each point for a series of points + # + # @private + @gradients: (coords) -> + grad = (a, b) -> (a.y - b.y) / (a.x - b.x) + for coord, i in coords + if coord.y? + nextCoord = coords[i + 1] or {y: null} + prevCoord = coords[i - 1] or {y: null} + if prevCoord.y? and nextCoord.y? + grad(prevCoord, nextCoord) + else if prevCoord.y? + grad(prevCoord, coord) + else if nextCoord.y? + grad(coord, nextCoord) + else + null + else + null + + # @private + 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 @pointShrinkSeries(i) + if index isnt null and @prevHilight isnt index + for i in [0..@seriesPoints.length-1] + if @seriesPoints[i][index] + @seriesPoints[i][index].animate @pointGrowSeries(i) + @prevHilight = index + + colorFor: (row, sidx, type) -> + if typeof @options.lineColors is 'function' + @options.lineColors.call(@, row, sidx, type) + else if type is 'point' + @options.pointFillColors[sidx % @options.pointFillColors.length] || @options.lineColors[sidx % @options.lineColors.length] + else + @options.lineColors[sidx % @options.lineColors.length] + + drawXAxisLabel: (xPos, yPos, text) -> + @raphael.text(xPos, yPos, text) + .attr('font-size', @options.gridTextSize) + .attr('font-family', @options.gridTextFamily) + .attr('font-weight', @options.gridTextWeight) + .attr('fill', @options.gridTextColor) + + drawLinePath: (path, lineColor, lineIndex) -> + @raphael.path(path) + .attr('stroke', lineColor) + .attr('stroke-width', @lineWidthForSeries(lineIndex)) + + drawLinePoint: (xPos, yPos, pointColor, lineIndex) -> + @raphael.circle(xPos, yPos, @pointSizeForSeries(lineIndex)) + .attr('fill', pointColor) + .attr('stroke-width', @pointStrokeWidthForSeries(lineIndex)) + .attr('stroke', @pointStrokeColorForSeries(lineIndex)) + + # @private + pointStrokeWidthForSeries: (index) -> + @options.pointStrokeWidths[index % @options.pointStrokeWidths.length] + + # @private + pointStrokeColorForSeries: (index) -> + @options.pointStrokeColors[index % @options.pointStrokeColors.length] + + # @private + lineWidthForSeries: (index) -> + if (@options.lineWidth instanceof Array) + @options.lineWidth[index % @options.lineWidth.length] + else + @options.lineWidth + + # @private + pointSizeForSeries: (index) -> + if (@options.pointSize instanceof Array) + @options.pointSize[index % @options.pointSize.length] + else + @options.pointSize + + # @private + pointGrowSeries: (index) -> + Raphael.animation r: @pointSizeForSeries(index) + 3, 25, 'linear' + + # @private + pointShrinkSeries: (index) -> + Raphael.animation r: @pointSizeForSeries(index), 25, 'linear' + +# generate a series of label, timestamp pairs for x-axis labels +# +# @private +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 + +# @private +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.setUTCMinutes(d.getUTCMinutes() + interval) + +# @private +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.setUTCSeconds(d.getUTCSeconds() + interval) + +Morris.LABEL_SPECS = + "decade": + span: 172800000000 # 10 * 365 * 24 * 60 * 60 * 1000 + start: (d) -> new Date(d.getFullYear() - d.getFullYear() % 10, 0, 1) + fmt: (d) -> "#{d.getFullYear()}" + incr: (d) -> d.setFullYear(d.getFullYear() + 10) + "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) + "week": + span: 604800000 # 7 * 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() + 7) + "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 = [ + "decade", "year", "month", "week", "day", "hour", + "30min", "15min", "10min", "5min", "minute", + "30sec", "15sec", "10sec", "5sec", "second" +] |