Merge pull request #134 from oesmith/html-hover

Hover refactor (work in progress)
This commit is contained in:
Olly Smith 2012-12-19 14:40:18 -08:00
commit 8b06d9972d
34 changed files with 783 additions and 483 deletions

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Area charts</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Bar charts</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Bar charts</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Formatting Dates YYYY-MM-DD</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Decimal Data</h1>
@ -25,7 +26,11 @@ window.m = Morris.Line({
xkey: 'x',
ykeys: ['y'],
labels: ['sin(x)'],
parseTime: false
parseTime: false,
hoverCallback: function (index, options) {
var row = options.data[index];
return "sin(" + row.x + ") = " + row.y;
}
});
</pre>
</body>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Donut Chart</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Donut Chart</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Time Events</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Value Goals</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Formatting Dates with YYYY-MM</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Negative values</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Non-continuous data</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Formatting Non-date Arbitrary X-axis</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Formatting Dates with Quarters</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Stacked Bars chart</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Timestamps</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Updating data</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Formatting Dates With Weeks</h1>

View File

@ -7,6 +7,7 @@
<script src="lib/example.js"></script>
<link rel="stylesheet" href="lib/example.css">
<link rel="stylesheet" href="lib/prettify.css">
<link rel="stylesheet" href="../morris.css">
</head>
<body>
<h1>Formatting Dates YYYY</h1>

View File

@ -16,6 +16,7 @@ module.exports = function (grunt) {
'build/morris.coffee': [
'lib/morris.coffee',
'lib/morris.grid.coffee',
'lib/morris.hover.coffee',
'lib/morris.line.coffee',
'lib/morris.area.coffee',
'lib/morris.bar.coffee',
@ -23,6 +24,15 @@ module.exports = function (grunt) {
],
'build/spec.coffee': ['spec/support/**/*.coffee', 'spec/lib/**/*.coffee']
},
less: {
all: {
src: 'less/*.less',
dest: 'morris.css',
options: {
compress: true
}
}
},
min: {
'morris.min.js': 'morris.js'
},
@ -33,13 +43,14 @@ module.exports = function (grunt) {
}
},
watch: {
files: ['lib/**/*.coffee', 'spec/lib/**/*.coffee', 'spec/support/**/*.coffee'],
files: ['lib/**/*.coffee', 'spec/lib/**/*.coffee', 'spec/support/**/*.coffee', 'less/**/*.less'],
tasks: 'default'
}
});
grunt.loadNpmTasks('grunt-coffee');
grunt.loadNpmTasks('grunt-mocha');
grunt.loadNpmTasks('grunt-less');
grunt.registerTask('default', 'concat coffee min mocha');
grunt.registerTask('default', 'concat coffee less min mocha');
};

27
less/morris.core.less Normal file
View File

@ -0,0 +1,27 @@
.morris-hover {
position: absolute;
z-index: 1000;
&.morris-default-style {
border-radius: 10px;
padding: 6px;
color: #666;
background: rgba(255, 255, 255, 0.8);
border: solid 2px rgba(230, 230, 230, 0.8);
font-family: sans-serif;
font-size: 12px;
text-align: center;
.morris-hover-row-label {
font-weight: bold;
margin: 0.25em 0;
}
.morris-hover-point {
white-space: nowrap;
margin: 0.1em 0;
}
}
}

View File

@ -16,6 +16,7 @@ class Morris.Area extends Morris.Line
row._y = for y in row.y
total += (y || 0)
@transY(total)
row._ymax = row._y[row._y.length - 1]
# draw the data series
#
@ -31,7 +32,7 @@ class Morris.Area extends Morris.Line
super()
fillForSeries: (i) ->
color = Raphael.rgb2hsl @colorForSeries(i)
color = Raphael.rgb2hsl @colorFor(@data[i], i, 'line')
Raphael.hsl(
color.h,
Math.min(255, color.s * 0.75),

View File

@ -1,27 +1,15 @@
class Morris.Bar extends Morris.Grid
# Initialise the graph.
#
constructor: (options) ->
return new Morris.Bar(options) unless (@ instanceof Morris.Bar)
super($.extend {}, options, parseTime: false)
# setup event handlers
#
init: ->
@cumulative = @options.stacked
@prevHilight = null
@el.mousemove (evt) =>
@updateHilight evt.pageX
if @options.hideHover
@el.mouseout (evt) =>
@hilight null
touchHandler = (evt) =>
touch = evt.originalEvent.touches[0] or evt.originalEvent.changedTouches[0]
@updateHilight touch.pageX
return touch
@el.bind 'touchstart', touchHandler
@el.bind 'touchmove', touchHandler
@el.bind 'touchend', touchHandler
if @options.hideHover isnt 'always'
@hover = new Morris.Hover(parent: @el)
@on('hovermove', @onHoverMove)
@on('hoverout', @onHoverOut)
# Default configuration
#
@ -37,23 +25,14 @@ class Morris.Bar extends Morris.Grid
'#cb4b4b'
'#9440ed'
]
hoverPaddingX: 10
hoverPaddingY: 5
hoverMargin: 10
hoverFillColor: '#fff'
hoverBorderColor: '#ccc'
hoverBorderWidth: 2
hoverOpacity: 0.95
hoverLabelColor: '#444'
hoverFontSize: 12
hideHover: false
# Do any size-related calculations
#
# @private
calc: ->
@calcBars()
@calcHoverMargins()
if @options.hideHover is false
@hover.update(@hoverContentForRow(@data.length - 1)...)
# calculate series data bars coordinates and sizes
#
@ -64,20 +43,11 @@ class Morris.Bar extends Morris.Grid
row._y = for y in row.y
if y? then @transY(y) else null
# calculate hover margins
#
# @private
calcHoverMargins: ->
@hoverMargins = for i in [1...@data.length]
@left + i * @width / @data.length
# Draws the bar chart.
#
draw: ->
@drawXAxis()
@drawSeries()
@drawHover()
@hilight(if @options.hideHover then null else @data.length - 1)
# draw the x-axis labels
#
@ -134,74 +104,11 @@ class Morris.Bar extends Morris.Grid
else
null
# draw the hover tooltip
#
# @private
drawHover: ->
# hover labels
@hoverHeight = @options.hoverFontSize * 1.5 * (@options.ykeys.length + 1)
@hover = @r.rect(-10, -@hoverHeight / 2 - @options.hoverPaddingY, 20, @hoverHeight + @options.hoverPaddingY * 2, 10)
.attr('fill', @options.hoverFillColor)
.attr('stroke', @options.hoverBorderColor)
.attr('stroke-width', @options.hoverBorderWidth)
.attr('opacity', @options.hoverOpacity)
@xLabel = @r.text(0, (@options.hoverFontSize * 0.75) - @hoverHeight / 2, '')
.attr('fill', @options.hoverLabelColor)
.attr('font-weight', 'bold')
.attr('font-size', @options.hoverFontSize)
@hoverSet = @r.set()
@hoverSet.push(@hover)
@hoverSet.push(@xLabel)
@yLabels = []
for i in [0...@options.ykeys.length]
yLabel = @r.text(0, @options.hoverFontSize * 1.5 * (i + 1.5) - @hoverHeight / 2, '')
.attr('font-size', @options.hoverFontSize)
@yLabels.push(yLabel)
@hoverSet.push(yLabel)
# @private
updateHover: (index) =>
@hoverSet.show()
row = @data[index]
@xLabel.attr('text', row.label)
for y, i in row.y
@yLabels[i].attr('fill', @colorFor(row, i, 'hover'))
@yLabels[i].attr('text', "#{@options.labels[i]}: #{@yLabelFormat(y)}")
# recalculate hover box width
maxLabelWidth = Math.max.apply null, (l.getBBox().width for l in @yLabels)
maxLabelWidth = Math.max maxLabelWidth, @xLabel.getBBox().width
@hover.attr 'width', maxLabelWidth + @options.hoverPaddingX * 2
@hover.attr 'x', -@options.hoverPaddingX - maxLabelWidth / 2
# move to y pos
yloc = (@bottom + @top) / 2
xloc = Math.min @right - maxLabelWidth / 2 - @options.hoverPaddingX, @data[index]._x
xloc = Math.max @left + maxLabelWidth / 2 + @options.hoverPaddingX, xloc
@hoverSet.attr 'transform', "t#{xloc},#{yloc}"
# @private
hideHover: ->
@hoverSet.hide()
# @private
hilight: (index) =>
if index isnt null and @prevHilight isnt index
@updateHover index
@prevHilight = index
if not index?
@hideHover()
# @private
updateHilight: (x) =>
x -= @el.offset().left
for hoverIndex in [0...@hoverMargins.length]
break if @hoverMargins[hoverIndex] > x
@hilight hoverIndex
# @private
#
# @param row [Object] row data
# @param sidx [Number] series index
# @param type [String] "bar" or "hover"
# @param type [String] "bar", "hover" or "label"
colorFor: (row, sidx, type) ->
if typeof @options.barColors is 'function'
r = { x: row.x, y: row.y[sidx], label: row.label }
@ -209,3 +116,43 @@ class Morris.Bar extends Morris.Grid
@options.barColors.call(@, r, s, type)
else
@options.barColors[sidx % @options.barColors.length]
# hit test - returns the index of the row beneath the given coordinate
#
hitTest: (x, y) ->
x = Math.max(Math.min(x, @right), @left)
Math.min(@data.length - 1,
Math.floor((x - @left) / ((@right - @left) / @data.length)))
# hover movement event handler
#
# @private
onHoverMove: (x, y) =>
index = @hitTest(x, y)
@hover.update(@hoverContentForRow(index)...)
# hover out event handler
#
# @private
onHoverOut: =>
if @options.hideHover is 'auto'
@hover.hide()
# hover content for a point
#
# @private
hoverContentForRow: (index) ->
if typeof @options.hoverCallback is 'function'
content = @options.hoverCallback(index, @options)
else
row = @data[index]
content = "<div class='morris-hover-row-label'>#{row.label}</div>"
for y, j in row.y
content += """
<div class='morris-hover-point' style='color: #{@colorFor(row, j, 'label')}'>
#{@options.labels[j]}:
#{@yLabelFormat(y)}
</div>
"""
x = @left + (index + 0.5) * (@right - @left) / @data.length
[content, x]

