visage-app 2.0.5 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,6 +9,7 @@ require 'digest/md5'
9
9
  module Visage
10
10
  class Profile
11
11
  attr_reader :options, :selected_hosts, :hosts, :selected_metrics, :metrics,
12
+ :selected_percentiles, :percentiles,
12
13
  :name, :errors
13
14
 
14
15
  def self.old_format?
@@ -33,7 +34,8 @@ module Visage
33
34
  def self.all(opts={})
34
35
  sort = opts[:sort]
35
36
  profiles = self.load
36
- profiles = sort == "name" ? profiles.sort_by {|k,v| v[:profile_name]}.map {|i| i.last } : profiles.values
37
+ profiles = ((sort == "name") or not sort) ? profiles.sort_by {|k,v| v[:profile_name]}.map {|i| i.last } : profiles.values
38
+ # FIXME - to sort by creation time we need to save creation time on each profile
37
39
  profiles.map { |prof| self.new(prof) }
38
40
  end
39
41
 
@@ -41,8 +43,9 @@ module Visage
41
43
  @options = opts
42
44
  @options[:url] = @options[:profile_name] ? @options[:profile_name].downcase.gsub(/[^\w]+/, "+") : nil
43
45
  @errors = {}
44
- @options[:hosts] = @options[:hosts].values if @options[:hosts].class == Hash
45
- @options[:metrics] = @options[:metrics].values if @options[:metrics].class == Hash
46
+ @options[:hosts] = @options[:hosts].values if @options[:hosts].class == Hash
47
+ @options[:metrics] = @options[:metrics].values if @options[:metrics].class == Hash
48
+ @options[:percentiles] = @options[:percentiles].values if @options[:percentiles].class == Hash
46
49
  end
47
50
 
48
51
  # Hashed based access to @options.
@@ -55,6 +58,7 @@ module Visage
55
58
  # Construct record.
56
59
  attrs = { :hosts => @options[:hosts],
57
60
  :metrics => @options[:metrics],
61
+ :percentiles => @options[:percentiles],
58
62
  :profile_name => @options[:profile_name],
59
63
  :url => @options[:profile_name].downcase.gsub(/[^\w]+/, "+") }
60
64
 
@@ -78,9 +82,10 @@ module Visage
78
82
  end
79
83
 
80
84
  def graphs
81
- graphs = []
82
- hosts = @options[:hosts]
83
- metrics = @options[:metrics]
85
+ graphs = []
86
+ hosts = @options[:hosts]
87
+ metrics = @options[:metrics]
88
+ percentiles = @options[:percentiles]
84
89
 
85
90
  hosts.each do |host|
86
91
  attrs = {}
@@ -96,7 +101,8 @@ module Visage
96
101
  attrs.each_pair do |plugin, instances|
97
102
  graphs << Visage::Graph.new(:host => host,
98
103
  :plugin => plugin,
99
- :instances => instances)
104
+ :instances => instances,
105
+ :percentiles => percentiles)
100
106
  end
101
107
  end
102
108
 
