morris.js/lib/morris.bar.coffee

315 lines
10 KiB
CoffeeScript
Raw Normal View History

class Morris.Bar extends Morris.Grid
# Initialise the graph.
#
constructor: (options) ->
return new Morris.Bar(options) unless (@ instanceof Morris.Bar)
super(options)
init: ->
# Some instance variables for later
@barFace = Raphael.animation opacity: @options.barHoverOpacity, 25, 'linear'
@barDeface = Raphael.animation opacity: 1.0, 25, 'linear'
# data 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
# Default configuration
#
defaults:
barSizeRatio: 0.5
barGap: 3
barStrokeWidths: [0]
barStrokeColors: ['#ffffff']
barFillColors: [
'#0b62a4'
'#7a92a3'
'#4da74d'
'#afd8f8'
'#edc240'
'#cb4b4b'
'#9440ed'
]
barHoverOpacity: 0.95
hoverPaddingX: 10
hoverPaddingY: 5
hoverMargin: 10
hoverFillColor: '#fff'
hoverBorderColor: '#ccc'
hoverBorderWidth: 2
hoverOpacity: 0.95
hoverLabelColor: '#444'
hoverFontSize: 12
hideHover: false
xLabels: 'auto'
xLabelFormat: null
# Override padding
#
# @private
overridePadding: ->
maxYLabelWidth = Math.max(
@measureText(@yAxisFormat(@ymin), @options.gridTextSize).width,
@measureText(@yAxisFormat(@ymax), @options.gridTextSize).width)
@left = maxYLabelWidth + @paddingLeft
@right = @elementWidth - @paddingRight
@width = @right - @left
xgap = @width / @data.length
@barsoffset = @options.barSizeRatio * xgap / 2.0;
@barwidth = (@options.barSizeRatio * xgap - ( @options.ykeys.length - 1 ) * @options.barGap ) / @options.ykeys.length
@halfBarsWidth = Math.round( @options.ykeys.length / 2 ) * ( @barwidth + @options.barGap )
@paddingLeft += @halfBarsWidth
@paddingRight += @halfBarsWidth
# Do any size-related calculations
#
# @private
calc: ->
@calcBars()
@generateBars()
@calcHoverMargins()
# calculate series data bars coordinates and sizes
#
# @private
calcBars: ->
for row in @data
row._x = @transX(row.x)
row._y = for y in row.y
if y is null
null
else
@transY(y)
# calculate hover margins
#
# @private
calcHoverMargins: ->
@hoverMargins = $.map @data.slice(1), (r, i) => (r._x + @data[i]._x) / 2
# generate bars for series
#
# @private
generateBars: ->
@bars = for i in [0..@options.ykeys.length]
coords = ({x: r._x - @barsoffset + i * (@options.barGap + @barwidth) , y: r._y[i], v: r.y[i] } for r in @data when r._y[i] isnt null)
if coords.length > 1
@createBars i, coords, @barwidth
else
null
# Draws the bar chart.
#
draw: ->
@drawXAxis()
@drawSeries()
@drawHover()
@hilight(if @options.hideHover then null else @data.length - 1)
# 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 (prevLabelMargin is null 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: ->
@seriesBars = ([] for i in [0...@options.ykeys.length])
for i in [@options.ykeys.length-1..0]
bars = @bars[i]
if bars.length > 0
for bar in bars
if bar isnt null
rect = @r.rect(bar.x, bar.y, bar.width, bar.height)
.attr('fill', bar.fill)
.attr('stroke', @strokeForSeries(i))
.attr('stroke-width', @strokeWidthForSeries(i))
else
rect = null
@seriesBars[i].push(rect)
# create bars for a data series
#
# @private
createBars: (index, coords, barwidth) ->
bars = []
for coord in coords
bars.push(
x: coord.x
y: coord.y
width: barwidth
height: @bottom - coord.y
fill: @colorForSeriesAndValue(index, coord.v)
)
return bars
# draw the hover tooltip
#
# @private
drawHover: ->
# hover labels
@hoverHeight = @options.hoverFontSize * 1.5 * (@options.ykeys.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...@options.ykeys.length]
idx = if @cumulative then (@options.ykeys.length - i - 1) else i
yLabel = @r.text(0, @options.hoverFontSize * 1.5 * (idx + 1.5) - @hoverHeight / 2, '')
.attr('font-size', @options.hoverFontSize)
@yLabels.push(yLabel)
@hoverSet.push(yLabel)
# @private
updateHover: (index) =>
@hoverSet.show()
row = @data[index]
@xLabel.attr('text', row.label)
for y, i in row.y
@yLabels[i].attr('fill', @hoverColorForSeriesAndValue(i, y))
@yLabels[i].attr('text', "#{@options.labels[i]}: #{@yLabelFormat(y)}")
# 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, (y for y in row._y when y isnt null).concat(@bottom)
if yloc > @hoverHeight + @options.hoverPaddingY * 2 + @options.hoverMargin + @top
yloc = yloc - @hoverHeight / 2 - @options.hoverPaddingY - @options.hoverMargin
else
yloc = yloc + @hoverHeight / 2 + @options.hoverPaddingY + @options.hoverMargin
yloc = Math.max @top + @hoverHeight / 2 + @options.hoverPaddingY, yloc
yloc = Math.min @bottom - @hoverHeight / 2 - @options.hoverPaddingY, yloc
xloc = Math.min @right - maxLabelWidth / 2 - @options.hoverPaddingX, @data[index]._x
xloc = Math.max @left + maxLabelWidth / 2 + @options.hoverPaddingX, xloc
@hoverSet.attr 'transform', "t#{xloc},#{yloc}"
# @private
hideHover: ->
@hoverSet.hide()
# @private
hilight: (index) =>
if @prevHilight isnt null and @prevHilight isnt index
for i in [0..@seriesBars.length-1]
if @seriesBars[i][@prevHilight]
@seriesBars[i][@prevHilight].animate @barDeface
if index isnt null and @prevHilight isnt index
for i in [0..@seriesBars.length-1]
if @seriesBars[i][index]
@seriesBars[i][index].animate @barFace
@updateHover index
@prevHilight = index
if index is null
@hideHover()
# @private
updateHilight: (x) =>
x -= @el.offset().left
for hoverIndex in [0...@hoverMargins.length]
break if @hoverMargins[hoverIndex] > x
@hilight hoverIndex
# @private
strokeWidthForSeries: (index) ->
@options.barStrokeWidths[index % @options.barStrokeWidths.length]
# @private
strokeForSeries: (index) ->
@options.barStrokeColors[index % @options.barStrokeColors.length]
# @private
hoverColorForSeriesAndValue: (index, value) =>
colorOrGradient = @colorForSeriesAndValue index, value
if typeof colorOrGradient is 'string'
return colorOrGradient.split('-').pop()
return colorOrGradient
# @private
colorForSeriesAndValue: (index, value) =>
color = @options.barFillColors[index % @options.barFillColors.length]
if color.indexOf(' ') is -1
return color
color = color.split(/\s/)
colorAt = (top, bottom, relPos) ->
chan = (a, b) -> a + Math.round((b-a)*relPos)
newColor =
r: chan(top.r, bottom.r)
g: chan(top.g, bottom.g)
b: chan(top.b, bottom.b)
return Raphael.color("rgb(#{newColor.r},#{newColor.g},#{newColor.b})")
position = 1.0 - (value - @ymin) / (@ymax - @ymin)
top = Raphael.color(color[0])
bottom = Raphael.color(color[1])
if color.length is 3
bottom = Raphael.color(color[2])
middle = Raphael.color(color[1])
if position > 0.5
start = colorAt(middle, bottom, 2 * (position - 0.5))
return "90-#{bottom.hex}-#{start.hex}"
else
start = colorAt(top, middle, position * 2)
middlepos = 100 - Math.round(100 * (0.5 - position) / (1.0 - position))
return "90-#{bottom.hex}-#{middle.hex}:#{middlepos}-#{start.hex}"
start = colorAt(top, bottom, position)
return "90-#{bottom.hex}-#{start.hex}"