Blame | Last modification | View Log | Download | RSS feed
class Morris.Grid extends Morris.EventEmitter# A generic pair of axes for line/area/bar charts.## Draws grid lines and axis labels.#constructor: (options) -># find the container to draw the graph inif typeof options.element is 'string'@el = $ document.getElementById(options.element)else@el = $ options.elementif not @el? or @el.length == 0throw new Error("Graph container element not found")if @el.css('position') == 'static'@el.css('position', 'relative')@options = $.extend {}, @gridDefaults, (@defaults || {}), options# backwards compatibility for units -> postUnitsif typeof @options.units is 'string'@options.postUnits = options.units# the raphael drawing instance@raphael = new Raphael(@el[0])# some redraw stuff@elementWidth = null@elementHeight = null@dirty = false# range selection@selectFrom = null# more stuff@init() if @init# load data@setData @options.data# hover@el.bind 'mousemove', (evt) =>offset = @el.offset()x = evt.pageX - offset.leftif @selectFromleft = @data[@hitTest(Math.min(x, @selectFrom))]._xright = @data[@hitTest(Math.max(x, @selectFrom))]._xwidth = right - left@selectionRect.attr({ x: left, width: width })else@fire 'hovermove', x, evt.pageY - offset.top@el.bind 'mouseleave', (evt) =>if @selectFrom@selectionRect.hide()@selectFrom = null@fire 'hoverout'@el.bind 'touchstart touchmove touchend', (evt) =>touch = evt.originalEvent.touches[0] or evt.originalEvent.changedTouches[0]offset = @el.offset()@fire 'hovermove', touch.pageX - offset.left, touch.pageY - offset.top@el.bind 'click', (evt) =>offset = @el.offset()@fire 'gridclick', evt.pageX - offset.left, evt.pageY - offset.topif @options.rangeSelect@selectionRect = @raphael.rect(0, 0, 0, @el.innerHeight()).attr({ fill: @options.rangeSelectColor, stroke: false }).toBack().hide()@el.bind 'mousedown', (evt) =>offset = @el.offset()@startRange evt.pageX - offset.left@el.bind 'mouseup', (evt) =>offset = @el.offset()@endRange evt.pageX - offset.left@fire 'hovermove', evt.pageX - offset.left, evt.pageY - offset.topif @options.resize$(window).bind 'resize', (evt) =>if @timeoutId?window.clearTimeout @timeoutId@timeoutId = window.setTimeout @resizeHandler, 100# Disable tap highlight on iOS.@el.css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)')@postInit() if @postInit# Default options#gridDefaults:dateFormat: nullaxes: truegrid: truegridLineColor: '#aaa'gridStrokeWidth: 0.5gridTextColor: '#888'gridTextSize: 12gridTextFamily: 'sans-serif'gridTextWeight: 'normal'hideHover: falseyLabelFormat: nullxLabelAngle: 0numLines: 5padding: 25parseTime: truepostUnits: ''preUnits: ''ymax: 'auto'ymin: 'auto 0'goals: []goalStrokeWidth: 1.0goalLineColors: ['#666633''#999966''#cc6666''#663333']events: []eventStrokeWidth: 1.0eventLineColors: ['#005a04''#ccffbb''#3a5f0b''#005502']rangeSelect: nullrangeSelectColor: '#eef'resize: false# Update the data series and redraw the chart.#setData: (data, redraw = true) ->@options.data = dataif !data? or data.length == 0@data = []@raphael.clear()@hover.hide() if @hover?returnymax = if @cumulative then 0 else nullymin = if @cumulative then 0 else nullif @options.goals.length > 0minGoal = Math.min @options.goals...maxGoal = Math.max @options.goals...ymin = if ymin? then Math.min(ymin, minGoal) else minGoalymax = if ymax? then Math.max(ymax, maxGoal) else maxGoal@data = for row, index in dataret = {src: row}ret.label = row[@options.xkey]if @options.parseTimeret.x = Morris.parseDate(ret.label)if @options.dateFormatret.label = @options.dateFormat ret.xelse if typeof ret.label is 'number'ret.label = new Date(ret.label).toString()elseret.x = indexif @options.xLabelFormatret.label = @options.xLabelFormat rettotal = 0ret.y = for ykey, idx in @options.ykeysyval = row[ykey]yval = parseFloat(yval) if typeof yval is 'string'yval = null if yval? and typeof yval isnt 'number'if yval?if @cumulativetotal += yvalelseif ymax?ymax = Math.max(yval, ymax)ymin = Math.min(yval, ymin)elseymax = ymin = yvalif @cumulative and total?ymax = Math.max(total, ymax)ymin = Math.min(total, ymin)yvalretif @options.parseTime@data = @data.sort (a, b) -> (a.x > b.x) - (b.x > a.x)# calculate horizontal range of the graph@xmin = @data[0].x@xmax = @data[@data.length - 1].x@events = []if @options.events.length > 0if @options.parseTime@events = (Morris.parseDate(e) for e in @options.events)else@events = @options.events@xmax = Math.max(@xmax, Math.max(@events...))@xmin = Math.min(@xmin, Math.min(@events...))if @xmin is @xmax@xmin -= 1@xmax += 1@ymin = @yboundary('min', ymin)@ymax = @yboundary('max', ymax)if @ymin is @ymax@ymin -= 1 if ymin@ymax += 1if @options.axes in [true, 'both', 'y'] or @options.grid is trueif (@options.ymax == @gridDefaults.ymax and@options.ymin == @gridDefaults.ymin)# calculate 'magic' grid placement@grid = @autoGridLines(@ymin, @ymax, @options.numLines)@ymin = Math.min(@ymin, @grid[0])@ymax = Math.max(@ymax, @grid[@grid.length - 1])elsestep = (@ymax - @ymin) / (@options.numLines - 1)@grid = (y for y in [@ymin..@ymax] by step)@dirty = true@redraw() if redrawyboundary: (boundaryType, currentValue) ->boundaryOption = @options["y#{boundaryType}"]if typeof boundaryOption is 'string'if boundaryOption[0..3] is 'auto'if boundaryOption.length > 5suggestedValue = parseInt(boundaryOption[5..], 10)return suggestedValue unless currentValue?Math[boundaryType](currentValue, suggestedValue)elseif currentValue? then currentValue else 0elseparseInt(boundaryOption, 10)elseboundaryOptionautoGridLines: (ymin, ymax, nlines) ->span = ymax - yminymag = Math.floor(Math.log(span) / Math.log(10))unit = Math.pow(10, ymag)# calculate initial grid min and max valuesgmin = Math.floor(ymin / unit) * unitgmax = Math.ceil(ymax / unit) * unitstep = (gmax - gmin) / (nlines - 1)if unit == 1 and step > 1 and Math.ceil(step) != stepstep = Math.ceil(step)gmax = gmin + step * (nlines - 1)# ensure zero is plotted where the range includes zeroif gmin < 0 and gmax > 0gmin = Math.floor(ymin / step) * stepgmax = Math.ceil(ymax / step) * step# special case for decimal numbersif step < 1smag = Math.floor(Math.log(step) / Math.log(10))grid = for y in [gmin..gmax] by stepparseFloat(y.toFixed(1 - smag))elsegrid = (y for y in [gmin..gmax] by step)grid_calc: ->w = @el.width()h = @el.height()if @elementWidth != w or @elementHeight != h or @dirty@elementWidth = w@elementHeight = h@dirty = false# recalculate grid dimensions@left = @options.padding@right = @elementWidth - @options.padding@top = @options.padding@bottom = @elementHeight - @options.paddingif @options.axes in [true, 'both', 'y']yLabelWidths = for gridLine in @grid@measureText(@yAxisFormat(gridLine)).width@left += Math.max(yLabelWidths...)if @options.axes in [true, 'both', 'x']bottomOffsets = for i in [0...@data.length]@measureText(@data[i].text, -@options.xLabelAngle).height@bottom -= Math.max(bottomOffsets...)@width = Math.max(1, @right - @left)@height = Math.max(1, @bottom - @top)@dx = @width / (@xmax - @xmin)@dy = @height / (@ymax - @ymin)@calc() if @calc# Quick translation helpers#transY: (y) -> @bottom - (y - @ymin) * @dytransX: (x) ->if @data.length == 1(@left + @right) / 2else@left + (x - @xmin) * @dx# Draw it!## If you need to re-size your charts, call this method after changing the# size of the container element.redraw: ->@raphael.clear()@_calc()@drawGrid()@drawGoals()@drawEvents()@draw() if @draw# @private#measureText: (text, angle = 0) ->tt = @raphael.text(100, 100, text).attr('font-size', @options.gridTextSize).attr('font-family', @options.gridTextFamily).attr('font-weight', @options.gridTextWeight).rotate(angle)ret = tt.getBBox()tt.remove()ret# @private#yAxisFormat: (label) -> @yLabelFormat(label)# @private#yLabelFormat: (label) ->if typeof @options.yLabelFormat is 'function'@options.yLabelFormat(label)else"#{@options.preUnits}#{Morris.commas(label)}#{@options.postUnits}"# draw y axis labels, horizontal lines#drawGrid: ->return if @options.grid is false and @options.axes not in [true, 'both', 'y']for lineY in @gridy = @transY(lineY)if @options.axes in [true, 'both', 'y']@drawYAxisLabel(@left - @options.padding / 2, y, @yAxisFormat(lineY))if @options.grid@drawGridLine("M#{@left},#{y}H#{@left + @width}")# draw goals horizontal lines#drawGoals: ->for goal, i in @options.goalscolor = @options.goalLineColors[i % @options.goalLineColors.length]@drawGoal(goal, color)# draw events vertical linesdrawEvents: ->for event, i in @eventscolor = @options.eventLineColors[i % @options.eventLineColors.length]@drawEvent(event, color)drawGoal: (goal, color) ->@raphael.path("M#{@left},#{@transY(goal)}H#{@right}").attr('stroke', color).attr('stroke-width', @options.goalStrokeWidth)drawEvent: (event, color) ->@raphael.path("M#{@transX(event)},#{@bottom}V#{@top}").attr('stroke', color).attr('stroke-width', @options.eventStrokeWidth)drawYAxisLabel: (xPos, yPos, text) ->@raphael.text(xPos, yPos, text).attr('font-size', @options.gridTextSize).attr('font-family', @options.gridTextFamily).attr('font-weight', @options.gridTextWeight).attr('fill', @options.gridTextColor).attr('text-anchor', 'end')drawGridLine: (path) ->@raphael.path(path).attr('stroke', @options.gridLineColor).attr('stroke-width', @options.gridStrokeWidth)# Range selection#startRange: (x) ->@hover.hide()@selectFrom = x@selectionRect.attr({ x: x, width: 0 }).show()endRange: (x) ->if @selectFromstart = Math.min(@selectFrom, x)end = Math.max(@selectFrom, x)@options.rangeSelect.call @el,start: @data[@hitTest(start)].xend: @data[@hitTest(end)].x@selectFrom = nullresizeHandler: =>@timeoutId = null@raphael.setSize @el.width(), @el.height()@redraw()# Parse a date into a javascript timestamp##Morris.parseDate = (date) ->if typeof date is 'number'return datem = date.match /^(\d+) Q(\d)$/n = date.match /^(\d+)-(\d+)$/o = date.match /^(\d+)-(\d+)-(\d+)$/p = date.match /^(\d+) W(\d+)$/q = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+)(Z|([+-])(\d\d):?(\d\d))?$/r = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+):(\d+(\.\d+)?)(Z|([+-])(\d\d):?(\d\d))?$/if mnew Date(parseInt(m[1], 10),parseInt(m[2], 10) * 3 - 1,1).getTime()else if nnew Date(parseInt(n[1], 10),parseInt(n[2], 10) - 1,1).getTime()else if onew Date(parseInt(o[1], 10),parseInt(o[2], 10) - 1,parseInt(o[3], 10)).getTime()else if p# calculate number of weeks in year givenret = new Date(parseInt(p[1], 10), 0, 1);# first thursday in year (ISO 8601 standard)if ret.getDay() isnt 4ret.setMonth(0, 1 + ((4 - ret.getDay()) + 7) % 7);# add weeksret.getTime() + parseInt(p[2], 10) * 604800000else if qif not q[6]# no timezone info, use localnew Date(parseInt(q[1], 10),parseInt(q[2], 10) - 1,parseInt(q[3], 10),parseInt(q[4], 10),parseInt(q[5], 10)).getTime()else# timezone info supplied, use UTCoffsetmins = 0if q[6] != 'Z'offsetmins = parseInt(q[8], 10) * 60 + parseInt(q[9], 10)offsetmins = 0 - offsetmins if q[7] == '+'Date.UTC(parseInt(q[1], 10),parseInt(q[2], 10) - 1,parseInt(q[3], 10),parseInt(q[4], 10),parseInt(q[5], 10) + offsetmins)else if rsecs = parseFloat(r[6])isecs = Math.floor(secs)msecs = Math.round((secs - isecs) * 1000)if not r[8]# no timezone info, use localnew Date(parseInt(r[1], 10),parseInt(r[2], 10) - 1,parseInt(r[3], 10),parseInt(r[4], 10),parseInt(r[5], 10),isecs,msecs).getTime()else# timezone info supplied, use UTCoffsetmins = 0if r[8] != 'Z'offsetmins = parseInt(r[10], 10) * 60 + parseInt(r[11], 10)offsetmins = 0 - offsetmins if r[9] == '+'Date.UTC(parseInt(r[1], 10),parseInt(r[2], 10) - 1,parseInt(r[3], 10),parseInt(r[4], 10),parseInt(r[5], 10) + offsetmins,isecs,msecs)elsenew Date(parseInt(date, 10), 0, 1).getTime()