View File

@ -12,6 +12,9 @@ class Morris.Grid extends Morris.EventEmitter
if not @el? or @el.length == 0
throw new Error("Graph container element not found")
if @el.css('position') == 'static'
@el.css('position', 'relative')
@options = $.extend {}, @gridDefaults, (@defaults || {}), options
# bail if there's no data
@ -36,6 +39,22 @@ class Morris.Grid extends Morris.EventEmitter
# load data
@setData @options.data
# hover
@el.bind 'mousemove', (evt) =>
offset = @el.offset()
@fire 'hovermove', evt.pageX - offset.left, evt.pageY - offset.top
@el.bind 'mouseout', (evt) =>
@fire 'hoverout'
@el.bind 'touchstart touchmove touchend', (evt) =>
touch = evt.originalEvent.touches[0] or evt.originalEvent.changedTouches[0]
offset = @el.offset()
@fire 'hover', touch.pageX - offset.left, touch.pageY - offset.top
touch
@postInit() if @postInit
# Default options
#
gridDefaults:
@ -44,6 +63,7 @@ class Morris.Grid extends Morris.EventEmitter
gridStrokeWidth: 0.5
gridTextColor: '#888'
gridTextSize: 12
hideHover: false
numLines: 5
padding: 25
parseTime: true
@ -256,6 +276,10 @@ class Morris.Grid extends Morris.EventEmitter
yLabelFormat: (label) ->
"#{@options.preUnits}#{Morris.commas(label)}#{@options.postUnits}"
updateHover: (x, y) ->
hit = @hitTest(x, y)
if hit?
@hover.update(hit...)
# Parse a date into a javascript timestamp
#

41
lib/morris.hover.coffee Normal file
View File

