mouth 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/Capfile +26 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +138 -0
- data/Rakefile +19 -0
- data/TODO +32 -0
- data/bin/mouth +77 -0
- data/bin/mouth-console +18 -0
- data/bin/mouth-endoscope +18 -0
- data/lib/mouth.rb +61 -0
- data/lib/mouth/dashboard.rb +25 -0
- data/lib/mouth/endoscope.rb +120 -0
- data/lib/mouth/endoscope/public/222222_256x240_icons_icons.png +0 -0
- data/lib/mouth/endoscope/public/application.css +464 -0
- data/lib/mouth/endoscope/public/application.js +938 -0
- data/lib/mouth/endoscope/public/backbone.js +1158 -0
- data/lib/mouth/endoscope/public/d3.js +4707 -0
- data/lib/mouth/endoscope/public/d3.time.js +687 -0
- data/lib/mouth/endoscope/public/jquery-ui-1.8.16.custom.min.js +177 -0
- data/lib/mouth/endoscope/public/jquery.js +4 -0
- data/lib/mouth/endoscope/public/json2.js +480 -0
- data/lib/mouth/endoscope/public/keymaster.js +163 -0
- data/lib/mouth/endoscope/public/linen.js +46 -0
- data/lib/mouth/endoscope/public/seven.css +68 -0
- data/lib/mouth/endoscope/public/seven.js +291 -0
- data/lib/mouth/endoscope/public/underscore.js +931 -0
- data/lib/mouth/endoscope/views/dashboard.erb +67 -0
- data/lib/mouth/graph.rb +58 -0
- data/lib/mouth/instrument.rb +56 -0
- data/lib/mouth/record.rb +72 -0
- data/lib/mouth/runner.rb +89 -0
- data/lib/mouth/sequence.rb +284 -0
- data/lib/mouth/source.rb +76 -0
- data/lib/mouth/sucker.rb +235 -0
- data/lib/mouth/version.rb +3 -0
- data/mouth.gemspec +28 -0
- data/test/sequence_test.rb +163 -0
- data/test/sucker_test.rb +55 -0
- data/test/test_helper.rb +5 -0
- metadata +167 -0
@@ -0,0 +1,163 @@
|
|
1
|
+
// keymaster.js
|
2
|
+
// (c) 2011 Thomas Fuchs
|
3
|
+
// keymaster.js may be freely distributed under the MIT license.
|
4
|
+
|
5
|
+
;(function(global){
|
6
|
+
var k,
|
7
|
+
_handlers = {},
|
8
|
+
_mods = { 16: false, 18: false, 17: false, 91: false },
|
9
|
+
_scope = 'all',
|
10
|
+
// modifier keys
|
11
|
+
_MODIFIERS = {
|
12
|
+
'⇧': 16, shift: 16,
|
13
|
+
'⌥': 18, alt: 18, option: 18,
|
14
|
+
'⌃': 17, ctrl: 17, control: 17,
|
15
|
+
'⌘': 91, command: 91
|
16
|
+
},
|
17
|
+
// special keys
|
18
|
+
_MAP = {
|
19
|
+
backspace: 8, tab: 9, clear: 12,
|
20
|
+
enter: 13, 'return': 13,
|
21
|
+
esc: 27, escape: 27, space: 32,
|
22
|
+
left: 37, up: 38,
|
23
|
+
right: 39, down: 40,
|
24
|
+
del: 46, 'delete': 46,
|
25
|
+
home: 36, end: 35,
|
26
|
+
pageup: 33, pagedown: 34,
|
27
|
+
',': 188, '.': 190, '/': 191,
|
28
|
+
'`': 192, '-': 189, '=': 187,
|
29
|
+
';': 186, '\'': 222,
|
30
|
+
'[': 219, ']': 221, '\\': 220
|
31
|
+
};
|
32
|
+
|
33
|
+
for(k=1;k<20;k++) _MODIFIERS['f'+k] = 111+k;
|
34
|
+
|
35
|
+
// IE doesn't support Array#indexOf, so have a simple replacement
|
36
|
+
function index(array, item){
|
37
|
+
var i = array.length;
|
38
|
+
while(i--) if(array[i]===item) return i;
|
39
|
+
return -1;
|
40
|
+
}
|
41
|
+
|
42
|
+
// handle keydown event
|
43
|
+
function dispatch(event){
|
44
|
+
var key, tagName, handler, k, i, modifiersMatch;
|
45
|
+
tagName = (event.target || event.srcElement).tagName;
|
46
|
+
key = event.keyCode;
|
47
|
+
|
48
|
+
// if a modifier key, set the key.<modifierkeyname> property to true and return
|
49
|
+
if(key == 93 || key == 224) key = 91; // right command on webkit, command on Gecko
|
50
|
+
if(key in _mods) {
|
51
|
+
_mods[key] = true;
|
52
|
+
// 'assignKey' from inside this closure is exported to window.key
|
53
|
+
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = true;
|
54
|
+
return;
|
55
|
+
}
|
56
|
+
|
57
|
+
// ignore keypressed in any elements that support keyboard data input
|
58
|
+
if (tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA') return;
|
59
|
+
|
60
|
+
// abort if no potentially matching shortcuts found
|
61
|
+
if (!(key in _handlers)) return;
|
62
|
+
|
63
|
+
// for each potential shortcut
|
64
|
+
for (i = 0; i < _handlers[key].length; i++) {
|
65
|
+
handler = _handlers[key][i];
|
66
|
+
|
67
|
+
// see if it's in the current scope
|
68
|
+
if(handler.scope == _scope || handler.scope == 'all'){
|
69
|
+
// check if modifiers match if any
|
70
|
+
modifiersMatch = handler.mods.length > 0;
|
71
|
+
for(k in _mods)
|
72
|
+
if((!_mods[k] && index(handler.mods, +k) > -1) ||
|
73
|
+
(_mods[k] && index(handler.mods, +k) == -1)) modifiersMatch = false;
|
74
|
+
// call the handler and stop the event if neccessary
|
75
|
+
if((handler.mods.length == 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91]) || modifiersMatch){
|
76
|
+
if(handler.method(event, handler)===false){
|
77
|
+
if(event.preventDefault) event.preventDefault();
|
78
|
+
else event.returnValue = false;
|
79
|
+
if(event.stopPropagation) event.stopPropagation();
|
80
|
+
if(event.cancelBubble) event.cancelBubble = true;
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
};
|
86
|
+
|
87
|
+
// unset modifier keys on keyup
|
88
|
+
function clearModifier(event){
|
89
|
+
var key = event.keyCode, k;
|
90
|
+
if(key == 93 || key == 224) key = 91;
|
91
|
+
if(key in _mods) {
|
92
|
+
_mods[key] = false;
|
93
|
+
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = false;
|
94
|
+
}
|
95
|
+
};
|
96
|
+
|
97
|
+
function resetModifiers() {
|
98
|
+
for(k in _mods) _mods[k] = false;
|
99
|
+
for(k in _MODIFIERS) assignKey[k] = false;
|
100
|
+
}
|
101
|
+
|
102
|
+
// parse and assign shortcut
|
103
|
+
function assignKey(key, scope, method){
|
104
|
+
var keys, mods, i, mi;
|
105
|
+
if (method === undefined) {
|
106
|
+
method = scope;
|
107
|
+
scope = 'all';
|
108
|
+
}
|
109
|
+
key = key.replace(/\s/g,'');
|
110
|
+
keys = key.split(',');
|
111
|
+
|
112
|
+
if((keys[keys.length-1])=='')
|
113
|
+
keys[keys.length-2] += ',';
|
114
|
+
// for each shortcut
|
115
|
+
for (i = 0; i < keys.length; i++) {
|
116
|
+
// set modifier keys if any
|
117
|
+
mods = [];
|
118
|
+
key = keys[i].split('+');
|
119
|
+
if(key.length > 1){
|
120
|
+
mods = key.slice(0,key.length-1);
|
121
|
+
for (mi = 0; mi < mods.length; mi++)
|
122
|
+
mods[mi] = _MODIFIERS[mods[mi]];
|
123
|
+
key = [key[key.length-1]];
|
124
|
+
}
|
125
|
+
// convert to keycode and...
|
126
|
+
key = key[0]
|
127
|
+
key = _MAP[key] || key.toUpperCase().charCodeAt(0);
|
128
|
+
// ...store handler
|
129
|
+
if (!(key in _handlers)) _handlers[key] = [];
|
130
|
+
_handlers[key].push({ shortcut: keys[i], scope: scope, method: method, key: keys[i], mods: mods });
|
131
|
+
}
|
132
|
+
};
|
133
|
+
|
134
|
+
// initialize key.<modifier> to false
|
135
|
+
for(k in _MODIFIERS) assignKey[k] = false;
|
136
|
+
|
137
|
+
// set current scope (default 'all')
|
138
|
+
function setScope(scope){ _scope = scope || 'all' };
|
139
|
+
function getScope(){ return _scope || 'all' };
|
140
|
+
|
141
|
+
// cross-browser events
|
142
|
+
function addEvent(object, event, method) {
|
143
|
+
if (object.addEventListener)
|
144
|
+
object.addEventListener(event, method, false);
|
145
|
+
else if(object.attachEvent)
|
146
|
+
object.attachEvent('on'+event, function(){ method(window.event) });
|
147
|
+
};
|
148
|
+
|
149
|
+
// set the handlers globally on document
|
150
|
+
addEvent(document, 'keydown', dispatch);
|
151
|
+
addEvent(document, 'keyup', clearModifier);
|
152
|
+
|
153
|
+
// reset modifiers to false whenever the window is (re)focused.
|
154
|
+
addEvent(window, 'focus', resetModifiers);
|
155
|
+
|
156
|
+
// set window.key and window.key.setScope
|
157
|
+
global.key = assignKey;
|
158
|
+
global.key.setScope = setScope;
|
159
|
+
global.key.getScope = getScope;
|
160
|
+
|
161
|
+
if(typeof module !== 'undefined') module.exports = key;
|
162
|
+
|
163
|
+
})(this);
|
@@ -0,0 +1,46 @@
|
|
1
|
+
// Me function for linenizing the background
|
2
|
+
$(function() {
|
3
|
+
// temporary substitute for parameters:
|
4
|
+
var el = document.getElementById('current-dashboard');
|
5
|
+
|
6
|
+
|
7
|
+
var canvas = document.createElement('canvas');
|
8
|
+
var context = canvas.getContext('2d');
|
9
|
+
var x = parseInt(getStyle(el, 'width')), y = parseInt(getStyle(el, 'height'));
|
10
|
+
var hue = 0;
|
11
|
+
|
12
|
+
canvas.width = x;
|
13
|
+
canvas.height = y;
|
14
|
+
|
15
|
+
context.lineWidth = 0.25;
|
16
|
+
context.strokeStyle = 'hsla(0,0%,16%,1)';
|
17
|
+
|
18
|
+
var grad = context.createLinearGradient(0, 0, x, 0);
|
19
|
+
grad.addColorStop(0, 'hsla(0,0%,10%,1)');
|
20
|
+
grad.addColorStop(0.5, 'hsla(0,0%,13%,1)');
|
21
|
+
grad.addColorStop(1, 'hsla(0,0%,10%,1)');
|
22
|
+
context.fillStyle = grad;
|
23
|
+
context.fillRect(0, 0, x, y);
|
24
|
+
|
25
|
+
for (var i = 0; i <= x; i++) {
|
26
|
+
for (var j = 0; j <= y; j++) {
|
27
|
+
if (Math.round(Math.random() * 30) === 30) {
|
28
|
+
context.moveTo(i, j + 0.5);
|
29
|
+
context.lineTo(i + (Math.ceil(Math.random() * 200)), j + 0.5);
|
30
|
+
}
|
31
|
+
|
32
|
+
if (Math.round(Math.random() * 30) === 30) {
|
33
|
+
context.moveTo(i + 0.5, j + 0.5);
|
34
|
+
context.lineTo(i + 0.5, j + (Math.ceil(Math.random() * 200)));
|
35
|
+
}
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
context.stroke();
|
40
|
+
|
41
|
+
el.style.backgroundImage = 'url(' + canvas.toDataURL() + ')';
|
42
|
+
|
43
|
+
function getStyle(el, property) {
|
44
|
+
return el.style[property] || getComputedStyle(el, '').getPropertyValue(property);
|
45
|
+
}
|
46
|
+
});
|
@@ -0,0 +1,68 @@
|
|
1
|
+
path {
|
2
|
+
fill: none;
|
3
|
+
}
|
4
|
+
|
5
|
+
.seven-tip {
|
6
|
+
position: absolute;
|
7
|
+
background: #2e3a4f;
|
8
|
+
background: -moz-linear-gradient(top, #62769b 0%, #2e3a4f 35%, #2e3a4f 65%, #212a3d 100%);
|
9
|
+
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62769b), color-stop(35%, #2e3a4f), color-stop(65%, #2e3a4f), color-stop(100%, #212a3d));
|
10
|
+
background: -webkit-linear-gradient(top, #62769b 0%, #2e3a4f 35%, #2e3a4f 65%, #212a3d 100%);
|
11
|
+
background: -o-linear-gradient(top, #62769b 0%, #2e3a4f 35%, #2e3a4f 65%, #212a3d 100%);
|
12
|
+
background: -ms-linear-gradient(top, #62769b 0%, #2e3a4f 35%, #2e3a4f 65%, #212a3d 100%);
|
13
|
+
background: linear-gradient(top, #62769b 0%, #2e3a4f 35%, #2e3a4f 65%, #212a3d 100%);
|
14
|
+
|
15
|
+
border-radius: 4px 4px 4px 4px;
|
16
|
+
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.2);
|
17
|
+
|
18
|
+
min-width: 120px;
|
19
|
+
top: 0;
|
20
|
+
left: 0;
|
21
|
+
padding: 2px;
|
22
|
+
|
23
|
+
font-family: sans-serif;
|
24
|
+
font-size: 11px;
|
25
|
+
line-height: 12px;
|
26
|
+
|
27
|
+
}
|
28
|
+
|
29
|
+
.seven-tip-inner {
|
30
|
+
background-color: #FFF;
|
31
|
+
border-radius: 2px 2px 2px 2px;
|
32
|
+
}
|
33
|
+
|
34
|
+
.seven-tip .seven-tip-time {
|
35
|
+
background-color: #F3F6FA;
|
36
|
+
padding: 4px;
|
37
|
+
}
|
38
|
+
|
39
|
+
.seven-tip li {
|
40
|
+
padding: 2px 4px;
|
41
|
+
}
|
42
|
+
|
43
|
+
.seven-tip .color {
|
44
|
+
display: inline-block;
|
45
|
+
height: 12px;
|
46
|
+
margin-right: 4px;
|
47
|
+
vertical-align: text-top;
|
48
|
+
width: 12px;
|
49
|
+
}
|
50
|
+
|
51
|
+
.seven-scrubber {
|
52
|
+
top: 7px;
|
53
|
+
left: 0;
|
54
|
+
position: absolute;
|
55
|
+
width: 1px;
|
56
|
+
background: black;
|
57
|
+
z-index: 9000;
|
58
|
+
filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=30);
|
59
|
+
opacity: 0.3;
|
60
|
+
-moz-box-shadow: 1px 0 0 rgba(255, 255, 255, 0.4);
|
61
|
+
-webkit-box-shadow: 1px 0 0 rgba(255, 255, 255, 0.4);
|
62
|
+
-o-box-shadow: 1px 0 0 rgba(255, 255, 255, 0.4);
|
63
|
+
box-shadow: 1px 0 0 rgba(255, 255, 255, 0.4);
|
64
|
+
}
|
65
|
+
|
66
|
+
.seven .novalue {
|
67
|
+
display: none;
|
68
|
+
}
|
@@ -0,0 +1,291 @@
|
|
1
|
+
(function($, window) {
|
2
|
+
|
3
|
+
// Throttles the calling of the callback function by delay milliseconds
|
4
|
+
function throttle(callback, delay) {
|
5
|
+
var lasttime = 0;
|
6
|
+
|
7
|
+
return function() {
|
8
|
+
var elapsed = +new Date() - lasttime
|
9
|
+
, args = arguments
|
10
|
+
, self = this;
|
11
|
+
|
12
|
+
if (elapsed > delay) {
|
13
|
+
lasttime = +new Date();
|
14
|
+
callback.apply(self, args);
|
15
|
+
}
|
16
|
+
}
|
17
|
+
};
|
18
|
+
|
19
|
+
function dateFor(startDate, granularityInMinutes, dataPoints) {
|
20
|
+
return new Date(+startDate + (granularityInMinutes * 60000 * (dataPoints - 1)));
|
21
|
+
}
|
22
|
+
|
23
|
+
var colors = d3.scale.ordinal().range("#007AD1 #469623 #EDDC00 #EB6D0E #BA0211 #572382 #029186 #9EA81D #F2A500 #E04107 #911258 #2144C7".split(' '));
|
24
|
+
|
25
|
+
window.Seven = function(selector, opts) {
|
26
|
+
this.selector = selector;
|
27
|
+
this.element = $(selector);
|
28
|
+
this.dataSets = {};
|
29
|
+
this.startDate = new Date(opts.start);
|
30
|
+
this.granularityInMinutes = opts.granularityInMinutes;
|
31
|
+
this.points = opts.points;
|
32
|
+
this.endDate = dateFor(this.startDate, this.granularityInMinutes, this.points);
|
33
|
+
this.kind = opts.kind || 'counter';
|
34
|
+
this.dateFormat = d3.time.format("%Y-%m-%d %H:%M:%S");
|
35
|
+
}
|
36
|
+
|
37
|
+
Seven.prototype = {
|
38
|
+
|
39
|
+
graph: function(opts) {
|
40
|
+
var self = this
|
41
|
+
, name = opts.name || "default"
|
42
|
+
;
|
43
|
+
|
44
|
+
// Save our data. Counters are named and there can be many. Timers take up the whole graph.
|
45
|
+
this.dataSets[name] = {data: opts.data};
|
46
|
+
if (this.kind == 'timer') {
|
47
|
+
this.timingData = opts.data;
|
48
|
+
}
|
49
|
+
|
50
|
+
// Empty out the graph.
|
51
|
+
// NOTE: improvement opportunity
|
52
|
+
this.element.empty();
|
53
|
+
|
54
|
+
this.graphWidth = this.element.width();
|
55
|
+
this.graphHeight = this.element.height();
|
56
|
+
|
57
|
+
var elD3 = d3.select(this.selector);
|
58
|
+
|
59
|
+
// Calculate max value in slightly different ways for counter vs timer
|
60
|
+
var maxValue = null;
|
61
|
+
if (this.kind == 'counter') {
|
62
|
+
var allData = [];
|
63
|
+
_.each(this.dataSets, function(v, k) {
|
64
|
+
_.each(v.data, function(d) {
|
65
|
+
allData.push(d);
|
66
|
+
})
|
67
|
+
});
|
68
|
+
maxValue = d3.max(allData);
|
69
|
+
} else if (this.kind == 'timer') {
|
70
|
+
var allData = [];
|
71
|
+
_.each(this.timingData, function(d) {
|
72
|
+
if (d.mean !== null) {
|
73
|
+
allData.push(d.mean + d.stddev);
|
74
|
+
}
|
75
|
+
})
|
76
|
+
maxValue = d3.max(allData);
|
77
|
+
}
|
78
|
+
|
79
|
+
// Calculate scales
|
80
|
+
this.xScale = d3.time.scale().domain([this.startDate, this.endDate]).range([7, this.graphWidth - 12]);
|
81
|
+
this.xScaleIndex = d3.time.scale().domain([this.startDate, this.endDate]).range([0, this.points - 1]);
|
82
|
+
this.yScale = d3.scale.linear().domain([0, maxValue]).range([7, this.graphHeight - 7]);
|
83
|
+
|
84
|
+
// Add in our svg element
|
85
|
+
var svg = elD3
|
86
|
+
.append('svg:svg')
|
87
|
+
.attr('class', 'seven ' + this.kind)
|
88
|
+
.attr("width", this.graphWidth)
|
89
|
+
.attr("height", this.graphHeight);
|
90
|
+
|
91
|
+
// Create a translating group
|
92
|
+
var svgGroup = svg.append("svg:g")
|
93
|
+
.attr("transform", "translate(" + 0 + "," + this.graphHeight + ")");
|
94
|
+
|
95
|
+
if (this.kind == 'counter') {
|
96
|
+
// Add the paths
|
97
|
+
var i = 0;
|
98
|
+
_.each(this.dataSets, function(datasetObj, datasetName) {
|
99
|
+
var datasetValues = datasetObj.data;
|
100
|
+
datasetObj.color = colors(i);
|
101
|
+
|
102
|
+
var line = d3.svg.line()
|
103
|
+
//.interpolate("basis")
|
104
|
+
.x(function(d, i) { return self.xScale(dateFor(self.startDate, self.granularityInMinutes, i + 1)); })
|
105
|
+
.y(function(d) { return -1 * self.yScale(d); });
|
106
|
+
|
107
|
+
var path = svgGroup.append("svg:path").attr("d", line(datasetValues))
|
108
|
+
.attr('stroke', datasetObj.color)
|
109
|
+
.attr('stroke-width', 1);
|
110
|
+
|
111
|
+
i += 1;
|
112
|
+
});
|
113
|
+
} else if (this.kind == 'timer') {
|
114
|
+
this.barWidth = (this.graphWidth - 19) / this.points;
|
115
|
+
this.barScale = d3.scale.linear().domain([0, 1]).range([0, this.barWidth]);
|
116
|
+
|
117
|
+
svgGroup.selectAll('rect.stddev')
|
118
|
+
.data(this.timingData)
|
119
|
+
.enter().append("rect")
|
120
|
+
.attr('class', function(d, i) { return d.mean !== null ? 'stddev' : 'novalue'; })
|
121
|
+
.attr("x", function(d, i) { return self.barScale(i) + 6.5; })
|
122
|
+
.attr("y", function(d) { return -self.yScale(d.mean + d.stddev) - .5; })
|
123
|
+
.attr("width", this.barWidth)
|
124
|
+
.attr("height", function(d) { return self.yScale(d.stddev * 2); })
|
125
|
+
.attr('fill', colors(0));
|
126
|
+
|
127
|
+
svgGroup.selectAll('rect.mean')
|
128
|
+
.data(this.timingData)
|
129
|
+
.enter().append("rect")
|
130
|
+
.attr('class', function(d, i) { return d.mean !== null ? 'mean' : 'novalue'; })
|
131
|
+
.attr("x", function(d, i) { return self.barScale(i) + 6.5; })
|
132
|
+
.attr("y", function(d) { return -self.yScale(d.mean) + 2; })
|
133
|
+
.attr("width", this.barWidth)
|
134
|
+
.attr("height", function(d) { return 2; })
|
135
|
+
.attr('fill', 'white');
|
136
|
+
}
|
137
|
+
|
138
|
+
// Draw axis
|
139
|
+
svgGroup.append("svg:line") // x-axis
|
140
|
+
.attr("x1", this.xScale(this.startDate) - 2)
|
141
|
+
.attr("y1", -1 * this.yScale(0) + 2.5)
|
142
|
+
.attr("x2", this.xScale(this.endDate))
|
143
|
+
.attr("y2", -1 * this.yScale(0) + 2.5)
|
144
|
+
.attr('stroke', '#AAA').attr('stroke-width', 1);
|
145
|
+
|
146
|
+
svgGroup.append("svg:line") // y-axis
|
147
|
+
.attr("x1", this.xScale(this.startDate) - 2.5)
|
148
|
+
.attr("y1", -1 * this.yScale(0) + 2)
|
149
|
+
.attr("x2", this.xScale(this.startDate) - 2.5)
|
150
|
+
.attr("y2", -1 * this.graphHeight - 7)
|
151
|
+
.attr('stroke', '#AAA').attr('stroke-width', 1);
|
152
|
+
|
153
|
+
// Handle the tool tip
|
154
|
+
this.element.bind('mousemove', throttle(function(e) {
|
155
|
+
var offset = self.element.offset()
|
156
|
+
, cx = e.pageX - offset.left
|
157
|
+
, cy = e.pageY - offset.top
|
158
|
+
;
|
159
|
+
|
160
|
+
self.showTip({
|
161
|
+
x: cx,
|
162
|
+
y: cy
|
163
|
+
});
|
164
|
+
}, 50));
|
165
|
+
|
166
|
+
this.element.bind('mouseleave', function(e) {
|
167
|
+
self.hideTip();
|
168
|
+
});
|
169
|
+
|
170
|
+
return this;
|
171
|
+
},
|
172
|
+
|
173
|
+
showTip: function(opts) {
|
174
|
+
var self = this
|
175
|
+
, html
|
176
|
+
, date = this.xScale.invert(opts.x)
|
177
|
+
, index = Math.round(this.xScaleIndex(date))
|
178
|
+
, nearestDate = this.xScaleIndex.invert(index)
|
179
|
+
, leftAnchor = this.xScale(nearestDate)
|
180
|
+
, tipRows = self.kind == 'counter' ? _.size(this.dataSets) : 7
|
181
|
+
, tipWidth
|
182
|
+
, tipHeight
|
183
|
+
, tipLeft
|
184
|
+
, tipTop
|
185
|
+
;
|
186
|
+
|
187
|
+
if (index < 0 || index >= this.points) {
|
188
|
+
this.hideTip();
|
189
|
+
return;
|
190
|
+
}
|
191
|
+
|
192
|
+
// Timers have a slightly different 'center' to align to the scrubber
|
193
|
+
if (this.kind == 'timer') {
|
194
|
+
leftAnchor = (index + 0.5) * this.barWidth + 6;
|
195
|
+
}
|
196
|
+
|
197
|
+
// Build the tip if it doesn't exist
|
198
|
+
if (!this.tip) {
|
199
|
+
html = [
|
200
|
+
'<div class="seven-tip">',
|
201
|
+
'<div class="seven-tip-inner">',
|
202
|
+
'<div class="seven-tip-time"></div>',
|
203
|
+
'<ul class="seven-tip-values"></ul>',
|
204
|
+
'</div>',
|
205
|
+
'</div>'
|
206
|
+
];
|
207
|
+
|
208
|
+
this.tip = $(html.join(''));
|
209
|
+
this.element.append(this.tip);
|
210
|
+
this.tipSide = 'right';
|
211
|
+
}
|
212
|
+
|
213
|
+
this.tip.find('.seven-tip-time').text(this.dateFormat(nearestDate));
|
214
|
+
|
215
|
+
this.tip.css({display: 'block'});
|
216
|
+
|
217
|
+
tipWidth = this.tip.width();
|
218
|
+
tipHeight = this.tip.height();
|
219
|
+
if (this.tipSide == 'right' && ((leftAnchor + 25 + tipWidth) >= this.graphWidth)) {
|
220
|
+
this.tipSide = 'left';
|
221
|
+
} else if (this.tipSide == 'left' && ((leftAnchor - 25 - tipWidth) <= 0)) {
|
222
|
+
this.tipSide = 'right';
|
223
|
+
}
|
224
|
+
|
225
|
+
if (this.tipSide == 'right') {
|
226
|
+
tipLeft = leftAnchor + 20;
|
227
|
+
} else {
|
228
|
+
tipLeft = leftAnchor - 20 - tipWidth;
|
229
|
+
}
|
230
|
+
|
231
|
+
tipTop = opts.y - 10;
|
232
|
+
if ((tipTop + tipHeight + 10) > this.graphHeight) {
|
233
|
+
tipTop = this.graphHeight - tipHeight - 10;
|
234
|
+
}
|
235
|
+
|
236
|
+
if (tipTop < 0) {
|
237
|
+
tipTop = 5;
|
238
|
+
}
|
239
|
+
|
240
|
+
this.tip.css({
|
241
|
+
top: tipTop,
|
242
|
+
left: tipLeft
|
243
|
+
});
|
244
|
+
|
245
|
+
html = [];
|
246
|
+
_.each(this.dataSets, function(datasetObj, datasetName) {
|
247
|
+
var datasetValues = datasetObj.data
|
248
|
+
|
249
|
+
if (self.kind == 'counter') {
|
250
|
+
var n = datasetValues[index];
|
251
|
+
if (n !== null) n = Math.round(n * 100) / 100;
|
252
|
+
html.push([
|
253
|
+
'<li>',
|
254
|
+
'<span class="key"><span class="color" style="background-color:' + datasetObj.color + '"></span>' + datasetName + ': </span>',
|
255
|
+
'<span class="value">' + n + '</span>',
|
256
|
+
'</li>'
|
257
|
+
].join(''));
|
258
|
+
} else if (self.kind == 'timer') {
|
259
|
+
var t = datasetValues[index]
|
260
|
+
, fields = ['count', 'mean', 'median', 'stddev', 'min', 'max', 'sum']
|
261
|
+
;
|
262
|
+
|
263
|
+
_.each(fields, function(f) {
|
264
|
+
var n = t[f];
|
265
|
+
if (n !== null) n = Math.round(n * 100) / 100;
|
266
|
+
html.push('<li><span class="key">' + f + ': </span><span class="value">' + n + '</span></li>');
|
267
|
+
});
|
268
|
+
}
|
269
|
+
});
|
270
|
+
this.tip.find('.seven-tip-values').html(html.join(''));
|
271
|
+
|
272
|
+
if (!this.scrubber) {
|
273
|
+
html = '<div class="seven-scrubber"></div>'
|
274
|
+
this.scrubber = $(html);
|
275
|
+
this.element.append(this.scrubber);
|
276
|
+
}
|
277
|
+
this.scrubber.css({display: 'block', left: leftAnchor, height: this.graphHeight - 14});
|
278
|
+
},
|
279
|
+
|
280
|
+
hideTip: function() {
|
281
|
+
if (this.tip) {
|
282
|
+
this.tip.css({display: 'none'});
|
283
|
+
}
|
284
|
+
if (this.scrubber) {
|
285
|
+
this.scrubber.css({display: 'none'});
|
286
|
+
}
|
287
|
+
}
|
288
|
+
|
289
|
+
};
|
290
|
+
|
291
|
+
}(jQuery, this));
|