visage-app 2.0.5 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }());