class Morris.Line extends Morris.Grid @include Morris.Hover # Initialise the graph. # constructor: (options) -> return new Morris.Line(options) unless (@ instanceof Morris.Line) super(options) init: -> # Some instance variables for later @pointGrow = Raphael.animation r: @options.pointSize + 3, 25, 'linear' @pointShrink = Raphael.animation r: @options.pointSize, 25, 'linear' @hoverConfigure @options.hoverOptions # column hilight events if @options.hilight @prevHilight = null @el.mousemove (evt) => @updateHilight evt.pageX if @options.hilightAutoHide @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 postInit: -> @hoverInit() # Default configuration # defaults: lineWidth: 3 pointSize: 4 lineColors: [ '#0b62a4' '#7A92A3' '#4da74d' '#afd8f8' '#edc240' '#cb4b4b' '#9440ed' ] pointWidths: [1] pointStrokeColors: ['#ffffff'] pointFillColors: [] smooth: true hilight: true hilightAutoHide: false xLabels: 'auto' xLabelFormat: null continuousLine: true # Do any size-related calculations # # @private calc: -> @calcPoints() @hoverCalculateMargins() @generatePaths() @calcHilightMargins() # 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 # calculate hilight margins # # @private calcHilightMargins: -> @hilightMargins = ((r._x + @data[i]._x) / 2 for r, i in @data.slice(1)) hoverCalculateMargins: -> @hoverMargins = ((r._x + @data[i]._x) / 2 for r, i in @data.slice(1)) # generate paths for series lines # # @private generatePaths: -> @paths = for i in [0...@options.ykeys.length] smooth = @options.smooth is true or @options.ykeys[i] in @options.smooth coords = ({x: r._x, y: r._y[i]} for r in @data when r._y[i] isnt undefined) coords = (c for c in coords when c.y isnt null) if @options.continuousLine if coords.length > 1 Morris.Line.createPath coords, smooth, @bottom else null # Draws the line chart. # draw: -> @drawXAxis() @drawSeries() @hilight(if @options.hilightAutoHide then null else @data.length - 1) if @options.hilight # draw the x-axis labels # # @private drawXAxis: -> # draw x axis labels ypos = @bottom + @options.gridTextSize * 1.25 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, and ensure # labels don't overflow the container if (not prevLabelMargin? or prevLabelMargin >= labelBox.x + labelBox.width) and labelBox.x >= 0 and (labelBox.x + labelBox.width) < @el.width() prevLabelMargin = labelBox.x - 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: -> for i in [@options.ykeys.length-1..0] path = @paths[i] if path isnt null @r.path(path) .attr('stroke', @colorFor(row, i, 'line')) .attr('stroke-width', @options.lineWidth) @seriesPoints = ([] for i in [0...@options.ykeys.length]) for i in [@options.ykeys.length-1..0] for row in @data if row._y[i] == null circle = null else circle = @r.circle(row._x, row._y[i], @options.pointSize) .attr('fill', @colorFor(row, i, 'point')) .attr('stroke-width', @strokeWidthForSeries(i)) .attr('stroke', @strokeForSeries(i)) @seriesPoints[i].push(circle) # 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.max(bottom, prevCoord.y + ix * lg) x2 = coord.x - ix y2 = Math.max(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 @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 @prevHilight = index # @private updateHilight: (x) => x -= @el.offset().left for hilightIndex in [0...@hilightMargins.length] break if @hilightMargins[hilightIndex] > x @hilight hilightIndex # @private strokeWidthForSeries: (index) -> @options.pointWidths[index % @options.pointWidths.length] # @private strokeForSeries: (index) -> @options.pointStrokeColors[index % @options.pointStrokeColors.length] 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]