Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Morris.Bar revisited + necessary Morris.Grid changes #101

Merged
merged 1 commit into from
Oct 31, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions examples/bar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!doctype html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="https://raw.github.com/DmitryBaranovskiy/raphael/300aa589f5a0ba7fce667cd62c7cdda0bd5ad904/raphael-min.js"></script>
<script src="../morris.js"></script>
<script src="lib/prettify.js"></script>
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
</head>
<body>
<h1>Bar charts</h1>
<div id="graph"></div>
<pre id="code" class="prettyprint linenums">
// Use Morris.Bar
Morris.Bar({
element: 'graph',
data: [
{x: '2011 Q1', y: 3, z: 2, a: 3},
{x: '2011 Q2', y: 2, z: 1, a: 1},
{x: '2011 Q3', y: 0, z: 2, a: 4},
{x: '2011 Q4', y: 4, z: 4, a: 3}
],
xkey: 'x',
ykeys: ['y', 'z', 'a'],
labels: ['Y', 'Z', 'A']
});
</pre>
</body>
1 change: 1 addition & 0 deletions grunt.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = function (grunt) {
'lib/morris.grid.coffee',
'lib/morris.line.coffee',
'lib/morris.area.coffee',
'lib/morris.bar.coffee',
'lib/morris.donut.coffee'
],
'build/spec.coffee': ['spec/support/**/*.coffee', 'spec/lib/**/*.coffee']
Expand Down
315 changes: 315 additions & 0 deletions lib/morris.bar.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
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 [[email protected]]
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 [[email protected]])
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 [[email protected]]
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', [email protected] - 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 [[email protected]]
if @seriesBars[i][@prevHilight]
@seriesBars[i][@prevHilight].animate @barDeface
if index isnt null and @prevHilight isnt index
for i in [[email protected]]
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 [[email protected]]
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}"
Loading