@ -0,0 +1,41 @@
class Morris.Hover
# Displays contextual information in a floating HTML div.
@defaults:
class: 'morris-hover morris-default-style'
constructor: (options = {}) ->
@options = $.extend {}, Morris.Hover.defaults, options
@el = $ "<div class='#{@options.class}'></div>"
@el.hide()
@options.parent.append(@el)
update: (html, x, y) ->
@html(html)
@show()
@moveTo(x, y)
html: (content) ->
@el.html(content)
moveTo: (x, 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
else
top = parentHeight / 2 - hoverHeight / 2
@el.css(left: left + "px", top: top + "px")
show: ->
@el.show()
hide: ->
@el.hide()

View File

@ -9,20 +9,11 @@ class Morris.Line extends Morris.Grid
# Some instance variables for later
@pointGrow = Raphael.animation r: @options.pointSize + 3, 25, 'linear'
@pointShrink = Raphael.animation r: @options.pointSize, 25, 'linear'
# column hilight events
@prevHilight = null
@el.mousemove (evt) =>
@updateHilight evt.pageX
if @options.hideHover
@el.mouseout (evt) =>
@hilight null
touchHandler = (evt) =>
touch = evt.originalEvent.touches[0] or evt.originalEvent.changedTouches[0]
@updateHilight touch.pageX
return touch
@el.bind 'touchstart', touchHandler
@el.bind 'touchmove', touchHandler
@el.bind 'touchend', touchHandler
if @options.hideHover isnt 'always'
@hover = new Morris.Hover(parent: @el)
@on('hovermove', @onHoverMove)
@on('hoverout', @onHoverOut)
# Default configuration
#
@ -41,20 +32,11 @@ class Morris.Line extends Morris.Grid
pointWidths: [1]
pointStrokeColors: ['#ffffff']
pointFillColors: []
hoverPaddingX: 10
hoverPaddingY: 5
hoverMargin: 10
hoverFillColor: '#fff'
hoverBorderColor: '#ccc'
hoverBorderWidth: 2
hoverOpacity: 0.95
hoverLabelColor: '#444'
hoverFontSize: 12
smooth: true
hideHover: false
xLabels: 'auto'
xLabelFormat: null
continuousLine: true
hideHover: false
# Do any size-related calculations
#
@ -62,7 +44,6 @@ class Morris.Line extends Morris.Grid
calc: ->
@calcPoints()
@generatePaths()
@calcHoverMargins()
# calculate series data point coordinates
#
@ -72,12 +53,59 @@ class Morris.Line extends Morris.Grid
row._x = @transX(row.x)
row._y = for y in row.y
if y? then @transY(y) else y
row._ymax = Math.min.apply(null, [@bottom].concat(y for y in row._y when y?))
# calculate hover margins
# hit test - returns the index of the row beneath the given coordinate
#
hitTest: (x, y) ->
# TODO better search algo
for r, index in @data.slice(1)
break if x < (r._x + @data[index]._x) / 2
index
# hover movement event handler
#
# @private
calcHoverMargins: ->
@hoverMargins = ((r._x + @data[i]._x) / 2 for r, i in @data.slice(1))
onHoverMove: (x, y) =>
index = @hitTest(x, y)
@displayHoverForRow(index)
# hover out event handler
#
# @private
onHoverOut: =>
if @options.hideHover is 'auto'
@displayHoverForIndex(null)
# display a hover popup over the given row
#
# @private
displayHoverForRow: (index) ->
if index?
@hover.update(@hoverContentForRow(index)...)
@hilight(index)
else
@hover.hide()
@hilight()
# hover content for a point
#
# @private
hoverContentForRow: (index) ->
row = @data[index]
if typeof @options.hoverCallback is 'function'
content = @options.hoverCallback(index, @options)
else
content = "<div class='morris-hover-row-label'>#{row.label}</div>"
for y, j in row.y
content += """
<div class='morris-hover-point' style='color: #{@colorFor(row, j, 'label')}'>
#{@options.labels[j]}:
#{@yLabelFormat(y)}
</div>
"""
[content, row._x, row._ymax]
# generate paths for series lines
#
@ -98,8 +126,8 @@ class Morris.Line extends Morris.Grid
draw: ->
@drawXAxis()
@drawSeries()
@drawHover()
@hilight(if @options.hideHover then null else @data.length - 1)
if @options.hideHover is false
@displayHoverForRow(@data.length - 1)
# draw the x-axis labels
#
@ -143,14 +171,14 @@ class Morris.Line extends Morris.Grid
path = @paths[i]
if path isnt null
@r.path(path)
.attr('stroke', @colorForSeries(i))
.attr('stroke', @colorFor(row, i, 'line'))
.attr('stroke-width', @options.lineWidth)
@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', @pointFillColorForSeries(i) || @colorForSeries(i))
.attr('fill', @colorFor(row, i, 'point'))
.attr('stroke-width', @strokeWidthForSeries(i))
.attr('stroke', @strokeForSeries(i))
else
@ -205,61 +233,6 @@ class Morris.Line extends Morris.Grid
else
null
# draw the hover tooltip
#
# @private
drawHover: ->
# hover labels
@hoverHeight = @options.hoverFontSize * 1.5 * (@options.ykeys.length + 1)
@hover = @r.rect(-10, -@hoverHeight / 2 - @options.hoverPaddingY, 20, @hoverHeight + @options.hoverPaddingY * 2, 10)
.attr('fill', @options.hoverFillColor)
.attr('stroke', @options.hoverBorderColor)
.attr('stroke-width', @options.hoverBorderWidth)
.attr('opacity', @options.hoverOpacity)
@xLabel = @r.text(0, (@options.hoverFontSize * 0.75) - @hoverHeight / 2, '')
.attr('fill', @options.hoverLabelColor)
.attr('font-weight', 'bold')
.attr('font-size', @options.hoverFontSize)
@hoverSet = @r.set()
@hoverSet.push(@hover)
@hoverSet.push(@xLabel)
@yLabels = []
for i in [0...@options.ykeys.length]
idx = if @cumulative then (@options.ykeys.length - i - 1) else i
yLabel = @r.text(0, @options.hoverFontSize * 1.5 * (idx + 1.5) - @hoverHeight / 2, '')
.attr('fill', @colorForSeries(i))
.attr('font-size', @options.hoverFontSize)
@yLabels.push(yLabel)
@hoverSet.push(yLabel)
# @private
updateHover: (index) =>
@hoverSet.show()
row = @data[index]
@xLabel.attr('text', row.label)
for y, i in row.y
@yLabels[i].attr('text', "#{@options.labels[i]}: #{@yLabelFormat(y)}")
# recalculate hover box width
maxLabelWidth = Math.max.apply null, (l.getBBox().width for l in @yLabels)
maxLabelWidth = Math.max maxLabelWidth, @xLabel.getBBox().width
@hover.attr 'width', maxLabelWidth + @options.hoverPaddingX * 2
@hover.attr 'x', -@options.hoverPaddingX - maxLabelWidth / 2
# move to y pos
yloc = Math.min.apply null, (y for y in row._y when y?).concat(@bottom)
if yloc > @hoverHeight + @options.hoverPaddingY * 2 + @options.hoverMargin + @top
yloc = yloc - @hoverHeight / 2 - @options.hoverPaddingY - @options.hoverMargin
else
yloc = yloc + @hoverHeight / 2 + @options.hoverPaddingY + @options.hoverMargin
yloc = Math.max @top + @hoverHeight / 2 + @options.hoverPaddingY, yloc
yloc = Math.min @bottom - @hoverHeight / 2 - @options.hoverPaddingY, yloc
xloc = Math.min @right - maxLabelWidth / 2 - @options.hoverPaddingX, @data[index]._x
xloc = Math.max @left + maxLabelWidth / 2 + @options.hoverPaddingX, xloc
@hoverSet.attr 'transform', "t#{xloc},#{yloc}"
# @private
hideHover: ->
@hoverSet.hide()
# @private
hilight: (index) =>
if @prevHilight isnt null and @prevHilight isnt index
@ -270,21 +243,7 @@ class Morris.Line extends Morris.Grid
for i in [0..@seriesPoints.length-1]
if @seriesPoints[i][index]
@seriesPoints[i][index].animate @pointGrow
@updateHover index
@prevHilight = index
if not index?
@hideHover()
# @private
updateHilight: (x) =>
x -= @el.offset().left
for hoverIndex in [0...@hoverMargins.length]
break if @hoverMargins[hoverIndex] > x
@hilight hoverIndex
# @private
colorForSeries: (index) ->
@options.lineColors[index % @options.lineColors.length]
# @private
strokeWidthForSeries: (index) ->
@ -294,9 +253,13 @@ class Morris.Line extends Morris.Grid
strokeForSeries: (index) ->
@options.pointStrokeColors[index % @options.pointStrokeColors.length]
# @private
pointFillColorForSeries: (index) ->
@options.pointFillColors[index % @options.pointFillColors.length]
colorFor: (row, sidx, type) ->
if typeof @options.lineColors is 'function'
@options.lineColors.call(@, row, sidx, type)
else if type is 'point'
@options.pointFillColors[sidx % @options.pointFillColors.length] || @options.lineColors[sidx % @options.lineColors.length]
else
@options.lineColors[sidx % @options.lineColors.length]
# generate a series of label, timestamp pairs for x-axis labels

2
morris.css Normal file
View File

@ -0,0 +1,2 @@
.morris-hover{position:absolute;z-index:1000;}.morris-hover.morris-default-style{border-radius:10px;padding:6px;color:#666;background:rgba(255, 255, 255, 0.8);border:solid 2px rgba(230, 230, 230, 0.8);font-family:sans-serif;font-size:12px;text-align:center;}.morris-hover.morris-default-style .morris-hover-row-label{font-weight:bold;margin:0.25em 0;}
.morris-hover.morris-default-style .morris-hover-point{white-space:nowrap;margin:0.1em 0;}

499
morris.js
View File

@ -68,6 +68,7 @@
__extends(Grid, _super);
function Grid(options) {
var _this = this;
if (typeof options.element === 'string') {
this.el = $(document.getElementById(options.element));
} else {
@ -76,6 +77,9 @@
if (!(this.el != null) || this.el.length === 0) {
throw new Error("Graph container element not found");
}
if (this.el.css('position') === 'static') {
this.el.css('position', 'relative');
}
this.options = $.extend({}, this.gridDefaults, this.defaults || {}, options);
if (this.options.data === void 0 || this.options.data.length === 0) {
return;
@ -91,6 +95,24 @@
this.init();
}
this.setData(this.options.data);
this.el.bind('mousemove', function(evt) {
var offset;
offset = _this.el.offset();
return _this.fire('hovermove', evt.pageX - offset.left, evt.pageY - offset.top);
});
this.el.bind('mouseout', function(evt) {
return _this.fire('hoverout');
});
this.el.bind('touchstart touchmove touchend', function(evt) {
var offset, touch;
touch = evt.originalEvent.touches[0] || evt.originalEvent.changedTouches[0];
offset = _this.el.offset();
_this.fire('hover', touch.pageX - offset.left, touch.pageY - offset.top);
return touch;
});
if (this.postInit) {
this.postInit();
}
}
Grid.prototype.gridDefaults = {
@ -99,6 +121,7 @@
gridStrokeWidth: 0.5,
gridTextColor: '#888',
gridTextSize: 12,
hideHover: false,
numLines: 5,
padding: 25,
parseTime: true,
@ -359,6 +382,14 @@
return "" + this.options.preUnits + (Morris.commas(label)) + this.options.postUnits;
};
Grid.prototype.updateHover = function(x, y) {
var hit, _ref;
hit = this.hitTest(x, y);
if (hit != null) {
return (_ref = this.hover).update.apply(_ref, hit);
}
};
return Grid;
})(Morris.EventEmitter);
@ -420,16 +451,78 @@
}
};
Morris.Hover = (function() {
Hover.defaults = {
"class": 'morris-hover morris-default-style'
};
function Hover(options) {
if (options == null) {
options = {};
}
this.options = $.extend({}, Morris.Hover.defaults, options);
this.el = $("<div class='" + this.options["class"] + "'></div>");
this.el.hide();
this.options.parent.append(this.el);
}
Hover.prototype.update = function(html, x, y) {
this.html(html);
this.show();
return this.moveTo(x, y);
};
Hover.prototype.html = function(content) {
return this.el.html(content);
};
Hover.prototype.moveTo = function(x, y) {
var hoverHeight, hoverWidth, left, parentHeight, parentWidth, top;
parentWidth = this.options.parent.innerWidth();
parentHeight = this.options.parent.innerHeight();
hoverWidth = this.el.outerWidth();
hoverHeight = this.el.outerHeight();
left = Math.min(Math.max(0, x - hoverWidth / 2), parentWidth - hoverWidth);
if (y != null) {
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;
}
return this.el.css({
left: left + "px",
top: top + "px"
});
};
Hover.prototype.show = function() {
return this.el.show();
};
Hover.prototype.hide = function() {
return this.el.hide();
};
return Hover;
})();
Morris.Line = (function(_super) {
__extends(Line, _super);
function Line(options) {
this.updateHilight = __bind(this.updateHilight, this);
this.hilight = __bind(this.hilight, this);
this.updateHover = __bind(this.updateHover, this);
this.onHoverOut = __bind(this.onHoverOut, this);
this.onHoverMove = __bind(this.onHoverMove, this);
if (!(this instanceof Morris.Line)) {
return new Morris.Line(options);
}
@ -437,32 +530,19 @@
}
Line.prototype.init = function() {
var touchHandler,
_this = this;
this.pointGrow = Raphael.animation({
r: this.options.pointSize + 3
}, 25, 'linear');
this.pointShrink = Raphael.animation({
r: this.options.pointSize
}, 25, 'linear');
this.prevHilight = null;
this.el.mousemove(function(evt) {
return _this.updateHilight(evt.pageX);
});
if (this.options.hideHover) {
this.el.mouseout(function(evt) {
return _this.hilight(null);
if (this.options.hideHover !== 'always') {
this.hover = new Morris.Hover({
parent: this.el
});
this.on('hovermove', this.onHoverMove);
return this.on('hoverout', this.onHoverOut);
}
touchHandler = function(evt) {
var touch;
touch = evt.originalEvent.touches[0] || evt.originalEvent.changedTouches[0];
_this.updateHilight(touch.pageX);
return touch;
};
this.el.bind('touchstart', touchHandler);
this.el.bind('touchmove', touchHandler);
return this.el.bind('touchend', touchHandler);
};
Line.prototype.defaults = {
@ -472,26 +552,16 @@
pointWidths: [1],
pointStrokeColors: ['#ffffff'],
pointFillColors: [],
hoverPaddingX: 10,
hoverPaddingY: 5,
hoverMargin: 10,
hoverFillColor: '#fff',
hoverBorderColor: '#ccc',
hoverBorderWidth: 2,
hoverOpacity: 0.95,
hoverLabelColor: '#444',
hoverFontSize: 12,
smooth: true,
hideHover: false,
xLabels: 'auto',
xLabelFormat: null,
continuousLine: true
continuousLine: true,
hideHover: false
};
Line.prototype.calc = function() {
this.calcPoints();
this.generatePaths();
return this.calcHoverMargins();
return this.generatePaths();
};
Line.prototype.calcPoints = function() {
@ -501,7 +571,7 @@
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
row = _ref[_i];
row._x = this.transX(row.x);
_results.push(row._y = (function() {
row._y = (function() {
var _j, _len1, _ref1, _results1;
_ref1 = row.y;
_results1 = [];
@ -514,23 +584,72 @@
}
}
return _results1;
}).call(this));
}).call(this);
_results.push(row._ymax = Math.min.apply(null, [this.bottom].concat((function() {
var _j, _len1, _ref1, _results1;
_ref1 = row._y;
_results1 = [];
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
y = _ref1[_j];
if (y != null) {
_results1.push(y);
}
}
return _results1;
})())));
}
return _results;
};
Line.prototype.calcHoverMargins = function() {
var i, r;
return this.hoverMargins = (function() {
var _i, _len, _ref, _results;
_ref = this.data.slice(1);
_results = [];
for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
r = _ref[i];
_results.push((r._x + this.data[i]._x) / 2);
Line.prototype.hitTest = function(x, y) {
var index, r, _i, _len, _ref;
_ref = this.data.slice(1);
for (index = _i = 0, _len = _ref.length; _i < _len; index = ++_i) {
r = _ref[index];
if (x < (r._x + this.data[index]._x) / 2) {
break;
}
return _results;
}).call(this);
}
return index;
};
Line.prototype.onHoverMove = function(x, y) {
var index;
index = this.hitTest(x, y);
return this.displayHoverForRow(index);
};
Line.prototype.onHoverOut = function() {
if (this.options.hideHover === 'auto') {
return this.displayHoverForIndex(null);
}
};
Line.prototype.displayHoverForRow = function(index) {
var _ref;
if (index != null) {
(_ref = this.hover).update.apply(_ref, this.hoverContentForRow(index));
return this.hilight(index);
} else {
this.hover.hide();
return this.hilight();
}
};
Line.prototype.hoverContentForRow = function(index) {
var content, j, row, y, _i, _len, _ref;
row = this.data[index];
if (typeof this.options.hoverCallback === 'function') {
content = this.options.hoverCallback(index, this.options);
} else {
content = "<div class='morris-hover-row-label'>" + row.label + "</div>";
_ref = row.y;
for (j = _i = 0, _len = _ref.length; _i < _len; j = ++_i) {
y = _ref[j];
content += "<div class='morris-hover-point' style='color: " + (this.colorFor(row, j, 'label')) + "'>\n " + this.options.labels[j] + ":\n " + (this.yLabelFormat(y)) + "\n</div>";
}
}
return [content, row._x, row._ymax];
};
Line.prototype.generatePaths = function() {
@ -581,8 +700,9 @@
Line.prototype.draw = function() {
this.drawXAxis();
this.drawSeries();
this.drawHover();
return this.hilight(this.options.hideHover ? null : this.data.length - 1);
if (this.options.hideHover === false) {
return this.displayHoverForRow(this.data.length - 1);
}
};
Line.prototype.drawXAxis = function() {
@ -633,7 +753,7 @@
for (i = _i = _ref = this.options.ykeys.length - 1; _ref <= 0 ? _i <= 0 : _i >= 0; i = _ref <= 0 ? ++_i : --_i) {
path = this.paths[i];
if (path !== null) {
this.r.path(path).attr('stroke', this.colorForSeries(i)).attr('stroke-width', this.options.lineWidth);
this.r.path(path).attr('stroke', this.colorFor(row, i, 'line')).attr('stroke-width', this.options.lineWidth);
}
}
this.seriesPoints = (function() {
@ -653,7 +773,7 @@
for (_k = 0, _len = _ref2.length; _k < _len; _k++) {
row = _ref2[_k];
if (row._y[i] != null) {
circle = this.r.circle(row._x, row._y[i], this.options.pointSize).attr('fill', this.pointFillColorForSeries(i) || this.colorForSeries(i)).attr('stroke-width', this.strokeWidthForSeries(i)).attr('stroke', this.strokeForSeries(i));
circle = this.r.circle(row._x, row._y[i], this.options.pointSize).attr('fill', this.colorFor(row, i, 'point')).attr('stroke-width', this.strokeWidthForSeries(i)).attr('stroke', this.strokeForSeries(i));
} else {
circle = null;
}
@ -732,76 +852,6 @@
return _results;
};
Line.prototype.drawHover = function() {
var i, idx, yLabel, _i, _ref, _results;
this.hoverHeight = this.options.hoverFontSize * 1.5 * (this.options.ykeys.length + 1);
this.hover = this.r.rect(-10, -this.hoverHeight / 2 - this.options.hoverPaddingY, 20, this.hoverHeight + this.options.hoverPaddingY * 2, 10).attr('fill', this.options.hoverFillColor).attr('stroke', this.options.hoverBorderColor).attr('stroke-width', this.options.hoverBorderWidth).attr('opacity', this.options.hoverOpacity);
this.xLabel = this.r.text(0, (this.options.hoverFontSize * 0.75) - this.hoverHeight / 2, '').attr('fill', this.options.hoverLabelColor).attr('font-weight', 'bold').attr('font-size', this.options.hoverFontSize);
this.hoverSet = this.r.set();
this.hoverSet.push(this.hover);
this.hoverSet.push(this.xLabel);
this.yLabels = [];
_results = [];
for (i = _i = 0, _ref = this.options.ykeys.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
idx = this.cumulative ? this.options.ykeys.length - i - 1 : i;
yLabel = this.r.text(0, this.options.hoverFontSize * 1.5 * (idx + 1.5) - this.hoverHeight / 2, '').attr('fill', this.colorForSeries(i)).attr('font-size', this.options.hoverFontSize);
this.yLabels.push(yLabel);
_results.push(this.hoverSet.push(yLabel));
}
return _results;
};
Line.prototype.updateHover = function(index) {
var i, l, maxLabelWidth, row, xloc, y, yloc, _i, _len, _ref;
this.hoverSet.show();
row = this.data[index];
this.xLabel.attr('text', row.label);
_ref = row.y;
for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
y = _ref[i];
this.yLabels[i].attr('text', "" + this.options.labels[i] + ": " + (this.yLabelFormat(y)));
}
maxLabelWidth = Math.max.apply(null, (function() {
var _j, _len1, _ref1, _results;
_ref1 = this.yLabels;
_results = [];
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
l = _ref1[_j];
_results.push(l.getBBox().width);
}
return _results;
}).call(this));
maxLabelWidth = Math.max(maxLabelWidth, this.xLabel.getBBox().width);
this.hover.attr('width', maxLabelWidth + this.options.hoverPaddingX * 2);
this.hover.attr('x', -this.options.hoverPaddingX - maxLabelWidth / 2);
yloc = Math.min.apply(null, ((function() {
var _j, _len1, _ref1, _results;
_ref1 = row._y;
_results = [];
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
y = _ref1[_j];
if (y != null) {
_results.push(y);
}
}
return _results;
})()).concat(this.bottom));
if (yloc > this.hoverHeight + this.options.hoverPaddingY * 2 + this.options.hoverMargin + this.top) {
yloc = yloc - this.hoverHeight / 2 - this.options.hoverPaddingY - this.options.hoverMargin;
} else {
yloc = yloc + this.hoverHeight / 2 + this.options.hoverPaddingY + this.options.hoverMargin;
}
yloc = Math.max(this.top + this.hoverHeight / 2 + this.options.hoverPaddingY, yloc);
yloc = Math.min(this.bottom - this.hoverHeight / 2 - this.options.hoverPaddingY, yloc);
xloc = Math.min(this.right - maxLabelWidth / 2 - this.options.hoverPaddingX, this.data[index]._x);
xloc = Math.max(this.left + maxLabelWidth / 2 + this.options.hoverPaddingX, xloc);
return this.hoverSet.attr('transform', "t" + xloc + "," + yloc);
};
Line.prototype.hideHover = function() {
return this.hoverSet.hide();
};
Line.prototype.hilight = function(index) {
var i, _i, _j, _ref, _ref1;
if (this.prevHilight !== null && this.prevHilight !== index) {
@ -817,27 +867,8 @@
this.seriesPoints[i][index].animate(this.pointGrow);
}
}
this.updateHover(index);
}
this.prevHilight = index;
if (!(index != null)) {
return this.hideHover();
}
};
Line.prototype.updateHilight = function(x) {
var hoverIndex, _i, _ref;
x -= this.el.offset().left;
for (hoverIndex = _i = 0, _ref = this.hoverMargins.length; 0 <= _ref ? _i < _ref : _i > _ref; hoverIndex = 0 <= _ref ? ++_i : --_i) {
if (this.hoverMargins[hoverIndex] > x) {
break;
}
}
return this.hilight(hoverIndex);
};
Line.prototype.colorForSeries = function(index) {
return this.options.lineColors[index % this.options.lineColors.length];
return this.prevHilight = index;
};
Line.prototype.strokeWidthForSeries = function(index) {
@ -848,8 +879,14 @@
return this.options.pointStrokeColors[index % this.options.pointStrokeColors.length];
};
Line.prototype.pointFillColorForSeries = function(index) {
return this.options.pointFillColors[index % this.options.pointFillColors.length];
Line.prototype.colorFor = function(row, sidx, type) {
if (typeof this.options.lineColors === 'function') {
return this.options.lineColors.call(this, row, sidx, type);
} else if (type === 'point') {
return this.options.pointFillColors[sidx % this.options.pointFillColors.length] || this.options.lineColors[sidx % this.options.lineColors.length];
} else {
return this.options.lineColors[sidx % this.options.lineColors.length];
}
};
return Line;
@ -1005,7 +1042,7 @@
row = _ref[_i];
row._x = this.transX(row.x);
total = 0;
_results.push(row._y = (function() {
row._y = (function() {
var _j, _len1, _ref1, _results1;
_ref1 = row.y;
_results1 = [];
@ -1015,7 +1052,8 @@
_results1.push(this.transY(total));
}
return _results1;
}).call(this));
}).call(this);
_results.push(row._ymax = row._y[row._y.length - 1]);
}
return _results;
};
@ -1034,7 +1072,7 @@
Area.prototype.fillForSeries = function(i) {
var color;
color = Raphael.rgb2hsl(this.colorForSeries(i));
color = Raphael.rgb2hsl(this.colorFor(this.data[i], i, 'line'));
return Raphael.hsl(color.h, Math.min(255, color.s * 0.75), Math.min(255, color.l * 1.25));
};
@ -1047,11 +1085,9 @@
__extends(Bar, _super);
function Bar(options) {
this.updateHilight = __bind(this.updateHilight, this);
this.onHoverOut = __bind(this.onHoverOut, this);
this.hilight = __bind(this.hilight, this);
this.updateHover = __bind(this.updateHover, this);
this.onHoverMove = __bind(this.onHoverMove, this);
if (!(this instanceof Morris.Bar)) {
return new Morris.Bar(options);
}
@ -1061,48 +1097,28 @@
}
Bar.prototype.init = function() {
var touchHandler,
_this = this;
this.cumulative = this.options.stacked;
this.prevHilight = null;
this.el.mousemove(function(evt) {
return _this.updateHilight(evt.pageX);
});
if (this.options.hideHover) {
this.el.mouseout(function(evt) {
return _this.hilight(null);
if (this.options.hideHover !== 'always') {
this.hover = new Morris.Hover({
parent: this.el
});
this.on('hovermove', this.onHoverMove);
return this.on('hoverout', this.onHoverOut);
}
touchHandler = function(evt) {
var touch;
touch = evt.originalEvent.touches[0] || evt.originalEvent.changedTouches[0];
_this.updateHilight(touch.pageX);
return touch;
};
this.el.bind('touchstart', touchHandler);
this.el.bind('touchmove', touchHandler);
return this.el.bind('touchend', touchHandler);
};
Bar.prototype.defaults = {
barSizeRatio: 0.75,
barGap: 3,
barColors: ['#0b62a4', '#7a92a3', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed'],
hoverPaddingX: 10,
hoverPaddingY: 5,
hoverMargin: 10,
hoverFillColor: '#fff',
hoverBorderColor: '#ccc',
hoverBorderWidth: 2,
hoverOpacity: 0.95,
hoverLabelColor: '#444',
hoverFontSize: 12,
hideHover: false
barColors: ['#0b62a4', '#7a92a3', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed']
};
Bar.prototype.calc = function() {
var _ref;
this.calcBars();
return this.calcHoverMargins();
if (this.options.hideHover === false) {
return (_ref = this.hover).update.apply(_ref, this.hoverContentForRow(this.data.length - 1));
}
};
Bar.prototype.calcBars = function() {
@ -1130,23 +1146,9 @@
return _results;
};
Bar.prototype.calcHoverMargins = function() {
var i;
return this.hoverMargins = (function() {
var _i, _ref, _results;
_results = [];
for (i = _i = 1, _ref = this.data.length; 1 <= _ref ? _i < _ref : _i > _ref; i = 1 <= _ref ? ++_i : --_i) {
_results.push(this.left + i * this.width / this.data.length);
}
return _results;
}).call(this);
};
Bar.prototype.draw = function() {
this.drawXAxis();
this.drawSeries();
this.drawHover();
return this.hilight(this.options.hideHover ? null : this.data.length - 1);
return this.drawSeries();
};
Bar.prototype.drawXAxis = function() {
@ -1217,79 +1219,6 @@
}).call(this);
};
Bar.prototype.drawHover = function() {
var i, yLabel, _i, _ref, _results;
this.hoverHeight = this.options.hoverFontSize * 1.5 * (this.options.ykeys.length + 1);
this.hover = this.r.rect(-10, -this.hoverHeight / 2 - this.options.hoverPaddingY, 20, this.hoverHeight + this.options.hoverPaddingY * 2, 10).attr('fill', this.options.hoverFillColor).attr('stroke', this.options.hoverBorderColor).attr('stroke-width', this.options.hoverBorderWidth).attr('opacity', this.options.hoverOpacity);
this.xLabel = this.r.text(0, (this.options.hoverFontSize * 0.75) - this.hoverHeight / 2, '').attr('fill', this.options.hoverLabelColor).attr('font-weight', 'bold').attr('font-size', this.options.hoverFontSize);
this.hoverSet = this.r.set();
this.hoverSet.push(this.hover);
this.hoverSet.push(this.xLabel);
this.yLabels = [];
_results = [];
for (i = _i = 0, _ref = this.options.ykeys.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
yLabel = this.r.text(0, this.options.hoverFontSize * 1.5 * (i + 1.5) - this.hoverHeight / 2, '').attr('font-size', this.options.hoverFontSize);
this.yLabels.push(yLabel);
_results.push(this.hoverSet.push(yLabel));
}
return _results;
};
Bar.prototype.updateHover = function(index) {
var i, l, maxLabelWidth, row, xloc, y, yloc, _i, _len, _ref;
this.hoverSet.show();
row = this.data[index];
this.xLabel.attr('text', row.label);
_ref = row.y;
for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
y = _ref[i];
this.yLabels[i].attr('fill', this.colorFor(row, i, 'hover'));
this.yLabels[i].attr('text', "" + this.options.labels[i] + ": " + (this.yLabelFormat(y)));
}
maxLabelWidth = Math.max.apply(null, (function() {
var _j, _len1, _ref1, _results;
_ref1 = this.yLabels;
_results = [];
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
l = _ref1[_j];
_results.push(l.getBBox().width);
}
return _results;
}).call(this));
maxLabelWidth = Math.max(maxLabelWidth, this.xLabel.getBBox().width);
this.hover.attr('width', maxLabelWidth + this.options.hoverPaddingX * 2);
this.hover.attr('x', -this.options.hoverPaddingX - maxLabelWidth / 2);
yloc = (this.bottom + this.top) / 2;
xloc = Math.min(this.right - maxLabelWidth / 2 - this.options.hoverPaddingX, this.data[index]._x);
xloc = Math.max(this.left + maxLabelWidth / 2 + this.options.hoverPaddingX, xloc);
return this.hoverSet.attr('transform', "t" + xloc + "," + yloc);
};
Bar.prototype.hideHover = function() {
return this.hoverSet.hide();
};
Bar.prototype.hilight = function(index) {
if (index !== null && this.prevHilight !== index) {
this.updateHover(index);
}
this.prevHilight = index;
if (!(index != null)) {
return this.hideHover();
}
};
Bar.prototype.updateHilight = function(x) {
var hoverIndex, _i, _ref;
x -= this.el.offset().left;
for (hoverIndex = _i = 0, _ref = this.hoverMargins.length; 0 <= _ref ? _i < _ref : _i > _ref; hoverIndex = 0 <= _ref ? ++_i : --_i) {
if (this.hoverMargins[hoverIndex] > x) {
break;
}
}
return this.hilight(hoverIndex);
};
Bar.prototype.colorFor = function(row, sidx, type) {
var r, s;
if (typeof this.options.barColors === 'function') {
@ -1309,6 +1238,40 @@
}
};
Bar.prototype.hitTest = function(x, y) {
x = Math.max(Math.min(x, this.right), this.left);
return Math.min(this.data.length - 1, Math.floor((x - this.left) / ((this.right - this.left) / this.data.length)));
};
Bar.prototype.onHoverMove = function(x, y) {
var index, _ref;
index = this.hitTest(x, y);
return (_ref = this.hover).update.apply(_ref, this.hoverContentForRow(index));
};
Bar.prototype.onHoverOut = function() {
if (this.options.hideHover === 'auto') {
return this.hover.hide();
}
};
Bar.prototype.hoverContentForRow = function(index) {
var content, j, row, x, y, _i, _len, _ref;
if (typeof this.options.hoverCallback === 'function') {
content = this.options.hoverCallback(index, this.options);
} else {
row = this.data[index];
content = "<div class='morris-hover-row-label'>" + row.label + "</div>";
_ref = row.y;
for (j = _i = 0, _len = _ref.length; _i < _len; j = ++_i) {
y = _ref[j];
content += "<div class='morris-hover-point' style='color: " + (this.colorFor(row, j, 'label')) + "'>\n " + this.options.labels[j] + ":\n " + (this.yLabelFormat(y)) + "\n</div>";
}
}
x = this.left + (index + 0.5) * (this.right - this.left) / this.data.length;
return [content, x];
};
return Bar;
})(Morris.Grid);