@@ -436,6 +436,7 @@ var ChartBuilder = new Class({
436
436
  this.searchers = new Object;
437
437
  this.setupHostSearch();
438
438
  this.setupMetricSearch();
439
+ this.setupPercentileSelection();
439
440
  this.setupShow();
440
441
 
441
442
  /* Display graphs if hosts + metrics have been selected */
@@ -460,6 +461,33 @@ var ChartBuilder = new Class({
460
461
  });
461
462
  this.searchers.metric = searcher;
462
463
  },
464
+ setupPercentileSelection: function() {
465
+ var container = this.builder.getElement("div#profile-options div.percentiles");
466
+ if (this.options.percentiles) {
467
+ if (this.options.percentiles.length > 0) {
468
+ this.options.percentile95 = true;
469
+ }
470
+ }
471
+ if (container) {
472
+ var percentileSelector = new Element('input', {
473
+ 'type': 'checkbox',
474
+ 'id': this.parentElement + '-percentile95',
475
+ 'name': 'percentile_95',
476
+ 'checked': this.options.percentile95,
477
+ 'events': {
478
+ 'click': function() {
479
+ this.options.percentile95 = !this.options.percentile95
480
+ }.bind(this)
481
+ },
482
+ 'styles': {
483
+ 'margin-right': '4px',
484
+ 'cursor': 'pointer'
485
+ }
486
+ });
487
+ container.grab(percentileSelector);
488
+ }
489
+
490
+ },
463
491
  setupSave: function() {
464
492
  if (!this.save) {
465
493
  var profile_name = this.profile_name = new Element('input', {
@@ -479,7 +507,12 @@ var ChartBuilder = new Class({
479
507
  'click': function() {
480
508
  var hosts = this.searchers.host.tokenValues(),
481
509
  metrics = this.searchers.metric.tokenValues();
510
+ percentiles = [];
511
+ percentile95 = this.options.percentile95;
482
512
 
513
+ if (percentile95) {
514
+ percentiles.push(95);
515
+ }
483
516
  var jsonRequest = new Request.JSON({
484
517
  method: 'post',
485
518
  url: '/builder',
@@ -495,7 +528,8 @@ var ChartBuilder = new Class({
495
528
  }).send({'data': {
496
529
  'hosts': hosts,
497
530
  'metrics': metrics,
498
- 'profile_name': profile_name.get('value')
531
+ 'profile_name': profile_name.get('value'),
532
+ 'percentiles': percentiles
499
533
  }});
500
534
 
501
535
  }.bind(this)
@@ -525,7 +559,14 @@ var ChartBuilder = new Class({
525
559
 
526
560
 
527
561
  var hosts = $(this.searchers.host).getElements("div.token.finalized"),
528
- metrics = $(this.searchers.metric).getElements("div.token.finalized");
562
+ metrics = $(this.searchers.metric).getElements("div.token.finalized"),
563
+ percentiles = [];
564
+ percentile95 = this.options.percentile95;
565
+
566
+ if (percentile95) {
567
+ percentiles.push(95);
568
+ }
569
+ this.options.percentiles = percentiles;
529
570
 
530
571
  if (hosts.length > 0 && metrics.length > 0) {
531
572
  this.showGraphs();
@@ -554,8 +595,9 @@ var ChartBuilder = new Class({
554
595
  var hosts = hosts.map(function(el) { return el.get('text') }),
555
596
  metrics = metrics.map(function(el) { return el.get('text') }),
556
597
  graphs = $('graphs'),
557
- save = this.save;
558
- profile_name = this.profile_name;
598
+ save = this.save,
599
+ profile_name = this.profile_name,
600
+ percentiles = this.options.percentiles;
559
601
 
560
602
  graphs.empty();
561
603
  hosts.each(function(host) {
@@ -579,7 +621,8 @@ var ChartBuilder = new Class({
579
621
  graphs.grab(element);
580
622
 
581
623
  var graph = new VisageGraph(element, host, plugin, {
582
- pluginInstance: metrics.join(',')
624
+ pluginInstance: metrics.join(','),
625
+ percentiles: percentiles
583
626
  });
584
627
 
585
628
  window.Graphs.include(graph);
@@ -121,6 +121,7 @@ function formatValue(value, options) {
121
121
  break
122
122
  }
123
123
 
124
+ if (!(label)) { label = 0; }
124
125
  return label.format({decimals: precision, suffix: unit})
125
126
  }
126
127
 
@@ -168,6 +169,7 @@ var VisageBase = new Class({
168
169
  },
169
170
  dataURL: function() {
170
171
  var url = ['data', this.options.host, this.options.plugin];
172
+
171
173
  // if the data exists on another host (useful for embedding)
172
174
  if (this.options.baseurl) {
173
175
  url.unshift(this.options.baseurl.replace(/\/$/, ''))
@@ -180,7 +182,14 @@ var VisageBase = new Class({
180
182
  if (!url[0].match(/http\:\/\//)) {
181
183
  url[0] = '/' + url[0]
182
184
  }
183
- return url.join('/')
185
+ var options = '';
186
+ if (this.options.percentiles) {
187
+ if (this.options.percentiles.length > 0) {
188
+ options = '?percentiles=true';
189
+ this.options.percentile95 = true;
190
+ }
191
+ }
192
+ return url.join('/') + options;
184
193
  },
185
194
  getData: function() {
186
195
  this.request = new Request.JSONP({
@@ -250,9 +259,11 @@ var VisageGraph = new Class({
250
259
  });
251
260
 
252
261
  this.chart.redraw();
262
+ this.drawPercentiles(this.chart)
253
263
  break;
254
264
  default:
255
265
  this.drawChart()
266
+ this.drawPercentiles(this.chart)
256
267
  break;
257
268
  }
258
269
  },
@@ -264,8 +275,8 @@ var VisageGraph = new Class({
264
275
 
265
276
  $each(data[host][plugin], function(instance, instanceName) {
266
277
  $each(instance, function(metric, metricName) {
267
- var start = metric.start,
268
- finish = metric.finish,
278
+ var start = metric.start,
279
+ finish = metric.finish,
269
280
  interval = (finish - start) / metric.data.length;
270
281
 
271
282
  var data = metric.data.map(function(value, index) {
@@ -278,12 +289,12 @@ var VisageGraph = new Class({
278
289
  var set = {
279
290
  name: [ host, plugin, instanceName, metricName ],
280
291
  data: data,
292
+ percentile95: metric.percentile_95
281
293
  };
282
294
 
283
295
  series.push(set)
284
296
  }, this);
285
297
  }, this);
286
-
287
298
  return series
288
299
  },
289
300
  getSeriesMinMax: function(series) {
@@ -313,7 +324,40 @@ var VisageGraph = new Class({
313
324
 
314
325
  return {'min': min, 'max': max};
315
326
  },
327
+ removePercentiles: function(chart) {
328
+
329
+ var series = this.series;
330
+
331
+ series.each(function(set) {
332
+ chart.yAxis[0].removePlotLine('95e_' + set.name[2] + set.name[3]);
333
+ });
334
+ },
335
+ drawPercentiles: function(chart) {
336
+ var series = this.series;
337
+
338
+ /* Get the maximum value across all sets.
339
+ * Used later on to determine the decimal place in the label. */
340
+ meta = this.getSeriesMinMax(series);
341
+ var min = meta.min,
342
+ max = meta.max;
343
+
344
+ series.each(function(set) {
345
+ formattedValue = formatValue(set.percentile95, { 'precision': 2, 'min': min, 'max': max });
346
+ chart.yAxis[0].removePlotLine('95e_' + set.name[2] + set.name[3]);
347
+ chart.yAxis[0].addPlotLine({
348
+ id: '95e_' + set.name[2] + set.name[3],
349
+ value: set.percentile95,
350
+ color: '#ff0000',
351
+ width: 1,
352
+ zIndex: 5,
353
+ label: {
354
+ text: '95e ' + set.name[3] + ": " + formattedValue
355
+ }
356
+ })
357
+ });
358
+ },
316
359
  drawChart: function() {
360
+
317
361
  var series = this.series,
318
362
  title = this.title(),
319
363
  element = this.parentElement,
@@ -344,6 +388,9 @@ var VisageGraph = new Class({
344
388
  'finish': this.lastFinish / 1000 + 10,
345
389
  'live': true };
346
390
  this.requestData = data;
391
+ // FIXME: for 95e plotLines - need to update them each data retrieval
392
+ // but perhaps 'live' just sends incremental data and so won't cause
393
+ // a recalculation of the 95e figures on the server side?
347
394
  this.getData()
348
395
  }
349
396
  }.bind(this), 10000);
@@ -400,7 +447,7 @@ var VisageGraph = new Class({
400
447
  });
401
448
  return value
402
449
  }
403
- }
450
+ },
404
451
  },
405
452
  plotOptions: {
406
453
  series: {
@@ -447,8 +494,9 @@ var VisageGraph = new Class({
447
494
  layout: 'horizontal',
448
495
  align: 'center',
449
496
  verticalAlign: 'top',
450
- y: 275,
497
+ y: 255,
451
498
  borderWidth: 0,
499
+ floating: true,
452
500
  labelFormatter: function() {
453
501
  return formatSeriesLabel(this.name)
454
502
  },
@@ -522,7 +570,7 @@ var VisageGraph = new Class({
522
570
  '7 days': 168,
523
571
  '2 weeks': 336,
524
572
  '1 month': 774,
525
- '3 month': 2322,
573
+ '3 months': 2322,
526
574
  '6 months': 4368,
527
575
  '1 year': 8760,
528
576
  '2 years': 17520 });
@@ -540,14 +588,45 @@ var VisageGraph = new Class({
540
588
  select.grab(option)
541
589
  });
542
590
 
543
- var liveToggler = new Element('input', {
591
+ /* Calendar month timescales dropdown */
592
+ var monthlyTimescales = new Hash({ 'current month': 0,
593
+ 'previous month': 1,
594
+ 'two months ago': 2,
595
+ 'three months ago': 3});
596
+
597
+ monthlyTimescales.each(function(monthsAgo, label) {
598
+ var current = this.currentTimePeriod == label;
599
+ var value = "start=" + (new Date().decrement('month', monthsAgo).set('date', 1).set('hr', 0).set('min', 0).set('sec', 0).getTime() / 1000);
600
+ value += '&finish=' + (new Date().decrement('month', monthsAgo - 1).set('date', 1).set('hr', 0).set('min', 0).set('sec', 0).getTime() / 1000);
601
+
602
+ var option = new Element('option', {
603
+ 'html': label,
604
+ 'value': value,
605
+ 'selected': (current ? 'selected' : ''),
606
+ });
607
+ select.grab(option)
608
+ });
609
+
610
+ var liveToggler = this.liveToggler = new Element('input', {
544
611
  'type': 'checkbox',
545
612
  'id': this.parentElement + '-live',
546
613
  'name': 'live',
547
614
  'checked': this.options.live,
615
+ 'disabled': this.options.percentile95,
548
616
  'events': {
549
- 'click': function() {
617
+ 'click': function(e) {
550
618
  this.options.live = !this.options.live
619
+ if (this.options.live) {
620
+ // tell percentiles95Toggler to be unchecked
621
+ if (this.options.percentile95) {
622
+ this.percentile95Toggler.fireEvent('click');
623
+ }
624
+ this.percentile95Toggler.set('disabled', true);
625
+ } else {
626
+ this.requestData.live = false;
627
+ this.percentile95Toggler.set('disabled', false);
628
+ e.target.form.fireEvent('submit', e)
629
+ }
551
630
  }.bind(this)
552
631
  },
553
632
  'styles': {
@@ -567,6 +646,60 @@ var VisageGraph = new Class({
567
646
  }
568
647
  });
569
648
 
649
+ var percentile95Toggler = this.percentile95Toggler = new Element('input', {
650
+ 'type': 'checkbox',
651
+ 'id': this.parentElement + '-percentile95',
652
+ 'name': 'percentile95',
653
+ 'checked': this.options.percentile95,
654
+ 'events': {
655
+ 'click': function() {
656
+ this.options.percentile95 = !this.options.percentile95;
657
+ if (!(this.options.percentiles)) {
658
+ this.options.percentiles = [];
659
+ }
660
+ if (this.options.percentile95) {
661
+ this.options.percentiles.push('95');
662
+ if ((this.chart) && (this.series[0].percentile95)) {
663
+ this.drawPercentiles(this.chart);
664
+ } else {
665
+ this.getData();
666
+ }
667
+ // tell liveToggler to be unchecked
668
+ if (this.options.live) {
669
+ this.liveToggler.fireEvent('click');
670
+ }
671
+ this.liveToggler.set('disabled', true);
672
+ } else {
673
+ // FIXME - when adding support for 5th and 50th percentiles
674
+ // etc we'll need to switch the options.percentiles array
675
+ // to a hash for easy enabling / disabling of percentiles
676
+ this.options.percentiles = [];
677
+ if (this.chart) {
678
+ this.removePercentiles(this.chart);
679
+ } else {
680
+ this.getData();
681
+ }
682
+ this.liveToggler.set('disabled', false);
683
+ }
684
+ }.bind(this)
685
+ },
686
+ 'styles': {
687
+ 'margin-right': '4px',
688
+ 'cursor': 'pointer'
689
+ }
690
+ });
691
+
692
+ var percentile95Label = new Element('label', {
693
+ 'for': this.parentElement + '-percentile95',
694
+ 'html': '95th Percentile',
695
+ 'styles': {
696
+ 'font-family': 'sans-serif',
697
+ 'font-size': '11px',
698
+ 'margin-right': '8px',
699
+ 'cursor': 'pointer'
700
+ }
701
+ });
702
+
570
703
  var exportLink = new Element('a', {
571
704
  'href': this.dataURL(),
572
705
  'html': 'Export data',
@@ -594,6 +727,8 @@ var VisageGraph = new Class({
594
727
  });
595
728
 
596
729
  form.grab(exportLink)
730
+ form.grab(percentile95Toggler)
731
+ form.grab(percentile95Label)
597
732
  form.grab(liveToggler)
598
733
  form.grab(liveLabel)
599
734
  form.grab(select)
@@ -0,0 +1,12 @@
1
+ /*
2
+ Highcharts JS v2.2.1 (2012-03-15)
3
+ MooTools adapter
4
+
5
+ (c) 2010-2011 Torstein H?nsi
6
+
7
+ License: www.highcharts.com/license
8
+ */
9
+ (function(){var e=window,i=document,f=e.MooTools.version.substring(0,3),g=f==="1.2"||f==="1.1",j=g||f==="1.3",h=e.$extend||function(){return Object.append.apply(Object,arguments)};e.HighchartsAdapter={init:function(a){var b=Fx.prototype,c=b.start,d=Fx.Morph.prototype,e=d.compute;b.start=function(b,d){var e=this.element;if(b.d)this.paths=a.init(e,e.d,this.toD);c.apply(this,arguments);return this};d.compute=function(b,c,d){var f=this.paths;if(f)this.element.attr("d",a.step(f[0],f[1],d,this.toD));else return e.apply(this,
10
+ arguments)}},getScript:function(a,b){var c=i.getElementsByTagName("head")[0],d=i.createElement("script");d.type="text/javascript";d.src=a;d.onload=b;c.appendChild(d)},animate:function(a,b,c){var d=a.attr,f=c&&c.complete;if(d&&!a.setStyle)a.getStyle=a.attr,a.setStyle=function(){var b=arguments;a.attr.call(a,b[0],b[1][0])},a.$family=function(){return!0};e.HighchartsAdapter.stop(a);c=new Fx.Morph(d?a:$(a),h({transition:Fx.Transitions.Quad.easeInOut},c));if(d)c.element=a;if(b.d)c.toD=b.d;f&&c.addEvent("complete",
11
+ f);c.start(b);a.fx=c},each:function(a,b){return g?$each(a,b):Array.each(a,b)},map:function(a,b){return a.map(b)},grep:function(a,b){return a.filter(b)},merge:function(){var a=arguments,b=[{}],c=a.length;if(g)a=$merge.apply(null,a);else{for(;c--;)typeof a[c]!=="boolean"&&(b[c+1]=a[c]);a=Object.merge.apply(Object,b)}return a},offset:function(a){a=$(a).getOffsets();return{left:a.x,top:a.y}},extendWithEvents:function(a){a.addEvent||(a.nodeName?$(a):h(a,new Events))},addEvent:function(a,b,c){typeof b===
12
+ "string"&&(b==="unload"&&(b="beforeunload"),e.HighchartsAdapter.extendWithEvents(a),a.addEvent(b,c))},removeEvent:function(a,b,c){typeof a!=="string"&&(e.HighchartsAdapter.extendWithEvents(a),b?(b==="unload"&&(b="beforeunload"),c?a.removeEvent(b,c):a.removeEvents(b)):a.removeEvents())},fireEvent:function(a,b,c,d){b={type:b,target:a};b=j?new Event(b):new DOMEvent(b);b=h(b,c);b.preventDefault=function(){d=null};a.fireEvent&&a.fireEvent(b.type,b);d&&d(b)},stop:function(a){a.fx&&a.fx.cancel()}}})();
@@ -0,0 +1,298 @@
1
+ /**
2
+ * @license Highcharts JS v2.2.1 (2012-03-15)
3
+ * MooTools adapter
4
+ *
5
+ * (c) 2010-2011 Torstein Hønsi
6
+ *
7
+ * License: www.highcharts.com/license
8
+ */
9
+
10
+ // JSLint options:
11
+ /*global Fx, $, $extend, $each, $merge, Events, Event, DOMEvent */
12
+
13
+ (function () {
14
+
15
+ var win = window,
16
+ doc = document,
17
+ mooVersion = win.MooTools.version.substring(0, 3), // Get the first three characters of the version number
18
+ legacy = mooVersion === '1.2' || mooVersion === '1.1', // 1.1 && 1.2 considered legacy, 1.3 is not.
19
+ legacyEvent = legacy || mooVersion === '1.3', // In versions 1.1 - 1.3 the event class is named Event, in newer versions it is named DOMEvent.
20
+ $extend = win.$extend || function () {
21
+ return Object.append.apply(Object, arguments);
22
+ };
23
+
24
+ win.HighchartsAdapter = {
25
+ /**
26
+ * Initialize the adapter. This is run once as Highcharts is first run.
27
+ * @param {Object} pathAnim The helper object to do animations across adapters.
28
+ */
29
+ init: function (pathAnim) {
30
+ var fxProto = Fx.prototype,
31
+ fxStart = fxProto.start,
32
+ morphProto = Fx.Morph.prototype,
33
+ morphCompute = morphProto.compute;
34
+
35
+ // override Fx.start to allow animation of SVG element wrappers
36
+ /*jslint unparam: true*//* allow unused parameters in fx functions */
37
+ fxProto.start = function (from, to) {
38
+ var fx = this,
39
+ elem = fx.element;
40
+
41
+ // special for animating paths
42
+ if (from.d) {
43
+ //this.fromD = this.element.d.split(' ');
44
+ fx.paths = pathAnim.init(
45
+ elem,
46
+ elem.d,
47
+ fx.toD
48
+ );
49
+ }
50
+ fxStart.apply(fx, arguments);
51
+
52
+ return this; // chainable
53
+ };
54
+
55
+ // override Fx.step to allow animation of SVG element wrappers
56
+ morphProto.compute = function (from, to, delta) {
57
+ var fx = this,
58
+ paths = fx.paths;
59
+
60
+ if (paths) {
61
+ fx.element.attr(
62
+ 'd',
63
+ pathAnim.step(paths[0], paths[1], delta, fx.toD)
64
+ );
65
+ } else {
66
+ return morphCompute.apply(fx, arguments);
67
+ }
68
+ };
69
+ /*jslint unparam: false*/
70
+ },
71
+
72
+ /**
73
+ * Downloads a script and executes a callback when done.
74
+ * @param {String} scriptLocation
75
+ * @param {Function} callback
76
+ */
77
+ getScript: function (scriptLocation, callback) {
78
+ // We cannot assume that Assets class from mootools-more is available so instead insert a script tag to download script.
79
+ var head = doc.getElementsByTagName('head')[0];
80
+ var script = doc.createElement('script');
81
+
82
+ script.type = 'text/javascript';
83
+ script.src = scriptLocation;
84
+ script.onload = callback;
85
+
86
+ head.appendChild(script);
87
+ },
88
+
89
+ /**
90
+ * Animate a HTML element or SVG element wrapper
91
+ * @param {Object} el
92
+ * @param {Object} params
93
+ * @param {Object} options jQuery-like animation options: duration, easing, callback
94
+ */
95
+ animate: function (el, params, options) {
96
+ var isSVGElement = el.attr,
97
+ effect,
98
+ complete = options && options.complete;
99
+
100
+ if (isSVGElement && !el.setStyle) {
101
+ // add setStyle and getStyle methods for internal use in Moo
102
+ el.getStyle = el.attr;
103
+ el.setStyle = function () { // property value is given as array in Moo - break it down
104
+ var args = arguments;
105
+ el.attr.call(el, args[0], args[1][0]);
106
+ };
107
+ // dirty hack to trick Moo into handling el as an element wrapper
108
+ el.$family = function () { return true; };
109
+ }
110
+
111
+ // stop running animations
112
+ win.HighchartsAdapter.stop(el);
113
+
114
+ // define and run the effect
115
+ effect = new Fx.Morph(
116
+ isSVGElement ? el : $(el),
117
+ $extend({
118
+ transition: Fx.Transitions.Quad.easeInOut
119
+ }, options)
120
+ );
121
+
122
+ // Make sure that the element reference is set when animating svg elements
123
+ if (isSVGElement) {
124
+ effect.element = el;
125
+ }
126
+
127
+ // special treatment for paths
128
+ if (params.d) {
129
+ effect.toD = params.d;
130
+ }
131
+
132
+ // jQuery-like events
133
+ if (complete) {
134
+ effect.addEvent('complete', complete);
135
+ }
136
+
137
+ // run
138
+ effect.start(params);
139
+
140
+ // record for use in stop method
141
+ el.fx = effect;
142
+ },
143
+
144
+ /**
145
+ * MooTool's each function
146
+ *
147
+ */
148
+ each: function (arr, fn) {
149
+ return legacy ?
150
+ $each(arr, fn) :
151
+ Array.each(arr, fn);
152
+ },
153
+
154
+ /**
155
+ * Map an array
156
+ * @param {Array} arr
157
+ * @param {Function} fn
158
+ */
159
+ map: function (arr, fn) {
160
+ return arr.map(fn);
161
+ },
162
+
163
+ /**
164
+ * Grep or filter an array
165
+ * @param {Array} arr
166
+ * @param {Function} fn
167
+ */
168
+ grep: function (arr, fn) {
169
+ return arr.filter(fn);
170
+ },
171
+
172
+ /**
173
+ * Deep merge two objects and return a third
174
+ */
175
+ merge: function () {
176
+ var args = arguments,
177
+ args13 = [{}], // MooTools 1.3+
178
+ i = args.length,
179
+ ret;
180
+
181
+ if (legacy) {
182
+ ret = $merge.apply(null, args);
183
+ } else {
184
+ while (i--) {
185
+ // Boolean argumens should not be merged.
186
+ // JQuery explicitly skips this, so we do it here as well.
187
+ if (typeof args[i] !== 'boolean') {
188
+ args13[i + 1] = args[i];
189
+ }
190
+ }
191
+ ret = Object.merge.apply(Object, args13);
192
+ }
193
+
194
+ return ret;
195
+ },
196
+
197
+ /**
198
+ * Get the offset of an element relative to the top left corner of the web page
199
+ */
200
+ offset: function (el) {
201
+ var offsets = $(el).getOffsets();
202
+ return {
203
+ left: offsets.x,
204
+ top: offsets.y
205
+ };
206
+ },
207
+
208
+ /**
209
+ * Extends an object with Events, if its not done
210
+ */
211
+ extendWithEvents: function (el) {
212
+ // if the addEvent method is not defined, el is a custom Highcharts object
213
+ // like series or point
214
+ if (!el.addEvent) {
215
+ if (el.nodeName) {
216
+ el = $(el); // a dynamically generated node
217
+ } else {
218
+ $extend(el, new Events()); // a custom object
219
+ }
220
+ }
221
+ },
222
+
223
+ /**
224
+ * Add an event listener
225
+ * @param {Object} el HTML element or custom object
226
+ * @param {String} type Event type
227
+ * @param {Function} fn Event handler
228
+ */
229
+ addEvent: function (el, type, fn) {
230
+ if (typeof type === 'string') { // chart broke due to el being string, type function
231
+
232
+ if (type === 'unload') { // Moo self destructs before custom unload events
233
+ type = 'beforeunload';
234
+ }
235
+
236
+ win.HighchartsAdapter.extendWithEvents(el);
237
+
238
+ el.addEvent(type, fn);
239
+ }
240
+ },
241
+
242
+ removeEvent: function (el, type, fn) {
243
+ if (typeof el === 'string') {
244
+ // el.removeEvents below apperantly calls this method again. Do not quite understand why, so for now just bail out.
245
+ return;
246
+ }
247
+ win.HighchartsAdapter.extendWithEvents(el);
248
+ if (type) {
249
+ if (type === 'unload') { // Moo self destructs before custom unload events
250
+ type = 'beforeunload';
251
+ }
252
+
253
+ if (fn) {
254
+ el.removeEvent(type, fn);
255
+ } else {
256
+ el.removeEvents(type);
257
+ }
258
+ } else {
259
+ el.removeEvents();
260
+ }
261
+ },
262
+
263
+ fireEvent: function (el, event, eventArguments, defaultFunction) {
264
+ var eventArgs = {
265
+ type: event,
266
+ target: el
267
+ };
268
+ // create an event object that keeps all functions
269
+ event = legacyEvent ? new Event(eventArgs) : new DOMEvent(eventArgs);
270
+ event = $extend(event, eventArguments);
271
+ // override the preventDefault function to be able to use
272
+ // this for custom events
273
+ event.preventDefault = function () {
274
+ defaultFunction = null;
275
+ };
276
+ // if fireEvent is not available on the object, there hasn't been added
277
+ // any events to it above
278
+ if (el.fireEvent) {
279
+ el.fireEvent(event.type, event);
280
+ }
281
+
282
+ // fire the default if it is passed and it is not prevented above
283
+ if (defaultFunction) {
284
+ defaultFunction(event);
285
+ }
286
+ },
287
+
288
+ /**
289
+ * Stop running animations on the object
290
+ */
291
+ stop: function (el) {
292
+ if (el.fx) {
293
+ el.fx.cancel();
294
+ }
295
+ }
296
+ };
297
+
298
+ }());