morris.js/lib/morris.line.coffee
2012-11-18 20:02:46 +01:00

231 lines
6.8 KiB
CoffeeScript

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
# 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 null
# 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 null)
if coords.length > 1
@createPath coords, smooth
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) ->
path = ""
if 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" + ("#{c.x},#{c.y}" for c in coords).join("L")
return path
# calculate a gradient at each point for a series of points
#
# @private
gradients: (coords) ->
for c, i in coords
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)
# @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]