2
morris.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -16,6 +16,7 @@
},
"devDependencies": {
"grunt-coffee": "~> 0.0.6",
"grunt-mocha": "~> 0.1.7"
"grunt-mocha": "~> 0.1.7",
"grunt-less": "~> 0.1.7"
}
}

View File

@ -0,0 +1,64 @@
describe "Morris.Hover", ->
describe "with dummy content", ->
beforeEach ->
parent = $('<div style="width:200px;height:180px"></div>')
.appendTo($('#test'))
@hover = new Morris.Hover(parent: parent)
@element = $('#test .morris-hover')
it "should initialise a hidden, empty popup", ->
@element.should.exist
@element.should.be.hidden
@element.should.be.empty
describe "#show", ->
it "should show the popup", ->
@hover.show()
@element.should.be.visible
describe "#hide", ->
it "should hide the popup", ->
@hover.show()
@hover.hide()
@element.should.be.hidden
describe "#html", ->
it "should replace the contents of the element", ->
@hover.html('<div>Foobarbaz</div>')
@element.should.have.html('<div>Foobarbaz</div>')
describe "#moveTo", ->
beforeEach ->
@hover.html('<div style="width:84px;height:84px"></div>')
it "should place the popup directly above the given point", ->
@hover.moveTo(100, 150)
@element.should.have.css('left', '50px')
@element.should.have.css('top', '40px')
it "should place the popup below the given point if it does not fit above", ->
@hover.moveTo(100, 50)
@element.should.have.css('left', '50px')
@element.should.have.css('top', '60px')
it "should center the popup vertically if it will not fit above or below", ->
@hover.moveTo(100, 100)
@element.should.have.css('left', '50px')
@element.should.have.css('top', '40px')
it "should center the popup vertically if no y value is supplied", ->
@hover.moveTo(100)
@element.should.have.css('left', '50px')
@element.should.have.css('top', '40px')
describe "#update", ->
it "should update content, show and reposition the popup", ->
hover = new Morris.Hover(parent: $('#test'))
html = "<div style='width:84px;height:84px'>Hello, Everyone!</div>"
hover.update(html, 150, 200)
el = $('#test .morris-hover')
el.should.have.css('left', '100px')
el.should.have.css('top', '90px')
el.should.have.text('Hello, Everyone!')

