mirror of
https://github.com/morrisjs/morris.js.git
synced 2024-11-10 21:36:34 +01:00
315 lines
10 KiB
CoffeeScript
315 lines
10 KiB
CoffeeScript
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}" |