From 715771431c9b3b8b54a3ebcfc6dc72e7ffa29536 Mon Sep 17 00:00:00 2001 From: Jelte Fennema Date: Mon, 14 Jul 2014 00:15:21 +0200 Subject: [PATCH] Add horizontal barchart option --- lib/morris.bar.coffee | 102 ++++++++++++++++++++++++++++--------- lib/morris.grid.coffee | 109 ++++++++++++++++++++++++++++++++++------ lib/morris.hover.coffee | 21 +++++--- 3 files changed, 186 insertions(+), 46 deletions(-) diff --git a/lib/morris.bar.coffee b/lib/morris.bar.coffee index 86fb32b..7fed9f0 100644 --- a/lib/morris.bar.coffee +++ b/lib/morris.bar.coffee @@ -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 - @drawBar(left, top, barWidth, size, @colorFor(row, sidx, 'bar'), - @options.barOpacity, @options.barRadius) + 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) - x = @left + (index + 0.5) * @width / @data.length - [content, x] + + 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) diff --git a/lib/morris.grid.coffee b/lib/morris.grid.coffee index db0882c..61b9e7d 100644 --- a/lib/morris.grid.coffee +++ b/lib/morris.grid.coffee @@ -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 - @left += Math.max(yLabelWidths...) + + 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 - @bottom -= Math.max(bottomOffsets...) + @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) - @dx = @width / (@xmax - @xmin) - @dy = @height / (@ymax - @ymin) + + 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) diff --git a/lib/morris.hover.coffee b/lib/morris.hover.coffee index 530cb08..fe04178 100644 --- a/lib/morris.hover.coffee +++ b/lib/morris.hover.coffee @@ -10,29 +10,34 @@ 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? - top = y - hoverHeight - 10 - if top < 0 - top = y + 10 - if top + hoverHeight > parentHeight - top = parentHeight / 2 - hoverHeight / 2 + 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 + if top + hoverHeight > parentHeight + top = parentHeight / 2 - hoverHeight / 2 else top = parentHeight / 2 - hoverHeight / 2 @el.css(left: left + "px", top: parseInt(top) + "px")