View File

@ -29,9 +29,8 @@ describe 'Morris.Line', ->
chart.strokeForSeries(0).should.equal red
chart.strokeWidthForSeries(1).should.equal 2
chart.strokeForSeries(1).should.equal blue
(null == chart.pointFillColorForSeries(0)).should.be
(chart.pointFillColorForSeries(0) || chart.colorForSeries(0)).should.equal chart.colorForSeries(0)
chart.pointFillColorForSeries(1).should.equal red
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
describe 'generating column labels', ->

View File

@ -3,6 +3,7 @@
<meta charset="utf-8">
<title>morris.js tests</title>
<link rel="stylesheet" href="vendor/mocha-1.6.0.css" type="text/css" media="screen" />
<link rel="stylesheet" href="../morris.css" type="text/css" media="screen" />
<script src="vendor/jquery-1.8.2.min.js"></script>
<script type="text/javascript" src="vendor/raphael-2.1.0.min.js"></script>
</head>
@ -15,6 +16,7 @@
</script>
<script type="text/javascript" src="vendor/chai-1.3.0.js"></script>
<script type="text/javascript" src="vendor/chai-jquery-1.1.0.js"></script>
<script type="text/javascript" src="vendor/sinon-1.5.0.js"></script>
<script type="text/javascript" src="vendor/sinon-chai-2.1.2.js"></script>
<script>
@ -23,7 +25,7 @@
<script type="text/javascript" src="../morris.js"></script>
<script type="text/javascript" src="../build/spec.js"></script>
<div id="test" style="visibility: hidden"></div>
<div id="test" style="width: 400px; height: 200px;"></div>
<script>
if (navigator.userAgent.indexOf('PhantomJS') < 0) {
mocha.run();

232
spec/vendor/chai-jquery-1.1.0.js vendored Normal file
View File

@ -0,0 +1,232 @@
(function (chaiJquery) {
// Module systems magic dance.
if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
// NodeJS
module.exports = chaiJquery;
} else if (typeof define === "function" && define.amd) {
// AMD
define(function () {
return chaiJquery;
});
} else {
// Other environment (usually <script> tag): plug in to global chai instance directly.
chai.use(chaiJquery);
}
}(function (chai, utils) {
var inspect = utils.inspect,
flag = utils.flag;
jQuery.fn.inspect = function (depth) {
var el = jQuery('<div />').append(this.clone());
if (depth) {
var children = el.children();
while (depth-- > 0)
children = children.children();
children.html('...');
}
return el.html();
};
var props = {attr: 'attribute', css: 'CSS property'};
for (var prop in props) {
(function (prop, description) {
chai.Assertion.addMethod(prop, function (name, val) {
var actual = flag(this, 'object')[prop](name);
if (!flag(this, 'negate') || undefined === val) {
this.assert(
undefined !== actual
, 'expected #{this} to have a #{exp} ' + description
, 'expected #{this} not to have a #{exp} ' + description
, name
);
}
if (undefined !== val) {
this.assert(
val === actual
, 'expected #{this} to have a ' + inspect(name) + ' ' + description + ' with the value #{exp}, but the value was #{act}'
, 'expected #{this} not to have a ' + inspect(name) + ' ' + description + ' with the value #{act}'
, val
, actual
);
}
flag(this, 'object', actual);
});
})(prop, props[prop]);
}
chai.Assertion.addMethod('data', function (name, val) {
// Work around a chai bug (https://github.com/logicalparadox/chai/issues/16)
if (flag(this, 'negate') && undefined !== val && undefined === flag(this, 'object').data(name)) {
return;
}
var assertion = new chai.Assertion(flag(this, 'object').data());
if (flag(this, 'negate'))
assertion = assertion.not;
return assertion.property(name, val);
});
chai.Assertion.addMethod('class', function (className) {
this.assert(
flag(this, 'object').hasClass(className)
, 'expected #{this} to have class #{exp}'
, 'expected #{this} not to have class #{exp}'
, className
);
});
chai.Assertion.addMethod('id', function (id) {
this.assert(
flag(this, 'object').attr('id') === id
, 'expected #{this} to have id #{exp}'
, 'expected #{this} not to have id #{exp}'
, id
);
});
chai.Assertion.addMethod('html', function (html) {
this.assert(
flag(this, 'object').html() === html
, 'expected #{this} to have HTML #{exp}'
, 'expected #{this} not to have HTML #{exp}'
, html
);
});
chai.Assertion.addMethod('text', function (text) {
this.assert(
flag(this, 'object').text() === text
, 'expected #{this} to have text #{exp}'
, 'expected #{this} not to have text #{exp}'
, text
);
});
chai.Assertion.addMethod('value', function (value) {
this.assert(
flag(this, 'object').val() === value
, 'expected #{this} to have value #{exp}'
, 'expected #{this} not to have value #{exp}'
, value
);
});
jQuery.each(['visible', 'hidden', 'selected', 'checked', 'disabled'], function (i, attr) {
chai.Assertion.addProperty(attr, function () {
this.assert(
flag(this, 'object').is(':' + attr)
, 'expected #{this} to be ' + attr
, 'expected #{this} not to be ' + attr);
});
});
chai.Assertion.overwriteProperty('exist', function (_super) {
return function () {
var obj = flag(this, 'object');
if (obj instanceof jQuery) {
this.assert(
obj.length > 0
, 'expected ' + inspect(obj.selector) + ' to exist'
, 'expected ' + inspect(obj.selector) + ' not to exist');
} else {
_super.apply(this, arguments);
}
};
});
chai.Assertion.overwriteProperty('empty', function (_super) {
return function () {
var obj = flag(this, 'object');
if (obj instanceof jQuery) {
this.assert(
obj.is(':empty')
, 'expected #{this} to be empty'
, 'expected #{this} not to be empty');
} else {
_super.apply(this, arguments);
}
};
});
chai.Assertion.overwriteProperty('be', function (_super) {
return function () {
var be = function (selector) {
var obj = flag(this, 'object');
if (obj instanceof jQuery) {
this.assert(
obj.is(selector)
, 'expected #{this} to be #{exp}'
, 'expected #{this} not to be #{exp}'
, selector
);
} else {
_super.apply(this, arguments);
}
};
be.__proto__ = this;
return be;
}
});
chai.Assertion.overwriteMethod('match', function (_super) {
return function (selector) {
var obj = flag(this, 'object');
if (obj instanceof jQuery) {
this.assert(
obj.is(selector)
, 'expected #{this} to match #{exp}'
, 'expected #{this} not to match #{exp}'
, selector
);
} else {
_super.apply(this, arguments);
}
}
});
chai.Assertion.overwriteProperty('contain', function (_super) {
return function () {
_super.call(this);
var contain = function (text) {
var obj = flag(this, 'object');
if (obj instanceof jQuery) {
this.assert(
obj.is(':contains(\'' + text + '\')')
, 'expected #{this} to contain #{exp}'
, 'expected #{this} not to contain #{exp}'
, text
);
} else {
Function.prototype.apply.call(_super.call(this), this, arguments);
}
};
contain.__proto__ = this;
return contain;
}
});
chai.Assertion.overwriteProperty('have', function (_super) {
return function () {
var obj = flag(this, 'object');
if (obj instanceof jQuery) {
var have = function (selector) {
this.assert(
// Using find() rather than has() to work around a jQuery bug:
// http://bugs.jquery.com/ticket/11706
obj.find(selector).length > 0
, 'expected #{this} to have #{exp}'
, 'expected #{this} not to have #{exp}'
, selector
);
};
have.__proto__ = this;
return have;
} else {
_super.call(this);
}
}
});
}));