mirror of
https://github.com/morrisjs/morris.js.git
synced 2024-11-10 21:36:34 +01:00
194 lines
5.8 KiB
CoffeeScript
194 lines
5.8 KiB
CoffeeScript
# The original line graph.
|
|
#
|
|
window.Morris = {}
|
|
class window.Morris.Line
|
|
|
|
# Initialise the graph.
|
|
#
|
|
# @param {string} id Target element's DOM ID
|
|
# @param {Object} options
|
|
constructor: (id, options) ->
|
|
@el = $ document.getElementById(id)
|
|
@options = $.extend @defaults, options
|
|
# bail if there's no data
|
|
if @options.data is undefined or @options.data.length is 0
|
|
return
|
|
@el.addClass 'graph-initialised'
|
|
@precalc()
|
|
@redraw()
|
|
|
|
# Default configuration
|
|
#
|
|
defaults:
|
|
lineWidth: 3
|
|
pointSize: 4
|
|
lineColors: [
|
|
'#0b62a4'
|
|
'#7A92A3'
|
|
'#4da74d'
|
|
'#afd8f8'
|
|
'#edc240'
|
|
'#cb4b4b'
|
|
'#9440ed'
|
|
]
|
|
marginTop: 25
|
|
marginRight: 25
|
|
marginBottom: 30
|
|
marginLeft: 25
|
|
numLines: 5
|
|
gridLineColor: '#aaa'
|
|
gridTextColor: '#888'
|
|
gridTextSize: 12
|
|
gridStrokeWidth: 0.5
|
|
|
|
# Do any necessary pre-processing for a new dataset
|
|
#
|
|
precalc: ->
|
|
# extract labels
|
|
@xlabels = $.map @options.data, (d) => d[@options.xkey]
|
|
@ylabels = @options.labels
|
|
|
|
# extract series data
|
|
@series = []
|
|
for ykey in @options.ykeys
|
|
@series.push $.map @options.data, (d) -> d[ykey]
|
|
|
|
# translate x labels into nominal dates
|
|
# note: currently using decimal years to specify dates
|
|
@xvals = $.map @xlabels, (x) => @parseYear x
|
|
@xmin = Math.min.apply null, @xvals
|
|
@xmax = Math.max.apply null, @xvals
|
|
if @xmin is @xmax
|
|
@xmin -= 1
|
|
@xmax += 1
|
|
|
|
# use $.map to flatten arrays and find the max y value
|
|
all_y_vals = $.map @series, (x) -> Math.max.apply null, x
|
|
@ymax = Math.max(20, Math.max.apply(null, all_y_vals))
|
|
|
|
# Clear and redraw the graph
|
|
#
|
|
redraw: ->
|
|
# remove child elements (get rid of old drawings)
|
|
@el.empty()
|
|
|
|
# the raphael drawing instance
|
|
@r = new Raphael(@el[0])
|
|
|
|
# calculate grid dimensions
|
|
left = @measureText(@ymax, @options.gridTextSize).width + @options.marginLeft
|
|
width = @el.width() - left - @options.marginRight
|
|
height = @el.height() - @options.marginTop - @options.marginBottom
|
|
dx = width / (@xmax - @xmin)
|
|
dy = height / @ymax
|
|
|
|
# quick translation helpers
|
|
transX = (x) =>
|
|
if @xvals.length is 1
|
|
left + width / 2
|
|
else
|
|
left + (x - @xmin) * dx
|
|
transY = (y) =>
|
|
return @options.marginTop + height - y * dy
|
|
|
|
# draw y axis labels, horizontal lines
|
|
lineInterval = height / (@options.numLines - 1)
|
|
for i in [0..@options.numLines-1]
|
|
y = @options.marginTop + i * lineInterval
|
|
v = Math.round((@options.numLines - 1 - i) * @ymax / (@options.numLines - 1))
|
|
@r.text(left - @options.marginLeft/2, y, v)
|
|
.attr('font-size', @options.gridTextSize)
|
|
.attr('fill', @options.gridTextColor)
|
|
.attr('text-anchor', 'end')
|
|
@r.path("M" + left + "," + y + 'H' + (left + width))
|
|
.attr('stroke', @options.gridLineColor)
|
|
.attr('stroke-width', @options.gridStrokeWidth)
|
|
|
|
# draw x axis labels
|
|
prevLabelMargin = null
|
|
xLabelMargin = 50 # make this an option?
|
|
for i in [Math.ceil(@xmin)..Math.floor(@xmax)]
|
|
label = @r.text(transX(i), @options.marginTop + height + @options.marginBottom / 2, i)
|
|
.attr('font-size', @options.gridTextSize)
|
|
.attr('fill', @options.gridTextColor)
|
|
labelBox = label.getBBox()
|
|
# ensure a minimum of `xLabelMargin` pixels between labels
|
|
if prevLabelMargin is null or prevLabelMargin <= labelBox.x
|
|
prevLabelMargin = labelBox.x + labelBox.width + xLabelMargin
|
|
else
|
|
label.remove()
|
|
|
|
# draw the actual series
|
|
columns = (transX(x) for x in @xvals)
|
|
seriesCoords = []
|
|
for s in @series
|
|
seriesCoords.push($.map(s, (y, i) -> x: columns[i], y: transY(y)))
|
|
for i in [seriesCoords.length-1..0]
|
|
coords = seriesCoords[i]
|
|
if coords.length > 1
|
|
path = @createPath coords, @options.marginTop, left, @options.marginTop + height, left + width
|
|
@r.path(path)
|
|
.attr('stroke', @options.lineColors[i])
|
|
.attr('stroke-width', @options.lineWidth)
|
|
seriesPoints = ([] for i in [0..seriesCoords.length-1])
|
|
for i in [seriesCoords.length-1..0]
|
|
for c in seriesCoords[i]
|
|
circle = @r.circle(c.x, c.y, @options.pointSize)
|
|
.attr('fill', @options.lineColors[i])
|
|
.attr('stroke-width', 1)
|
|
.attr('stroke', '#ffffff')
|
|
seriesPoints[i].push(circle)
|
|
|
|
# hover labels
|
|
hoverMargins = $.map columns.slice(1), (x, i) -> (x + columns[i]) / 2
|
|
|
|
# create a path for a data series
|
|
#
|
|
createPath: (coords, top, left, bottom, right) ->
|
|
path = ""
|
|
grads = @gradients coords
|
|
for i in [0..coords.length-1]
|
|
c = coords[i]
|
|
if i is 0
|
|
path += "M#{c.x},#{c.y}"
|
|
else
|
|
g = grads[i]
|
|
lc = coords[i - 1]
|
|
lg = grads[i - 1]
|
|
ix = (c.x - lc.x) / 4
|
|
x1 = lc.x + ix
|
|
y1 = Math.min(bottom, lc.y + ix * lg)
|
|
x2 = c.x - ix
|
|
y2 = Math.min(bottom, c.y - ix * g)
|
|
path += "C#{x1},#{y1},#{x2},#{y2},#{c.x},#{c.y}"
|
|
return path
|
|
|
|
# calculate a gradient at each point for a series of points
|
|
#
|
|
gradients: (coords) ->
|
|
$.map coords, (c, i) ->
|
|
if i is 0
|
|
(coords[1].y - c.y) / (coords[1].x - c.x)
|
|
else if i is (coords.length - 1)
|
|
(c.y - coords[i - 1].y) / (c.x - coords[i - 1].x)
|
|
else
|
|
(coords[i + 1].y - coords[i - 1].y) / (coords[i + 1].x - coords[i - 1].x)
|
|
|
|
measureText: (text, fontSize = 12) ->
|
|
tt = @r.text(100, 100, text).attr('font-size', fontSize)
|
|
ret = tt.getBBox()
|
|
tt.remove()
|
|
return ret
|
|
|
|
parseYear: (year) ->
|
|
m = year.toString().match /(\d+) Q(\d)/
|
|
n = year.toString().match /(\d+)\-(\d+)/
|
|
if m
|
|
parseInt(m[1], 10) + (parseInt(m[2], 10) * 3 - 1) / 12
|
|
else if n
|
|
parseInt(n[1], 10) + (parseInt(n[2], 10) - 1) / 12
|
|
else
|
|
parseInt(year, 10)
|
|
|
|
# vim: set et ts=2 sw=2 sts=2
|