Blame | Last modification | View Log | Download | RSS feed
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 laterif @options.hideHover isnt 'always'@hover = new Morris.Hover(parent: @el)@on('hovermove', @onHoverMove)@on('hoverout', @onHoverOut)@on('gridclick', @onGridClick)# Default configuration#defaults:lineWidth: 3pointSize: 4lineColors: ['#0b62a4''#7A92A3''#4da74d''#afd8f8''#edc240''#cb4b4b''#9440ed']pointStrokeWidths: [1]pointStrokeColors: ['#ffffff']pointFillColors: []smooth: truexLabels: 'auto'xLabelFormat: nullxLabelMargin: 24hideHover: false# Do any size-related calculations## @privatecalc: ->@calcPoints()@generatePaths()# calculate series data point coordinates## @privatecalcPoints: ->for row in @datarow._x = @transX(row.x)row._y = for y in row.yif y? then @transY(y) else yrow._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 algofor r, index in @data.slice(1)break if x < (r._x + @data[index]._x) / 2index# click on grid event handler## @privateonGridClick: (x, y) =>index = @hitTest(x)@fire 'click', index, @data[index].src, x, y# hover movement event handler## @privateonHoverMove: (x, y) =>index = @hitTest(x)@displayHoverForRow(index)# hover out event handler## @privateonHoverOut: =>if @options.hideHover isnt false@displayHoverForRow(null)# display a hover popup over the given row## @privatedisplayHoverForRow: (index) ->if index?@hover.update(@hoverContentForRow(index)...)@hilight(index)else@hover.hide()@hilight()# hover content for a point## @privatehoverContentForRow: (index) ->row = @data[index]content = "<div class='morris-hover-row-label'>#{row.label}</div>"for y, j in row.ycontent += """<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## @privategeneratePaths: ->@paths = for i in [0...@options.ykeys.length]smooth = if typeof @options.smooth is "boolean" then @options.smooth else @options.ykeys[i] in @options.smoothcoords = ({x: r._x, y: r._y[i]} for r in @data when r._y[i] isnt undefined)if coords.length > 1Morris.Line.createPath coords, smooth, @bottomelsenull# 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## @privatedrawXAxis: -># draw x axis labelsypos = @bottom + @options.padding / 2prevLabelMargin = nullprevAngleMargin = nulldrawLabel = (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 != 0offset = -0.5 * textBox.width *Math.cos(@options.xLabelAngle * Math.PI / 180.0)label.transform("t#{offset},0...")# try to avoid overlapslabelBox = label.getBBox()if (not prevLabelMargin? orprevLabelMargin >= labelBox.x + labelBox.width orprevAngleMargin? and prevAngleMargin >= labelBox.x) andlabelBox.x >= 0 and (labelBox.x + labelBox.width) < @el.width()if @options.xLabelAngle != 0margin = 1.25 * @options.gridTextSize /Math.sin(@options.xLabelAngle * Math.PI / 180.0)prevAngleMargin = labelBox.x - marginprevLabelMargin = labelBox.x - @options.xLabelMarginelselabel.remove()if @options.parseTimeif @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 labellabels = [[@data[0].label, @data[0].x]]elselabels = Morris.labelSeries(@xmin, @xmax, @width, @options.xLabels, @options.xLabelFormat)elselabels = ([row.label, row.x] for row in @data)labels.reverse()for l in labelsdrawLabel(l[0], l[1])# draw the data series## @privatedrawSeries: ->@seriesPoints = []for i in [@options.ykeys.length-1..0]@_drawLineFor ifor i in [@options.ykeys.length-1..0]@_drawPointFor i_drawPointFor: (index) ->@seriesPoints[index] = []for row in @datacircle = nullif 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 smoothprevCoord = {y: null}for coord, i in coordsif coord.y?if prevCoord.y?if smoothg = grads[i]lg = grads[i - 1]ix = (coord.x - prevCoord.x) / 4x1 = prevCoord.x + ixy1 = Math.min(bottom, prevCoord.y + ix * lg)x2 = coord.x - ixy2 = Math.min(bottom, coord.y - ix * g)path += "C#{x1},#{y1},#{x2},#{y2},#{coord.x},#{coord.y}"elsepath += "L#{coord.x},#{coord.y}"elseif not smooth or grads[i]?path += "M#{coord.x},#{coord.y}"prevCoord = coordreturn 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 coordsif 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)elsenullelsenull# @privatehilight: (index) =>if @prevHilight isnt null and @prevHilight isnt indexfor i in [0..@seriesPoints.length-1]if @seriesPoints[i][@prevHilight]@seriesPoints[i][@prevHilight].animate @pointShrinkSeries(i)if index isnt null and @prevHilight isnt indexfor i in [0..@seriesPoints.length-1]if @seriesPoints[i][index]@seriesPoints[i][index].animate @pointGrowSeries(i)@prevHilight = indexcolorFor: (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))# @privatepointStrokeWidthForSeries: (index) ->@options.pointStrokeWidths[index % @options.pointStrokeWidths.length]# @privatepointStrokeColorForSeries: (index) ->@options.pointStrokeColors[index % @options.pointStrokeColors.length]# @privatelineWidthForSeries: (index) ->if (@options.lineWidth instanceof Array)@options.lineWidth[index % @options.lineWidth.length]else@options.lineWidth# @privatepointSizeForSeries: (index) ->if (@options.pointSize instanceof Array)@options.pointSize[index % @options.pointSize.length]else@options.pointSize# @privatepointGrowSeries: (index) ->Raphael.animation r: @pointSizeForSeries(index) + 3, 25, 'linear'# @privatepointShrinkSeries: (index) ->Raphael.animation r: @pointSizeForSeries(index), 25, 'linear'# generate a series of label, timestamp pairs for x-axis labels## @privateMorris.labelSeries = (dmin, dmax, pxwidth, specName, xLabelFormat) ->ddensity = 200 * (dmax - dmin) / pxwidth # seconds per `margin` pixelsd0 = new Date(dmin)spec = Morris.LABEL_SPECS[specName]# if the spec doesn't exist, search for the closest one in the listif spec is undefinedfor name in Morris.AUTO_LABEL_ORDERs = Morris.LABEL_SPECS[name]if ddensity >= s.spanspec = sbreak# if we run out of options, use second-intervalsif spec is undefinedspec = Morris.LABEL_SPECS["second"]# check if there's a user-defined formatting functionif xLabelFormatspec = $.extend({}, spec, {fmt: xLabelFormat})# calculate labelsd = spec.start(d0)ret = []while (t = d.getTime()) <= dmaxif t >= dminret.push [spec.fmt(d), t]spec.incr(d)return ret# @privateminutesSpecHelper = (interval) ->span: interval * 60 * 1000start: (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)# @privatesecondsSpecHelper = (interval) ->span: interval * 1000start: (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 * 1000start: (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 * 1000start: (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 * 1000start: (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 * 1000start: (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 * 1000start: (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"]