morris.js/lib/morris.bar.coffee
2015-03-23 21:43:27 +01:00

320 lines
9.5 KiB
CoffeeScript

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
barHighlightOpacity: 1.0
highlightSpeed: 150
barRadius: [0, 0, 0, 0]
xLabelMargin: 50
horizontal: false
shown: true
inBarValue: false
inBarValueTextColor: 'white'
inBarValueMinTopMargin: 1
inBarValueRightMargin: 4
# 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 = @xStart + @xSize * (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
if not @options.horizontal
basePos = @getXAxisLabelY()
else
basePos = @getYAxisLabelX()
prevLabelMargin = null
prevAngleMargin = null
for i in [0...@data.length]
row = @data[@data.length - 1 - i]
if not @options.horizontal
label = @drawXAxisLabel(row._x, basePos, row.label)
else
label = @drawYAxisLabel(basePos, row._x - 0.5 * @options.gridTextSize, row.label)
if not @options.horizontal
angle = @options.xLabelAngle
else
angle = 0
textBox = label.getBBox()
label.transform("r#{-angle}")
labelBox = label.getBBox()
label.transform("t0,#{labelBox.height / 2}...")
if angle != 0
offset = -0.5 * textBox.width *
Math.cos(angle * Math.PI / 180.0)
label.transform("t#{offset},0...")
if not @options.horizontal
startPos = labelBox.x
size = labelBox.width
maxSize = @el.width()
else
startPos = labelBox.y
size = labelBox.height
maxSize = @el.height()
# try to avoid overlaps
if (not prevLabelMargin? or
prevLabelMargin >= startPos + size or
prevAngleMargin? and prevAngleMargin >= startPos) and
startPos >= 0 and (startPos + size) < maxSize
if angle != 0
margin = 1.25 * @options.gridTextSize /
Math.sin(angle * Math.PI / 180.0)
prevAngleMargin = startPos - margin
if not @options.horizontal
prevLabelMargin = startPos - @options.xLabelMargin
else
prevLabelMargin = startPos
else
label.remove()
# get the Y position of a label on the X axis
#
# @private
getXAxisLabelY: ->
@bottom + (@options.xAxisLabelTopPadding || @options.padding / 2)
# draw the data series
#
# @private
drawSeries: ->
@seriesBars = []
groupWidth = @xSize / @options.data.length
if @options.stacked
numBars = 1
else
numBars = 0
for i in [0..@options.ykeys.length-1]
if @hasToShow(i)
numBars += 1
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
@seriesBars[idx] = []
lastTop = 0
for ypos, sidx in row._y
if not @hasToShow(sidx)
continue
if ypos != null
if zeroPos
top = Math.min(ypos, zeroPos)
bottom = Math.max(ypos, zeroPos)
else
top = ypos
bottom = @bottom
left = @xStart + idx * groupWidth + leftPadding
left += sidx * (barWidth + @options.barGap) unless @options.stacked
size = bottom - top
if @options.verticalGridCondition and @options.verticalGridCondition(row.x)
if not @options.horizontal
@drawBar(@xStart + idx * groupWidth, @yEnd, groupWidth, @ySize, @options.verticalGridColor, @options.verticalGridOpacity, @options.barRadius)
else
@drawBar(@yStart, @xStart + idx * groupWidth, @ySize, groupWidth, @options.verticalGridColor, @options.verticalGridOpacity, @options.barRadius)
top -= lastTop if @options.stacked
if not @options.horizontal
lastTop += size
@seriesBars[idx][sidx] = @drawBar(left, top, barWidth, size, @colorFor(row, sidx, 'bar'),
@options.barOpacity, @options.barRadius)
else
lastTop -= size
@seriesBars[idx][sidx] = @drawBar(top, left, size, barWidth, @colorFor(row, sidx, 'bar'),
@options.barOpacity, @options.barRadius)
if @options.inBarValue and
barWidth > @options.gridTextSize + 2*@options.inBarValueMinTopMargin
barMiddle = left + 0.5 * barWidth
@raphael.text(bottom - @options.inBarValueRightMargin, barMiddle, @yLabelFormat(row.y[sidx], sidx))
.attr('font-size', @options.gridTextSize)
.attr('font-family', @options.gridTextFamily)
.attr('font-weight', @options.gridTextWeight)
.attr('fill', @options.inBarValueTextColor)
.attr('text-anchor', 'end')
else
null
@flat_bars = $.map @bars, (n) -> return n
@flat_bars = $.grep @flat_bars, (n) -> return n?
@bar_els = $($.map @flat_bars, (n) -> return n[0])
# hightlight the bar on hover
#
# @private
hilight: (index) ->
if @seriesBars && @seriesBars[@prevHilight] && @prevHilight != null && @prevHilight != index
for y,i in @seriesBars[@prevHilight]
if y
y.animate({'fill-opacity': @options.barOpacity}, @options.highlightSpeed)
if @seriesBars && @seriesBars[index] && index != null && @prevHilight != index
for y,i in @seriesBars[index]
if y
y.animate({'fill-opacity': @options.barHighlightOpacity}, @options.highlightSpeed)
@prevHilight = index
# @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, src: row.src}
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, y) ->
return null if @data.length == 0
if not @options.horizontal
pos = x
else
pos = y
pos = Math.max(Math.min(pos, @xEnd), @xStart)
Math.min(@data.length - 1,
Math.floor((pos - @xStart) / (@xSize / @data.length)))
# click on grid event handler
#
# @private
onGridClick: (x, y) =>
index = @hitTest(x, y)
bar_hit = !!@bar_els.filter(() -> $(@).is(':hover')).length
@fire 'click', index, @data[index].src, x, y, bar_hit
# hover movement event handler
#
# @private
onHoverMove: (x, y) =>
index = @hitTest(x, y)
@hilight(index)
if index?
@hover.update(@hoverContentForRow(index)...)
else
@hover.hide()
# hover out event handler
#
# @private
onHoverOut: =>
@hilight(-1)
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'>").text(row.label)
content = content.prop('outerHTML')
for y, j in row.y
if @options.labels[j] is false
continue
content += """
<div class='morris-hover-point' style='color: #{@colorFor(row, j, 'label')}'>
#{@options.labels[j]}:
#{@yLabelFormat(y, j)}
</div>
"""
if typeof @options.hoverCallback is 'function'
content = @options.hoverCallback(index, @options, content, row.src)
if not @options.horizontal
x = @left + (index + 0.5) * @width / @data.length
[content, x]
else
x = @left + 0.5 * @width
y = @top + (index + 0.5) * @height / @data.length
[content, x, y, true]
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" ]