Merge pull request #455 from JelteF/feature-horizontal-bar

Add horizontal barchart option
This commit is contained in:
Olly Smith 2014-07-14 20:51:41 +01:00
commit 62b4290315
3 changed files with 186 additions and 46 deletions

View File

@ -29,6 +29,7 @@ class Morris.Bar extends Morris.Grid
barOpacity: 1.0
barRadius: [0, 0, 0, 0]
xLabelMargin: 50
horizontal: false
# Do any size-related calculations
#
@ -43,7 +44,7 @@ class Morris.Bar extends Morris.Grid
# @private
calcBars: ->
for row, idx in @data
row._x = @left + @width * (idx + 0.5) / @data.length
row._x = @xStart + @xSize * (idx + 0.5) / @data.length
row._y = for y in row.y
if y? then @transY(y) else null
@ -58,39 +59,78 @@ class Morris.Bar extends Morris.Grid
# @private
drawXAxis: ->
# draw x axis labels
ypos = @bottom + (@options.xAxisLabelTopPadding || @options.padding / 2)
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]
label = @drawXAxisLabel(row._x, ypos, row.label)
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#{-@options.xLabelAngle}")
label.transform("r#{-angle}")
labelBox = label.getBBox()
label.transform("t0,#{labelBox.height / 2}...")
if @options.xLabelAngle != 0
if angle != 0
offset = -0.5 * textBox.width *
Math.cos(@options.xLabelAngle * Math.PI / 180.0)
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 >= labelBox.x + labelBox.width or
prevAngleMargin? and prevAngleMargin >= labelBox.x) and
labelBox.x >= 0 and (labelBox.x + labelBox.width) < @el.width()
if @options.xLabelAngle != 0
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(@options.xLabelAngle * Math.PI / 180.0)
prevAngleMargin = labelBox.x - margin
prevLabelMargin = labelBox.x - @options.xLabelMargin
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: ->
groupWidth = @width / @options.data.length
numBars = if @options.stacked then 1 else @options.ykeys.length
groupWidth = @xSize / @options.data.length
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)
@ -107,7 +147,7 @@ class Morris.Bar extends Morris.Grid
top = ypos
bottom = @bottom
left = @left + idx * groupWidth + leftPadding
left = @xStart + idx * groupWidth + leftPadding
left += sidx * (barWidth + @options.barGap) unless @options.stacked
size = bottom - top
@ -115,10 +155,15 @@ class Morris.Bar extends Morris.Grid
@drawBar(@left + idx * groupWidth, @top, groupWidth, Math.abs(@top - @bottom), @options.verticalGridColor, @options.verticalGridOpacity, @options.barRadius)
top -= lastTop if @options.stacked
if not @options.horizontal
@drawBar(left, top, barWidth, size, @colorFor(row, sidx, 'bar'),
@options.barOpacity, @options.barRadius)
else
@drawBar(top, left, size, barWidth, @colorFor(row, sidx, 'bar'),
@options.barOpacity, @options.barRadius)
lastTop += size
else
null
@ -137,11 +182,16 @@ class Morris.Bar extends Morris.Grid
# hit test - returns the index of the row at the given x-coordinate
#
hitTest: (x) ->
hitTest: (x, y) ->
return null if @data.length == 0
x = Math.max(Math.min(x, @right), @left)
if not @options.horizontal
pos = x
else
pos = y
pos = Math.max(Math.min(pos, @xEnd), @xStart)
Math.min(@data.length - 1,
Math.floor((x - @left) / (@width / @data.length)))
Math.floor((pos - @xStart) / (@xSize / @data.length)))
# click on grid event handler
#
@ -154,7 +204,7 @@ class Morris.Bar extends Morris.Grid
#
# @private
onHoverMove: (x, y) =>
index = @hitTest(x)
index = @hitTest(x, y)
@hover.update(@hoverContentForRow(index)...)
# hover out event handler
@ -179,8 +229,14 @@ class Morris.Bar extends Morris.Grid
"""
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]
drawXAxisLabel: (xPos, yPos, text) ->
label = @raphael.text(xPos, yPos, text)

View File

@ -286,25 +286,74 @@ class Morris.Grid extends Morris.EventEmitter
if @options.axes in [true, 'both', 'y']
yLabelWidths = for gridLine in @grid
@measureText(@yAxisFormat(gridLine)).width
if not @options.horizontal
@left += Math.max(yLabelWidths...)
else
@bottom -= Math.max(yLabelWidths...)
if @options.axes in [true, 'both', 'x']
if not @options.horizontal
angle = -@options.xLabelAngle
else
angle = -90
bottomOffsets = for i in [0...@data.length]
@measureText(@data[i].text, -@options.xLabelAngle).height
@measureText(@data[i].label, angle).height
if not @options.horizontal
@bottom -= Math.max(bottomOffsets...)
else
@left += Math.max(bottomOffsets...)
@width = Math.max(1, @right - @left)
@height = Math.max(1, @bottom - @top)
if not @options.horizontal
@dx = @width / (@xmax - @xmin)
@dy = @height / (@ymax - @ymin)
@yStart = @bottom
@yEnd = @top
@xStart = @left
@xEnd = @right
@xSize = @width
@ySize = @height
else
@dx = @height / (@xmax - @xmin)
@dy = @width / (@ymax - @ymin)
@yStart = @left
@yEnd = @right
@xStart = @top
@xEnd = @bottom
@xSize = @height
@ySize = @width
@calc() if @calc
# Quick translation helpers
#
transY: (y) -> @bottom - (y - @ymin) * @dy
transX: (x) ->
if @data.length == 1
(@left + @right) / 2
transY: (y) ->
if not @options.horizontal
@bottom - (y - @ymin) * @dy
else
@left + (x - @xmin) * @dx
@left + (y - @ymin) * @dy
transX: (x) ->
if @options.horizontal
start = @left
end = @right
else
start = @top
end = @bottom
if @data.length == 1
(start + end) / 2
else
start + (x - @xmin) * @dx
# Draw it!
#
@ -342,16 +391,36 @@ class Morris.Grid extends Morris.EventEmitter
else
"#{@options.preUnits}#{Morris.commas(label)}#{@options.postUnits}"
# get the X position of a label on the Y axis
#
# @private
getYAxisLabelX: ->
@left - @options.padding / 2
# draw y axis labels, horizontal lines
#
drawGrid: ->
return if @options.grid is false and @options.axes not in [true, 'both', 'y']
if not @options.horizontal
basePos = @getYAxisLabelX()
else
basePos = @getXAxisLabelY()
for lineY in @grid
y = @transY(lineY)
pos = @transY(lineY)
if @options.axes in [true, 'both', 'y']
@drawYAxisLabel(@left - @options.padding / 2, y, @yAxisFormat(lineY))
if not @options.horizontal
@drawYAxisLabel(basePos, pos, @yAxisFormat(lineY))
else
@drawXAxisLabel(pos, basePos, @yAxisFormat(lineY))
if @options.grid
@drawGridLine("M#{@left},#{y}H#{@left + @width}")
if not @options.horizontal
@drawGridLine("M#{@xStart},#{pos}H#{@xEnd}")
else
@drawGridLine("M#{pos},#{@xStart}V#{@xEnd}")
# draw goals horizontal lines
#
@ -367,12 +436,22 @@ class Morris.Grid extends Morris.EventEmitter
@drawEvent(event, color)
drawGoal: (goal, color) ->
@raphael.path("M#{@left},#{@transY(goal)}H#{@right}")
if not @options.horizontal
path = "M#{@xStart},#{@transY(goal)}H#{@xEnd}"
else
path = "M#{@transY(goal)},#{@xStart}V#{@xEnd}"
@raphael.path(path)
.attr('stroke', color)
.attr('stroke-width', @options.goalStrokeWidth)
drawEvent: (event, color) ->
@raphael.path("M#{@transX(event)},#{@bottom}V#{@top}")
if not @options.horizontal
path = "M#{@transX(goal)},#{@yStart}V#{@yEnd}"
else
path = "M#{@yStart},#{@transX(goal)}H#{@yEnd}"
@raphael.path(path)
.attr('stroke', color)
.attr('stroke-width', @options.eventStrokeWidth)

View File

@ -10,24 +10,29 @@ class Morris.Hover
@el.hide()
@options.parent.append(@el)
update: (html, x, y) ->
update: (html, x, y, centre_y) ->
if not html
@hide()
else
@html(html)
@show()
@moveTo(x, y)
@moveTo(x, y, centre_y)
html: (content) ->
@el.html(content)
moveTo: (x, y) ->
moveTo: (x, y, centre_y) ->
parentWidth = @options.parent.innerWidth()
parentHeight = @options.parent.innerHeight()
hoverWidth = @el.outerWidth()
hoverHeight = @el.outerHeight()
left = Math.min(Math.max(0, x - hoverWidth / 2), parentWidth - hoverWidth)
if y?
if centre_y is true
top = y - hoverHeight / 2
if top < 0
top = 0
else
top = y - hoverHeight - 10
if top < 0
top = y + 10