encapsulate all interaction with raphael. Reuse some methods. Write tests for all svg structural elements and attributes

This commit is contained in:
Christopher Erin 2012-12-30 13:29:26 -06:00
parent 7848b49479
commit 14c56165c6
8 changed files with 107 additions and 58 deletions

View file

@ -15,6 +15,7 @@ module.exports = function (grunt) {
concat: {
'build/morris.coffee': [
'lib/morris.coffee',
'lib/morris.svg.coffee',
'lib/morris.grid.coffee',
'lib/morris.hover.coffee',
'lib/morris.line.coffee',

View file

@ -26,9 +26,7 @@ class Morris.Area extends Morris.Line
path = @paths[i]
if path isnt null
path = path + "L#{@transX(@xmax)},#{@bottom}L#{@transX(@xmin)},#{@bottom}Z"
@r.path(path)
.attr('fill', @fillForSeries(i))
.attr('stroke-width', 0)
@morrisSVG.drawFilledPath(path, @fillForSeries(i))
super()
fillForSeries: (i) ->

View file

@ -59,9 +59,7 @@ class Morris.Bar extends Morris.Grid
prevLabelMargin = null
for i in [0...@data.length]
row = @data[@data.length - 1 - i]
label = @r.text(row._x, ypos, row.label)
.attr('font-size', @options.gridTextSize)
.attr('fill', @options.gridTextColor)
label = @morrisSVG.drawXAxisLabel(row._x, ypos, row.label)
labelBox = label.getBBox()
# ensure a minimum of `xLabelMargin` pixels between labels, and ensure
# labels don't overflow the container
@ -96,9 +94,7 @@ class Morris.Bar extends Morris.Grid
size = bottom - top
top -= lastTop if @options.stacked
@r.rect(left, top, barWidth, size)
.attr('fill', @colorFor(row, sidx, 'bar'))
.attr('stroke-width', 0)
@morrisSVG.drawBar(left, top, barWidth, size, @colorFor(row, sidx, 'bar'))
lastTop += size
else

View file

@ -54,7 +54,7 @@ class Morris.Donut
redraw: ->
@el.empty()
@r = new Raphael(@el[0])
@morrisSVG = new Morris.SVG(@el[0], @options)
cx = @el.width() / 2
cy = @el.height() / 2
@ -72,13 +72,13 @@ class Morris.Donut
for d in @data
next = last + min + C * (d.value / total)
seg = new Morris.DonutSegment(cx, cy, w*2, w, last, next, @options.colors[idx % @options.colors.length], d)
seg.render @r
seg.render @morrisSVG
@segments.push seg
seg.on 'hover', @select
last = next
idx += 1
@text1 = @r.text(cx, cy - 10, '').attr('font-size': 15, 'font-weight': 800)
@text2 = @r.text(cx, cy + 10, '').attr('font-size': 14)
@text1 = @morrisSVG.drawEmptyDonutLabel(cx, cy - 10, 15, 800)
@text2 = @morrisSVG.drawEmptyDonutLabel(cx, cy + 10, 14)
max_value = Math.max.apply(null, d.value for d in @data)
idx = 0
for d in @data
@ -147,11 +147,9 @@ class Morris.DonutSegment extends Morris.EventEmitter
"M#{ix0},#{iy0}" +
"A#{r},#{r},0,#{@long},0,#{ix1},#{iy1}")
render: (raphael) ->
@arc = raphael.path(@hilight).attr(stroke: @color, 'stroke-width': 2, opacity: 0)
@seg = raphael.path(@path)
.attr(fill: @color, stroke: 'white', 'stroke-width': 3)
.hover(=> @fire('hover', @))
render: (morrisSVG) ->
@arc = morrisSVG.drawDonutArc(@hilight, @color)
@seg = morrisSVG.drawDonutSegment(@path, @color, => @fire('hover', @))
select: =>
unless @selected

View file

@ -27,6 +27,7 @@ class Morris.Grid extends Morris.EventEmitter
# the raphael drawing instance
@r = new Raphael(@el[0])
@morrisSVG = new Morris.SVG(@el[0], @options)
# some redraw stuff
@elementWidth = null
@ -227,7 +228,7 @@ class Morris.Grid extends Morris.EventEmitter
# If you need to re-size your charts, call this method after changing the
# size of the container element.
redraw: ->
@r.clear()
@morrisSVG.clear()
@_calc()
@drawGrid()
@drawGoals()
@ -238,16 +239,12 @@ class Morris.Grid extends Morris.EventEmitter
#
drawGoals: ->
for goal, i in @options.goals
@r.path("M#{@left},#{@transY(goal)}H#{@left + @width}")
.attr('stroke', @options.goalLineColors[i % @options.goalLineColors.length])
.attr('stroke-width', @options.goalStrokeWidth)
@morrisSVG.drawGoal("M#{@left},#{@transY(goal)}H#{@left + @width}")
# draw events vertical lines
drawEvents: ->
for event, i in @events
@r.path("M#{@transX(event)},#{@bottom}V#{@top}")
.attr('stroke', @options.eventLineColors[i % @options.eventLineColors.length])
.attr('stroke-width', @options.eventStrokeWidth)
@morrisSVG.drawEvent("M#{@transX(event)},#{@bottom}V#{@top}")
# draw y axis labels, horizontal lines
#
@ -259,22 +256,14 @@ class Morris.Grid extends Morris.EventEmitter
v = parseFloat(lineY.toFixed(@precision))
y = @transY(v)
if @options.axes
@r.text(@left - @options.padding / 2, y, @yAxisFormat(v))
.attr('font-size', @options.gridTextSize)
.attr('fill', @options.gridTextColor)
.attr('text-anchor', 'end')
@morrisSVG.drawYAxisLabel(@left - @options.padding / 2, y, @yAxisFormat(v))
if @options.grid
@r.path("M#{@left},#{y}H#{@left + @width}")
.attr('stroke', @options.gridLineColor)
.attr('stroke-width', @options.gridStrokeWidth)
@morrisSVG.drawGridLine("M#{@left},#{y}H#{@left + @width}")
# @private
#
measureText: (text, fontSize = 12) ->
tt = @r.text(100, 100, text).attr('font-size', fontSize)
ret = tt.getBBox()
tt.remove()
ret
@morrisSVG.measureText(text, fontSize)
# @private
#

View file

@ -138,9 +138,7 @@ class Morris.Line extends Morris.Grid
ypos = @bottom + @options.gridTextSize * 1.25
prevLabelMargin = null
drawLabel = (labelText, xpos) =>
label = @r.text(@transX(xpos), ypos, labelText)
.attr('font-size', @options.gridTextSize)
.attr('fill', @options.gridTextColor)
label = @morrisSVG.drawXAxisLabel(@transX(xpos), ypos, labelText)
labelBox = label.getBBox()
# ensure a minimum of `xLabelMargin` pixels between labels, and ensure
# labels don't overflow the container
@ -170,17 +168,12 @@ class Morris.Line extends Morris.Grid
for i in [@options.ykeys.length-1..0]
path = @paths[i]
if path isnt null
@r.path(path)
.attr('stroke', @colorFor(row, i, 'line'))
.attr('stroke-width', @options.lineWidth)
@morrisSVG.drawLinePath(path, @colorFor(row, i, 'line')) #row isn't available here?
@seriesPoints = ([] for i in [0...@options.ykeys.length])
for i in [@options.ykeys.length-1..0]
for row in @data
if row._y[i]?
circle = @r.circle(row._x, row._y[i], @options.pointSize)
.attr('fill', @colorFor(row, i, 'point'))
.attr('stroke-width', @strokeWidthForSeries(i))
.attr('stroke', @strokeForSeries(i))
circle = @morrisSVG.drawLinePoint(row._x, row._y[i], @options.pointSize, @colorFor(row, i, 'point'), i)
else
circle = null
@seriesPoints[i].push(circle)
@ -245,14 +238,6 @@ class Morris.Line extends Morris.Grid
@seriesPoints[i][index].animate @pointGrow
@prevHilight = index
# @private
strokeWidthForSeries: (index) ->
@options.pointWidths[index % @options.pointWidths.length]
# @private
strokeForSeries: (index) ->
@options.pointStrokeColors[index % @options.pointStrokeColors.length]
colorFor: (row, sidx, type) ->
if typeof @options.lineColors is 'function'
@options.lineColors.call(@, row, sidx, type)

82
lib/morris.svg.coffee Normal file
View file

@ -0,0 +1,82 @@
class Morris.SVG
constructor: (element, options) ->
@raphael = new Raphael(element)
@options = options
clear: ->
@raphael.clear()
drawGoal: (path) ->
@raphael.path(path)
.attr('stroke', @options.goalLineColors[i % @options.goalLineColors.length])
.attr('stroke-width', @options.goalStrokeWidth)
drawEvent: (path) ->
@raphael.path(path)
.attr('stroke', @options.eventLineColors[i % @options.eventLineColors.length])
.attr('stroke-width', @options.eventStrokeWidth)
drawYAxisLabel: (xPos, yPos, text) ->
@raphael.text(xPos, yPos, text)
.attr('font-size', @options.gridTextSize)
.attr('fill', @options.gridTextColor)
.attr('text-anchor', 'end')
drawXAxisLabel: (xPos, yPos, text) ->
label = @raphael.text(xPos, yPos, text)
.attr('font-size', @options.gridTextSize)
.attr('fill', @options.gridTextColor)
drawGridLine: (path) ->
@raphael.path(path)
.attr('stroke', @options.gridLineColor)
.attr('stroke-width', @options.gridStrokeWidth)
measureText: (text, fontSize = 12) ->
tt = @raphael.text(100, 100, text).attr('font-size', fontSize)
ret = tt.getBBox()
tt.remove()
ret
drawEmptyDonutLabel: (xPos, yPos, fontSize, fontWeight) ->
text = @raphael.text(xPos, yPos, '').attr('font-size', fontSize)
text.attr('font-weight', fontWeight) if fontWeight?
return text
drawDonutArc: (path, color) ->
@raphael.path(path).attr(stroke: color, 'stroke-width': 2, opacity: 0)
drawDonutSegment: (path, color, hoverFunction) ->
@raphael.path(path)
.attr(fill: color, stroke: 'white', 'stroke-width': 3)
.hover(hoverFunction)
drawFilledPath: (path, fill) ->
@raphael.path(path)
.attr('fill', fill)
.attr('stroke-width', 0)
drawLinePath: (path, lineColor) ->
@raphael.path(path)
.attr('stroke', lineColor)
.attr('stroke-width', @options.lineWidth)
drawLinePoint: (xPos, yPos, size, pointColor, lineIndex) ->
circle = @raphael.circle(xPos, yPos, size)
.attr('fill', pointColor)
.attr('stroke-width', @strokeWidthForSeries(lineIndex))
.attr('stroke', @strokeForSeries(lineIndex))
drawBar: (xPos, yPos, width, height, barColor) ->
@raphael.rect(xPos, yPos, width, height)
.attr('fill', barColor)
.attr('stroke-width', 0)
# @private
strokeWidthForSeries: (index) ->
@options.pointWidths[index % @options.pointWidths.length]
# @private
strokeForSeries: (index) ->
@options.pointStrokeColors[index % @options.pointStrokeColors.length]

View file

@ -25,10 +25,10 @@ describe 'Morris.Line', ->
pointStrokeColors: [red, blue]
pointWidths: [1, 2]
pointFillColors: [null, red]
chart.strokeWidthForSeries(0).should.equal 1
chart.strokeForSeries(0).should.equal red
chart.strokeWidthForSeries(1).should.equal 2
chart.strokeForSeries(1).should.equal blue
chart.morrisSVG.strokeWidthForSeries(0).should.equal 1
chart.morrisSVG.strokeForSeries(0).should.equal red
chart.morrisSVG.strokeWidthForSeries(1).should.equal 2
chart.morrisSVG.strokeForSeries(1).should.equal blue
chart.colorFor(chart.data[0], 0, 'point').should.equal chart.colorFor(chart.data[0], 0, 'line')
chart.colorFor(chart.data[1], 1, 'point').should.equal red