Page MenuHomePhabricator

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/bootstrap/css/daterangepicker-bs2.css b/bootstrap/css/daterangepicker-bs2.css
new file mode 100644
index 0000000..fed6d83
--- /dev/null
+++ b/bootstrap/css/daterangepicker-bs2.css
@@ -0,0 +1,288 @@
+/*!
+ * Stylesheet for the Date Range Picker, for use with Bootstrap 2.x
+ *
+ * Copyright 2013 Dan Grossman ( http://www.dangrossman.info )
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Built for http://www.improvely.com
+ */
+
+.daterangepicker.dropdown-menu {
+ max-width: none;
+ z-index: 3000;
+}
+
+.daterangepicker.opensleft .ranges, .daterangepicker.opensleft .calendar {
+ float: left;
+ margin: 4px;
+}
+
+.daterangepicker.opensright .ranges, .daterangepicker.opensright .calendar,
+.daterangepicker.openscenter .ranges, .daterangepicker.openscenter .calendar {
+ float: right;
+ margin: 4px;
+}
+
+.daterangepicker .ranges {
+ width: 160px;
+ text-align: left;
+}
+
+.daterangepicker .ranges .range_inputs>div {
+ float: left;
+}
+
+.daterangepicker .ranges .range_inputs>div:nth-child(2) {
+ padding-left: 11px;
+}
+
+.daterangepicker .calendar {
+ display: none;
+ max-width: 250px;
+}
+.daterangepicker.show-calendar .calendar {
+ display: block;
+}
+
+.daterangepicker .calendar.single .calendar-date {
+ border: none;
+}
+
+.daterangepicker .calendar th, .daterangepicker .calendar td {
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ white-space: nowrap;
+ text-align: center;
+}
+
+.daterangepicker .daterangepicker_start_input label,
+.daterangepicker .daterangepicker_end_input label {
+ color: #333;
+ font-size: 11px;
+ margin-bottom: 2px;
+ text-transform: uppercase;
+ text-shadow: 1px 1px 0 #fff;
+}
+
+.daterangepicker .ranges input {
+ font-size: 11px;
+}
+
+.daterangepicker .ranges ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.daterangepicker .ranges li {
+ font-size: 13px;
+ background: #f5f5f5;
+ border: 1px solid #f5f5f5;
+ color: #08c;
+ padding: 3px 12px;
+ margin-bottom: 8px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+ cursor: pointer;
+}
+
+.daterangepicker .ranges li.active, .daterangepicker .ranges li:hover {
+ background: #08c;
+ border: 1px solid #08c;
+ color: #fff;
+}
+
+.daterangepicker .calendar-date {
+ border: 1px solid #ddd;
+ padding: 4px;
+ border-radius: 4px;
+ background: #fff;
+}
+
+.daterangepicker .calendar-time {
+ text-align: center;
+ margin: 8px auto 0 auto;
+ line-height: 30px;
+}
+
+.daterangepicker {
+ position: absolute;
+ background: #fff;
+ top: 100px;
+ left: 20px;
+ padding: 4px;
+ margin-top: 1px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+
+.daterangepicker.opensleft:before {
+ position: absolute;
+ top: -7px;
+ right: 9px;
+ display: inline-block;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid #ccc;
+ border-left: 7px solid transparent;
+ border-bottom-color: rgba(0, 0, 0, 0.2);
+ content: '';
+}
+
+.daterangepicker.opensleft:after {
+ position: absolute;
+ top: -6px;
+ right: 10px;
+ display: inline-block;
+ border-right: 6px solid transparent;
+ border-bottom: 6px solid #fff;
+ border-left: 6px solid transparent;
+ content: '';
+}
+
+.daterangepicker.openscenter:before {
+ position: absolute;
+ top: -7px;
+ left: 0;
+ right: 0;
+ width: 0;
+ margin-left: auto;
+ margin-right: auto;
+ display: inline-block;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid #ccc;
+ border-left: 7px solid transparent;
+ border-bottom-color: rgba(0, 0, 0, 0.2);
+ content: '';
+}
+
+.daterangepicker.openscenter:after {
+ position: absolute;
+ top: -6px;
+ left: 0;
+ right: 0;
+ width: 0;
+ margin-left: auto;
+ margin-right: auto;
+ display: inline-block;
+ border-right: 6px solid transparent;
+ border-bottom: 6px solid #fff;
+ border-left: 6px solid transparent;
+ content: '';
+}
+
+.daterangepicker.opensright:before {
+ position: absolute;
+ top: -7px;
+ left: 9px;
+ display: inline-block;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid #ccc;
+ border-left: 7px solid transparent;
+ border-bottom-color: rgba(0, 0, 0, 0.2);
+ content: '';
+}
+
+.daterangepicker.opensright:after {
+ position: absolute;
+ top: -6px;
+ left: 10px;
+ display: inline-block;
+ border-right: 6px solid transparent;
+ border-bottom: 6px solid #fff;
+ border-left: 6px solid transparent;
+ content: '';
+}
+
+.daterangepicker table {
+ width: 100%;
+ margin: 0;
+}
+
+.daterangepicker td, .daterangepicker th {
+ text-align: center;
+ width: 20px;
+ height: 20px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.daterangepicker td.off {
+ color: #999;
+}
+
+.daterangepicker td.disabled, .daterangepicker option.disabled {
+ color: #999;
+}
+
+.daterangepicker td.available:hover, .daterangepicker th.available:hover {
+ background: #eee;
+}
+
+.daterangepicker td.in-range {
+ background: #ebf4f8;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+
+.daterangepicker td.active, .daterangepicker td.active:hover {
+ background-color: #006dcc;
+ background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
+ background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
+ background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
+ background-image: -o-linear-gradient(top, #0088cc, #0044cc);
+ background-image: linear-gradient(top, #0088cc, #0044cc);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
+ border-color: #0044cc #0044cc #002a80;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+ color: #fff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+
+.daterangepicker td.week, .daterangepicker th.week {
+ font-size: 80%;
+ color: #ccc;
+}
+
+.daterangepicker select.monthselect, .daterangepicker select.yearselect {
+ font-size: 12px;
+ padding: 1px;
+ height: auto;
+ margin: 0;
+ cursor: default;
+}
+
+.daterangepicker select.monthselect {
+ margin-right: 2%;
+ width: 56%;
+}
+
+.daterangepicker select.yearselect {
+ width: 40%;
+}
+
+.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect {
+ width: 60px;
+ margin-bottom: 0;
+}
+
+.daterangepicker_start_input {
+ float: left;
+}
+
+.daterangepicker_end_input {
+ float: left;
+ padding-left: 11px
+}
+
+.daterangepicker th.month {
+ width: auto;
+}
diff --git a/bootstrap/js-used/application.js b/bootstrap/js-used/application.js
index 5ccc704..1d86bba 100644
--- a/bootstrap/js-used/application.js
+++ b/bootstrap/js-used/application.js
@@ -1,579 +1,865 @@
-// NOTICE!! DO NOT USE ANY OF THIS JAVASCRIPT
-// IT'S ALL JUST JUNK FOR OUR DOCS!
-// ++++++++++++++++++++++++++++++++++++++++++
+
+// GRAPHS
+
+function bytes(bytes, label) {
+ if (bytes == 0) return '';
+ var s = ['', 'K', 'M', 'G', 'T', 'P'];
+ var e = Math.floor(Math.log(bytes)/Math.log(1000));
+ var value = ((bytes/Math.pow(1000, Math.floor(e))).toFixed(1));
+ e = (e<0) ? (-e) : e;
+ if (label) value += ' ' + s[e];
+ console.log(value);
+ return value;
+}
+
+function formatDate(myDate){
+ var day = [];
+ var month = [];
+
+ day[0]="Sunday";
+ day[1]="Monday";
+ day[2]="Tuesday";
+ day[3]="Wednesday";
+ day[4]="Thursday";
+ day[5]="Friday";
+ day[6]="Saturday";
+ month[0]="Jan";
+ month[1]="Feb";
+ month[2]="Mar";
+ month[3]="Apr";
+ month[4]="May";
+ month[5]="Jun";
+ month[6]="Jul";
+ month[7]="Aug";
+ month[8]="Sep";
+ month[9]="Oct";
+ month[10]="Nov";
+ month[11]="Dec";
+
+ var hours = myDate.getUTCHours();
+ var minutes = myDate.getMinutes();
+ minutes = minutes < 10 ? '0'+minutes : minutes;
+ var strTime = hours + ':' + minutes;
+
+ return(day[myDate.getDay()]+", "+month[myDate.getMonth()]+ ' '+ myDate.getDate()+", "+strTime);
+}
+
+
+function isBigEnough(value) {
+ return function(element, index, array) {
+ return (element[0] >= value);
+ };
+}
+function isSmallEnough(value) {
+ return function(element, index, array) {
+ return (element[0] <= value);
+ };
+}
+
+function basicTimeGraph(container,legend, flotr_data, extra_options) {
+ var default_options = {
+ title: '',
+ ytitle: '',
+ suffix: '',
+ ticks1:
+ function(y) {
+ return Math.round(y);
+ },
+ scaling: 'linear',
+ trackY: false,
+ minY: 0,
+ maxY: ''
+ };
+
+ if (typeof extra_options != "undefined") {
+ $.extend(default_options, extra_options);
+ }
+
+ var
+ data, graph, offset, i,
+ options = {
+ shadowSize: 0,
+ xaxis : {
+ mode : 'time',
+ },
+ selection : {
+ mode : 'x',
+ },
+ title: default_options.title,
+ yaxis: {
+ min: default_options.minY,
+ title : default_options.ytitle,
+ tickFormatter: default_options.ticks1,
+ autoscale: true,
+ autoscaleMargin: 2,
+ noTicks: 4,
+ scaling: default_options.scaling
+ },
+ y2axis: {
+ min: 0,
+ autoscale: true,
+ autoscaleMargin: 2,
+ noTicks: 3,
+ },
+ HtmlText : false,
+ grid: {
+ outline: 'ws',
+ minorHorizontalLines: true,
+ },
+ legend: {
+ noColumns: 2,
+ container: legend,
+ labelBoxBorderColor: '#FFFFFF',
+ },
+ mouse : {
+ track : true,
+ trackAll: true,
+ relative: true,
+ trackY: default_options.trackY,
+ trackFormatter: function(obj){
+ if (typeof obj.series !== undefined) {
+ return formatDate(new Date(Math.round(obj.x))) +
+ '<br>' +
+ obj.series.label +
+ ': '+
+ default_options.ticks1(obj.y) +
+ default_options.suffix;
+ }
+ }
+ },
+ lines: {
+ lineWidth: 1,
+ }
+ };
+
+ if ( default_options.scaling !== 'linear') {
+ options.yaxis.ticks= [10e-5, 10e-4, 10e-3, 10e-2, 10e-1, 10e0, 10e1, 10e2, 10e3, 10e4, 10e5];
+ options.yaxis.min = 10e-2;
+ }
+ var g ='';
+
+ // Draw graph with default options, overwriting with passed options
+ function drawGraph (opts) {
+
+ // Clone the options, so the 'options' variable always keeps intact.
+ o = Flotr._.extend(Flotr._.clone(options), opts || {});
+
+ // Return a new graph.
+ g = Flotr.draw(
+ container,
+ flotr_data,
+ o
+ );
+
+ return g;
+ }
+
+ graph = drawGraph();
+
+ // Selection of an interval
+ Flotr.EventAdapter.observe(container, 'flotr:select', function(area){
+
+ // Prevent too small selection interval (20 mins)
+ if (area.x2-area.x1< 1200000) {
+ area.x1 = area.x2 - 1200000;
+ }
+
+ // Get maximum of max two series, only process selected interval
+ function result_max() {
+ result_a = flotr_data[0].data.filter(isBigEnough(area.x1)).filter(isSmallEnough(area.x2)).reduce(
+ function(max, arr) {
+ return max >= arr[1] ? max : arr[1];
+ }, -Infinity);
+
+ if (typeof flotr_data[1] !== "undefined") {
+ if ( typeof flotr_data[1].yaxis === "undefined") {
+ result_b = flotr_data[1].data.filter(isBigEnough(area.x1)).filter(isSmallEnough(area.x2)).reduce(function(max, arr) {
+ return max >= arr[1] ? max : arr[1];
+ }, -Infinity);
+ if (result_b > result_a ){
+ return result_b;
+ }
+ }
+ }
+ return result_a;
+ }
+
+ op = {
+ xaxis : {
+ min : area.x1,
+ max : area.x2,
+ mode : 'time'
+ },
+ yaxis : {
+ title : default_options.ytitle,
+ min : 0,
+ max : result_max()*1.2,
+ tickFormatter: default_options.ticks1,
+ scaling: default_options.scaling
+ },
+ };
+
+ if ( default_options.scaling !== 'linear') {
+ op.yaxis.min = 10e-2;
+ //op.yaxis.max = op.yaxis.max*10;
+ //op.yaxis.ticks = [10e-5, 10e-4, 10e-3, 10e-2, 10e-1, 10e0, 10e1, 10e2, 10e3, 10e4, 10e5];
+ }
+ if (typeof flotr_data[1] !== "undefined") {
+ if ( typeof flotr_data[1].yaxis !== "undefined") {
+ result_b = flotr_data[1]['data'].filter(isBigEnough(area.x1)).filter(isSmallEnough(area.x2)).reduce(function(max, arr) {
+ return max >= arr[1] ? max : arr[1];
+ }, -Infinity);
+ op.y2axis = {min :0, max: result_b*1.2};
+ }
+ }
+ graph = drawGraph(op);
+ });
+
+ // Reset graph
+ Flotr.EventAdapter.observe(container, 'flotr:click', function () {
+
+ new_opts ={
+ yaxis : {
+ min: 0,
+ title:extra_options.ytitle,
+ tickFormatter: default_options.ticks1,
+ autoscaleMargin: 2,
+ autoscale: true,
+ noTicks: 3,
+ scaling: default_options.scaling
+
+ },
+ };
+ if ( default_options.scaling !== 'linear') {
+ new_opts.yaxis.min = 10e-2;
+ new_opts.yaxis.ticks = [10e-5, 10e-4, 10e-3, 10e-2, 10e-1, 10e0, 10e1, 10e2, 10e3, 10e4, 10e5];
+ }
+ graph = drawGraph(new_opts);
+ });
+
+ $(window).resize(function() {
+ new_opts ={
+ yaxis : {
+ min: 0,
+ title:extra_options.ytitle,
+ tickFormatter: default_options.ticks1,
+ autoscaleMargin: 2,
+ autoscale: true,
+ noTicks: 3,
+ scaling: default_options.scaling
+
+ },
+ };
+ if ( default_options.scaling !== 'linear') {
+ new_opts.yaxis.min = 10e-2;
+ new_opts.yaxis.ticks = [10e-5, 10e-4, 10e-3, 10e-2, 10e-1, 10e0, 10e1, 10e2, 10e3, 10e4, 10e5];
+ }
+ graph = drawGraph(new_opts);
+
+ });
+}
!function ($) {
$(function(){
jQuery.fn.exists = function(){
return this.length>0;
};
if ( $('#timepicker1').exists()) {
$('#timepicker1').timepicker();
}
if ( $('#timepicker2').exists()) {
$('#timepicker2').timepicker();
}
+ if ( $('#reportrange').exists()) {
+ picker = $('#reportrange').daterangepicker({
+ ranges: {
+ 'Today': [moment(), moment()],
+ 'Yesterday': [moment().subtract('days', 1), moment().subtract('days', 1)],
+ 'Last 7 Days': [moment().subtract('days', 6), moment().endOf('day')],
+ 'Last 30 Days': [moment().subtract('days', 29), moment().endOf('day')],
+ 'This Month': [moment().startOf('month'), moment().endOf('month')],
+ 'Last Month': [moment().subtract('month', 1).startOf('month'), moment().subtract('month', 1).endOf('month')]
+ },
+ startDate: moment().subtract('days', 29),
+ endDate: moment(),
+ timePicker: true, timePickerIncrement: 30,
+ format: 'YYYY-MM-DD HH:mm '
+ },
+ function(start,end) {
+ var url = window.location.href;
+ if (url.indexOf('?') > -1){
+ url = url.substring(0,url.indexOf('?'));
+ }
+ url += '?start_date='+ encodeURIComponent(start.format('YYYY-MM-DD HH:mm'))+"&stop_date="+ encodeURIComponent(end.format('YYYY-MM-DD HH:mm'));
+ window.location.href = url;
+ });
+ if ( typeof stop_date_set !== "undefined") {
+ picker.data('daterangepicker').setEndDate(stop_date_set);
+ }
+ if ( typeof start_date_set !== "undefined") {
+ picker.data('daterangepicker').setStartDate(start_date_set);
+ }
+ }
if ( $('#begin_date').exists()) {
$('#begin_date').datepicker();
}
if ( $('#thorMap').exists()) {
var draw = SVG('thorMap');
draw.viewbox(0, 0, thorData.imgsize, thorData.imgsize);
// draw.line(0,315,630,315).stroke({ width: 1, color:'grey'});
// draw.line(315,0,315,630).stroke({ width: 1, color:'grey'});
draw.circle()
.radius(315*0.6)
.fill('none')
.stroke({
width: 10,
color:'#ebebeb'
})
.cy(315)
.cx(315);
cloud = draw.text("\uf0c2")
.font({
family: 'FontAwesome',
size: '100'
})
.y(265)
.x(265)
.fill('#bbb');
var dns_nodes = Object.keys(thorData.dns_managers).length;
index = 0 ;
console.log(thorData);
$.each(thorData.dns_managers, function( key, value ) {
// draw.text("\ue9a4").font({
// family: 'picol',
// size: '40'
// })
// .cy(20)
// .cx(610-(40*dns_nodes)+(index*45))
// .fill('#88a2d2');
// draw.text("DNS"+(index+1)).font({
// family: 'Verdana',
// size: '10'
// })
// .cy(50)
// .cx(610-(40*dns_nodes)+(index*45));
draw.circle()
.radius(4)
.fill('green')
.cx(490)
.cy(21+(index*12));
var hostname = key;
if (thorData.hostnames[key]){
hostname = thorData.hostnames[key];
}
draw.text("DNS"+(index+1)+': '+hostname)
.font({
family: 'Verdana',
size: '10'
})
.cy(20+(index*12))
.x(500);
index++;
});
var sip_proxies = Object.keys(thorData.node_statistics).length + Object.keys(thorData.conference_servers).length + Object.keys(thorData.voicemail_servers).length;
//var sip_proxies = Object.keys(thorData.node_statistics).length
var counter= 0;
$.each(thorData.voicemail_servers, function( key, value ) {
position = ((2/sip_proxies)*counter);
counter++;
// draw.circle()
// .radius(10)
// .fill('white')
// .stroke({color:'green', width:2})
// .cx(315+(315*0.6)*Math.sin(position*Math.PI))
// .cy(315+(315*0.6)*Math.cos(position*Math.PI));
draw.rect(32,24)
.fill('white')
.stroke({
color:'green',
width:2
})
.radius(5)
.x(315).y(315).animate(500, SVG.easing.quartIn)
.cx(315+(315*0.6)*Math.sin(position*Math.PI))
.cy(315+(315*0.6)*Math.cos(position*Math.PI));
draw.text("\uf0e0")
.font({
family: 'FontAwesome',
size: '14',
})
.x(315).y(315).animate(500, SVG.easing.quartIn)
.cx(315+(315*0.6)*Math.sin(position*Math.PI))
.cy(315+(315*0.6)*Math.cos(position*Math.PI));
var hostname = key;
if (thorData.hostnames[key]){
hostname = thorData.hostnames[key];
}
align='start';
x=0;
y=0;
if (position === 0.5 ) {
x= -10;
align='start';
} else if (position === 1.5) {
x= 10;
align='end';
} else if ((position > 1 && position < 1.5 )|| (position > 1.5 && position < 2)) {
x = 25;
align='end';
} else if ((position > 0 && position < 0.5 )|| (position > 0.5 && position < 1)) {
x = -25;
} else if (position === 1 ) {
align='middle';
y=20;
} else if (position === 0 ) {
align='middle';
y=-25;
}
draw.text(hostname ).font({
family: 'Verdana-bold',
size: '10',
anchor: align,
})
.x(315).y(315).animate(500, SVG.easing.quartIn)
.cy(315+(315*0.6)*Math.cos(position*Math.PI)-y).x(
315+(315*0.6)*Math.sin(position*Math.PI)-x).fill('#5177bd');
});
$.each(thorData.conference_servers, function( key, value ) {
position = ((2/sip_proxies)*counter);
counter++;
// draw.circle()
// .radius(10)
// .fill('white')
// .stroke({color:'green', width:2})
// .cx(315+(315*0.6)*Math.sin(position*Math.PI))
// .cy(315+(315*0.6)*Math.cos(position*Math.PI));
draw.rect(32,24).fill('white').stroke({color:'green', width:2}).radius(5)
.x(315).y(315).animate(500, SVG.easing.quartIn).cx(315+(315*0.6)*Math.sin(position*Math.PI))
.cy(315+(315*0.6)*Math.cos(position*Math.PI));
draw.text("\uf0c0").font({
family: 'FontAwesome',
size: '15',
})
.x(315).y(315).animate(500, SVG.easing.quartIn)
.cx(315+(315*0.6)*Math.sin(position*Math.PI))
.cy(315+(315*0.6)*Math.cos(position*Math.PI));
var hostname = key;
if (thorData.hostnames[key]){
hostname = thorData.hostnames[key];
}
align='start';
x=0;
y=0;
if (position === 0.5 ) {
x= -10;
align='start';
} else if (position === 1.5) {
x= 10;
align='end';
} else if ((position > 1 && position < 1.5 )|| (position > 1.5 && position < 2)) {
x = 25;
align='end';
}
else if ((position > 0 && position < 0.5 )|| (position > 0.5 && position < 1)) {
x = -25;
}
else if (position === 1 ) {
align='middle';
y=20;
} else if (position === 0 ) {
align='middle';
y=-20;
}
draw.text(hostname ).font({
family: 'Verdana-bold',
size: '10',
anchor: align,
})
.x(315).y(315).animate(500, SVG.easing.quartIn)
.cy(315+(315*0.6)*Math.cos(position*Math.PI)-y)
.x(315+(315*0.6)*Math.sin(position*Math.PI)-x).fill('#5177bd');
});
$.each(thorData.node_statistics, function( key, value ) {
position = ((2/sip_proxies)*counter);
counter++;
// draw.text("\ue9a4").font({
// family: 'picol',
// size: '40'
// }).y(290+(315*0.69)*Math.sin(position*Math.PI)).x(
// 300+(315*0.69)*Math.cos(position*Math.PI)).fill('#5177bd');
draw.rect(32,24).fill('white').stroke({color:'green', width:2}).radius(4)
.x(315).y(315).animate(500, SVG.easing.quartIn)
.cx(315+(315*0.6)*Math.sin(position*Math.PI))
.cy(315+(315*0.6)*Math.cos(position*Math.PI));
// draw.circle()
// .radius(10)
// .fill('white')
// .stroke({color:'green', width:2})
// .cx(315+(315*0.6)*Math.sin(position*Math.PI))
// .cy(315+(315*0.6)*Math.cos(position*Math.PI));
if (thorData.sip_proxies[key]){
draw.text("\uf0b2").font({
family: 'FontAwesome',
size: '15',
})
.x(315).y(315).animate(500, SVG.easing.quartIn)
.cx(315+(315*0.6)*Math.sin(position*Math.PI))
.cy(315+(315*0.6)*Math.cos(position*Math.PI));
}
x =0 ;
y = 0;
// if (position > 0 && position < 0.5) {
// x = -45;
// y = -20;
// } else if (position === 0.5) {
// x = 0;
// y = -55;
// } else if (position > 0.5 && position < 1) {
// x = 120;
// y = -15;
// } else if (position === 1) {
// x = 30;
// y = 15;
// } else if (position > 1 && position < 1.5) {
// x = 120;
// y = -20;
// } else if (position > 1.5 && position < 2) {
// x = -45;
// y = -20;
// } else {
// x = 0;
// y = 15;
// }
var accounts = '';
if (thorData.node_statistics[key].online_accounts ){
accounts= thorData.node_statistics[key].online_accounts+' accounts';
}
var hostname = key;
if (thorData.hostnames[key]){
hostname = thorData.hostnames[key];
}
align='start';
if (position === 0.5 ) {
x= -10;
align='start';
} else if (position === 1.5) {
x= 10;
align='end';
} else if ((position > 1 && position < 1.5 )|| (position > 1.5 && position < 2)) {
x = 25;
align='end';
}
else if ((position > 0 && position < 0.5 )|| (position > 0.5 && position < 1)) {
x = -25;
}
else if (position === 1 ) {
align='middle';
y=30;
} else if (position === 0 ) {
align='middle';
y=-30;
}
draw.text(hostname+"\n"+accounts ).font({
family: 'Verdana-bold',
size: '10',
anchor: align,
}).x(315).y(315).animate(500, SVG.easing.quartIn).cy(315+(315*0.6)*Math.cos(position*Math.PI)-y).x(
315+(315*0.6)*Math.sin(position*Math.PI)-x).fill('#5177bd');
align='start';
x =0 ;
y = 0;
if (position === 0.5) {
align='end';
}else if (position === 1.5 ) {
align='start';
x= -50;
} else if ((position > 1 && position < 1.5 )|| (position > 1.5 && position < 2)) {
align='start';
x= -50;
}
else if ((position > 0 && position < 0.5 )|| (position > 0.5 && position < 1)) {
align='end';
}
else if (position === 1 ) {
//align='middle';
y=-24;
} else if (position === 0 ) {
//align='middle';
y=25;
}
if (thorData.node_statistics[key].sessions) {
draw.text(thorData.node_statistics[key].sessions +' sessions').font({
family: 'Verdana',
size: '10',
anchor: align,
}).x(315).y(315).animate(400, SVG.easing.quartIn)
.cy(315+(315*0.6)*Math.cos(position*Math.PI)-y).cx(
315+(315*0.6)*Math.sin(position*Math.PI)-x).fill('#5177bd');
}
});
}
if ( $('#end_date').exists()) {
$('#end_date').datepicker();
}
$('tr[rel=tooltip]').tooltip();
$('.tooltip-test').tooltip();
$('.popover-test').popover();
// popover demo
$("select[rel=popover]")
.popover()
.click(function(e) {
e.preventDefault();
});
$("a[rel=popover]")
.popover()
.click(function(e) {
e.preventDefault();
});
$("button[rel=popover]")
.popover();
$("input[rel=popover]")
.popover()
.click(function(e) {
e.preventDefault();
});
$("textarea[rel=popover]")
.popover()
.click(function(e) {
e.preventDefault();
});
if ($('fileupload').exists()){
$('fileupload').fileupload();
}
// if ( $('#download_password').exists()) {
// $('#download_password').click(function(){
// var password = $('#password').val();
// var ha1 = $('#ha1').val();
// var ha1b = $('#ha1b').val();
// var username = $('#username').val();
// var domain = $('#domain').val();
// var str = username + ":"+
// domain + ":" +
// password;
// var ha_calc = MD5(str);
// if (ha1 === ha_calc) {
// $('#java_buttons').removeClass('hide');
// $('#password_download').addClass('hide');
// var content = decodeURIComponent($('[name=file_content]').val());
// var obj= $.parseJSON(content);
// obj.password=password;
// var new_content= JSON.stringify(obj);
// new_content= encodeURIComponent(new_content);
// $('[name=file_content]').val(new_content);
// } else {
// $('#pass_group').addClass('error');
// $('#help-text').remove();
// $('#controls_password').append('<span id="help-text" class="help-inline">Entered password does not match your account</span>');
// }
// return false;
// });
// }
// request built javascript
$('.download-btn').on('click', function () {
var css = $("#components.download input:checked")
.map(function () { return this.value })
.toArray()
, js = $("#plugins.download input:checked")
.map(function () { return this.value })
.toArray()
, vars = {}
, img = ['glyphicons-halflings.png', 'glyphicons-halflings-white.png']
$("#variables.download input")
.each(function () {
$(this).val() && (vars[ $(this).prev().text() ] = $(this).val())
})
$.ajax({
type: 'POST'
, url: /\?dev/.test(window.location) ? 'http://localhost:3000' : 'http://bootstrap.herokuapp.com'
, dataType: 'jsonpi'
, params: {
js: js
, css: css
, vars: vars
, img: img
}
})
})
});
// Modified from the original jsonpi https://github.com/benvinegar/jquery-jsonpi
$.ajaxTransport('jsonpi', function(opts, originalOptions, jqXHR) {
var url = opts.url;
return {
send: function(_, completeCallback) {
var name = 'jQuery_iframe_' + jQuery.now()
, iframe, form
iframe = $('<iframe>')
.attr('name', name)
.appendTo('head')
form = $('<form>')
.attr('method', opts.type) // GET or POST
.attr('action', url)
.attr('target', name)
$.each(opts.params, function(k, v) {
$('<input>')
.attr('type', 'hidden')
.attr('name', k)
.attr('value', typeof v == 'string' ? v : JSON.stringify(v))
.appendTo(form)
})
form.appendTo('body').submit()
}
}
});
Highcharts.theme = {
chart: {
backgroundColor: 'transparent',
plotBackgroundColor: 'transparent',
plotShadow: false,
plotBorderWidth: 0
},
title: {
style: {
color: '#5177bd',
font: 'bold 16px Verdana, Arial,Telex, sans-serif'
}
},
subtitle: {
style: {
color: '#5177bd',
font: 'bold 12px Verdana, Arial,Telex, sans-serif'
}
},
xAxis: {
minorTickInterval: 'auto',
//majorTickInterval: '',
lineColor: '#000',
lineWidth: 1,
tickWidth: 1,
tickColor: '#000',
labels: {
style: {
color: '#000',
font: '10px Verdana, Arial,Telex, sans-serif'
}
},
},
yAxis: {
minorTickInterval: 'auto',
lineColor: '#000',
lineWidth: 1,
tickWidth: 1,
tickColor: '#000',
labels: {
style: {
color: '#000',
font: '10px Verdana, Arial,Telex, sans-serif'
}
},
},
legend: {
itemStyle: {
font: '11px Verdana, Arial,Telex, sans-serif',
color: '#333'
},
itemHoverStyle: {
color: '#039'
},
itemHiddenStyle: {
color: 'gray'
}
},
navigation: {
buttonOptions: {
theme: {
stroke: '#e6e6e6'
}
}
}
};
// Apply the theme
var highchartsOptions = Highcharts.setOptions(Highcharts.theme);
-
}(window.jQuery)
diff --git a/bootstrap/js-used/daterangepicker.js b/bootstrap/js-used/daterangepicker.js
new file mode 100644
index 0000000..32ce272
--- /dev/null
+++ b/bootstrap/js-used/daterangepicker.js
@@ -0,0 +1,1254 @@
+/**
+* @version: 1.3.17
+* @author: Dan Grossman http://www.dangrossman.info/
+* @date: 2014-11-25
+* @copyright: Copyright (c) 2012-2014 Dan Grossman. All rights reserved.
+* @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php
+* @website: http://www.improvely.com/
+*/
+
+(function(root, factory) {
+
+ if (typeof define === 'function' && define.amd) {
+ define(['moment', 'jquery', 'exports'], function(momentjs, $, exports) {
+ root.daterangepicker = factory(root, exports, momentjs, $);
+ });
+
+ } else if (typeof exports !== 'undefined') {
+ var momentjs = require('moment');
+ var jQuery;
+ try {
+ jQuery = require('jquery');
+ } catch (err) {
+ jQuery = window.jQuery;
+ if (!jQuery) throw new Error('jQuery dependency not found');
+ }
+
+ factory(root, exports, momentjs, jQuery);
+
+ // Finally, as a browser global.
+ } else {
+ root.daterangepicker = factory(root, {}, root.moment, (root.jQuery || root.Zepto || root.ender || root.$));
+ }
+
+}(this, function(root, daterangepicker, moment, $) {
+
+ var DateRangePicker = function (element, options, cb) {
+
+ // by default, the daterangepicker element is placed at the bottom of HTML body
+ this.parentEl = 'body';
+
+ //element that triggered the date range picker
+ this.element = $(element);
+
+ //tracks visible state
+ this.isShowing = false;
+
+ //create the picker HTML object
+ var DRPTemplate = '<div class="daterangepicker dropdown-menu">' +
+ '<div class="calendar first left"></div>' +
+ '<div class="calendar second right"></div>' +
+ '<div class="ranges">' +
+ '<div class="range_inputs">' +
+ '<div class="daterangepicker_start_input">' +
+ '<label for="daterangepicker_start"></label>' +
+ '<input class="input-mini" type="text" name="daterangepicker_start" value="" />' +
+ '</div>' +
+ '<div class="daterangepicker_end_input">' +
+ '<label for="daterangepicker_end"></label>' +
+ '<input class="input-mini" type="text" name="daterangepicker_end" value="" />' +
+ '</div>' +
+ '<button class="applyBtn" disabled="disabled"></button>&nbsp;' +
+ '<button class="cancelBtn"></button>' +
+ '</div>' +
+ '</div>' +
+ '</div>';
+
+ //custom options
+ if (typeof options !== 'object' || options === null)
+ options = {};
+
+ this.parentEl = (typeof options === 'object' && options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl);
+ this.container = $(DRPTemplate).appendTo(this.parentEl);
+
+ this.setOptions(options, cb);
+
+ //apply CSS classes and labels to buttons
+ var c = this.container;
+ $.each(this.buttonClasses, function (idx, val) {
+ c.find('button').addClass(val);
+ });
+ this.container.find('.daterangepicker_start_input label').html(this.locale.fromLabel);
+ this.container.find('.daterangepicker_end_input label').html(this.locale.toLabel);
+ if (this.applyClass.length)
+ this.container.find('.applyBtn').addClass(this.applyClass);
+ if (this.cancelClass.length)
+ this.container.find('.cancelBtn').addClass(this.cancelClass);
+ this.container.find('.applyBtn').html(this.locale.applyLabel);
+ this.container.find('.cancelBtn').html(this.locale.cancelLabel);
+
+ //event listeners
+
+ this.container.find('.calendar')
+ .on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this))
+ .on('click.daterangepicker', '.next', $.proxy(this.clickNext, this))
+ .on('click.daterangepicker', 'td.available', $.proxy(this.clickDate, this))
+ .on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this))
+ .on('mouseleave.daterangepicker', 'td.available', $.proxy(this.updateFormInputs, this))
+ .on('change.daterangepicker', 'select.yearselect', $.proxy(this.updateMonthYear, this))
+ .on('change.daterangepicker', 'select.monthselect', $.proxy(this.updateMonthYear, this))
+ .on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.updateTime, this));
+
+ this.container.find('.ranges')
+ .on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this))
+ .on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this))
+ .on('click.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.showCalendars, this))
+ .on('change.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.inputsChanged, this))
+ .on('keydown.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.inputsKeydown, this))
+ .on('click.daterangepicker', 'li', $.proxy(this.clickRange, this))
+ .on('mouseenter.daterangepicker', 'li', $.proxy(this.enterRange, this))
+ .on('mouseleave.daterangepicker', 'li', $.proxy(this.updateFormInputs, this));
+
+ if (this.element.is('input')) {
+ this.element.on({
+ 'click.daterangepicker': $.proxy(this.show, this),
+ 'focus.daterangepicker': $.proxy(this.show, this),
+ 'keyup.daterangepicker': $.proxy(this.updateFromControl, this)
+ });
+ } else {
+ this.element.on('click.daterangepicker', $.proxy(this.toggle, this));
+ }
+
+ };
+
+ DateRangePicker.prototype = {
+
+ constructor: DateRangePicker,
+
+ setOptions: function(options, callback) {
+
+ this.startDate = moment().startOf('day');
+ this.endDate = moment().endOf('day');
+ this.timeZone = moment().zone();
+ this.minDate = false;
+ this.maxDate = false;
+ this.dateLimit = false;
+
+ this.showDropdowns = false;
+ this.showWeekNumbers = false;
+ this.timePicker = false;
+ this.timePickerSeconds = false;
+ this.timePickerIncrement = 30;
+ this.timePicker12Hour = true;
+ this.singleDatePicker = false;
+ this.ranges = {};
+
+ this.opens = 'right';
+ if (this.element.hasClass('pull-right'))
+ this.opens = 'left';
+
+ this.buttonClasses = ['btn', 'btn-small btn-sm'];
+ this.applyClass = 'btn-success';
+ this.cancelClass = 'btn-default';
+
+ this.format = 'MM/DD/YYYY';
+ this.separator = ' - ';
+
+ this.locale = {
+ applyLabel: 'Apply',
+ cancelLabel: 'Cancel',
+ fromLabel: 'From',
+ toLabel: 'To',
+ weekLabel: 'W',
+ customRangeLabel: 'Custom Range',
+ daysOfWeek: moment.weekdaysMin(),
+ monthNames: moment.monthsShort(),
+ firstDay: moment.localeData()._week.dow
+ };
+
+ this.cb = function () { };
+
+ if (typeof options.format === 'string')
+ this.format = options.format;
+
+ if (typeof options.separator === 'string')
+ this.separator = options.separator;
+
+ if (typeof options.startDate === 'string')
+ this.startDate = moment(options.startDate, this.format);
+
+ if (typeof options.endDate === 'string')
+ this.endDate = moment(options.endDate, this.format);
+
+ if (typeof options.minDate === 'string')
+ this.minDate = moment(options.minDate, this.format);
+
+ if (typeof options.maxDate === 'string')
+ this.maxDate = moment(options.maxDate, this.format);
+
+ if (typeof options.startDate === 'object')
+ this.startDate = moment(options.startDate);
+
+ if (typeof options.endDate === 'object')
+ this.endDate = moment(options.endDate);
+
+ if (typeof options.minDate === 'object')
+ this.minDate = moment(options.minDate);
+
+ if (typeof options.maxDate === 'object')
+ this.maxDate = moment(options.maxDate);
+
+ if (typeof options.applyClass === 'string')
+ this.applyClass = options.applyClass;
+
+ if (typeof options.cancelClass === 'string')
+ this.cancelClass = options.cancelClass;
+
+ if (typeof options.dateLimit === 'object')
+ this.dateLimit = options.dateLimit;
+
+ if (typeof options.locale === 'object') {
+
+ if (typeof options.locale.daysOfWeek === 'object') {
+ // Create a copy of daysOfWeek to avoid modification of original
+ // options object for reusability in multiple daterangepicker instances
+ this.locale.daysOfWeek = options.locale.daysOfWeek.slice();
+ }
+
+ if (typeof options.locale.monthNames === 'object') {
+ this.locale.monthNames = options.locale.monthNames.slice();
+ }
+
+ if (typeof options.locale.firstDay === 'number') {
+ this.locale.firstDay = options.locale.firstDay;
+ }
+
+ if (typeof options.locale.applyLabel === 'string') {
+ this.locale.applyLabel = options.locale.applyLabel;
+ }
+
+ if (typeof options.locale.cancelLabel === 'string') {
+ this.locale.cancelLabel = options.locale.cancelLabel;
+ }
+
+ if (typeof options.locale.fromLabel === 'string') {
+ this.locale.fromLabel = options.locale.fromLabel;
+ }
+
+ if (typeof options.locale.toLabel === 'string') {
+ this.locale.toLabel = options.locale.toLabel;
+ }
+
+ if (typeof options.locale.weekLabel === 'string') {
+ this.locale.weekLabel = options.locale.weekLabel;
+ }
+
+ if (typeof options.locale.customRangeLabel === 'string') {
+ this.locale.customRangeLabel = options.locale.customRangeLabel;
+ }
+ }
+
+ if (typeof options.opens === 'string')
+ this.opens = options.opens;
+
+ if (typeof options.showWeekNumbers === 'boolean') {
+ this.showWeekNumbers = options.showWeekNumbers;
+ }
+
+ if (typeof options.buttonClasses === 'string') {
+ this.buttonClasses = [options.buttonClasses];
+ }
+
+ if (typeof options.buttonClasses === 'object') {
+ this.buttonClasses = options.buttonClasses;
+ }
+
+ if (typeof options.showDropdowns === 'boolean') {
+ this.showDropdowns = options.showDropdowns;
+ }
+
+ if (typeof options.singleDatePicker === 'boolean') {
+ this.singleDatePicker = options.singleDatePicker;
+ if (this.singleDatePicker) {
+ this.endDate = this.startDate.clone();
+ }
+ }
+
+ if (typeof options.timePicker === 'boolean') {
+ this.timePicker = options.timePicker;
+ }
+
+ if (typeof options.timePickerSeconds === 'boolean') {
+ this.timePickerSeconds = options.timePickerSeconds;
+ }
+
+ if (typeof options.timePickerIncrement === 'number') {
+ this.timePickerIncrement = options.timePickerIncrement;
+ }
+
+ if (typeof options.timePicker12Hour === 'boolean') {
+ this.timePicker12Hour = options.timePicker12Hour;
+ }
+
+ // update day names order to firstDay
+ if (this.locale.firstDay != 0) {
+ var iterator = this.locale.firstDay;
+ while (iterator > 0) {
+ this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift());
+ iterator--;
+ }
+ }
+
+ var start, end, range;
+
+ //if no start/end dates set, check if an input element contains initial values
+ if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') {
+ if ($(this.element).is('input[type=text]')) {
+ var val = $(this.element).val(),
+ split = val.split(this.separator);
+
+ start = end = null;
+
+ if (split.length == 2) {
+ start = moment(split[0], this.format);
+ end = moment(split[1], this.format);
+ } else if (this.singleDatePicker && val !== "") {
+ start = moment(val, this.format);
+ end = moment(val, this.format);
+ }
+ if (start !== null && end !== null) {
+ this.startDate = start;
+ this.endDate = end;
+ }
+ }
+ }
+
+ // bind the time zone used to build the calendar to either the timeZone passed in through the options or the zone of the startDate (which will be the local time zone by default)
+ if (typeof options.timeZone === 'string' || typeof options.timeZone === 'number') {
+ this.timeZone = options.timeZone;
+ this.startDate.zone(this.timeZone);
+ this.endDate.zone(this.timeZone);
+ } else {
+ this.timeZone = moment(this.startDate).zone();
+ }
+
+ if (typeof options.ranges === 'object') {
+ for (range in options.ranges) {
+
+ if (typeof options.ranges[range][0] === 'string')
+ start = moment(options.ranges[range][0], this.format);
+ else
+ start = moment(options.ranges[range][0]);
+
+ if (typeof options.ranges[range][1] === 'string')
+ end = moment(options.ranges[range][1], this.format);
+ else
+ end = moment(options.ranges[range][1]);
+
+ // If we have a min/max date set, bound this range
+ // to it, but only if it would otherwise fall
+ // outside of the min/max.
+ if (this.minDate && start.isBefore(this.minDate))
+ start = moment(this.minDate);
+
+ if (this.maxDate && end.isAfter(this.maxDate))
+ end = moment(this.maxDate);
+
+ // If the end of the range is before the minimum (if min is set) OR
+ // the start of the range is after the max (also if set) don't display this
+ // range option.
+ if ((this.minDate && end.isBefore(this.minDate)) || (this.maxDate && start.isAfter(this.maxDate))) {
+ continue;
+ }
+
+ this.ranges[range] = [start, end];
+ }
+
+ var list = '<ul>';
+ for (range in this.ranges) {
+ list += '<li>' + range + '</li>';
+ }
+ list += '<li>' + this.locale.customRangeLabel + '</li>';
+ list += '</ul>';
+ this.container.find('.ranges ul').remove();
+ this.container.find('.ranges').prepend(list);
+ }
+
+ if (typeof callback === 'function') {
+ this.cb = callback;
+ }
+
+ if (!this.timePicker) {
+ this.startDate = this.startDate.startOf('day');
+ this.endDate = this.endDate.endOf('day');
+ }
+
+ if (this.singleDatePicker) {
+ this.opens = 'right';
+ this.container.addClass('single');
+ this.container.find('.calendar.right').show();
+ this.container.find('.calendar.left').hide();
+ if (!this.timePicker) {
+ this.container.find('.ranges').hide();
+ } else {
+ this.container.find('.ranges .daterangepicker_start_input, .ranges .daterangepicker_end_input').hide();
+ }
+ if (!this.container.find('.calendar.right').hasClass('single'))
+ this.container.find('.calendar.right').addClass('single');
+ } else {
+ this.container.removeClass('single');
+ this.container.find('.calendar.right').removeClass('single');
+ this.container.find('.ranges').show();
+ }
+
+ this.oldStartDate = this.startDate.clone();
+ this.oldEndDate = this.endDate.clone();
+ this.oldChosenLabel = this.chosenLabel;
+
+ this.leftCalendar = {
+ month: moment([this.startDate.year(), this.startDate.month(), 1, this.startDate.hour(), this.startDate.minute(), this.startDate.second()]),
+ calendar: []
+ };
+
+ this.rightCalendar = {
+ month: moment([this.endDate.year(), this.endDate.month(), 1, this.endDate.hour(), this.endDate.minute(), this.endDate.second()]),
+ calendar: []
+ };
+
+ if (this.opens == 'right' || this.opens == 'center') {
+ //swap calendar positions
+ var first = this.container.find('.calendar.first');
+ var second = this.container.find('.calendar.second');
+
+ if (second.hasClass('single')) {
+ second.removeClass('single');
+ first.addClass('single');
+ }
+
+ first.removeClass('left').addClass('right');
+ second.removeClass('right').addClass('left');
+
+ if (this.singleDatePicker) {
+ first.show();
+ second.hide();
+ }
+ }
+
+ if (typeof options.ranges === 'undefined' && !this.singleDatePicker) {
+ this.container.addClass('show-calendar');
+ }
+
+ this.container.addClass('opens' + this.opens);
+
+ this.updateView();
+ this.updateCalendars();
+
+ },
+
+ setStartDate: function(startDate) {
+ if (typeof startDate === 'string')
+ this.startDate = moment(startDate, this.format).zone(this.timeZone);
+
+ if (typeof startDate === 'object')
+ this.startDate = moment(startDate);
+
+ if (!this.timePicker)
+ this.startDate = this.startDate.startOf('day');
+
+ this.oldStartDate = this.startDate.clone();
+
+ this.updateView();
+ this.updateCalendars();
+ this.updateInputText();
+ },
+
+ setEndDate: function(endDate) {
+ if (typeof endDate === 'string')
+ this.endDate = moment(endDate, this.format).zone(this.timeZone);
+
+ if (typeof endDate === 'object')
+ this.endDate = moment(endDate);
+
+ if (!this.timePicker)
+ this.endDate = this.endDate.endOf('day');
+
+ this.oldEndDate = this.endDate.clone();
+
+ this.updateView();
+ this.updateCalendars();
+ this.updateInputText();
+ },
+
+ updateView: function () {
+ this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()).hour(this.startDate.hour()).minute(this.startDate.minute());
+ this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()).hour(this.endDate.hour()).minute(this.endDate.minute());
+ this.updateFormInputs();
+ },
+
+ updateFormInputs: function () {
+ this.container.find('input[name=daterangepicker_start]').val(this.startDate.format(this.format));
+ this.container.find('input[name=daterangepicker_end]').val(this.endDate.format(this.format));
+
+ if (this.startDate.isSame(this.endDate) || this.startDate.isBefore(this.endDate)) {
+ this.container.find('button.applyBtn').removeAttr('disabled');
+ } else {
+ this.container.find('button.applyBtn').attr('disabled', 'disabled');
+ }
+ },
+
+ updateFromControl: function () {
+ if (!this.element.is('input')) return;
+ if (!this.element.val().length) return;
+
+ var dateString = this.element.val().split(this.separator),
+ start = null,
+ end = null;
+
+ if(dateString.length === 2) {
+ start = moment(dateString[0], this.format).zone(this.timeZone);
+ end = moment(dateString[1], this.format).zone(this.timeZone);
+ }
+
+ if (this.singleDatePicker || start === null || end === null) {
+ start = moment(this.element.val(), this.format).zone(this.timeZone);
+ end = start;
+ }
+
+ if (end.isBefore(start)) return;
+
+ this.oldStartDate = this.startDate.clone();
+ this.oldEndDate = this.endDate.clone();
+
+ this.startDate = start;
+ this.endDate = end;
+
+ if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate))
+ this.notify();
+
+ this.updateCalendars();
+ },
+
+ notify: function () {
+ this.updateView();
+ this.cb(this.startDate, this.endDate, this.chosenLabel);
+ },
+
+ move: function () {
+ var parentOffset = { top: 0, left: 0 };
+ var parentRightEdge = $(window).width();
+ if (!this.parentEl.is('body')) {
+ parentOffset = {
+ top: this.parentEl.offset().top - this.parentEl.scrollTop(),
+ left: this.parentEl.offset().left - this.parentEl.scrollLeft()
+ };
+ parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left;
+ }
+
+ if (this.opens == 'left') {
+ this.container.css({
+ top: this.element.offset().top + this.element.outerHeight() - parentOffset.top,
+ right: parentRightEdge - this.element.offset().left - this.element.outerWidth(),
+ left: 'auto'
+ });
+ if (this.container.offset().left < 0) {
+ this.container.css({
+ right: 'auto',
+ left: 9
+ });
+ }
+ } else if (this.opens == 'center') {
+ this.container.css({
+ top: this.element.offset().top + this.element.outerHeight() - parentOffset.top,
+ left: this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2
+ - this.container.outerWidth() / 2,
+ right: 'auto'
+ });
+ if (this.container.offset().left < 0) {
+ this.container.css({
+ right: 'auto',
+ left: 9
+ });
+ }
+ } else {
+ this.container.css({
+ top: this.element.offset().top + this.element.outerHeight() - parentOffset.top,
+ left: this.element.offset().left - parentOffset.left,
+ right: 'auto'
+ });
+ if (this.container.offset().left + this.container.outerWidth() > $(window).width()) {
+ this.container.css({
+ left: 'auto',
+ right: 0
+ });
+ }
+ }
+ },
+
+ toggle: function (e) {
+ if (this.element.hasClass('active')) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ },
+
+ show: function (e) {
+ if (this.isShowing) return;
+
+ this.element.addClass('active');
+ this.container.show();
+ this.move();
+
+ // Create a click proxy that is private to this instance of datepicker, for unbinding
+ this._outsideClickProxy = $.proxy(function (e) { this.outsideClick(e); }, this);
+ // Bind global datepicker mousedown for hiding and
+ $(document)
+ .on('mousedown.daterangepicker', this._outsideClickProxy)
+ // also support mobile devices
+ .on('touchend.daterangepicker', this._outsideClickProxy)
+ // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them
+ .on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy)
+ // and also close when focus changes to outside the picker (eg. tabbing between controls)
+ .on('focusin.daterangepicker', this._outsideClickProxy);
+
+ this.isShowing = true;
+ this.element.trigger('show.daterangepicker', this);
+ },
+
+ outsideClick: function (e) {
+ var target = $(e.target);
+ // if the page is clicked anywhere except within the daterangerpicker/button
+ // itself then call this.hide()
+ if (
+ // ie modal dialog fix
+ e.type == "focusin" ||
+ target.closest(this.element).length ||
+ target.closest(this.container).length ||
+ target.closest('.calendar-date').length
+ ) return;
+ this.hide();
+ },
+
+ hide: function (e) {
+ if (!this.isShowing) return;
+
+ $(document)
+ .off('.daterangepicker');
+
+ this.element.removeClass('active');
+ this.container.hide();
+
+ if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate))
+ this.notify();
+
+ this.oldStartDate = this.startDate.clone();
+ this.oldEndDate = this.endDate.clone();
+
+ this.isShowing = false;
+ this.element.trigger('hide.daterangepicker', this);
+ },
+
+ enterRange: function (e) {
+ // mouse pointer has entered a range label
+ var label = e.target.innerHTML;
+ if (label == this.locale.customRangeLabel) {
+ this.updateView();
+ } else {
+ var dates = this.ranges[label];
+ this.container.find('input[name=daterangepicker_start]').val(dates[0].format(this.format));
+ this.container.find('input[name=daterangepicker_end]').val(dates[1].format(this.format));
+ }
+ },
+
+ showCalendars: function() {
+ this.container.addClass('show-calendar');
+ this.move();
+ this.element.trigger('showCalendar.daterangepicker', this);
+ },
+
+ hideCalendars: function() {
+ this.container.removeClass('show-calendar');
+ this.element.trigger('hideCalendar.daterangepicker', this);
+ },
+
+ // when a date is typed into the start to end date textboxes
+ inputsChanged: function (e) {
+ var el = $(e.target);
+ var date = moment(el.val(), this.format);
+ if (!date.isValid()) return;
+
+ var startDate, endDate;
+ if (el.attr('name') === 'daterangepicker_start') {
+ startDate = date;
+ endDate = this.endDate;
+ } else {
+ startDate = this.startDate;
+ endDate = date;
+ }
+ this.setCustomDates(startDate, endDate);
+ },
+
+ inputsKeydown: function(e) {
+ if (e.keyCode === 13) {
+ this.inputsChanged(e);
+ this.notify();
+ }
+ },
+
+ updateInputText: function() {
+ if (this.element.is('input') && !this.singleDatePicker) {
+ this.element.val(this.startDate.format(this.format) + this.separator + this.endDate.format(this.format));
+ } else if (this.element.is('input')) {
+ this.element.val(this.endDate.format(this.format));
+ }
+ },
+
+ clickRange: function (e) {
+ var label = e.target.innerHTML;
+ this.chosenLabel = label;
+ if (label == this.locale.customRangeLabel) {
+ this.showCalendars();
+ } else {
+ var dates = this.ranges[label];
+
+ this.startDate = dates[0];
+ this.endDate = dates[1];
+
+ if (!this.timePicker) {
+ this.startDate.startOf('day');
+ this.endDate.endOf('day');
+ }
+
+ this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()).hour(this.startDate.hour()).minute(this.startDate.minute());
+ this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()).hour(this.endDate.hour()).minute(this.endDate.minute());
+ this.updateCalendars();
+
+ this.updateInputText();
+
+ this.hideCalendars();
+ this.hide();
+ this.element.trigger('apply.daterangepicker', this);
+ }
+ },
+
+ clickPrev: function (e) {
+ var cal = $(e.target).parents('.calendar');
+ if (cal.hasClass('left')) {
+ this.leftCalendar.month.subtract(1, 'month');
+ } else {
+ this.rightCalendar.month.subtract(1, 'month');
+ }
+ this.updateCalendars();
+ },
+
+ clickNext: function (e) {
+ var cal = $(e.target).parents('.calendar');
+ if (cal.hasClass('left')) {
+ this.leftCalendar.month.add(1, 'month');
+ } else {
+ this.rightCalendar.month.add(1, 'month');
+ }
+ this.updateCalendars();
+ },
+
+ hoverDate: function (e) {
+ var title = $(e.target).attr('data-title');
+ var row = title.substr(1, 1);
+ var col = title.substr(3, 1);
+ var cal = $(e.target).parents('.calendar');
+
+ if (cal.hasClass('left')) {
+ this.container.find('input[name=daterangepicker_start]').val(this.leftCalendar.calendar[row][col].format(this.format));
+ } else {
+ this.container.find('input[name=daterangepicker_end]').val(this.rightCalendar.calendar[row][col].format(this.format));
+ }
+ },
+
+ setCustomDates: function(startDate, endDate) {
+ this.chosenLabel = this.locale.customRangeLabel;
+ if (startDate.isAfter(endDate)) {
+ var difference = this.endDate.diff(this.startDate);
+ endDate = moment(startDate).add(difference, 'ms');
+ }
+ this.startDate = startDate;
+ this.endDate = endDate;
+
+ this.updateView();
+ this.updateCalendars();
+ },
+
+ clickDate: function (e) {
+ var title = $(e.target).attr('data-title');
+ var row = title.substr(1, 1);
+ var col = title.substr(3, 1);
+ var cal = $(e.target).parents('.calendar');
+
+ var startDate, endDate;
+ if (cal.hasClass('left')) {
+ startDate = this.leftCalendar.calendar[row][col];
+ endDate = this.endDate;
+ if (typeof this.dateLimit === 'object') {
+ var maxDate = moment(startDate).add(this.dateLimit).startOf('day');
+ if (endDate.isAfter(maxDate)) {
+ endDate = maxDate;
+ }
+ }
+ } else {
+ startDate = this.startDate;
+ endDate = this.rightCalendar.calendar[row][col];
+ if (typeof this.dateLimit === 'object') {
+ var minDate = moment(endDate).subtract(this.dateLimit).startOf('day');
+ if (startDate.isBefore(minDate)) {
+ startDate = minDate;
+ }
+ }
+ }
+
+ if (this.singleDatePicker && cal.hasClass('left')) {
+ endDate = startDate.clone();
+ } else if (this.singleDatePicker && cal.hasClass('right')) {
+ startDate = endDate.clone();
+ }
+
+ cal.find('td').removeClass('active');
+
+ $(e.target).addClass('active');
+
+ this.setCustomDates(startDate, endDate);
+
+ if (!this.timePicker)
+ endDate.endOf('day');
+
+ if (this.singleDatePicker && !this.timePicker)
+ this.clickApply();
+ },
+
+ clickApply: function (e) {
+ this.updateInputText();
+ this.hide();
+ this.element.trigger('apply.daterangepicker', this);
+ },
+
+ clickCancel: function (e) {
+ this.startDate = this.oldStartDate;
+ this.endDate = this.oldEndDate;
+ this.chosenLabel = this.oldChosenLabel;
+ this.updateView();
+ this.updateCalendars();
+ this.hide();
+ this.element.trigger('cancel.daterangepicker', this);
+ },
+
+ updateMonthYear: function (e) {
+ var isLeft = $(e.target).closest('.calendar').hasClass('left'),
+ leftOrRight = isLeft ? 'left' : 'right',
+ cal = this.container.find('.calendar.'+leftOrRight);
+
+ // Month must be Number for new moment versions
+ var month = parseInt(cal.find('.monthselect').val(), 10);
+ var year = cal.find('.yearselect').val();
+
+ this[leftOrRight+'Calendar'].month.month(month).year(year);
+ this.updateCalendars();
+ },
+
+ updateTime: function(e) {
+
+ var cal = $(e.target).closest('.calendar'),
+ isLeft = cal.hasClass('left');
+
+ var hour = parseInt(cal.find('.hourselect').val(), 10);
+ var minute = parseInt(cal.find('.minuteselect').val(), 10);
+ var second = 0;
+
+ if (this.timePickerSeconds) {
+ second = parseInt(cal.find('.secondselect').val(), 10);
+ }
+
+ if (this.timePicker12Hour) {
+ var ampm = cal.find('.ampmselect').val();
+ if (ampm === 'PM' && hour < 12)
+ hour += 12;
+ if (ampm === 'AM' && hour === 12)
+ hour = 0;
+ }
+
+ if (isLeft) {
+ var start = this.startDate.clone();
+ start.hour(hour);
+ start.minute(minute);
+ start.second(second);
+ this.startDate = start;
+ this.leftCalendar.month.hour(hour).minute(minute).second(second);
+ if (this.singleDatePicker)
+ this.endDate = start.clone();
+ } else {
+ var end = this.endDate.clone();
+ end.hour(hour);
+ end.minute(minute);
+ end.second(second);
+ this.endDate = end;
+ if (this.singleDatePicker)
+ this.startDate = end.clone();
+ this.rightCalendar.month.hour(hour).minute(minute).second(second);
+ }
+
+ this.updateView();
+ this.updateCalendars();
+ },
+
+ updateCalendars: function () {
+ this.leftCalendar.calendar = this.buildCalendar(this.leftCalendar.month.month(), this.leftCalendar.month.year(), this.leftCalendar.month.hour(), this.leftCalendar.month.minute(), this.leftCalendar.month.second(), 'left');
+ this.rightCalendar.calendar = this.buildCalendar(this.rightCalendar.month.month(), this.rightCalendar.month.year(), this.rightCalendar.month.hour(), this.rightCalendar.month.minute(), this.rightCalendar.month.second(), 'right');
+ this.container.find('.calendar.left').empty().html(this.renderCalendar(this.leftCalendar.calendar, this.startDate, this.minDate, this.maxDate, 'left'));
+ this.container.find('.calendar.right').empty().html(this.renderCalendar(this.rightCalendar.calendar, this.endDate, this.singleDatePicker ? this.minDate : this.startDate, this.maxDate, 'right'));
+
+ this.container.find('.ranges li').removeClass('active');
+ var customRange = true;
+ var i = 0;
+ for (var range in this.ranges) {
+ if (this.timePicker) {
+ if (this.startDate.isSame(this.ranges[range][0]) && this.endDate.isSame(this.ranges[range][1])) {
+ customRange = false;
+ this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')')
+ .addClass('active').html();
+ }
+ } else {
+ //ignore times when comparing dates if time picker is not enabled
+ if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) {
+ customRange = false;
+ this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')')
+ .addClass('active').html();
+ }
+ }
+ i++;
+ }
+ if (customRange) {
+ this.chosenLabel = this.container.find('.ranges li:last').addClass('active').html();
+ this.showCalendars();
+ }
+ },
+
+ buildCalendar: function (month, year, hour, minute, second, side) {
+ var daysInMonth = moment([year, month]).daysInMonth();
+ var firstDay = moment([year, month, 1]);
+ var lastDay = moment([year, month, daysInMonth]);
+ var lastMonth = moment(firstDay).subtract(1, 'month').month();
+ var lastYear = moment(firstDay).subtract(1, 'month').year();
+
+ var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth();
+
+ var dayOfWeek = firstDay.day();
+
+ var i;
+
+ //initialize a 6 rows x 7 columns array for the calendar
+ var calendar = [];
+ calendar.firstDay = firstDay;
+ calendar.lastDay = lastDay;
+
+ for (i = 0; i < 6; i++) {
+ calendar[i] = [];
+ }
+
+ //populate the calendar with date objects
+ var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1;
+ if (startDay > daysInLastMonth)
+ startDay -= 7;
+
+ if (dayOfWeek == this.locale.firstDay)
+ startDay = daysInLastMonth - 6;
+
+ var curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]).zone(this.timeZone);
+
+ var col, row;
+ for (i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) {
+ if (i > 0 && col % 7 === 0) {
+ col = 0;
+ row++;
+ }
+ calendar[row][col] = curDate.clone().hour(hour);
+ curDate.hour(12);
+
+ if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side == 'left') {
+ calendar[row][col] = this.minDate.clone();
+ }
+
+ if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side == 'right') {
+ calendar[row][col] = this.maxDate.clone();
+ }
+
+ }
+
+ return calendar;
+ },
+
+ renderDropdowns: function (selected, minDate, maxDate) {
+ var currentMonth = selected.month();
+ var currentYear = selected.year();
+ var maxYear = (maxDate && maxDate.year()) || (currentYear + 5);
+ var minYear = (minDate && minDate.year()) || (currentYear - 50);
+
+ var monthHtml = '<select class="monthselect">';
+ var inMinYear = currentYear == minYear;
+ var inMaxYear = currentYear == maxYear;
+
+ for (var m = 0; m < 12; m++) {
+ if ((!inMinYear || m >= minDate.month()) && (!inMaxYear || m <= maxDate.month())) {
+ monthHtml += "<option value='" + m + "'" +
+ (m === currentMonth ? " selected='selected'" : "") +
+ ">" + this.locale.monthNames[m] + "</option>";
+ }
+ }
+ monthHtml += "</select>";
+
+ var yearHtml = '<select class="yearselect">';
+
+ for (var y = minYear; y <= maxYear; y++) {
+ yearHtml += '<option value="' + y + '"' +
+ (y === currentYear ? ' selected="selected"' : '') +
+ '>' + y + '</option>';
+ }
+
+ yearHtml += '</select>';
+
+ return monthHtml + yearHtml;
+ },
+
+ renderCalendar: function (calendar, selected, minDate, maxDate, side) {
+
+ var html = '<div class="calendar-date">';
+ html += '<table class="table-condensed">';
+ html += '<thead>';
+ html += '<tr>';
+
+ // add empty cell for week number
+ if (this.showWeekNumbers)
+ html += '<th></th>';
+
+ if (!minDate || minDate.isBefore(calendar.firstDay)) {
+ html += '<th class="prev available"><i class="fa fa-arrow-left icon icon-arrow-left glyphicon glyphicon-arrow-left"></i></th>';
+ } else {
+ html += '<th></th>';
+ }
+
+ var dateHtml = this.locale.monthNames[calendar[1][1].month()] + calendar[1][1].format(" YYYY");
+
+ if (this.showDropdowns) {
+ dateHtml = this.renderDropdowns(calendar[1][1], minDate, maxDate);
+ }
+
+ html += '<th colspan="5" class="month">' + dateHtml + '</th>';
+ if (!maxDate || maxDate.isAfter(calendar.lastDay)) {
+ html += '<th class="next available"><i class="fa fa-arrow-right icon icon-arrow-right glyphicon glyphicon-arrow-right"></i></th>';
+ } else {
+ html += '<th></th>';
+ }
+
+ html += '</tr>';
+ html += '<tr>';
+
+ // add week number label
+ if (this.showWeekNumbers)
+ html += '<th class="week">' + this.locale.weekLabel + '</th>';
+
+ $.each(this.locale.daysOfWeek, function (index, dayOfWeek) {
+ html += '<th>' + dayOfWeek + '</th>';
+ });
+
+ html += '</tr>';
+ html += '</thead>';
+ html += '<tbody>';
+
+ for (var row = 0; row < 6; row++) {
+ html += '<tr>';
+
+ // add week number
+ if (this.showWeekNumbers)
+ html += '<td class="week">' + calendar[row][0].week() + '</td>';
+
+ for (var col = 0; col < 7; col++) {
+ var cname = 'available ';
+ cname += (calendar[row][col].month() == calendar[1][1].month()) ? '' : 'off';
+
+ if ((minDate && calendar[row][col].isBefore(minDate, 'day')) || (maxDate && calendar[row][col].isAfter(maxDate, 'day'))) {
+ cname = ' off disabled ';
+ } else if (calendar[row][col].format('YYYY-MM-DD') == selected.format('YYYY-MM-DD')) {
+ cname += ' active ';
+ if (calendar[row][col].format('YYYY-MM-DD') == this.startDate.format('YYYY-MM-DD')) {
+ cname += ' start-date ';
+ }
+ if (calendar[row][col].format('YYYY-MM-DD') == this.endDate.format('YYYY-MM-DD')) {
+ cname += ' end-date ';
+ }
+ } else if (calendar[row][col] >= this.startDate && calendar[row][col] <= this.endDate) {
+ cname += ' in-range ';
+ if (calendar[row][col].isSame(this.startDate)) { cname += ' start-date '; }
+ if (calendar[row][col].isSame(this.endDate)) { cname += ' end-date '; }
+ }
+
+ var title = 'r' + row + 'c' + col;
+ html += '<td class="' + cname.replace(/\s+/g, ' ').replace(/^\s?(.*?)\s?$/, '$1') + '" data-title="' + title + '">' + calendar[row][col].date() + '</td>';
+ }
+ html += '</tr>';
+ }
+
+ html += '</tbody>';
+ html += '</table>';
+ html += '</div>';
+
+ var i;
+ if (this.timePicker) {
+
+ html += '<div class="calendar-time">';
+ html += '<select class="hourselect">';
+
+ // Disallow selections before the minDate or after the maxDate
+ var min_hour = 0;
+ var max_hour = 23;
+
+ if (minDate && (side == 'left' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == minDate.format('YYYY-MM-DD')) {
+ min_hour = minDate.hour();
+ if (selected.hour() < min_hour)
+ selected.hour(min_hour);
+ if (this.timePicker12Hour && min_hour >= 12 && selected.hour() >= 12)
+ min_hour -= 12;
+ if (this.timePicker12Hour && min_hour == 12)
+ min_hour = 1;
+ }
+
+ if (maxDate && (side == 'right' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == maxDate.format('YYYY-MM-DD')) {
+ max_hour = maxDate.hour();
+ if (selected.hour() > max_hour)
+ selected.hour(max_hour);
+ if (this.timePicker12Hour && max_hour >= 12 && selected.hour() >= 12)
+ max_hour -= 12;
+ }
+
+ var start = 0;
+ var end = 23;
+ var selected_hour = selected.hour();
+ if (this.timePicker12Hour) {
+ start = 1;
+ end = 12;
+ if (selected_hour >= 12)
+ selected_hour -= 12;
+ if (selected_hour === 0)
+ selected_hour = 12;
+ }
+
+ for (i = start; i <= end; i++) {
+
+ if (i == selected_hour) {
+ html += '<option value="' + i + '" selected="selected">' + i + '</option>';
+ } else if (i < min_hour || i > max_hour) {
+ html += '<option value="' + i + '" disabled="disabled" class="disabled">' + i + '</option>';
+ } else {
+ html += '<option value="' + i + '">' + i + '</option>';
+ }
+ }
+
+ html += '</select> : ';
+
+ html += '<select class="minuteselect">';
+
+ // Disallow selections before the minDate or after the maxDate
+ var min_minute = 0;
+ var max_minute = 59;
+
+ if (minDate && (side == 'left' || this.singleDatePicker) && selected.format('YYYY-MM-DD h A') == minDate.format('YYYY-MM-DD h A')) {
+ min_minute = minDate.minute();
+ if (selected.minute() < min_minute)
+ selected.minute(min_minute);
+ }
+
+ if (maxDate && (side == 'right' || this.singleDatePicker) && selected.format('YYYY-MM-DD h A') == maxDate.format('YYYY-MM-DD h A')) {
+ max_minute = maxDate.minute();
+ if (selected.minute() > max_minute)
+ selected.minute(max_minute);
+ }
+
+ for (i = 0; i < 60; i += this.timePickerIncrement) {
+ var num = i;
+ if (num < 10)
+ num = '0' + num;
+ if (i == selected.minute()) {
+ html += '<option value="' + i + '" selected="selected">' + num + '</option>';
+ } else if (i < min_minute || i > max_minute) {
+ html += '<option value="' + i + '" disabled="disabled" class="disabled">' + num + '</option>';
+ } else {
+ html += '<option value="' + i + '">' + num + '</option>';
+ }
+ }
+
+ html += '</select> ';
+
+ if (this.timePickerSeconds) {
+ html += ': <select class="secondselect">';
+
+ for (i = 0; i < 60; i += this.timePickerIncrement) {
+ var num = i;
+ if (num < 10)
+ num = '0' + num;
+ if (i == selected.second()) {
+ html += '<option value="' + i + '" selected="selected">' + num + '</option>';
+ } else {
+ html += '<option value="' + i + '">' + num + '</option>';
+ }
+ }
+
+ html += '</select>';
+ }
+
+ if (this.timePicker12Hour) {
+ html += '<select class="ampmselect">';
+
+ // Disallow selection before the minDate or after the maxDate
+ var am_html = '';
+ var pm_html = '';
+
+ if (minDate && (side == 'left' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == minDate.format('YYYY-MM-DD') && minDate.hour() >= 12) {
+ am_html = ' disabled="disabled" class="disabled"';
+ }
+
+ if (maxDate && (side == 'right' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == maxDate.format('YYYY-MM-DD') && maxDate.hour() < 12) {
+ pm_html = ' disabled="disabled" class="disabled"';
+ }
+
+ if (selected.hour() >= 12) {
+ html += '<option value="AM"' + am_html + '>AM</option><option value="PM" selected="selected"' + pm_html + '>PM</option>';
+ } else {
+ html += '<option value="AM" selected="selected"' + am_html + '>AM</option><option value="PM"' + pm_html + '>PM</option>';
+ }
+ html += '</select>';
+ }
+
+ html += '</div>';
+
+ }
+
+ return html;
+
+ },
+
+ remove: function() {
+
+ this.container.remove();
+ this.element.off('.daterangepicker');
+ this.element.removeData('daterangepicker');
+
+ }
+
+ };
+
+ $.fn.daterangepicker = function (options, cb) {
+ this.each(function () {
+ var el = $(this);
+ if (el.data('daterangepicker'))
+ el.data('daterangepicker').remove();
+ el.data('daterangepicker', new DateRangePicker(el, options, cb));
+ });
+ return this;
+ };
+
+}));
diff --git a/bootstrap/js-used/moment.min.js b/bootstrap/js-used/moment.min.js
new file mode 100644
index 0000000..c30bbff
--- /dev/null
+++ b/bootstrap/js-used/moment.min.js
@@ -0,0 +1,6 @@
+//! moment.js
+//! version : 2.8.1
+//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+//! license : MIT
+//! momentjs.com
+(function(a){function b(a,b,c){switch(arguments.length){case 2:return null!=a?a:b;case 3:return null!=a?a:null!=b?b:c;default:throw new Error("Implement me")}}function c(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function d(a){rb.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+a)}function e(a,b){var c=!0;return l(function(){return c&&(d(a),c=!1),b.apply(this,arguments)},b)}function f(a,b){nc[a]||(d(b),nc[a]=!0)}function g(a,b){return function(c){return o(a.call(this,c),b)}}function h(a,b){return function(c){return this.localeData().ordinal(a.call(this,c),b)}}function i(){}function j(a,b){b!==!1&&E(a),m(this,a),this._d=new Date(+a._d)}function k(a){var b=x(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;this._milliseconds=+k+1e3*j+6e4*i+36e5*h,this._days=+g+7*f,this._months=+e+3*d+12*c,this._data={},this._locale=rb.localeData(),this._bubble()}function l(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return b.hasOwnProperty("toString")&&(a.toString=b.toString),b.hasOwnProperty("valueOf")&&(a.valueOf=b.valueOf),a}function m(a,b){var c,d,e;if("undefined"!=typeof b._isAMomentObject&&(a._isAMomentObject=b._isAMomentObject),"undefined"!=typeof b._i&&(a._i=b._i),"undefined"!=typeof b._f&&(a._f=b._f),"undefined"!=typeof b._l&&(a._l=b._l),"undefined"!=typeof b._strict&&(a._strict=b._strict),"undefined"!=typeof b._tzm&&(a._tzm=b._tzm),"undefined"!=typeof b._isUTC&&(a._isUTC=b._isUTC),"undefined"!=typeof b._offset&&(a._offset=b._offset),"undefined"!=typeof b._pf&&(a._pf=b._pf),"undefined"!=typeof b._locale&&(a._locale=b._locale),Fb.length>0)for(c in Fb)d=Fb[c],e=b[d],"undefined"!=typeof e&&(a[d]=e);return a}function n(a){return 0>a?Math.ceil(a):Math.floor(a)}function o(a,b,c){for(var d=""+Math.abs(a),e=a>=0;d.length<b;)d="0"+d;return(e?c?"+":"":"-")+d}function p(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function q(a,b){var c;return b=J(b,a),a.isBefore(b)?c=p(a,b):(c=p(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c}function r(a,b){return function(c,d){var e,g;return null===d||isNaN(+d)||(f(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period)."),g=c,c=d,d=g),c="string"==typeof c?+c:c,e=rb.duration(c,d),s(this,e,a),this}}function s(a,b,c,d){var e=b._milliseconds,f=b._days,g=b._months;d=null==d?!0:d,e&&a._d.setTime(+a._d+e*c),f&&lb(a,"Date",kb(a,"Date")+f*c),g&&jb(a,kb(a,"Month")+g*c),d&&rb.updateOffset(a,f||g)}function t(a){return"[object Array]"===Object.prototype.toString.call(a)}function u(a){return"[object Date]"===Object.prototype.toString.call(a)||a instanceof Date}function v(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&z(a[d])!==z(b[d]))&&g++;return g+f}function w(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=gc[a]||hc[b]||b}return a}function x(a){var b,c,d={};for(c in a)a.hasOwnProperty(c)&&(b=w(c),b&&(d[b]=a[c]));return d}function y(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}rb[b]=function(e,f){var g,h,i=rb._locale[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=rb().utc().set(d,a);return i.call(rb._locale,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function z(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function A(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function B(a,b,c){return fb(rb([a,11,31+b-c]),b,c).week}function C(a){return D(a)?366:365}function D(a){return a%4===0&&a%100!==0||a%400===0}function E(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[yb]<0||a._a[yb]>11?yb:a._a[zb]<1||a._a[zb]>A(a._a[xb],a._a[yb])?zb:a._a[Ab]<0||a._a[Ab]>23?Ab:a._a[Bb]<0||a._a[Bb]>59?Bb:a._a[Cb]<0||a._a[Cb]>59?Cb:a._a[Db]<0||a._a[Db]>999?Db:-1,a._pf._overflowDayOfYear&&(xb>b||b>zb)&&(b=zb),a._pf.overflow=b)}function F(a){return null==a._isValid&&(a._isValid=!isNaN(a._d.getTime())&&a._pf.overflow<0&&!a._pf.empty&&!a._pf.invalidMonth&&!a._pf.nullInput&&!a._pf.invalidFormat&&!a._pf.userInvalidated,a._strict&&(a._isValid=a._isValid&&0===a._pf.charsLeftOver&&0===a._pf.unusedTokens.length)),a._isValid}function G(a){return a?a.toLowerCase().replace("_","-"):a}function H(a){for(var b,c,d,e,f=0;f<a.length;){for(e=G(a[f]).split("-"),b=e.length,c=G(a[f+1]),c=c?c.split("-"):null;b>0;){if(d=I(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&v(e,c,!0)>=b-1)break;b--}f++}return null}function I(a){var b=null;if(!Eb[a]&&Gb)try{b=rb.locale(),require("./locale/"+a),rb.locale(b)}catch(c){}return Eb[a]}function J(a,b){return b._isUTC?rb(a).zone(b._offset||0):rb(a).local()}function K(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function L(a){var b,c,d=a.match(Kb);for(b=0,c=d.length;c>b;b++)d[b]=mc[d[b]]?mc[d[b]]:K(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function M(a,b){return a.isValid()?(b=N(b,a.localeData()),ic[b]||(ic[b]=L(b)),ic[b](a)):a.localeData().invalidDate()}function N(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Lb.lastIndex=0;d>=0&&Lb.test(a);)a=a.replace(Lb,c),Lb.lastIndex=0,d-=1;return a}function O(a,b){var c,d=b._strict;switch(a){case"Q":return Wb;case"DDDD":return Yb;case"YYYY":case"GGGG":case"gggg":return d?Zb:Ob;case"Y":case"G":case"g":return _b;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return d?$b:Pb;case"S":if(d)return Wb;case"SS":if(d)return Xb;case"SSS":if(d)return Yb;case"DDD":return Nb;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Rb;case"a":case"A":return b._locale._meridiemParse;case"X":return Ub;case"Z":case"ZZ":return Sb;case"T":return Tb;case"SSSS":return Qb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return d?Xb:Mb;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Mb;case"Do":return Vb;default:return c=new RegExp(X(W(a.replace("\\","")),"i"))}}function P(a){a=a||"";var b=a.match(Sb)||[],c=b[b.length-1]||[],d=(c+"").match(ec)||["-",0,0],e=+(60*d[1])+z(d[2]);return"+"===d[0]?-e:e}function Q(a,b,c){var d,e=c._a;switch(a){case"Q":null!=b&&(e[yb]=3*(z(b)-1));break;case"M":case"MM":null!=b&&(e[yb]=z(b)-1);break;case"MMM":case"MMMM":d=c._locale.monthsParse(b),null!=d?e[yb]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[zb]=z(b));break;case"Do":null!=b&&(e[zb]=z(parseInt(b,10)));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=z(b));break;case"YY":e[xb]=rb.parseTwoDigitYear(b);break;case"YYYY":case"YYYYY":case"YYYYYY":e[xb]=z(b);break;case"a":case"A":c._isPm=c._locale.isPM(b);break;case"H":case"HH":case"h":case"hh":e[Ab]=z(b);break;case"m":case"mm":e[Bb]=z(b);break;case"s":case"ss":e[Cb]=z(b);break;case"S":case"SS":case"SSS":case"SSSS":e[Db]=z(1e3*("0."+b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=P(b);break;case"dd":case"ddd":case"dddd":d=c._locale.weekdaysParse(b),null!=d?(c._w=c._w||{},c._w.d=d):c._pf.invalidWeekday=b;break;case"w":case"ww":case"W":case"WW":case"d":case"e":case"E":a=a.substr(0,1);case"gggg":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=z(b));break;case"gg":case"GG":c._w=c._w||{},c._w[a]=rb.parseTwoDigitYear(b)}}function R(a){var c,d,e,f,g,h,i;c=a._w,null!=c.GG||null!=c.W||null!=c.E?(g=1,h=4,d=b(c.GG,a._a[xb],fb(rb(),1,4).year),e=b(c.W,1),f=b(c.E,1)):(g=a._locale._week.dow,h=a._locale._week.doy,d=b(c.gg,a._a[xb],fb(rb(),g,h).year),e=b(c.w,1),null!=c.d?(f=c.d,g>f&&++e):f=null!=c.e?c.e+g:g),i=gb(d,e,f,h,g),a._a[xb]=i.year,a._dayOfYear=i.dayOfYear}function S(a){var c,d,e,f,g=[];if(!a._d){for(e=U(a),a._w&&null==a._a[zb]&&null==a._a[yb]&&R(a),a._dayOfYear&&(f=b(a._a[xb],e[xb]),a._dayOfYear>C(f)&&(a._pf._overflowDayOfYear=!0),d=bb(f,0,a._dayOfYear),a._a[yb]=d.getUTCMonth(),a._a[zb]=d.getUTCDate()),c=0;3>c&&null==a._a[c];++c)a._a[c]=g[c]=e[c];for(;7>c;c++)a._a[c]=g[c]=null==a._a[c]?2===c?1:0:a._a[c];a._d=(a._useUTC?bb:ab).apply(null,g),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()+a._tzm)}}function T(a){var b;a._d||(b=x(a._i),a._a=[b.year,b.month,b.day,b.hour,b.minute,b.second,b.millisecond],S(a))}function U(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function V(a){if(a._f===rb.ISO_8601)return void Z(a);a._a=[],a._pf.empty=!0;var b,c,d,e,f,g=""+a._i,h=g.length,i=0;for(d=N(a._f,a._locale).match(Kb)||[],b=0;b<d.length;b++)e=d[b],c=(g.match(O(e,a))||[])[0],c&&(f=g.substr(0,g.indexOf(c)),f.length>0&&a._pf.unusedInput.push(f),g=g.slice(g.indexOf(c)+c.length),i+=c.length),mc[e]?(c?a._pf.empty=!1:a._pf.unusedTokens.push(e),Q(e,c,a)):a._strict&&!c&&a._pf.unusedTokens.push(e);a._pf.charsLeftOver=h-i,g.length>0&&a._pf.unusedInput.push(g),a._isPm&&a._a[Ab]<12&&(a._a[Ab]+=12),a._isPm===!1&&12===a._a[Ab]&&(a._a[Ab]=0),S(a),E(a)}function W(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function X(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function Y(a){var b,d,e,f,g;if(0===a._f.length)return a._pf.invalidFormat=!0,void(a._d=new Date(0/0));for(f=0;f<a._f.length;f++)g=0,b=m({},a),b._pf=c(),b._f=a._f[f],V(b),F(b)&&(g+=b._pf.charsLeftOver,g+=10*b._pf.unusedTokens.length,b._pf.score=g,(null==e||e>g)&&(e=g,d=b));l(a,d||b)}function Z(a){var b,c,d=a._i,e=ac.exec(d);if(e){for(a._pf.iso=!0,b=0,c=cc.length;c>b;b++)if(cc[b][1].exec(d)){a._f=cc[b][0]+(e[6]||" ");break}for(b=0,c=dc.length;c>b;b++)if(dc[b][1].exec(d)){a._f+=dc[b][0];break}d.match(Sb)&&(a._f+="Z"),V(a)}else a._isValid=!1}function $(a){Z(a),a._isValid===!1&&(delete a._isValid,rb.createFromInputFallback(a))}function _(b){var c,d=b._i;d===a?b._d=new Date:u(d)?b._d=new Date(+d):null!==(c=Hb.exec(d))?b._d=new Date(+c[1]):"string"==typeof d?$(b):t(d)?(b._a=d.slice(0),S(b)):"object"==typeof d?T(b):"number"==typeof d?b._d=new Date(d):rb.createFromInputFallback(b)}function ab(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 1970>a&&h.setFullYear(a),h}function bb(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function cb(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function db(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function eb(a,b,c){var d=rb.duration(a).abs(),e=wb(d.as("s")),f=wb(d.as("m")),g=wb(d.as("h")),h=wb(d.as("d")),i=wb(d.as("M")),j=wb(d.as("y")),k=e<jc.s&&["s",e]||1===f&&["m"]||f<jc.m&&["mm",f]||1===g&&["h"]||g<jc.h&&["hh",g]||1===h&&["d"]||h<jc.d&&["dd",h]||1===i&&["M"]||i<jc.M&&["MM",i]||1===j&&["y"]||["yy",j];return k[2]=b,k[3]=+a>0,k[4]=c,db.apply({},k)}function fb(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=rb(a).add(f,"d"),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function gb(a,b,c,d,e){var f,g,h=bb(a,0,1).getUTCDay();return h=0===h?7:h,c=null!=c?c:e,f=e-h+(h>d?7:0)-(e>h?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:C(a-1)+g}}function hb(b){var c=b._i,d=b._f;return b._locale=b._locale||rb.localeData(b._l),null===c||d===a&&""===c?rb.invalid({nullInput:!0}):("string"==typeof c&&(b._i=c=b._locale.preparse(c)),rb.isMoment(c)?new j(c,!0):(d?t(d)?Y(b):V(b):_(b),new j(b)))}function ib(a,b){var c,d;if(1===b.length&&t(b[0])&&(b=b[0]),!b.length)return rb();for(c=b[0],d=1;d<b.length;++d)b[d][a](c)&&(c=b[d]);return c}function jb(a,b){var c;return"string"==typeof b&&(b=a.localeData().monthsParse(b),"number"!=typeof b)?a:(c=Math.min(a.date(),A(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a)}function kb(a,b){return a._d["get"+(a._isUTC?"UTC":"")+b]()}function lb(a,b,c){return"Month"===b?jb(a,c):a._d["set"+(a._isUTC?"UTC":"")+b](c)}function mb(a,b){return function(c){return null!=c?(lb(this,a,c),rb.updateOffset(this,b),this):kb(this,a)}}function nb(a){return 400*a/146097}function ob(a){return 146097*a/400}function pb(a){rb.duration.fn[a]=function(){return this._data[a]}}function qb(a){"undefined"==typeof ender&&(sb=vb.moment,vb.moment=a?e("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.",rb):rb)}for(var rb,sb,tb,ub="2.8.1",vb="undefined"!=typeof global?global:this,wb=Math.round,xb=0,yb=1,zb=2,Ab=3,Bb=4,Cb=5,Db=6,Eb={},Fb=[],Gb="undefined"!=typeof module&&module.exports,Hb=/^\/?Date\((\-?\d+)/i,Ib=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Jb=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,Kb=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,Lb=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,Mb=/\d\d?/,Nb=/\d{1,3}/,Ob=/\d{1,4}/,Pb=/[+\-]?\d{1,6}/,Qb=/\d+/,Rb=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Sb=/Z|[\+\-]\d\d:?\d\d/gi,Tb=/T/i,Ub=/[\+\-]?\d+(\.\d{1,3})?/,Vb=/\d{1,2}/,Wb=/\d/,Xb=/\d\d/,Yb=/\d{3}/,Zb=/\d{4}/,$b=/[+-]?\d{6}/,_b=/[+-]?\d+/,ac=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,bc="YYYY-MM-DDTHH:mm:ssZ",cc=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],dc=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],ec=/([\+\-]|\d\d)/gi,fc=("Date|Hours|Minutes|Seconds|Milliseconds".split("|"),{Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6}),gc={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",Q:"quarter",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},hc={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},ic={},jc={s:45,m:45,h:22,d:26,M:11},kc="DDD w W M D d".split(" "),lc="M D H h m s w W".split(" "),mc={M:function(){return this.month()+1},MMM:function(a){return this.localeData().monthsShort(this,a)},MMMM:function(a){return this.localeData().months(this,a)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(a){return this.localeData().weekdaysMin(this,a)},ddd:function(a){return this.localeData().weekdaysShort(this,a)},dddd:function(a){return this.localeData().weekdays(this,a)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return o(this.year()%100,2)},YYYY:function(){return o(this.year(),4)},YYYYY:function(){return o(this.year(),5)},YYYYYY:function(){var a=this.year(),b=a>=0?"+":"-";return b+o(Math.abs(a),6)},gg:function(){return o(this.weekYear()%100,2)},gggg:function(){return o(this.weekYear(),4)},ggggg:function(){return o(this.weekYear(),5)},GG:function(){return o(this.isoWeekYear()%100,2)},GGGG:function(){return o(this.isoWeekYear(),4)},GGGGG:function(){return o(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return z(this.milliseconds()/100)},SS:function(){return o(z(this.milliseconds()/10),2)},SSS:function(){return o(this.milliseconds(),3)},SSSS:function(){return o(this.milliseconds(),3)},Z:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+o(z(a/60),2)+":"+o(z(a)%60,2)},ZZ:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+o(z(a/60),2)+o(z(a)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},nc={},oc=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];kc.length;)tb=kc.pop(),mc[tb+"o"]=h(mc[tb],tb);for(;lc.length;)tb=lc.pop(),mc[tb+tb]=g(mc[tb],2);mc.DDDD=g(mc.DDD,3),l(i.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a){var b,c,d;for(this._monthsParse||(this._monthsParse=[]),b=0;12>b;b++)if(this._monthsParse[b]||(c=rb.utc([2e3,b]),d="^"+this.months(c,"")+"|^"+this.monthsShort(c,""),this._monthsParse[b]=new RegExp(d.replace(".",""),"i")),this._monthsParse[b].test(a))return b},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=rb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY LT",LLLL:"dddd, MMMM D, YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b){var c=this._calendar[a];return"function"==typeof c?c.apply(b):c},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",preparse:function(a){return a},postformat:function(a){return a},week:function(a){return fb(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),rb=function(b,d,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._i=b,g._f=d,g._l=e,g._strict=f,g._isUTC=!1,g._pf=c(),hb(g)},rb.suppressDeprecationWarnings=!1,rb.createFromInputFallback=e("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(a){a._d=new Date(a._i)}),rb.min=function(){var a=[].slice.call(arguments,0);return ib("isBefore",a)},rb.max=function(){var a=[].slice.call(arguments,0);return ib("isAfter",a)},rb.utc=function(b,d,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._useUTC=!0,g._isUTC=!0,g._l=e,g._i=b,g._f=d,g._strict=f,g._pf=c(),hb(g).utc()},rb.unix=function(a){return rb(1e3*a)},rb.duration=function(a,b){var c,d,e,f,g=a,h=null;return rb.isDuration(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(g={},b?g[b]=a:g.milliseconds=a):(h=Ib.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:z(h[zb])*c,h:z(h[Ab])*c,m:z(h[Bb])*c,s:z(h[Cb])*c,ms:z(h[Db])*c}):(h=Jb.exec(a))?(c="-"===h[1]?-1:1,e=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*c},g={y:e(h[2]),M:e(h[3]),d:e(h[4]),h:e(h[5]),m:e(h[6]),s:e(h[7]),w:e(h[8])}):"object"==typeof g&&("from"in g||"to"in g)&&(f=q(rb(g.from),rb(g.to)),g={},g.ms=f.milliseconds,g.M=f.months),d=new k(g),rb.isDuration(a)&&a.hasOwnProperty("_locale")&&(d._locale=a._locale),d},rb.version=ub,rb.defaultFormat=bc,rb.ISO_8601=function(){},rb.momentProperties=Fb,rb.updateOffset=function(){},rb.relativeTimeThreshold=function(b,c){return jc[b]===a?!1:c===a?jc[b]:(jc[b]=c,!0)},rb.lang=e("moment.lang is deprecated. Use moment.locale instead.",function(a,b){return rb.locale(a,b)}),rb.locale=function(a,b){var c;return a&&(c="undefined"!=typeof b?rb.defineLocale(a,b):rb.localeData(a),c&&(rb.duration._locale=rb._locale=c)),rb._locale._abbr},rb.defineLocale=function(a,b){return null!==b?(b.abbr=a,Eb[a]||(Eb[a]=new i),Eb[a].set(b),rb.locale(a),Eb[a]):(delete Eb[a],null)},rb.langData=e("moment.langData is deprecated. Use moment.localeData instead.",function(a){return rb.localeData(a)}),rb.localeData=function(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return rb._locale;if(!t(a)){if(b=I(a))return b;a=[a]}return H(a)},rb.isMoment=function(a){return a instanceof j||null!=a&&a.hasOwnProperty("_isAMomentObject")},rb.isDuration=function(a){return a instanceof k};for(tb=oc.length-1;tb>=0;--tb)y(oc[tb]);rb.normalizeUnits=function(a){return w(a)},rb.invalid=function(a){var b=rb.utc(0/0);return null!=a?l(b._pf,a):b._pf.userInvalidated=!0,b},rb.parseZone=function(){return rb.apply(null,arguments).parseZone()},rb.parseTwoDigitYear=function(a){return z(a)+(z(a)>68?1900:2e3)},l(rb.fn=j.prototype,{clone:function(){return rb(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var a=rb(this).utc();return 0<a.year()&&a.year()<=9999?M(a,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):M(a,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var a=this;return[a.year(),a.month(),a.date(),a.hours(),a.minutes(),a.seconds(),a.milliseconds()]},isValid:function(){return F(this)},isDSTShifted:function(){return this._a?this.isValid()&&v(this._a,(this._isUTC?rb.utc(this._a):rb(this._a)).toArray())>0:!1},parsingFlags:function(){return l({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(a){return this.zone(0,a)},local:function(a){return this._isUTC&&(this.zone(0,a),this._isUTC=!1,a&&this.add(this._d.getTimezoneOffset(),"m")),this},format:function(a){var b=M(this,a||rb.defaultFormat);return this.localeData().postformat(b)},add:r(1,"add"),subtract:r(-1,"subtract"),diff:function(a,b,c){var d,e,f=J(a,this),g=6e4*(this.zone()-f.zone());return b=w(b),"year"===b||"month"===b?(d=432e5*(this.daysInMonth()+f.daysInMonth()),e=12*(this.year()-f.year())+(this.month()-f.month()),e+=(this-rb(this).startOf("month")-(f-rb(f).startOf("month")))/d,e-=6e4*(this.zone()-rb(this).startOf("month").zone()-(f.zone()-rb(f).startOf("month").zone()))/d,"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:n(e)},from:function(a,b){return rb.duration({to:this,from:a}).locale(this.locale()).humanize(!b)},fromNow:function(a){return this.from(rb(),a)},calendar:function(a){var b=a||rb(),c=J(b,this).startOf("day"),d=this.diff(c,"days",!0),e=-6>d?"sameElse":-1>d?"lastWeek":0>d?"lastDay":1>d?"sameDay":2>d?"nextDay":7>d?"nextWeek":"sameElse";return this.format(this.localeData().calendar(e,this))},isLeapYear:function(){return D(this.year())},isDST:function(){return this.zone()<this.clone().month(0).zone()||this.zone()<this.clone().month(5).zone()},day:function(a){var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=cb(a,this.localeData()),this.add(a-b,"d")):b},month:mb("Month",!0),startOf:function(a){switch(a=w(a)){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a?this.weekday(0):"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this},endOf:function(a){return a=w(a),this.startOf(a).add(1,"isoWeek"===a?"week":a).subtract(1,"ms")},isAfter:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)>+rb(a).startOf(b)},isBefore:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)<+rb(a).startOf(b)},isSame:function(a,b){return b=b||"ms",+this.clone().startOf(b)===+J(a,this).startOf(b)},min:e("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(a){return a=rb.apply(null,arguments),this>a?this:a}),max:e("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(a){return a=rb.apply(null,arguments),a>this?this:a}),zone:function(a,b){var c,d=this._offset||0;return null==a?this._isUTC?d:this._d.getTimezoneOffset():("string"==typeof a&&(a=P(a)),Math.abs(a)<16&&(a=60*a),!this._isUTC&&b&&(c=this._d.getTimezoneOffset()),this._offset=a,this._isUTC=!0,null!=c&&this.subtract(c,"m"),d!==a&&(!b||this._changeInProgress?s(this,rb.duration(d-a,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,rb.updateOffset(this,!0),this._changeInProgress=null)),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(a){return a=a?rb(a).zone():0,(this.zone()-a)%60===0},daysInMonth:function(){return A(this.year(),this.month())},dayOfYear:function(a){var b=wb((rb(this).startOf("day")-rb(this).startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")},quarter:function(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)},weekYear:function(a){var b=fb(this,this.localeData()._week.dow,this.localeData()._week.doy).year;return null==a?b:this.add(a-b,"y")},isoWeekYear:function(a){var b=fb(this,1,4).year;return null==a?b:this.add(a-b,"y")},week:function(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")},isoWeek:function(a){var b=fb(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")},weekday:function(a){var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},isoWeeksInYear:function(){return B(this.year(),1,4)},weeksInYear:function(){var a=this.localeData()._week;return B(this.year(),a.dow,a.doy)},get:function(a){return a=w(a),this[a]()},set:function(a,b){return a=w(a),"function"==typeof this[a]&&this[a](b),this},locale:function(b){return b===a?this._locale._abbr:(this._locale=rb.localeData(b),this)},lang:e("moment().lang() is deprecated. Use moment().localeData() instead.",function(b){return b===a?this.localeData():(this._locale=rb.localeData(b),this)}),localeData:function(){return this._locale}}),rb.fn.millisecond=rb.fn.milliseconds=mb("Milliseconds",!1),rb.fn.second=rb.fn.seconds=mb("Seconds",!1),rb.fn.minute=rb.fn.minutes=mb("Minutes",!1),rb.fn.hour=rb.fn.hours=mb("Hours",!0),rb.fn.date=mb("Date",!0),rb.fn.dates=e("dates accessor is deprecated. Use date instead.",mb("Date",!0)),rb.fn.year=mb("FullYear",!0),rb.fn.years=e("years accessor is deprecated. Use year instead.",mb("FullYear",!0)),rb.fn.days=rb.fn.day,rb.fn.months=rb.fn.month,rb.fn.weeks=rb.fn.week,rb.fn.isoWeeks=rb.fn.isoWeek,rb.fn.quarters=rb.fn.quarter,rb.fn.toJSON=rb.fn.toISOString,l(rb.duration.fn=k.prototype,{_bubble:function(){var a,b,c,d=this._milliseconds,e=this._days,f=this._months,g=this._data,h=0;g.milliseconds=d%1e3,a=n(d/1e3),g.seconds=a%60,b=n(a/60),g.minutes=b%60,c=n(b/60),g.hours=c%24,e+=n(c/24),h=n(nb(e)),e-=n(ob(h)),f+=n(e/30),e%=30,h+=n(f/12),f%=12,g.days=e,g.months=f,g.years=h},abs:function(){return this._milliseconds=Math.abs(this._milliseconds),this._days=Math.abs(this._days),this._months=Math.abs(this._months),this._data.milliseconds=Math.abs(this._data.milliseconds),this._data.seconds=Math.abs(this._data.seconds),this._data.minutes=Math.abs(this._data.minutes),this._data.hours=Math.abs(this._data.hours),this._data.months=Math.abs(this._data.months),this._data.years=Math.abs(this._data.years),this},weeks:function(){return n(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*z(this._months/12)},humanize:function(a){var b=eb(this,!a,this.localeData());return a&&(b=this.localeData().pastFuture(+this,b)),this.localeData().postformat(b)},add:function(a,b){var c=rb.duration(a,b);return this._milliseconds+=c._milliseconds,this._days+=c._days,this._months+=c._months,this._bubble(),this},subtract:function(a,b){var c=rb.duration(a,b);return this._milliseconds-=c._milliseconds,this._days-=c._days,this._months-=c._months,this._bubble(),this},get:function(a){return a=w(a),this[a.toLowerCase()+"s"]()},as:function(a){var b,c;if(a=w(a),b=this._days+this._milliseconds/864e5,"month"===a||"year"===a)return c=this._months+12*nb(b),"month"===a?c:c/12;switch(b+=ob(this._months/12),a){case"week":return b/7;case"day":return b;case"hour":return 24*b;case"minute":return 24*b*60;case"second":return 24*b*60*60;case"millisecond":return 24*b*60*60*1e3;default:throw new Error("Unknown unit "+a)}},lang:rb.fn.lang,locale:rb.fn.locale,toIsoString:e("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",function(){return this.toISOString()}),toISOString:function(){var a=Math.abs(this.years()),b=Math.abs(this.months()),c=Math.abs(this.days()),d=Math.abs(this.hours()),e=Math.abs(this.minutes()),f=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"},localeData:function(){return this._locale}});for(tb in fc)fc.hasOwnProperty(tb)&&pb(tb.toLowerCase());rb.duration.fn.asMilliseconds=function(){return this.as("ms")},rb.duration.fn.asSeconds=function(){return this.as("s")},rb.duration.fn.asMinutes=function(){return this.as("m")},rb.duration.fn.asHours=function(){return this.as("h")},rb.duration.fn.asDays=function(){return this.as("d")},rb.duration.fn.asWeeks=function(){return this.as("weeks")},rb.duration.fn.asMonths=function(){return this.as("M")},rb.duration.fn.asYears=function(){return this.as("y")},rb.locale("en",{ordinal:function(a){var b=a%10,c=1===z(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),Gb?module.exports=rb:"function"==typeof define&&define.amd?(define("moment",function(a,b,c){return c.config&&c.config()&&c.config().noGlobal===!0&&(vb.moment=sb),rb}),qb(!0)):qb()}).call(this);
\ No newline at end of file
diff --git a/header.phtml b/header.phtml
index 89b4933..9ef2797 100644
--- a/header.phtml
+++ b/header.phtml
@@ -1,93 +1,98 @@
<?
header("Cache-Control: no-cache, must-revalidate");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Expires: " . gmdate("D, d M Y H:i:s") . " GMT");
header("ETag: ",randomstring());
global $CDRTool;
print "<!DOCTYPE html>\n";
printf("<HTML>
<HEAD>
<TITLE>CDRTool - $title</TITLE>
<META NAME=Description CONTENT=
\"CDR mediation and rating engine for OpenSIPS\">
<META NAME=Keywords CONTENT=\"OpenSIPS, Asterisk, SIP Express router, Cisco\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
<link rel=\"apple-touch-icon\" sizes=\"57x57\" href=\"images/favicons/apple-touch-icon-57x57.png\">
<link rel=\"apple-touch-icon\" sizes=\"60x60\" href=\"images/favicons/apple-touch-icon-60x60.png\">
<link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"images/favicons/apple-touch-icon-72x72.png\">
<link rel=\"apple-touch-icon\" sizes=\"76x76\" href=\"images/favicons/apple-touch-icon-76x76.png\">
<link rel=\"apple-touch-icon\" sizes=\"114x114\" href=\"images/favicons/apple-touch-icon-114x114.png\">
<link rel=\"apple-touch-icon\" sizes=\"120x120\" href=\"images/favicons/apple-touch-icon-120x120.png\">
<link rel=\"apple-touch-icon\" sizes=\"144x144\" href=\"images/favicons/apple-touch-icon-144x144.png\">
<link rel=\"apple-touch-icon\" sizes=\"152x152\" href=\"images/favicons/apple-touch-icon-152x152.png\">
<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"images/favicons/apple-touch-icon-180x180.png\">
<link rel=\"icon\" type=\"image/png\" href=\"images/favicons/favicon-32x32.png\" sizes=\"32x32\">
<link rel=\"icon\" type=\"image/png\" href=\"images/favicons/favicon-194x194.png\" sizes=\"194x194\">
<link rel=\"icon\" type=\"image/png\" href=\"images/favicons/favicon-96x96.png\" sizes=\"96x96\">
<link rel=\"icon\" type=\"image/png\" href=\"images/favicons/android-chrome-192x192.png\" sizes=\"192x192\">
<link rel=\"icon\" type=\"image/png\" href=\"images/favicons/favicon-16x16.png\" sizes=\"16x16\">
<link rel=\"manifest\" href=\"images/favicons/manifest.json\">
<link rel=\"mask-icon\" href=\"images/favicons/safari-pinned-tab.svg\" color=\"#5670d4\">
<meta name=\"msapplication-TileColor\" content=\"#2d89ef\">
<meta name=\"msapplication-TileImage\" content=\"images/favicons/mstile-144x144.png\">
<meta name=\"theme-color\" content=\"#5670d4\">
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>
<script>(function(w){var dpr=((w.devicePixelRatio===undefined)?1:w.devicePixelRatio);if(!!w.navigator.standalone){var r=new XMLHttpRequest();r.open('GET','/retinaimages.php?devicePixelRatio='+dpr,false);r.send()}else{document.cookie='devicePixelRatio='+dpr+'; path=/'}})(window)</script>
<noscript><style id=\"devicePixelRatio\" media=\"only screen and (-moz-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2)\">html{background-image:url(\"/retinaimages.php?devicePixelRatio=2\")}</style></noscript>
<SCRIPT language=\"JavaScript1.2\" src=\"main.js\" type=\"text/javascript\"></SCRIPT>
<link href=\"bootstrap/css/bootstrap.css\" rel=\"stylesheet\">
<link href=\"bootstrap/css/bootstrap-responsive.css\" rel=\"stylesheet\">
<link href=\"bootstrap/css/datepicker.css\" rel=\"stylesheet\">
<link href=\"bootstrap/css/bootstrap-timepicker.min.css\" rel=\"stylesheet\">
+ <link rel=\"stylesheet\" type=\"text/css\" href=\"bootstrap/css/daterangepicker-bs2.css\" />
<link href=\"bootstrap/css/picol.css\" rel=\"stylesheet\">
<link rel=\"stylesheet\" type=\"text/css\" href=\"%s/style.css\">
",$CDRTool['tld']);
if (isset($refreshURL)) {
if (!$refreshTime) $refreshTime=0;
printf ("<meta http-equiv='Refresh' content='%s; URL=%s'>",$refreshTime,$refreshURL);
}
printf ("</HEAD>
<body data-offset=\"50\" data-spy=\"scroll\" background=\"images/gradient_texture.png\" >
");
?>
<script src="bootstrap/js-used/jquery.js"></script>
<script src="bootstrap/js-used/bootstrap-transition.js"></script>
<script src="bootstrap/js-used/bootstrap-scrollspy.js"></script>
<script src="bootstrap/js-used/bootstrap-tooltip.js"></script>
<script src="bootstrap/js-used/bootstrap-popover.js"></script>
<script src="bootstrap/js-used/bootstrap-dropdown.js"></script>
<script src="bootstrap/js-used/bootstrap-collapse.js"></script>
<script src="bootstrap/js-used/bootstrap-button.js"></script>
<script src="bootstrap/js-used/bootstrap-fileupload.js"></script>
<script src="bootstrap/js-used/bootstrap-datepicker.js"></script>
<script src="bootstrap/js-used/bootstrap-timepicker.js"></script>
<script src="library/highCharts/js/highcharts.js"></script>
<script src="library/highCharts/js/modules/exporting.js"></script>
+ <script src="library/d3/d3.min.js" charset="utf-8"></script>
+ <script src="library/flotr2/flotr2.js"></script>
<script src='library/svg/svg.js'></script>
- <script src='library/svg/svg.easing.min.js'></script>
+<script src='library/svg/svg.easing.min.js'></script>
+<script type="text/javascript" src="bootstrap/js-used/moment.min.js"></script>
+<script type="text/javascript" src="bootstrap/js-used/daterangepicker.js"></script>
<script src="bootstrap/js-used/application.js"></script>
<DIV id="TipLayer" style="visibility:hidden;position:absolute;z-index:1000;top:-100"></DIV>
<SCRIPT language="JavaScript1.2" src="style.js" type="text/javascript"></SCRIPT>
<SCRIPT>
function toggleVisibility(rowid) {
if (document.getElementById) {
row = document.getElementById(rowid);
if (row.style.display == "table-row") {
row.style.display = "none";
} else {
row.style.display = "table-row";
}
return false;
} else {
return true;
}
}
</SCRIPT>
diff --git a/library/flotr2/LICENSE b/library/flotr2/LICENSE
new file mode 100644
index 0000000..eeb8ce5
--- /dev/null
+++ b/library/flotr2/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2012 Carl Sutherland
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/library/flotr2/flotr2.js b/library/flotr2/flotr2.js
new file mode 100644
index 0000000..9f50d96
--- /dev/null
+++ b/library/flotr2/flotr2.js
@@ -0,0 +1,7207 @@
+/*!
+ * bean.js - copyright Jacob Thornton 2011
+ * https://github.com/fat/bean
+ * MIT License
+ * special thanks to:
+ * dean edwards: http://dean.edwards.name/
+ * dperini: https://github.com/dperini/nwevents
+ * the entire mootools team: github.com/mootools/mootools-core
+ */
+/*global module:true, define:true*/
+!function (name, context, definition) {
+ if (typeof module !== 'undefined') module.exports = definition(name, context);
+ else if (typeof define === 'function' && typeof define.amd === 'object') define(definition);
+ else context[name] = definition(name, context);
+}('bean', this, function (name, context) {
+ var win = window
+ , old = context[name]
+ , overOut = /over|out/
+ , namespaceRegex = /[^\.]*(?=\..*)\.|.*/
+ , nameRegex = /\..*/
+ , addEvent = 'addEventListener'
+ , attachEvent = 'attachEvent'
+ , removeEvent = 'removeEventListener'
+ , detachEvent = 'detachEvent'
+ , doc = document || {}
+ , root = doc.documentElement || {}
+ , W3C_MODEL = root[addEvent]
+ , eventSupport = W3C_MODEL ? addEvent : attachEvent
+ , slice = Array.prototype.slice
+ , mouseTypeRegex = /click|mouse|menu|drag|drop/i
+ , touchTypeRegex = /^touch|^gesture/i
+ , ONE = { one: 1 } // singleton for quick matching making add() do one()
+
+ , nativeEvents = (function (hash, events, i) {
+ for (i = 0; i < events.length; i++)
+ hash[events[i]] = 1
+ return hash
+ })({}, (
+ 'click dblclick mouseup mousedown contextmenu ' + // mouse buttons
+ 'mousewheel DOMMouseScroll ' + // mouse wheel
+ 'mouseover mouseout mousemove selectstart selectend ' + // mouse movement
+ 'keydown keypress keyup ' + // keyboard
+ 'orientationchange ' + // mobile
+ 'focus blur change reset select submit ' + // form elements
+ 'load unload beforeunload resize move DOMContentLoaded readystatechange ' + // window
+ 'error abort scroll ' + // misc
+ (W3C_MODEL ? // element.fireEvent('onXYZ'... is not forgiving if we try to fire an event
+ // that doesn't actually exist, so make sure we only do these on newer browsers
+ 'show ' + // mouse buttons
+ 'input invalid ' + // form elements
+ 'touchstart touchmove touchend touchcancel ' + // touch
+ 'gesturestart gesturechange gestureend ' + // gesture
+ 'message readystatechange pageshow pagehide popstate ' + // window
+ 'hashchange offline online ' + // window
+ 'afterprint beforeprint ' + // printing
+ 'dragstart dragenter dragover dragleave drag drop dragend ' + // dnd
+ 'loadstart progress suspend emptied stalled loadmetadata ' + // media
+ 'loadeddata canplay canplaythrough playing waiting seeking ' + // media
+ 'seeked ended durationchange timeupdate play pause ratechange ' + // media
+ 'volumechange cuechange ' + // media
+ 'checking noupdate downloading cached updateready obsolete ' + // appcache
+ '' : '')
+ ).split(' ')
+ )
+
+ , customEvents = (function () {
+ function isDescendant(parent, node) {
+ while ((node = node.parentNode) !== null) {
+ if (node === parent) return true
+ }
+ return false
+ }
+
+ function check(event) {
+ var related = event.relatedTarget
+ if (!related) return related === null
+ return (related !== this && related.prefix !== 'xul' && !/document/.test(this.toString()) && !isDescendant(this, related))
+ }
+
+ return {
+ mouseenter: { base: 'mouseover', condition: check }
+ , mouseleave: { base: 'mouseout', condition: check }
+ , mousewheel: { base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' }
+ }
+ })()
+
+ , fixEvent = (function () {
+ var commonProps = 'altKey attrChange attrName bubbles cancelable ctrlKey currentTarget detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey srcElement target timeStamp type view which'.split(' ')
+ , mouseProps = commonProps.concat('button buttons clientX clientY dataTransfer fromElement offsetX offsetY pageX pageY screenX screenY toElement'.split(' '))
+ , keyProps = commonProps.concat('char charCode key keyCode'.split(' '))
+ , touchProps = commonProps.concat('touches targetTouches changedTouches scale rotation'.split(' '))
+ , preventDefault = 'preventDefault'
+ , createPreventDefault = function (event) {
+ return function () {
+ if (event[preventDefault])
+ event[preventDefault]()
+ else
+ event.returnValue = false
+ }
+ }
+ , stopPropagation = 'stopPropagation'
+ , createStopPropagation = function (event) {
+ return function () {
+ if (event[stopPropagation])
+ event[stopPropagation]()
+ else
+ event.cancelBubble = true
+ }
+ }
+ , createStop = function (synEvent) {
+ return function () {
+ synEvent[preventDefault]()
+ synEvent[stopPropagation]()
+ synEvent.stopped = true
+ }
+ }
+ , copyProps = function (event, result, props) {
+ var i, p
+ for (i = props.length; i--;) {
+ p = props[i]
+ if (!(p in result) && p in event) result[p] = event[p]
+ }
+ }
+
+ return function (event, isNative) {
+ var result = { originalEvent: event, isNative: isNative }
+ if (!event)
+ return result
+
+ var props
+ , type = event.type
+ , target = event.target || event.srcElement
+
+ result[preventDefault] = createPreventDefault(event)
+ result[stopPropagation] = createStopPropagation(event)
+ result.stop = createStop(result)
+ result.target = target && target.nodeType === 3 ? target.parentNode : target
+
+ if (isNative) { // we only need basic augmentation on custom events, the rest is too expensive
+ if (type.indexOf('key') !== -1) {
+ props = keyProps
+ result.keyCode = event.which || event.keyCode
+ } else if (mouseTypeRegex.test(type)) {
+ props = mouseProps
+ result.rightClick = event.which === 3 || event.button === 2
+ result.pos = { x: 0, y: 0 }
+ if (event.pageX || event.pageY) {
+ result.clientX = event.pageX
+ result.clientY = event.pageY
+ } else if (event.clientX || event.clientY) {
+ result.clientX = event.clientX + doc.body.scrollLeft + root.scrollLeft
+ result.clientY = event.clientY + doc.body.scrollTop + root.scrollTop
+ }
+ if (overOut.test(type))
+ result.relatedTarget = event.relatedTarget || event[(type === 'mouseover' ? 'from' : 'to') + 'Element']
+ } else if (touchTypeRegex.test(type)) {
+ props = touchProps
+ }
+ copyProps(event, result, props || commonProps)
+ }
+ return result
+ }
+ })()
+
+ // if we're in old IE we can't do onpropertychange on doc or win so we use doc.documentElement for both
+ , targetElement = function (element, isNative) {
+ return !W3C_MODEL && !isNative && (element === doc || element === win) ? root : element
+ }
+
+ // we use one of these per listener, of any type
+ , RegEntry = (function () {
+ function entry(element, type, handler, original, namespaces) {
+ this.element = element
+ this.type = type
+ this.handler = handler
+ this.original = original
+ this.namespaces = namespaces
+ this.custom = customEvents[type]
+ this.isNative = nativeEvents[type] && element[eventSupport]
+ this.eventType = W3C_MODEL || this.isNative ? type : 'propertychange'
+ this.customType = !W3C_MODEL && !this.isNative && type
+ this.target = targetElement(element, this.isNative)
+ this.eventSupport = this.target[eventSupport]
+ }
+
+ entry.prototype = {
+ // given a list of namespaces, is our entry in any of them?
+ inNamespaces: function (checkNamespaces) {
+ var i, j
+ if (!checkNamespaces)
+ return true
+ if (!this.namespaces)
+ return false
+ for (i = checkNamespaces.length; i--;) {
+ for (j = this.namespaces.length; j--;) {
+ if (checkNamespaces[i] === this.namespaces[j])
+ return true
+ }
+ }
+ return false
+ }
+
+ // match by element, original fn (opt), handler fn (opt)
+ , matches: function (checkElement, checkOriginal, checkHandler) {
+ return this.element === checkElement &&
+ (!checkOriginal || this.original === checkOriginal) &&
+ (!checkHandler || this.handler === checkHandler)
+ }
+ }
+
+ return entry
+ })()
+
+ , registry = (function () {
+ // our map stores arrays by event type, just because it's better than storing
+ // everything in a single array. uses '$' as a prefix for the keys for safety
+ var map = {}
+
+ // generic functional search of our registry for matching listeners,
+ // `fn` returns false to break out of the loop
+ , forAll = function (element, type, original, handler, fn) {
+ if (!type || type === '*') {
+ // search the whole registry
+ for (var t in map) {
+ if (t.charAt(0) === '$')
+ forAll(element, t.substr(1), original, handler, fn)
+ }
+ } else {
+ var i = 0, l, list = map['$' + type], all = element === '*'
+ if (!list)
+ return
+ for (l = list.length; i < l; i++) {
+ if (all || list[i].matches(element, original, handler))
+ if (!fn(list[i], list, i, type))
+ return
+ }
+ }
+ }
+
+ , has = function (element, type, original) {
+ // we're not using forAll here simply because it's a bit slower and this
+ // needs to be fast
+ var i, list = map['$' + type]
+ if (list) {
+ for (i = list.length; i--;) {
+ if (list[i].matches(element, original, null))
+ return true
+ }
+ }
+ return false
+ }
+
+ , get = function (element, type, original) {
+ var entries = []
+ forAll(element, type, original, null, function (entry) { return entries.push(entry) })
+ return entries
+ }
+
+ , put = function (entry) {
+ (map['$' + entry.type] || (map['$' + entry.type] = [])).push(entry)
+ return entry
+ }
+
+ , del = function (entry) {
+ forAll(entry.element, entry.type, null, entry.handler, function (entry, list, i) {
+ list.splice(i, 1)
+ if (list.length === 0)
+ delete map['$' + entry.type]
+ return false
+ })
+ }
+
+ // dump all entries, used for onunload
+ , entries = function () {
+ var t, entries = []
+ for (t in map) {
+ if (t.charAt(0) === '$')
+ entries = entries.concat(map[t])
+ }
+ return entries
+ }
+
+ return { has: has, get: get, put: put, del: del, entries: entries }
+ })()
+
+ // add and remove listeners to DOM elements
+ , listener = W3C_MODEL ? function (element, type, fn, add) {
+ element[add ? addEvent : removeEvent](type, fn, false)
+ } : function (element, type, fn, add, custom) {
+ if (custom && add && element['_on' + custom] === null)
+ element['_on' + custom] = 0
+ element[add ? attachEvent : detachEvent]('on' + type, fn)
+ }
+
+ , nativeHandler = function (element, fn, args) {
+ return function (event) {
+ event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, true)
+ return fn.apply(element, [event].concat(args))
+ }
+ }
+
+ , customHandler = function (element, fn, type, condition, args, isNative) {
+ return function (event) {
+ if (condition ? condition.apply(this, arguments) : W3C_MODEL ? true : event && event.propertyName === '_on' + type || !event) {
+ if (event)
+ event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, isNative)
+ fn.apply(element, event && (!args || args.length === 0) ? arguments : slice.call(arguments, event ? 0 : 1).concat(args))
+ }
+ }
+ }
+
+ , once = function (rm, element, type, fn, originalFn) {
+ // wrap the handler in a handler that does a remove as well
+ return function () {
+ rm(element, type, originalFn)
+ fn.apply(this, arguments)
+ }
+ }
+
+ , removeListener = function (element, orgType, handler, namespaces) {
+ var i, l, entry
+ , type = (orgType && orgType.replace(nameRegex, ''))
+ , handlers = registry.get(element, type, handler)
+
+ for (i = 0, l = handlers.length; i < l; i++) {
+ if (handlers[i].inNamespaces(namespaces)) {
+ if ((entry = handlers[i]).eventSupport)
+ listener(entry.target, entry.eventType, entry.handler, false, entry.type)
+ // TODO: this is problematic, we have a registry.get() and registry.del() that
+ // both do registry searches so we waste cycles doing this. Needs to be rolled into
+ // a single registry.forAll(fn) that removes while finding, but the catch is that
+ // we'll be splicing the arrays that we're iterating over. Needs extra tests to
+ // make sure we don't screw it up. @rvagg
+ registry.del(entry)
+ }
+ }
+ }
+
+ , addListener = function (element, orgType, fn, originalFn, args) {
+ var entry
+ , type = orgType.replace(nameRegex, '')
+ , namespaces = orgType.replace(namespaceRegex, '').split('.')
+
+ if (registry.has(element, type, fn))
+ return element // no dupe
+ if (type === 'unload')
+ fn = once(removeListener, element, type, fn, originalFn) // self clean-up
+ if (customEvents[type]) {
+ if (customEvents[type].condition)
+ fn = customHandler(element, fn, type, customEvents[type].condition, true)
+ type = customEvents[type].base || type
+ }
+ entry = registry.put(new RegEntry(element, type, fn, originalFn, namespaces[0] && namespaces))
+ entry.handler = entry.isNative ?
+ nativeHandler(element, entry.handler, args) :
+ customHandler(element, entry.handler, type, false, args, false)
+ if (entry.eventSupport)
+ listener(entry.target, entry.eventType, entry.handler, true, entry.customType)
+ }
+
+ , del = function (selector, fn, $) {
+ return function (e) {
+ var target, i, array = typeof selector === 'string' ? $(selector, this) : selector
+ for (target = e.target; target && target !== this; target = target.parentNode) {
+ for (i = array.length; i--;) {
+ if (array[i] === target) {
+ return fn.apply(target, arguments)
+ }
+ }
+ }
+ }
+ }
+
+ , remove = function (element, typeSpec, fn) {
+ var k, m, type, namespaces, i
+ , rm = removeListener
+ , isString = typeSpec && typeof typeSpec === 'string'
+
+ if (isString && typeSpec.indexOf(' ') > 0) {
+ // remove(el, 't1 t2 t3', fn) or remove(el, 't1 t2 t3')
+ typeSpec = typeSpec.split(' ')
+ for (i = typeSpec.length; i--;)
+ remove(element, typeSpec[i], fn)
+ return element
+ }
+ type = isString && typeSpec.replace(nameRegex, '')
+ if (type && customEvents[type])
+ type = customEvents[type].type
+ if (!typeSpec || isString) {
+ // remove(el) or remove(el, t1.ns) or remove(el, .ns) or remove(el, .ns1.ns2.ns3)
+ if (namespaces = isString && typeSpec.replace(namespaceRegex, ''))
+ namespaces = namespaces.split('.')
+ rm(element, type, fn, namespaces)
+ } else if (typeof typeSpec === 'function') {
+ // remove(el, fn)
+ rm(element, null, typeSpec)
+ } else {
+ // remove(el, { t1: fn1, t2, fn2 })
+ for (k in typeSpec) {
+ if (typeSpec.hasOwnProperty(k))
+ remove(element, k, typeSpec[k])
+ }
+ }
+ return element
+ }
+
+ , add = function (element, events, fn, delfn, $) {
+ var type, types, i, args
+ , originalFn = fn
+ , isDel = fn && typeof fn === 'string'
+
+ if (events && !fn && typeof events === 'object') {
+ for (type in events) {
+ if (events.hasOwnProperty(type))
+ add.apply(this, [ element, type, events[type] ])
+ }
+ } else {
+ args = arguments.length > 3 ? slice.call(arguments, 3) : []
+ types = (isDel ? fn : events).split(' ')
+ isDel && (fn = del(events, (originalFn = delfn), $)) && (args = slice.call(args, 1))
+ // special case for one()
+ this === ONE && (fn = once(remove, element, events, fn, originalFn))
+ for (i = types.length; i--;) addListener(element, types[i], fn, originalFn, args)
+ }
+ return element
+ }
+
+ , one = function () {
+ return add.apply(ONE, arguments)
+ }
+
+ , fireListener = W3C_MODEL ? function (isNative, type, element) {
+ var evt = doc.createEvent(isNative ? 'HTMLEvents' : 'UIEvents')
+ evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1)
+ element.dispatchEvent(evt)
+ } : function (isNative, type, element) {
+ element = targetElement(element, isNative)
+ // if not-native then we're using onpropertychange so we just increment a custom property
+ isNative ? element.fireEvent('on' + type, doc.createEventObject()) : element['_on' + type]++
+ }
+
+ , fire = function (element, type, args) {
+ var i, j, l, names, handlers
+ , types = type.split(' ')
+
+ for (i = types.length; i--;) {
+ type = types[i].replace(nameRegex, '')
+ if (names = types[i].replace(namespaceRegex, ''))
+ names = names.split('.')
+ if (!names && !args && element[eventSupport]) {
+ fireListener(nativeEvents[type], type, element)
+ } else {
+ // non-native event, either because of a namespace, arguments or a non DOM element
+ // iterate over all listeners and manually 'fire'
+ handlers = registry.get(element, type)
+ args = [false].concat(args)
+ for (j = 0, l = handlers.length; j < l; j++) {
+ if (handlers[j].inNamespaces(names))
+ handlers[j].handler.apply(element, args)
+ }
+ }
+ }
+ return element
+ }
+
+ , clone = function (element, from, type) {
+ var i = 0
+ , handlers = registry.get(from, type)
+ , l = handlers.length
+
+ for (;i < l; i++)
+ handlers[i].original && add(element, handlers[i].type, handlers[i].original)
+ return element
+ }
+
+ , bean = {
+ add: add
+ , one: one
+ , remove: remove
+ , clone: clone
+ , fire: fire
+ , noConflict: function () {
+ context[name] = old
+ return this
+ }
+ }
+
+ if (win[attachEvent]) {
+ // for IE, clean up on unload to avoid leaks
+ var cleanup = function () {
+ var i, entries = registry.entries()
+ for (i in entries) {
+ if (entries[i].type && entries[i].type !== 'unload')
+ remove(entries[i].element, entries[i].type)
+ }
+ win[detachEvent]('onunload', cleanup)
+ win.CollectGarbage && win.CollectGarbage()
+ }
+ win[attachEvent]('onunload', cleanup)
+ }
+
+ return bean
+});
+// Underscore.js 1.1.7
+// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
+// Underscore is freely distributable under the MIT license.
+// Portions of Underscore are inspired or borrowed from Prototype,
+// Oliver Steele's Functional, and John Resig's Micro-Templating.
+// For all details and documentation:
+// http://documentcloud.github.com/underscore
+
+(function() {
+
+ // Baseline setup
+ // --------------
+
+ // Establish the root object, `window` in the browser, or `global` on the server.
+ var root = this;
+
+ // Save the previous value of the `_` variable.
+ var previousUnderscore = root._;
+
+ // Establish the object that gets returned to break out of a loop iteration.
+ var breaker = {};
+
+ // Save bytes in the minified (but not gzipped) version:
+ var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
+
+ // Create quick reference variables for speed access to core prototypes.
+ var slice = ArrayProto.slice,
+ unshift = ArrayProto.unshift,
+ toString = ObjProto.toString,
+ hasOwnProperty = ObjProto.hasOwnProperty;
+
+ // All **ECMAScript 5** native function implementations that we hope to use
+ // are declared here.
+ var
+ nativeForEach = ArrayProto.forEach,
+ nativeMap = ArrayProto.map,
+ nativeReduce = ArrayProto.reduce,
+ nativeReduceRight = ArrayProto.reduceRight,
+ nativeFilter = ArrayProto.filter,
+ nativeEvery = ArrayProto.every,
+ nativeSome = ArrayProto.some,
+ nativeIndexOf = ArrayProto.indexOf,
+ nativeLastIndexOf = ArrayProto.lastIndexOf,
+ nativeIsArray = Array.isArray,
+ nativeKeys = Object.keys,
+ nativeBind = FuncProto.bind;
+
+ // Create a safe reference to the Underscore object for use below.
+ var _ = function(obj) { return new wrapper(obj); };
+
+ // Export the Underscore object for **CommonJS**, with backwards-compatibility
+ // for the old `require()` API. If we're not in CommonJS, add `_` to the
+ // global object.
+ if (typeof module !== 'undefined' && module.exports) {
+ module.exports = _;
+ _._ = _;
+ } else {
+ // Exported as a string, for Closure Compiler "advanced" mode.
+ root['_'] = _;
+ }
+
+ // Current version.
+ _.VERSION = '1.1.7';
+
+ // Collection Functions
+ // --------------------
+
+ // The cornerstone, an `each` implementation, aka `forEach`.
+ // Handles objects with the built-in `forEach`, arrays, and raw objects.
+ // Delegates to **ECMAScript 5**'s native `forEach` if available.
+ var each = _.each = _.forEach = function(obj, iterator, context) {
+ if (obj == null) return;
+ if (nativeForEach && obj.forEach === nativeForEach) {
+ obj.forEach(iterator, context);
+ } else if (obj.length === +obj.length) {
+ for (var i = 0, l = obj.length; i < l; i++) {
+ if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
+ }
+ } else {
+ for (var key in obj) {
+ if (hasOwnProperty.call(obj, key)) {
+ if (iterator.call(context, obj[key], key, obj) === breaker) return;
+ }
+ }
+ }
+ };
+
+ // Return the results of applying the iterator to each element.
+ // Delegates to **ECMAScript 5**'s native `map` if available.
+ _.map = function(obj, iterator, context) {
+ var results = [];
+ if (obj == null) return results;
+ if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
+ each(obj, function(value, index, list) {
+ results[results.length] = iterator.call(context, value, index, list);
+ });
+ return results;
+ };
+
+ // **Reduce** builds up a single result from a list of values, aka `inject`,
+ // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
+ _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
+ var initial = memo !== void 0;
+ if (obj == null) obj = [];
+ if (nativeReduce && obj.reduce === nativeReduce) {
+ if (context) iterator = _.bind(iterator, context);
+ return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
+ }
+ each(obj, function(value, index, list) {
+ if (!initial) {
+ memo = value;
+ initial = true;
+ } else {
+ memo = iterator.call(context, memo, value, index, list);
+ }
+ });
+ if (!initial) throw new TypeError("Reduce of empty array with no initial value");
+ return memo;
+ };
+
+ // The right-associative version of reduce, also known as `foldr`.
+ // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
+ _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
+ if (obj == null) obj = [];
+ if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
+ if (context) iterator = _.bind(iterator, context);
+ return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
+ }
+ var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse();
+ return _.reduce(reversed, iterator, memo, context);
+ };
+
+ // Return the first value which passes a truth test. Aliased as `detect`.
+ _.find = _.detect = function(obj, iterator, context) {
+ var result;
+ any(obj, function(value, index, list) {
+ if (iterator.call(context, value, index, list)) {
+ result = value;
+ return true;
+ }
+ });
+ return result;
+ };
+
+ // Return all the elements that pass a truth test.
+ // Delegates to **ECMAScript 5**'s native `filter` if available.
+ // Aliased as `select`.
+ _.filter = _.select = function(obj, iterator, context) {
+ var results = [];
+ if (obj == null) return results;
+ if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
+ each(obj, function(value, index, list) {
+ if (iterator.call(context, value, index, list)) results[results.length] = value;
+ });
+ return results;
+ };
+
+ // Return all the elements for which a truth test fails.
+ _.reject = function(obj, iterator, context) {
+ var results = [];
+ if (obj == null) return results;
+ each(obj, function(value, index, list) {
+ if (!iterator.call(context, value, index, list)) results[results.length] = value;
+ });
+ return results;
+ };
+
+ // Determine whether all of the elements match a truth test.
+ // Delegates to **ECMAScript 5**'s native `every` if available.
+ // Aliased as `all`.
+ _.every = _.all = function(obj, iterator, context) {
+ var result = true;
+ if (obj == null) return result;
+ if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
+ each(obj, function(value, index, list) {
+ if (!(result = result && iterator.call(context, value, index, list))) return breaker;
+ });
+ return result;
+ };
+
+ // Determine if at least one element in the object matches a truth test.
+ // Delegates to **ECMAScript 5**'s native `some` if available.
+ // Aliased as `any`.
+ var any = _.some = _.any = function(obj, iterator, context) {
+ iterator = iterator || _.identity;
+ var result = false;
+ if (obj == null) return result;
+ if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
+ each(obj, function(value, index, list) {
+ if (result |= iterator.call(context, value, index, list)) return breaker;
+ });
+ return !!result;
+ };
+
+ // Determine if a given value is included in the array or object using `===`.
+ // Aliased as `contains`.
+ _.include = _.contains = function(obj, target) {
+ var found = false;
+ if (obj == null) return found;
+ if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
+ any(obj, function(value) {
+ if (found = value === target) return true;
+ });
+ return found;
+ };
+
+ // Invoke a method (with arguments) on every item in a collection.
+ _.invoke = function(obj, method) {
+ var args = slice.call(arguments, 2);
+ return _.map(obj, function(value) {
+ return (method.call ? method || value : value[method]).apply(value, args);
+ });
+ };
+
+ // Convenience version of a common use case of `map`: fetching a property.
+ _.pluck = function(obj, key) {
+ return _.map(obj, function(value){ return value[key]; });
+ };
+
+ // Return the maximum element or (element-based computation).
+ _.max = function(obj, iterator, context) {
+ if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
+ var result = {computed : -Infinity};
+ each(obj, function(value, index, list) {
+ var computed = iterator ? iterator.call(context, value, index, list) : value;
+ computed >= result.computed && (result = {value : value, computed : computed});
+ });
+ return result.value;
+ };
+
+ // Return the minimum element (or element-based computation).
+ _.min = function(obj, iterator, context) {
+ if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
+ var result = {computed : Infinity};
+ each(obj, function(value, index, list) {
+ var computed = iterator ? iterator.call(context, value, index, list) : value;
+ computed < result.computed && (result = {value : value, computed : computed});
+ });
+ return result.value;
+ };
+
+ // Sort the object's values by a criterion produced by an iterator.
+ _.sortBy = function(obj, iterator, context) {
+ return _.pluck(_.map(obj, function(value, index, list) {
+ return {
+ value : value,
+ criteria : iterator.call(context, value, index, list)
+ };
+ }).sort(function(left, right) {
+ var a = left.criteria, b = right.criteria;
+ return a < b ? -1 : a > b ? 1 : 0;
+ }), 'value');
+ };
+
+ // Groups the object's values by a criterion produced by an iterator
+ _.groupBy = function(obj, iterator) {
+ var result = {};
+ each(obj, function(value, index) {
+ var key = iterator(value, index);
+ (result[key] || (result[key] = [])).push(value);
+ });
+ return result;
+ };
+
+ // Use a comparator function to figure out at what index an object should
+ // be inserted so as to maintain order. Uses binary search.
+ _.sortedIndex = function(array, obj, iterator) {
+ iterator || (iterator = _.identity);
+ var low = 0, high = array.length;
+ while (low < high) {
+ var mid = (low + high) >> 1;
+ iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
+ }
+ return low;
+ };
+
+ // Safely convert anything iterable into a real, live array.
+ _.toArray = function(iterable) {
+ if (!iterable) return [];
+ if (iterable.toArray) return iterable.toArray();
+ if (_.isArray(iterable)) return slice.call(iterable);
+ if (_.isArguments(iterable)) return slice.call(iterable);
+ return _.values(iterable);
+ };
+
+ // Return the number of elements in an object.
+ _.size = function(obj) {
+ return _.toArray(obj).length;
+ };
+
+ // Array Functions
+ // ---------------
+
+ // Get the first element of an array. Passing **n** will return the first N
+ // values in the array. Aliased as `head`. The **guard** check allows it to work
+ // with `_.map`.
+ _.first = _.head = function(array, n, guard) {
+ return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
+ };
+
+ // Returns everything but the first entry of the array. Aliased as `tail`.
+ // Especially useful on the arguments object. Passing an **index** will return
+ // the rest of the values in the array from that index onward. The **guard**
+ // check allows it to work with `_.map`.
+ _.rest = _.tail = function(array, index, guard) {
+ return slice.call(array, (index == null) || guard ? 1 : index);
+ };
+
+ // Get the last element of an array.
+ _.last = function(array) {
+ return array[array.length - 1];
+ };
+
+ // Trim out all falsy values from an array.
+ _.compact = function(array) {
+ return _.filter(array, function(value){ return !!value; });
+ };
+
+ // Return a completely flattened version of an array.
+ _.flatten = function(array) {
+ return _.reduce(array, function(memo, value) {
+ if (_.isArray(value)) return memo.concat(_.flatten(value));
+ memo[memo.length] = value;
+ return memo;
+ }, []);
+ };
+
+ // Return a version of the array that does not contain the specified value(s).
+ _.without = function(array) {
+ return _.difference(array, slice.call(arguments, 1));
+ };
+
+ // Produce a duplicate-free version of the array. If the array has already
+ // been sorted, you have the option of using a faster algorithm.
+ // Aliased as `unique`.
+ _.uniq = _.unique = function(array, isSorted) {
+ return _.reduce(array, function(memo, el, i) {
+ if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el;
+ return memo;
+ }, []);
+ };
+
+ // Produce an array that contains the union: each distinct element from all of
+ // the passed-in arrays.
+ _.union = function() {
+ return _.uniq(_.flatten(arguments));
+ };
+
+ // Produce an array that contains every item shared between all the
+ // passed-in arrays. (Aliased as "intersect" for back-compat.)
+ _.intersection = _.intersect = function(array) {
+ var rest = slice.call(arguments, 1);
+ return _.filter(_.uniq(array), function(item) {
+ return _.every(rest, function(other) {
+ return _.indexOf(other, item) >= 0;
+ });
+ });
+ };
+
+ // Take the difference between one array and another.
+ // Only the elements present in just the first array will remain.
+ _.difference = function(array, other) {
+ return _.filter(array, function(value){ return !_.include(other, value); });
+ };
+
+ // Zip together multiple lists into a single array -- elements that share
+ // an index go together.
+ _.zip = function() {
+ var args = slice.call(arguments);
+ var length = _.max(_.pluck(args, 'length'));
+ var results = new Array(length);
+ for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
+ return results;
+ };
+
+ // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
+ // we need this function. Return the position of the first occurrence of an
+ // item in an array, or -1 if the item is not included in the array.
+ // Delegates to **ECMAScript 5**'s native `indexOf` if available.
+ // If the array is large and already in sort order, pass `true`
+ // for **isSorted** to use binary search.
+ _.indexOf = function(array, item, isSorted) {
+ if (array == null) return -1;
+ var i, l;
+ if (isSorted) {
+ i = _.sortedIndex(array, item);
+ return array[i] === item ? i : -1;
+ }
+ if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
+ for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
+ return -1;
+ };
+
+
+ // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
+ _.lastIndexOf = function(array, item) {
+ if (array == null) return -1;
+ if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
+ var i = array.length;
+ while (i--) if (array[i] === item) return i;
+ return -1;
+ };
+
+ // Generate an integer Array containing an arithmetic progression. A port of
+ // the native Python `range()` function. See
+ // [the Python documentation](http://docs.python.org/library/functions.html#range).
+ _.range = function(start, stop, step) {
+ if (arguments.length <= 1) {
+ stop = start || 0;
+ start = 0;
+ }
+ step = arguments[2] || 1;
+
+ var len = Math.max(Math.ceil((stop - start) / step), 0);
+ var idx = 0;
+ var range = new Array(len);
+
+ while(idx < len) {
+ range[idx++] = start;
+ start += step;
+ }
+
+ return range;
+ };
+
+ // Function (ahem) Functions
+ // ------------------
+
+ // Create a function bound to a given object (assigning `this`, and arguments,
+ // optionally). Binding with arguments is also known as `curry`.
+ // Delegates to **ECMAScript 5**'s native `Function.bind` if available.
+ // We check for `func.bind` first, to fail fast when `func` is undefined.
+ _.bind = function(func, obj) {
+ if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
+ var args = slice.call(arguments, 2);
+ return function() {
+ return func.apply(obj, args.concat(slice.call(arguments)));
+ };
+ };
+
+ // Bind all of an object's methods to that object. Useful for ensuring that
+ // all callbacks defined on an object belong to it.
+ _.bindAll = function(obj) {
+ var funcs = slice.call(arguments, 1);
+ if (funcs.length == 0) funcs = _.functions(obj);
+ each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
+ return obj;
+ };
+
+ // Memoize an expensive function by storing its results.
+ _.memoize = function(func, hasher) {
+ var memo = {};
+ hasher || (hasher = _.identity);
+ return function() {
+ var key = hasher.apply(this, arguments);
+ return hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
+ };
+ };
+
+ // Delays a function for the given number of milliseconds, and then calls
+ // it with the arguments supplied.
+ _.delay = function(func, wait) {
+ var args = slice.call(arguments, 2);
+ return setTimeout(function(){ return func.apply(func, args); }, wait);
+ };
+
+ // Defers a function, scheduling it to run after the current call stack has
+ // cleared.
+ _.defer = function(func) {
+ return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
+ };
+
+ // Internal function used to implement `_.throttle` and `_.debounce`.
+ var limit = function(func, wait, debounce) {
+ var timeout;
+ return function() {
+ var context = this, args = arguments;
+ var throttler = function() {
+ timeout = null;
+ func.apply(context, args);
+ };
+ if (debounce) clearTimeout(timeout);
+ if (debounce || !timeout) timeout = setTimeout(throttler, wait);
+ };
+ };
+
+ // Returns a function, that, when invoked, will only be triggered at most once
+ // during a given window of time.
+ _.throttle = function(func, wait) {
+ return limit(func, wait, false);
+ };
+
+ // Returns a function, that, as long as it continues to be invoked, will not
+ // be triggered. The function will be called after it stops being called for
+ // N milliseconds.
+ _.debounce = function(func, wait) {
+ return limit(func, wait, true);
+ };
+
+ // Returns a function that will be executed at most one time, no matter how
+ // often you call it. Useful for lazy initialization.
+ _.once = function(func) {
+ var ran = false, memo;
+ return function() {
+ if (ran) return memo;
+ ran = true;
+ return memo = func.apply(this, arguments);
+ };
+ };
+
+ // Returns the first function passed as an argument to the second,
+ // allowing you to adjust arguments, run code before and after, and
+ // conditionally execute the original function.
+ _.wrap = function(func, wrapper) {
+ return function() {
+ var args = [func].concat(slice.call(arguments));
+ return wrapper.apply(this, args);
+ };
+ };
+
+ // Returns a function that is the composition of a list of functions, each
+ // consuming the return value of the function that follows.
+ _.compose = function() {
+ var funcs = slice.call(arguments);
+ return function() {
+ var args = slice.call(arguments);
+ for (var i = funcs.length - 1; i >= 0; i--) {
+ args = [funcs[i].apply(this, args)];
+ }
+ return args[0];
+ };
+ };
+
+ // Returns a function that will only be executed after being called N times.
+ _.after = function(times, func) {
+ return function() {
+ if (--times < 1) { return func.apply(this, arguments); }
+ };
+ };
+
+
+ // Object Functions
+ // ----------------
+
+ // Retrieve the names of an object's properties.
+ // Delegates to **ECMAScript 5**'s native `Object.keys`
+ _.keys = nativeKeys || function(obj) {
+ if (obj !== Object(obj)) throw new TypeError('Invalid object');
+ var keys = [];
+ for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key;
+ return keys;
+ };
+
+ // Retrieve the values of an object's properties.
+ _.values = function(obj) {
+ return _.map(obj, _.identity);
+ };
+
+ // Return a sorted list of the function names available on the object.
+ // Aliased as `methods`
+ _.functions = _.methods = function(obj) {
+ var names = [];
+ for (var key in obj) {
+ if (_.isFunction(obj[key])) names.push(key);
+ }
+ return names.sort();
+ };
+
+ // Extend a given object with all the properties in passed-in object(s).
+ _.extend = function(obj) {
+ each(slice.call(arguments, 1), function(source) {
+ for (var prop in source) {
+ if (source[prop] !== void 0) obj[prop] = source[prop];
+ }
+ });
+ return obj;
+ };
+
+ // Fill in a given object with default properties.
+ _.defaults = function(obj) {
+ each(slice.call(arguments, 1), function(source) {
+ for (var prop in source) {
+ if (obj[prop] == null) obj[prop] = source[prop];
+ }
+ });
+ return obj;
+ };
+
+ // Create a (shallow-cloned) duplicate of an object.
+ _.clone = function(obj) {
+ return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
+ };
+
+ // Invokes interceptor with the obj, and then returns obj.
+ // The primary purpose of this method is to "tap into" a method chain, in
+ // order to perform operations on intermediate results within the chain.
+ _.tap = function(obj, interceptor) {
+ interceptor(obj);
+ return obj;
+ };
+
+ // Perform a deep comparison to check if two objects are equal.
+ _.isEqual = function(a, b) {
+ // Check object identity.
+ if (a === b) return true;
+ // Different types?
+ var atype = typeof(a), btype = typeof(b);
+ if (atype != btype) return false;
+ // Basic equality test (watch out for coercions).
+ if (a == b) return true;
+ // One is falsy and the other truthy.
+ if ((!a && b) || (a && !b)) return false;
+ // Unwrap any wrapped objects.
+ if (a._chain) a = a._wrapped;
+ if (b._chain) b = b._wrapped;
+ // One of them implements an isEqual()?
+ if (a.isEqual) return a.isEqual(b);
+ if (b.isEqual) return b.isEqual(a);
+ // Check dates' integer values.
+ if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime();
+ // Both are NaN?
+ if (_.isNaN(a) && _.isNaN(b)) return false;
+ // Compare regular expressions.
+ if (_.isRegExp(a) && _.isRegExp(b))
+ return a.source === b.source &&
+ a.global === b.global &&
+ a.ignoreCase === b.ignoreCase &&
+ a.multiline === b.multiline;
+ // If a is not an object by this point, we can't handle it.
+ if (atype !== 'object') return false;
+ // Check for different array lengths before comparing contents.
+ if (a.length && (a.length !== b.length)) return false;
+ // Nothing else worked, deep compare the contents.
+ var aKeys = _.keys(a), bKeys = _.keys(b);
+ // Different object sizes?
+ if (aKeys.length != bKeys.length) return false;
+ // Recursive comparison of contents.
+ for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false;
+ return true;
+ };
+
+ // Is a given array or object empty?
+ _.isEmpty = function(obj) {
+ if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
+ for (var key in obj) if (hasOwnProperty.call(obj, key)) return false;
+ return true;
+ };
+
+ // Is a given value a DOM element?
+ _.isElement = function(obj) {
+ return !!(obj && obj.nodeType == 1);
+ };
+
+ // Is a given value an array?
+ // Delegates to ECMA5's native Array.isArray
+ _.isArray = nativeIsArray || function(obj) {
+ return toString.call(obj) === '[object Array]';
+ };
+
+ // Is a given variable an object?
+ _.isObject = function(obj) {
+ return obj === Object(obj);
+ };
+
+ // Is a given variable an arguments object?
+ _.isArguments = function(obj) {
+ return !!(obj && hasOwnProperty.call(obj, 'callee'));
+ };
+
+ // Is a given value a function?
+ _.isFunction = function(obj) {
+ return !!(obj && obj.constructor && obj.call && obj.apply);
+ };
+
+ // Is a given value a string?
+ _.isString = function(obj) {
+ return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
+ };
+
+ // Is a given value a number?
+ _.isNumber = function(obj) {
+ return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed));
+ };
+
+ // Is the given value `NaN`? `NaN` happens to be the only value in JavaScript
+ // that does not equal itself.
+ _.isNaN = function(obj) {
+ return obj !== obj;
+ };
+
+ // Is a given value a boolean?
+ _.isBoolean = function(obj) {
+ return obj === true || obj === false;
+ };
+
+ // Is a given value a date?
+ _.isDate = function(obj) {
+ return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear);
+ };
+
+ // Is the given value a regular expression?
+ _.isRegExp = function(obj) {
+ return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false));
+ };
+
+ // Is a given value equal to null?
+ _.isNull = function(obj) {
+ return obj === null;
+ };
+
+ // Is a given variable undefined?
+ _.isUndefined = function(obj) {
+ return obj === void 0;
+ };
+
+ // Utility Functions
+ // -----------------
+
+ // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
+ // previous owner. Returns a reference to the Underscore object.
+ _.noConflict = function() {
+ root._ = previousUnderscore;
+ return this;
+ };
+
+ // Keep the identity function around for default iterators.
+ _.identity = function(value) {
+ return value;
+ };
+
+ // Run a function **n** times.
+ _.times = function (n, iterator, context) {
+ for (var i = 0; i < n; i++) iterator.call(context, i);
+ };
+
+ // Add your own custom functions to the Underscore object, ensuring that
+ // they're correctly added to the OOP wrapper as well.
+ _.mixin = function(obj) {
+ each(_.functions(obj), function(name){
+ addToWrapper(name, _[name] = obj[name]);
+ });
+ };
+
+ // Generate a unique integer id (unique within the entire client session).
+ // Useful for temporary DOM ids.
+ var idCounter = 0;
+ _.uniqueId = function(prefix) {
+ var id = idCounter++;
+ return prefix ? prefix + id : id;
+ };
+
+ // By default, Underscore uses ERB-style template delimiters, change the
+ // following template settings to use alternative delimiters.
+ _.templateSettings = {
+ evaluate : /<%([\s\S]+?)%>/g,
+ interpolate : /<%=([\s\S]+?)%>/g
+ };
+
+ // JavaScript micro-templating, similar to John Resig's implementation.
+ // Underscore templating handles arbitrary delimiters, preserves whitespace,
+ // and correctly escapes quotes within interpolated code.
+ _.template = function(str, data) {
+ var c = _.templateSettings;
+ var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
+ 'with(obj||{}){__p.push(\'' +
+ str.replace(/\\/g, '\\\\')
+ .replace(/'/g, "\\'")
+ .replace(c.interpolate, function(match, code) {
+ return "'," + code.replace(/\\'/g, "'") + ",'";
+ })
+ .replace(c.evaluate || null, function(match, code) {
+ return "');" + code.replace(/\\'/g, "'")
+ .replace(/[\r\n\t]/g, ' ') + "__p.push('";
+ })
+ .replace(/\r/g, '\\r')
+ .replace(/\n/g, '\\n')
+ .replace(/\t/g, '\\t')
+ + "');}return __p.join('');";
+ var func = new Function('obj', tmpl);
+ return data ? func(data) : func;
+ };
+
+ // The OOP Wrapper
+ // ---------------
+
+ // If Underscore is called as a function, it returns a wrapped object that
+ // can be used OO-style. This wrapper holds altered versions of all the
+ // underscore functions. Wrapped objects may be chained.
+ var wrapper = function(obj) { this._wrapped = obj; };
+
+ // Expose `wrapper.prototype` as `_.prototype`
+ _.prototype = wrapper.prototype;
+
+ // Helper function to continue chaining intermediate results.
+ var result = function(obj, chain) {
+ return chain ? _(obj).chain() : obj;
+ };
+
+ // A method to easily add functions to the OOP wrapper.
+ var addToWrapper = function(name, func) {
+ wrapper.prototype[name] = function() {
+ var args = slice.call(arguments);
+ unshift.call(args, this._wrapped);
+ return result(func.apply(_, args), this._chain);
+ };
+ };
+
+ // Add all of the Underscore functions to the wrapper object.
+ _.mixin(_);
+
+ // Add all mutator Array functions to the wrapper.
+ each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
+ var method = ArrayProto[name];
+ wrapper.prototype[name] = function() {
+ method.apply(this._wrapped, arguments);
+ return result(this._wrapped, this._chain);
+ };
+ });
+
+ // Add all accessor Array functions to the wrapper.
+ each(['concat', 'join', 'slice'], function(name) {
+ var method = ArrayProto[name];
+ wrapper.prototype[name] = function() {
+ return result(method.apply(this._wrapped, arguments), this._chain);
+ };
+ });
+
+ // Start chaining a wrapped Underscore object.
+ wrapper.prototype.chain = function() {
+ this._chain = true;
+ return this;
+ };
+
+ // Extracts the result from a wrapped and chained object.
+ wrapper.prototype.value = function() {
+ return this._wrapped;
+ };
+
+})();
+/**
+ * Flotr2 (c) 2012 Carl Sutherland
+ * MIT License
+ * Special thanks to:
+ * Flotr: http://code.google.com/p/flotr/ (fork)
+ * Flot: https://github.com/flot/flot (original fork)
+ */
+(function () {
+
+var
+ global = this,
+ previousFlotr = this.Flotr,
+ Flotr;
+
+Flotr = {
+ _: _,
+ bean: bean,
+ isIphone: /iphone/i.test(navigator.userAgent),
+ isIE: (navigator.appVersion.indexOf("MSIE") != -1 ? parseFloat(navigator.appVersion.split("MSIE")[1]) : false),
+
+ /**
+ * An object of the registered graph types. Use Flotr.addType(type, object)
+ * to add your own type.
+ */
+ graphTypes: {},
+
+ /**
+ * The list of the registered plugins
+ */
+ plugins: {},
+
+ /**
+ * Can be used to add your own chart type.
+ * @param {String} name - Type of chart, like 'pies', 'bars' etc.
+ * @param {String} graphType - The object containing the basic drawing functions (draw, etc)
+ */
+ addType: function(name, graphType){
+ Flotr.graphTypes[name] = graphType;
+ Flotr.defaultOptions[name] = graphType.options || {};
+ Flotr.defaultOptions.defaultType = Flotr.defaultOptions.defaultType || name;
+ },
+
+ /**
+ * Can be used to add a plugin
+ * @param {String} name - The name of the plugin
+ * @param {String} plugin - The object containing the plugin's data (callbacks, options, function1, function2, ...)
+ */
+ addPlugin: function(name, plugin){
+ Flotr.plugins[name] = plugin;
+ Flotr.defaultOptions[name] = plugin.options || {};
+ },
+
+ /**
+ * Draws the graph. This function is here for backwards compatibility with Flotr version 0.1.0alpha.
+ * You could also draw graphs by directly calling Flotr.Graph(element, data, options).
+ * @param {Element} el - element to insert the graph into
+ * @param {Object} data - an array or object of dataseries
+ * @param {Object} options - an object containing options
+ * @param {Class} _GraphKlass_ - (optional) Class to pass the arguments to, defaults to Flotr.Graph
+ * @return {Object} returns a new graph object and of course draws the graph.
+ */
+ draw: function(el, data, options, GraphKlass){
+ GraphKlass = GraphKlass || Flotr.Graph;
+ return new GraphKlass(el, data, options);
+ },
+
+ /**
+ * Recursively merges two objects.
+ * @param {Object} src - source object (likely the object with the least properties)
+ * @param {Object} dest - destination object (optional, object with the most properties)
+ * @return {Object} recursively merged Object
+ * @TODO See if we can't remove this.
+ */
+ merge: function(src, dest){
+ var i, v, result = dest || {};
+
+ for (i in src) {
+ v = src[i];
+ if (v && typeof(v) === 'object') {
+ if (v.constructor === Array) {
+ result[i] = this._.clone(v);
+ } else if (
+ v.constructor !== RegExp &&
+ !this._.isElement(v) &&
+ !v.jquery
+ ) {
+ result[i] = Flotr.merge(v, (dest ? dest[i] : undefined));
+ } else {
+ result[i] = v;
+ }
+ } else {
+ result[i] = v;
+ }
+ }
+
+ return result;
+ },
+
+ /**
+ * Recursively clones an object.
+ * @param {Object} object - The object to clone
+ * @return {Object} the clone
+ * @TODO See if we can't remove this.
+ */
+ clone: function(object){
+ return Flotr.merge(object, {});
+ },
+
+ /**
+ * Function calculates the ticksize and returns it.
+ * @param {Integer} noTicks - number of ticks
+ * @param {Integer} min - lower bound integer value for the current axis
+ * @param {Integer} max - upper bound integer value for the current axis
+ * @param {Integer} decimals - number of decimals for the ticks
+ * @return {Integer} returns the ticksize in pixels
+ */
+ getTickSize: function(noTicks, min, max, decimals){
+ var delta = (max - min) / noTicks,
+ magn = Flotr.getMagnitude(delta),
+ tickSize = 10,
+ norm = delta / magn; // Norm is between 1.0 and 10.0.
+
+ if(norm < 1.5) tickSize = 1;
+ else if(norm < 2.25) tickSize = 2;
+ else if(norm < 3) tickSize = ((decimals === 0) ? 2 : 2.5);
+ else if(norm < 7.5) tickSize = 5;
+
+ return tickSize * magn;
+ },
+
+ /**
+ * Default tick formatter.
+ * @param {String, Integer} val - tick value integer
+ * @param {Object} axisOpts - the axis' options
+ * @return {String} formatted tick string
+ */
+ defaultTickFormatter: function(val, axisOpts){
+ return val+'';
+ },
+
+ /**
+ * Formats the mouse tracker values.
+ * @param {Object} obj - Track value Object {x:..,y:..}
+ * @return {String} Formatted track string
+ */
+ defaultTrackFormatter: function(obj){
+ return '('+obj.x+', '+obj.y+')';
+ },
+
+ /**
+ * Utility function to convert file size values in bytes to kB, MB, ...
+ * @param value {Number} - The value to convert
+ * @param precision {Number} - The number of digits after the comma (default: 2)
+ * @param base {Number} - The base (default: 1000)
+ */
+ engineeringNotation: function(value, precision, base){
+ var sizes = ['Y','Z','E','P','T','G','M','k',''],
+ fractionSizes = ['y','z','a','f','p','n','µ','m',''],
+ total = sizes.length;
+
+ base = base || 1000;
+ precision = Math.pow(10, precision || 2);
+
+ if (value === 0) return 0;
+
+ if (value > 1) {
+ while (total-- && (value >= base)) value /= base;
+ }
+ else {
+ sizes = fractionSizes;
+ total = sizes.length;
+ while (total-- && (value < 1)) value *= base;
+ }
+
+ return (Math.round(value * precision) / precision) + sizes[total];
+ },
+
+ /**
+ * Returns the magnitude of the input value.
+ * @param {Integer, Float} x - integer or float value
+ * @return {Integer, Float} returns the magnitude of the input value
+ */
+ getMagnitude: function(x){
+ return Math.pow(10, Math.floor(Math.log(x) / Math.LN10));
+ },
+ toPixel: function(val){
+ return Math.floor(val)+0.5;//((val-Math.round(val) < 0.4) ? (Math.floor(val)-0.5) : val);
+ },
+ toRad: function(angle){
+ return -angle * (Math.PI/180);
+ },
+ floorInBase: function(n, base) {
+ return base * Math.floor(n / base);
+ },
+ drawText: function(ctx, text, x, y, style) {
+ if (!ctx.fillText) {
+ ctx.drawText(text, x, y, style);
+ return;
+ }
+
+ style = this._.extend({
+ size: Flotr.defaultOptions.fontSize,
+ color: '#000000',
+ textAlign: 'left',
+ textBaseline: 'bottom',
+ weight: 1,
+ angle: 0
+ }, style);
+
+ ctx.save();
+ ctx.translate(x, y);
+ ctx.rotate(style.angle);
+ ctx.fillStyle = style.color;
+ ctx.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif";
+ ctx.textAlign = style.textAlign;
+ ctx.textBaseline = style.textBaseline;
+ ctx.fillText(text, 0, 0);
+ ctx.restore();
+ },
+ getBestTextAlign: function(angle, style) {
+ style = style || {textAlign: 'center', textBaseline: 'middle'};
+ angle += Flotr.getTextAngleFromAlign(style);
+
+ if (Math.abs(Math.cos(angle)) > 10e-3)
+ style.textAlign = (Math.cos(angle) > 0 ? 'right' : 'left');
+
+ if (Math.abs(Math.sin(angle)) > 10e-3)
+ style.textBaseline = (Math.sin(angle) > 0 ? 'top' : 'bottom');
+
+ return style;
+ },
+ alignTable: {
+ 'right middle' : 0,
+ 'right top' : Math.PI/4,
+ 'center top' : Math.PI/2,
+ 'left top' : 3*(Math.PI/4),
+ 'left middle' : Math.PI,
+ 'left bottom' : -3*(Math.PI/4),
+ 'center bottom': -Math.PI/2,
+ 'right bottom' : -Math.PI/4,
+ 'center middle': 0
+ },
+ getTextAngleFromAlign: function(style) {
+ return Flotr.alignTable[style.textAlign+' '+style.textBaseline] || 0;
+ },
+ noConflict : function () {
+ global.Flotr = previousFlotr;
+ return this;
+ }
+};
+
+global.Flotr = Flotr;
+
+})();
+
+/**
+ * Flotr Defaults
+ */
+Flotr.defaultOptions = {
+ colors: ['#00A8F0', '#C0D800', '#CB4B4B', '#4DA74D', '#9440ED'], //=> The default colorscheme. When there are > 5 series, additional colors are generated.
+ ieBackgroundColor: '#FFFFFF', // Background color for excanvas clipping
+ title: null, // => The graph's title
+ subtitle: null, // => The graph's subtitle
+ shadowSize: 4, // => size of the 'fake' shadow
+ defaultType: null, // => default series type
+ HtmlText: true, // => wether to draw the text using HTML or on the canvas
+ fontColor: '#545454', // => default font color
+ fontSize: 7.5, // => canvas' text font size
+ resolution: 1, // => resolution of the graph, to have printer-friendly graphs !
+ parseFloat: true, // => whether to preprocess data for floats (ie. if input is string)
+ preventDefault: true, // => preventDefault by default for mobile events. Turn off to enable scroll.
+ xaxis: {
+ ticks: null, // => format: either [1, 3] or [[1, 'a'], 3]
+ minorTicks: null, // => format: either [1, 3] or [[1, 'a'], 3]
+ showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise
+ showMinorLabels: false,// => true to show the axis minor ticks labels, false to hide
+ labelsAngle: 0, // => labels' angle, in degrees
+ title: null, // => axis title
+ titleAngle: 0, // => axis title's angle, in degrees
+ noTicks: 5, // => number of ticks for automagically generated ticks
+ minorTickFreq: null, // => number of minor ticks between major ticks for autogenerated ticks
+ tickFormatter: Flotr.defaultTickFormatter, // => fn: number, Object -> string
+ tickDecimals: null, // => no. of decimals, null means auto
+ min: null, // => min. value to show, null means set automatically
+ max: null, // => max. value to show, null means set automatically
+ autoscale: false, // => Turns autoscaling on with true
+ autoscaleMargin: 0, // => margin in % to add if auto-setting min/max
+ color: null, // => color of the ticks
+ mode: 'normal', // => can be 'time' or 'normal'
+ timeFormat: null,
+ timeMode:'UTC', // => For UTC time ('local' for local time).
+ timeUnit:'millisecond',// => Unit for time (millisecond, second, minute, hour, day, month, year)
+ scaling: 'linear', // => Scaling, can be 'linear' or 'logarithmic'
+ base: Math.E,
+ titleAlign: 'center',
+ margin: true // => Turn off margins with false
+ },
+ x2axis: {},
+ yaxis: {
+ ticks: null, // => format: either [1, 3] or [[1, 'a'], 3]
+ minorTicks: null, // => format: either [1, 3] or [[1, 'a'], 3]
+ showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise
+ showMinorLabels: false,// => true to show the axis minor ticks labels, false to hide
+ labelsAngle: 0, // => labels' angle, in degrees
+ title: null, // => axis title
+ titleAngle: 90, // => axis title's angle, in degrees
+ noTicks: 5, // => number of ticks for automagically generated ticks
+ minorTickFreq: null, // => number of minor ticks between major ticks for autogenerated ticks
+ tickFormatter: Flotr.defaultTickFormatter, // => fn: number, Object -> string
+ tickDecimals: null, // => no. of decimals, null means auto
+ min: null, // => min. value to show, null means set automatically
+ max: null, // => max. value to show, null means set automatically
+ autoscale: false, // => Turns autoscaling on with true
+ autoscaleMargin: 0, // => margin in % to add if auto-setting min/max
+ color: null, // => The color of the ticks
+ scaling: 'linear', // => Scaling, can be 'linear' or 'logarithmic'
+ base: Math.E,
+ titleAlign: 'center',
+ margin: true // => Turn off margins with false
+ },
+ y2axis: {
+ titleAngle: 270
+ },
+ grid: {
+ color: '#545454', // => primary color used for outline and labels
+ backgroundColor: null, // => null for transparent, else color
+ backgroundImage: null, // => background image. String or object with src, left and top
+ watermarkAlpha: 0.4, // =>
+ tickColor: '#DDDDDD', // => color used for the ticks
+ labelMargin: 3, // => margin in pixels
+ verticalLines: true, // => whether to show gridlines in vertical direction
+ minorVerticalLines: null, // => whether to show gridlines for minor ticks in vertical dir.
+ horizontalLines: true, // => whether to show gridlines in horizontal direction
+ minorHorizontalLines: null, // => whether to show gridlines for minor ticks in horizontal dir.
+ outlineWidth: 1, // => width of the grid outline/border in pixels
+ outline : 'nsew', // => walls of the outline to display
+ circular: false // => if set to true, the grid will be circular, must be used when radars are drawn
+ },
+ mouse: {
+ track: false, // => true to track the mouse, no tracking otherwise
+ trackAll: false,
+ position: 'se', // => position of the value box (default south-east). False disables.
+ relative: false, // => next to the mouse cursor
+ trackFormatter: Flotr.defaultTrackFormatter, // => formats the values in the value box
+ margin: 5, // => margin in pixels of the valuebox
+ lineColor: '#FF3F19', // => line color of points that are drawn when mouse comes near a value of a series
+ trackDecimals: 1, // => decimals for the track values
+ sensibility: 2, // => the lower this number, the more precise you have to aim to show a value
+ trackY: true, // => whether or not to track the mouse in the y axis
+ radius: 3, // => radius of the track point
+ fillColor: null, // => color to fill our select bar with only applies to bar and similar graphs (only bars for now)
+ fillOpacity: 0.4 // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
+ }
+};
+
+/**
+ * Flotr Color
+ */
+
+(function () {
+
+var
+ _ = Flotr._;
+
+// Constructor
+function Color (r, g, b, a) {
+ this.rgba = ['r','g','b','a'];
+ var x = 4;
+ while(-1<--x){
+ this[this.rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0);
+ }
+ this.normalize();
+}
+
+// Constants
+var COLOR_NAMES = {
+ aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],
+ brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],
+ darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],
+ darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],
+ darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],
+ khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],
+ lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],
+ maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],
+ violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]
+};
+
+Color.prototype = {
+ scale: function(rf, gf, bf, af){
+ var x = 4;
+ while (-1 < --x) {
+ if (!_.isUndefined(arguments[x])) this[this.rgba[x]] *= arguments[x];
+ }
+ return this.normalize();
+ },
+ alpha: function(alpha) {
+ if (!_.isUndefined(alpha) && !_.isNull(alpha)) {
+ this.a = alpha;
+ }
+ return this.normalize();
+ },
+ clone: function(){
+ return new Color(this.r, this.b, this.g, this.a);
+ },
+ limit: function(val,minVal,maxVal){
+ return Math.max(Math.min(val, maxVal), minVal);
+ },
+ normalize: function(){
+ var limit = this.limit;
+ this.r = limit(parseInt(this.r, 10), 0, 255);
+ this.g = limit(parseInt(this.g, 10), 0, 255);
+ this.b = limit(parseInt(this.b, 10), 0, 255);
+ this.a = limit(this.a, 0, 1);
+ return this;
+ },
+ distance: function(color){
+ if (!color) return;
+ color = new Color.parse(color);
+ var dist = 0, x = 3;
+ while(-1<--x){
+ dist += Math.abs(this[this.rgba[x]] - color[this.rgba[x]]);
+ }
+ return dist;
+ },
+ toString: function(){
+ return (this.a >= 1.0) ? 'rgb('+[this.r,this.g,this.b].join(',')+')' : 'rgba('+[this.r,this.g,this.b,this.a].join(',')+')';
+ },
+ contrast: function () {
+ var
+ test = 1 - ( 0.299 * this.r + 0.587 * this.g + 0.114 * this.b) / 255;
+ return (test < 0.5 ? '#000000' : '#ffffff');
+ }
+};
+
+_.extend(Color, {
+ /**
+ * Parses a color string and returns a corresponding Color.
+ * The different tests are in order of probability to improve speed.
+ * @param {String, Color} str - string thats representing a color
+ * @return {Color} returns a Color object or false
+ */
+ parse: function(color){
+ if (color instanceof Color) return color;
+
+ var result;
+
+ // #a0b1c2
+ if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)))
+ return new Color(parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16));
+
+ // rgb(num,num,num)
+ if((result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)))
+ return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10));
+
+ // #fff
+ if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)))
+ return new Color(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16));
+
+ // rgba(num,num,num,num)
+ if((result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(color)))
+ return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10), parseFloat(result[4]));
+
+ // rgb(num%,num%,num%)
+ if((result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)))
+ return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55);
+
+ // rgba(num%,num%,num%,num)
+ if((result = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(color)))
+ return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4]));
+
+ // Otherwise, we're most likely dealing with a named color.
+ var name = (color+'').replace(/^\s*([\S\s]*?)\s*$/, '$1').toLowerCase();
+ if(name == 'transparent'){
+ return new Color(255, 255, 255, 0);
+ }
+ return (result = COLOR_NAMES[name]) ? new Color(result[0], result[1], result[2]) : new Color(0, 0, 0, 0);
+ },
+
+ /**
+ * Process color and options into color style.
+ */
+ processColor: function(color, options) {
+
+ var opacity = options.opacity;
+ if (!color) return 'rgba(0, 0, 0, 0)';
+ if (color instanceof Color) return color.alpha(opacity).toString();
+ if (_.isString(color)) return Color.parse(color).alpha(opacity).toString();
+
+ var grad = color.colors ? color : {colors: color};
+
+ if (!options.ctx) {
+ if (!_.isArray(grad.colors)) return 'rgba(0, 0, 0, 0)';
+ return Color.parse(_.isArray(grad.colors[0]) ? grad.colors[0][1] : grad.colors[0]).alpha(opacity).toString();
+ }
+ grad = _.extend({start: 'top', end: 'bottom'}, grad);
+
+ if (/top/i.test(grad.start)) options.x1 = 0;
+ if (/left/i.test(grad.start)) options.y1 = 0;
+ if (/bottom/i.test(grad.end)) options.x2 = 0;
+ if (/right/i.test(grad.end)) options.y2 = 0;
+
+ var i, c, stop, gradient = options.ctx.createLinearGradient(options.x1, options.y1, options.x2, options.y2);
+ for (i = 0; i < grad.colors.length; i++) {
+ c = grad.colors[i];
+ if (_.isArray(c)) {
+ stop = c[0];
+ c = c[1];
+ }
+ else stop = i / (grad.colors.length-1);
+ gradient.addColorStop(stop, Color.parse(c).alpha(opacity));
+ }
+ return gradient;
+ }
+});
+
+Flotr.Color = Color;
+
+})();
+
+/**
+ * Flotr Date
+ */
+Flotr.Date = {
+
+ set : function (date, name, mode, value) {
+ mode = mode || 'UTC';
+ name = 'set' + (mode === 'UTC' ? 'UTC' : '') + name;
+ date[name](value);
+ },
+
+ get : function (date, name, mode) {
+ mode = mode || 'UTC';
+ name = 'get' + (mode === 'UTC' ? 'UTC' : '') + name;
+ return date[name]();
+ },
+
+ format: function(d, format, mode) {
+ if (!d) return;
+
+ // We should maybe use an "official" date format spec, like PHP date() or ColdFusion
+ // http://fr.php.net/manual/en/function.date.php
+ // http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=functions_c-d_29.html
+ var
+ get = this.get,
+ tokens = {
+ h: get(d, 'Hours', mode).toString(),
+ H: leftPad(get(d, 'Hours', mode)),
+ M: leftPad(get(d, 'Minutes', mode)),
+ S: leftPad(get(d, 'Seconds', mode)),
+ s: get(d, 'Milliseconds', mode),
+ d: get(d, 'Date', mode).toString(),
+ m: (get(d, 'Month', mode) + 1).toString(),
+ y: get(d, 'FullYear', mode).toString(),
+ b: Flotr.Date.monthNames[get(d, 'Month', mode)]
+ };
+
+ function leftPad(n){
+ n += '';
+ return n.length == 1 ? "0" + n : n;
+ }
+
+ var r = [], c,
+ escape = false;
+
+ for (var i = 0; i < format.length; ++i) {
+ c = format.charAt(i);
+
+ if (escape) {
+ r.push(tokens[c] || c);
+ escape = false;
+ }
+ else if (c == "%")
+ escape = true;
+ else
+ r.push(c);
+ }
+ return r.join('');
+ },
+ getFormat: function(time, span) {
+ var tu = Flotr.Date.timeUnits;
+ if (time < tu.second) return "%h:%M:%S.%s";
+ else if (time < tu.minute) return "%h:%M:%S";
+ else if (time < tu.day) return (span < 2 * tu.day) ? "%h:%M" : "%b %d %h:%M";
+ else if (time < tu.month) return "%b %d";
+ else if (time < tu.year) return (span < tu.year) ? "%b" : "%b %y";
+ else return "%y";
+ },
+ formatter: function (v, axis) {
+ var
+ options = axis.options,
+ scale = Flotr.Date.timeUnits[options.timeUnit],
+ d = new Date(v * scale);
+
+ // first check global format
+ if (axis.options.timeFormat)
+ return Flotr.Date.format(d, options.timeFormat, options.timeMode);
+
+ var span = (axis.max - axis.min) * scale,
+ t = axis.tickSize * Flotr.Date.timeUnits[axis.tickUnit];
+
+ return Flotr.Date.format(d, Flotr.Date.getFormat(t, span), options.timeMode);
+ },
+ generator: function(axis) {
+
+ var
+ set = this.set,
+ get = this.get,
+ timeUnits = this.timeUnits,
+ spec = this.spec,
+ options = axis.options,
+ mode = options.timeMode,
+ scale = timeUnits[options.timeUnit],
+ min = axis.min * scale,
+ max = axis.max * scale,
+ delta = (max - min) / options.noTicks,
+ ticks = [],
+ tickSize = axis.tickSize,
+ tickUnit,
+ formatter, i;
+
+ // Use custom formatter or time tick formatter
+ formatter = (options.tickFormatter === Flotr.defaultTickFormatter ?
+ this.formatter : options.tickFormatter
+ );
+
+ for (i = 0; i < spec.length - 1; ++i) {
+ var d = spec[i][0] * timeUnits[spec[i][1]];
+ if (delta < (d + spec[i+1][0] * timeUnits[spec[i+1][1]]) / 2 && d >= tickSize)
+ break;
+ }
+ tickSize = spec[i][0];
+ tickUnit = spec[i][1];
+
+ // special-case the possibility of several years
+ if (tickUnit == "year") {
+ tickSize = Flotr.getTickSize(options.noTicks*timeUnits.year, min, max, 0);
+
+ // Fix for 0.5 year case
+ if (tickSize == 0.5) {
+ tickUnit = "month";
+ tickSize = 6;
+ }
+ }
+
+ axis.tickUnit = tickUnit;
+ axis.tickSize = tickSize;
+
+ var step = tickSize * timeUnits[tickUnit];
+ d = new Date(min);
+
+ function setTick (name) {
+ set(d, name, mode, Flotr.floorInBase(
+ get(d, name, mode), tickSize
+ ));
+ }
+
+ switch (tickUnit) {
+ case "millisecond": setTick('Milliseconds'); break;
+ case "second": setTick('Seconds'); break;
+ case "minute": setTick('Minutes'); break;
+ case "hour": setTick('Hours'); break;
+ case "month": setTick('Month'); break;
+ case "year": setTick('FullYear'); break;
+ }
+
+ // reset smaller components
+ if (step >= timeUnits.second) set(d, 'Milliseconds', mode, 0);
+ if (step >= timeUnits.minute) set(d, 'Seconds', mode, 0);
+ if (step >= timeUnits.hour) set(d, 'Minutes', mode, 0);
+ if (step >= timeUnits.day) set(d, 'Hours', mode, 0);
+ if (step >= timeUnits.day * 4) set(d, 'Date', mode, 1);
+ if (step >= timeUnits.year) set(d, 'Month', mode, 0);
+
+ var carry = 0, v = NaN, prev;
+ do {
+ prev = v;
+ v = d.getTime();
+ ticks.push({ v: v / scale, label: formatter(v / scale, axis) });
+ if (tickUnit == "month") {
+ if (tickSize < 1) {
+ /* a bit complicated - we'll divide the month up but we need to take care of fractions
+ so we don't end up in the middle of a day */
+ set(d, 'Date', mode, 1);
+ var start = d.getTime();
+ set(d, 'Month', mode, get(d, 'Month', mode) + 1);
+ var end = d.getTime();
+ d.setTime(v + carry * timeUnits.hour + (end - start) * tickSize);
+ carry = get(d, 'Hours', mode);
+ set(d, 'Hours', mode, 0);
+ }
+ else
+ set(d, 'Month', mode, get(d, 'Month', mode) + tickSize);
+ }
+ else if (tickUnit == "year") {
+ set(d, 'FullYear', mode, get(d, 'FullYear', mode) + tickSize);
+ }
+ else
+ d.setTime(v + step);
+
+ } while (v < max && v != prev);
+
+ return ticks;
+ },
+ timeUnits: {
+ millisecond: 1,
+ second: 1000,
+ minute: 1000 * 60,
+ hour: 1000 * 60 * 60,
+ day: 1000 * 60 * 60 * 24,
+ month: 1000 * 60 * 60 * 24 * 30,
+ year: 1000 * 60 * 60 * 24 * 365.2425
+ },
+ // the allowed tick sizes, after 1 year we use an integer algorithm
+ spec: [
+ [1, "millisecond"], [20, "millisecond"], [50, "millisecond"], [100, "millisecond"], [200, "millisecond"], [500, "millisecond"],
+ [1, "second"], [2, "second"], [5, "second"], [10, "second"], [30, "second"],
+ [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], [30, "minute"],
+ [1, "hour"], [2, "hour"], [4, "hour"], [8, "hour"], [12, "hour"],
+ [1, "day"], [2, "day"], [3, "day"],
+ [0.25, "month"], [0.5, "month"], [1, "month"], [2, "month"], [3, "month"], [6, "month"],
+ [1, "year"]
+ ],
+ monthNames: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+};
+
+(function () {
+
+var _ = Flotr._;
+
+function getEl (el) {
+ return (el && el.jquery) ? el[0] : el;
+}
+
+Flotr.DOM = {
+ addClass: function(element, name){
+ element = getEl(element);
+ var classList = (element.className ? element.className : '');
+ if (_.include(classList.split(/\s+/g), name)) return;
+ element.className = (classList ? classList + ' ' : '') + name;
+ },
+ /**
+ * Create an element.
+ */
+ create: function(tag){
+ return document.createElement(tag);
+ },
+ node: function(html) {
+ var div = Flotr.DOM.create('div'), n;
+ div.innerHTML = html;
+ n = div.children[0];
+ div.innerHTML = '';
+ return n;
+ },
+ /**
+ * Remove all children.
+ */
+ empty: function(element){
+ element = getEl(element);
+ element.innerHTML = '';
+ /*
+ if (!element) return;
+ _.each(element.childNodes, function (e) {
+ Flotr.DOM.empty(e);
+ element.removeChild(e);
+ });
+ */
+ },
+ remove: function (element) {
+ element = getEl(element);
+ element.parentNode.removeChild(element);
+ },
+ hide: function(element){
+ element = getEl(element);
+ Flotr.DOM.setStyles(element, {display:'none'});
+ },
+ /**
+ * Insert a child.
+ * @param {Element} element
+ * @param {Element|String} Element or string to be appended.
+ */
+ insert: function(element, child){
+ element = getEl(element);
+ if(_.isString(child))
+ element.innerHTML += child;
+ else if (_.isElement(child))
+ element.appendChild(child);
+ },
+ // @TODO find xbrowser implementation
+ opacity: function(element, opacity) {
+ element = getEl(element);
+ element.style.opacity = opacity;
+ },
+ position: function(element, p){
+ element = getEl(element);
+ if (!element.offsetParent)
+ return {left: (element.offsetLeft || 0), top: (element.offsetTop || 0)};
+
+ p = this.position(element.offsetParent);
+ p.left += element.offsetLeft;
+ p.top += element.offsetTop;
+ return p;
+ },
+ removeClass: function(element, name) {
+ var classList = (element.className ? element.className : '');
+ element = getEl(element);
+ element.className = _.filter(classList.split(/\s+/g), function (c) {
+ if (c != name) return true; }
+ ).join(' ');
+ },
+ setStyles: function(element, o) {
+ element = getEl(element);
+ _.each(o, function (value, key) {
+ element.style[key] = value;
+ });
+ },
+ show: function(element){
+ element = getEl(element);
+ Flotr.DOM.setStyles(element, {display:''});
+ },
+ /**
+ * Return element size.
+ */
+ size: function(element){
+ element = getEl(element);
+ return {
+ height : element.offsetHeight,
+ width : element.offsetWidth };
+ }
+};
+
+})();
+
+/**
+ * Flotr Event Adapter
+ */
+(function () {
+var
+ F = Flotr,
+ bean = F.bean;
+F.EventAdapter = {
+ observe: function(object, name, callback) {
+ bean.add(object, name, callback);
+ return this;
+ },
+ fire: function(object, name, args) {
+ bean.fire(object, name, args);
+ if (typeof(Prototype) != 'undefined')
+ Event.fire(object, name, args);
+ // @TODO Someone who uses mootools, add mootools adapter for existing applciations.
+ return this;
+ },
+ stopObserving: function(object, name, callback) {
+ bean.remove(object, name, callback);
+ return this;
+ },
+ eventPointer: function(e) {
+ if (!F._.isUndefined(e.touches) && e.touches.length > 0) {
+ return {
+ x : e.touches[0].pageX,
+ y : e.touches[0].pageY
+ };
+ } else if (!F._.isUndefined(e.changedTouches) && e.changedTouches.length > 0) {
+ return {
+ x : e.changedTouches[0].pageX,
+ y : e.changedTouches[0].pageY
+ };
+ } else if (e.pageX || e.pageY) {
+ return {
+ x : e.pageX,
+ y : e.pageY
+ };
+ } else if (e.clientX || e.clientY) {
+ var
+ d = document,
+ b = d.body,
+ de = d.documentElement;
+ return {
+ x: e.clientX + b.scrollLeft + de.scrollLeft,
+ y: e.clientY + b.scrollTop + de.scrollTop
+ };
+ }
+ }
+};
+})();
+
+/**
+ * Text Utilities
+ */
+(function () {
+
+var
+ F = Flotr,
+ D = F.DOM,
+ _ = F._,
+
+Text = function (o) {
+ this.o = o;
+};
+
+Text.prototype = {
+
+ dimensions : function (text, canvasStyle, htmlStyle, className) {
+
+ if (!text) return { width : 0, height : 0 };
+
+ return (this.o.html) ?
+ this.html(text, this.o.element, htmlStyle, className) :
+ this.canvas(text, canvasStyle);
+ },
+
+ canvas : function (text, style) {
+
+ if (!this.o.textEnabled) return;
+ style = style || {};
+
+ var
+ metrics = this.measureText(text, style),
+ width = metrics.width,
+ height = style.size || F.defaultOptions.fontSize,
+ angle = style.angle || 0,
+ cosAngle = Math.cos(angle),
+ sinAngle = Math.sin(angle),
+ widthPadding = 2,
+ heightPadding = 6,
+ bounds;
+
+ bounds = {
+ width: Math.abs(cosAngle * width) + Math.abs(sinAngle * height) + widthPadding,
+ height: Math.abs(sinAngle * width) + Math.abs(cosAngle * height) + heightPadding
+ };
+
+ return bounds;
+ },
+
+ html : function (text, element, style, className) {
+
+ var div = D.create('div');
+
+ D.setStyles(div, { 'position' : 'absolute', 'top' : '-10000px' });
+ D.insert(div, '<div style="'+style+'" class="'+className+' flotr-dummy-div">' + text + '</div>');
+ D.insert(this.o.element, div);
+
+ return D.size(div);
+ },
+
+ measureText : function (text, style) {
+
+ var
+ context = this.o.ctx,
+ metrics;
+
+ if (!context.fillText || (F.isIphone && context.measure)) {
+ return { width : context.measure(text, style)};
+ }
+
+ style = _.extend({
+ size: F.defaultOptions.fontSize,
+ weight: 1,
+ angle: 0
+ }, style);
+
+ context.save();
+ context.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif";
+ metrics = context.measureText(text);
+ context.restore();
+
+ return metrics;
+ }
+};
+
+Flotr.Text = Text;
+
+})();
+
+/**
+ * Flotr Graph class that plots a graph on creation.
+ */
+(function () {
+
+var
+ D = Flotr.DOM,
+ E = Flotr.EventAdapter,
+ _ = Flotr._,
+ flotr = Flotr;
+/**
+ * Flotr Graph constructor.
+ * @param {Element} el - element to insert the graph into
+ * @param {Object} data - an array or object of dataseries
+ * @param {Object} options - an object containing options
+ */
+Graph = function(el, data, options){
+// Let's see if we can get away with out this [JS]
+// try {
+ this._setEl(el);
+ this._initMembers();
+ this._initPlugins();
+
+ E.fire(this.el, 'flotr:beforeinit', [this]);
+
+ this.data = data;
+ this.series = flotr.Series.getSeries(data);
+ this._initOptions(options);
+ this._initGraphTypes();
+ this._initCanvas();
+ this._text = new flotr.Text({
+ element : this.el,
+ ctx : this.ctx,
+ html : this.options.HtmlText,
+ textEnabled : this.textEnabled
+ });
+ E.fire(this.el, 'flotr:afterconstruct', [this]);
+ this._initEvents();
+
+ this.findDataRanges();
+ this.calculateSpacing();
+
+ this.draw(_.bind(function() {
+ E.fire(this.el, 'flotr:afterinit', [this]);
+ }, this));
+/*
+ try {
+ } catch (e) {
+ try {
+ console.error(e);
+ } catch (e2) {}
+ }*/
+};
+
+function observe (object, name, callback) {
+ E.observe.apply(this, arguments);
+ this._handles.push(arguments);
+ return this;
+}
+
+Graph.prototype = {
+
+ destroy: function () {
+ E.fire(this.el, 'flotr:destroy');
+ _.each(this._handles, function (handle) {
+ E.stopObserving.apply(this, handle);
+ });
+ this._handles = [];
+ this.el.graph = null;
+ },
+
+ observe : observe,
+
+ /**
+ * @deprecated
+ */
+ _observe : observe,
+
+ processColor: function(color, options){
+ var o = { x1: 0, y1: 0, x2: this.plotWidth, y2: this.plotHeight, opacity: 1, ctx: this.ctx };
+ _.extend(o, options);
+ return flotr.Color.processColor(color, o);
+ },
+ /**
+ * Function determines the min and max values for the xaxis and yaxis.
+ *
+ * TODO logarithmic range validation (consideration of 0)
+ */
+ findDataRanges: function(){
+ var a = this.axes,
+ xaxis, yaxis, range;
+
+ _.each(this.series, function (series) {
+ range = series.getRange();
+ if (range) {
+ xaxis = series.xaxis;
+ yaxis = series.yaxis;
+ xaxis.datamin = Math.min(range.xmin, xaxis.datamin);
+ xaxis.datamax = Math.max(range.xmax, xaxis.datamax);
+ yaxis.datamin = Math.min(range.ymin, yaxis.datamin);
+ yaxis.datamax = Math.max(range.ymax, yaxis.datamax);
+ xaxis.used = (xaxis.used || range.xused);
+ yaxis.used = (yaxis.used || range.yused);
+ }
+ }, this);
+
+ // Check for empty data, no data case (none used)
+ if (!a.x.used && !a.x2.used) a.x.used = true;
+ if (!a.y.used && !a.y2.used) a.y.used = true;
+
+ _.each(a, function (axis) {
+ axis.calculateRange();
+ });
+
+ var
+ types = _.keys(flotr.graphTypes),
+ drawn = false;
+
+ _.each(this.series, function (series) {
+ if (series.hide) return;
+ _.each(types, function (type) {
+ if (series[type] && series[type].show) {
+ this.extendRange(type, series);
+ drawn = true;
+ }
+ }, this);
+ if (!drawn) {
+ this.extendRange(this.options.defaultType, series);
+ }
+ }, this);
+ },
+
+ extendRange : function (type, series) {
+ if (this[type].extendRange) this[type].extendRange(series, series.data, series[type], this[type]);
+ if (this[type].extendYRange) this[type].extendYRange(series.yaxis, series.data, series[type], this[type]);
+ if (this[type].extendXRange) this[type].extendXRange(series.xaxis, series.data, series[type], this[type]);
+ },
+
+ /**
+ * Calculates axis label sizes.
+ */
+ calculateSpacing: function(){
+
+ var a = this.axes,
+ options = this.options,
+ series = this.series,
+ margin = options.grid.labelMargin,
+ T = this._text,
+ x = a.x,
+ x2 = a.x2,
+ y = a.y,
+ y2 = a.y2,
+ maxOutset = options.grid.outlineWidth,
+ i, j, l, dim;
+
+ // TODO post refactor, fix this
+ _.each(a, function (axis) {
+ axis.calculateTicks();
+ axis.calculateTextDimensions(T, options);
+ });
+
+ // Title height
+ dim = T.dimensions(
+ options.title,
+ {size: options.fontSize*1.5},
+ 'font-size:1em;font-weight:bold;',
+ 'flotr-title'
+ );
+ this.titleHeight = dim.height;
+
+ // Subtitle height
+ dim = T.dimensions(
+ options.subtitle,
+ {size: options.fontSize},
+ 'font-size:smaller;',
+ 'flotr-subtitle'
+ );
+ this.subtitleHeight = dim.height;
+
+ for(j = 0; j < options.length; ++j){
+ if (series[j].points.show){
+ maxOutset = Math.max(maxOutset, series[j].points.radius + series[j].points.lineWidth/2);
+ }
+ }
+
+ var p = this.plotOffset;
+ if (x.options.margin === false) {
+ p.bottom = 0;
+ p.top = 0;
+ } else
+ if (x.options.margin === true) {
+ p.bottom += (options.grid.circular ? 0 : (x.used && x.options.showLabels ? (x.maxLabel.height + margin) : 0)) +
+ (x.used && x.options.title ? (x.titleSize.height + margin) : 0) + maxOutset;
+
+ p.top += (options.grid.circular ? 0 : (x2.used && x2.options.showLabels ? (x2.maxLabel.height + margin) : 0)) +
+ (x2.used && x2.options.title ? (x2.titleSize.height + margin) : 0) + this.subtitleHeight + this.titleHeight + maxOutset;
+ } else {
+ p.bottom = x.options.margin;
+ p.top = x.options.margin;
+ }
+ if (y.options.margin === false) {
+ p.left = 0;
+ p.right = 0;
+ } else
+ if (y.options.margin === true) {
+ p.left += (options.grid.circular ? 0 : (y.used && y.options.showLabels ? (y.maxLabel.width + margin) : 0)) +
+ (y.used && y.options.title ? (y.titleSize.width + margin) : 0) + maxOutset;
+
+ p.right += (options.grid.circular ? 0 : (y2.used && y2.options.showLabels ? (y2.maxLabel.width + margin) : 0)) +
+ (y2.used && y2.options.title ? (y2.titleSize.width + margin) : 0) + maxOutset;
+ } else {
+ p.left = y.options.margin;
+ p.right = y.options.margin;
+ }
+
+ p.top = Math.floor(p.top); // In order the outline not to be blured
+
+ this.plotWidth = this.canvasWidth - p.left - p.right;
+ this.plotHeight = this.canvasHeight - p.bottom - p.top;
+
+ // TODO post refactor, fix this
+ x.length = x2.length = this.plotWidth;
+ y.length = y2.length = this.plotHeight;
+ y.offset = y2.offset = this.plotHeight;
+ x.setScale();
+ x2.setScale();
+ y.setScale();
+ y2.setScale();
+ },
+ /**
+ * Draws grid, labels, series and outline.
+ */
+ draw: function(after) {
+
+ var
+ context = this.ctx,
+ i;
+
+ E.fire(this.el, 'flotr:beforedraw', [this.series, this]);
+
+ if (this.series.length) {
+
+ context.save();
+ context.translate(this.plotOffset.left, this.plotOffset.top);
+
+ for (i = 0; i < this.series.length; i++) {
+ if (!this.series[i].hide) this.drawSeries(this.series[i]);
+ }
+
+ context.restore();
+ this.clip();
+ }
+
+ E.fire(this.el, 'flotr:afterdraw', [this.series, this]);
+ if (after) after();
+ },
+ /**
+ * Actually draws the graph.
+ * @param {Object} series - series to draw
+ */
+ drawSeries: function(series){
+
+ function drawChart (series, typeKey) {
+ var options = this.getOptions(series, typeKey);
+ this[typeKey].draw(options);
+ }
+
+ var drawn = false;
+ series = series || this.series;
+
+ _.each(flotr.graphTypes, function (type, typeKey) {
+ if (series[typeKey] && series[typeKey].show && this[typeKey]) {
+ drawn = true;
+ drawChart.call(this, series, typeKey);
+ }
+ }, this);
+
+ if (!drawn) drawChart.call(this, series, this.options.defaultType);
+ },
+
+ getOptions : function (series, typeKey) {
+ var
+ type = series[typeKey],
+ graphType = this[typeKey],
+ xaxis = series.xaxis,
+ yaxis = series.yaxis,
+ options = {
+ context : this.ctx,
+ width : this.plotWidth,
+ height : this.plotHeight,
+ fontSize : this.options.fontSize,
+ fontColor : this.options.fontColor,
+ textEnabled : this.textEnabled,
+ htmlText : this.options.HtmlText,
+ text : this._text, // TODO Is this necessary?
+ element : this.el,
+ data : series.data,
+ color : series.color,
+ shadowSize : series.shadowSize,
+ xScale : xaxis.d2p,
+ yScale : yaxis.d2p,
+ xInverse : xaxis.p2d,
+ yInverse : yaxis.p2d
+ };
+
+ options = flotr.merge(type, options);
+
+ // Fill
+ options.fillStyle = this.processColor(
+ type.fillColor || series.color,
+ {opacity: type.fillOpacity}
+ );
+
+ return options;
+ },
+ /**
+ * Calculates the coordinates from a mouse event object.
+ * @param {Event} event - Mouse Event object.
+ * @return {Object} Object with coordinates of the mouse.
+ */
+ getEventPosition: function (e){
+
+ var
+ d = document,
+ b = d.body,
+ de = d.documentElement,
+ axes = this.axes,
+ plotOffset = this.plotOffset,
+ lastMousePos = this.lastMousePos,
+ pointer = E.eventPointer(e),
+ dx = pointer.x - lastMousePos.pageX,
+ dy = pointer.y - lastMousePos.pageY,
+ r, rx, ry;
+
+ if ('ontouchstart' in this.el) {
+ r = D.position(this.overlay);
+ rx = pointer.x - r.left - plotOffset.left;
+ ry = pointer.y - r.top - plotOffset.top;
+ } else {
+ r = this.overlay.getBoundingClientRect();
+ rx = e.clientX - r.left - plotOffset.left - b.scrollLeft - de.scrollLeft;
+ ry = e.clientY - r.top - plotOffset.top - b.scrollTop - de.scrollTop;
+ }
+
+ return {
+ x: axes.x.p2d(rx),
+ x2: axes.x2.p2d(rx),
+ y: axes.y.p2d(ry),
+ y2: axes.y2.p2d(ry),
+ relX: rx,
+ relY: ry,
+ dX: dx,
+ dY: dy,
+ absX: pointer.x,
+ absY: pointer.y,
+ pageX: pointer.x,
+ pageY: pointer.y
+ };
+ },
+ /**
+ * Observes the 'click' event and fires the 'flotr:click' event.
+ * @param {Event} event - 'click' Event object.
+ */
+ clickHandler: function(event){
+ if(this.ignoreClick){
+ this.ignoreClick = false;
+ return this.ignoreClick;
+ }
+ E.fire(this.el, 'flotr:click', [this.getEventPosition(event), this]);
+ },
+ /**
+ * Observes mouse movement over the graph area. Fires the 'flotr:mousemove' event.
+ * @param {Event} event - 'mousemove' Event object.
+ */
+ mouseMoveHandler: function(event){
+ if (this.mouseDownMoveHandler) return;
+ var pos = this.getEventPosition(event);
+ E.fire(this.el, 'flotr:mousemove', [event, pos, this]);
+ this.lastMousePos = pos;
+ },
+ /**
+ * Observes the 'mousedown' event.
+ * @param {Event} event - 'mousedown' Event object.
+ */
+ mouseDownHandler: function (event){
+
+ /*
+ // @TODO Context menu?
+ if(event.isRightClick()) {
+ event.stop();
+
+ var overlay = this.overlay;
+ overlay.hide();
+
+ function cancelContextMenu () {
+ overlay.show();
+ E.stopObserving(document, 'mousemove', cancelContextMenu);
+ }
+ E.observe(document, 'mousemove', cancelContextMenu);
+ return;
+ }
+ */
+
+ if (this.mouseUpHandler) return;
+ this.mouseUpHandler = _.bind(function (e) {
+ E.stopObserving(document, 'mouseup', this.mouseUpHandler);
+ E.stopObserving(document, 'mousemove', this.mouseDownMoveHandler);
+ this.mouseDownMoveHandler = null;
+ this.mouseUpHandler = null;
+ // @TODO why?
+ //e.stop();
+ E.fire(this.el, 'flotr:mouseup', [e, this]);
+ }, this);
+ this.mouseDownMoveHandler = _.bind(function (e) {
+ var pos = this.getEventPosition(e);
+ E.fire(this.el, 'flotr:mousemove', [event, pos, this]);
+ this.lastMousePos = pos;
+ }, this);
+ E.observe(document, 'mouseup', this.mouseUpHandler);
+ E.observe(document, 'mousemove', this.mouseDownMoveHandler);
+ E.fire(this.el, 'flotr:mousedown', [event, this]);
+ this.ignoreClick = false;
+ },
+ drawTooltip: function(content, x, y, options) {
+ var mt = this.getMouseTrack(),
+ style = 'opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;',
+ p = options.position,
+ m = options.margin,
+ plotOffset = this.plotOffset;
+
+ if(x !== null && y !== null){
+ if (!options.relative) { // absolute to the canvas
+ if(p.charAt(0) == 'n') style += 'top:' + (m + plotOffset.top) + 'px;bottom:auto;';
+ else if(p.charAt(0) == 's') style += 'bottom:' + (m + plotOffset.bottom) + 'px;top:auto;';
+ if(p.charAt(1) == 'e') style += 'right:' + (m + plotOffset.right) + 'px;left:auto;';
+ else if(p.charAt(1) == 'w') style += 'left:' + (m + plotOffset.left) + 'px;right:auto;';
+ }
+ else { // relative to the mouse
+ if(p.charAt(0) == 'n') style += 'bottom:' + (m - plotOffset.top - y + this.canvasHeight) + 'px;top:auto;';
+ else if(p.charAt(0) == 's') style += 'top:' + (m + plotOffset.top + y) + 'px;bottom:auto;';
+ if(p.charAt(1) == 'e') style += 'left:' + (m + plotOffset.left + x) + 'px;right:auto;';
+ else if(p.charAt(1) == 'w') style += 'right:' + (m - plotOffset.left - x + this.canvasWidth) + 'px;left:auto;';
+ }
+
+ mt.style.cssText = style;
+ D.empty(mt);
+ D.insert(mt, content);
+ D.show(mt);
+ }
+ else {
+ D.hide(mt);
+ }
+ },
+
+ clip: function (ctx) {
+
+ var
+ o = this.plotOffset,
+ w = this.canvasWidth,
+ h = this.canvasHeight;
+
+ ctx = ctx || this.ctx;
+
+ if (
+ flotr.isIE && flotr.isIE < 9 && // IE w/o canvas
+ !flotr.isFlashCanvas // But not flash canvas
+ ) {
+
+ // Do not clip excanvas on overlay context
+ // Allow hits to overflow.
+ if (ctx === this.octx) {
+ return;
+ }
+
+ // Clipping for excanvas :-(
+ ctx.save();
+ ctx.fillStyle = this.processColor(this.options.ieBackgroundColor);
+ ctx.fillRect(0, 0, w, o.top);
+ ctx.fillRect(0, 0, o.left, h);
+ ctx.fillRect(0, h - o.bottom, w, o.bottom);
+ ctx.fillRect(w - o.right, 0, o.right,h);
+ ctx.restore();
+ } else {
+ ctx.clearRect(0, 0, w, o.top);
+ ctx.clearRect(0, 0, o.left, h);
+ ctx.clearRect(0, h - o.bottom, w, o.bottom);
+ ctx.clearRect(w - o.right, 0, o.right,h);
+ }
+ },
+
+ _initMembers: function() {
+ this._handles = [];
+ this.lastMousePos = {pageX: null, pageY: null };
+ this.plotOffset = {left: 0, right: 0, top: 0, bottom: 0};
+ this.ignoreClick = true;
+ this.prevHit = null;
+ },
+
+ _initGraphTypes: function() {
+ _.each(flotr.graphTypes, function(handler, graphType){
+ this[graphType] = flotr.clone(handler);
+ }, this);
+ },
+
+ _initEvents: function () {
+
+ var
+ el = this.el,
+ touchendHandler, movement, touchend;
+
+ if ('ontouchstart' in el) {
+
+ touchendHandler = _.bind(function (e) {
+ touchend = true;
+ E.stopObserving(document, 'touchend', touchendHandler);
+ E.fire(el, 'flotr:mouseup', [event, this]);
+ this.multitouches = null;
+
+ if (!movement) {
+ this.clickHandler(e);
+ }
+ }, this);
+
+ this.observe(this.overlay, 'touchstart', _.bind(function (e) {
+ movement = false;
+ touchend = false;
+ this.ignoreClick = false;
+
+ if (e.touches && e.touches.length > 1) {
+ this.multitouches = e.touches;
+ }
+
+ E.fire(el, 'flotr:mousedown', [event, this]);
+ this.observe(document, 'touchend', touchendHandler);
+ }, this));
+
+ this.observe(this.overlay, 'touchmove', _.bind(function (e) {
+
+ var pos = this.getEventPosition(e);
+
+ if (this.options.preventDefault) {
+ e.preventDefault();
+ }
+
+ movement = true;
+
+ if (this.multitouches || (e.touches && e.touches.length > 1)) {
+ this.multitouches = e.touches;
+ } else {
+ if (!touchend) {
+ E.fire(el, 'flotr:mousemove', [event, pos, this]);
+ }
+ }
+ this.lastMousePos = pos;
+ }, this));
+
+ } else {
+ this.
+ observe(this.overlay, 'mousedown', _.bind(this.mouseDownHandler, this)).
+ observe(el, 'mousemove', _.bind(this.mouseMoveHandler, this)).
+ observe(this.overlay, 'click', _.bind(this.clickHandler, this)).
+ observe(el, 'mouseout', function (e) {
+ E.fire(el, 'flotr:mouseout', e);
+ });
+ }
+ },
+
+ /**
+ * Initializes the canvas and it's overlay canvas element. When the browser is IE, this makes use
+ * of excanvas. The overlay canvas is inserted for displaying interactions. After the canvas elements
+ * are created, the elements are inserted into the container element.
+ */
+ _initCanvas: function(){
+ var el = this.el,
+ o = this.options,
+ children = el.children,
+ removedChildren = [],
+ child, i,
+ size, style;
+
+ // Empty the el
+ for (i = children.length; i--;) {
+ child = children[i];
+ if (!this.canvas && child.className === 'flotr-canvas') {
+ this.canvas = child;
+ } else if (!this.overlay && child.className === 'flotr-overlay') {
+ this.overlay = child;
+ } else {
+ removedChildren.push(child);
+ }
+ }
+ for (i = removedChildren.length; i--;) {
+ el.removeChild(removedChildren[i]);
+ }
+
+ D.setStyles(el, {position: 'relative'}); // For positioning labels and overlay.
+ size = {};
+ size.width = el.clientWidth;
+ size.height = el.clientHeight;
+
+ if(size.width <= 0 || size.height <= 0 || o.resolution <= 0){
+ throw 'Invalid dimensions for plot, width = ' + size.width + ', height = ' + size.height + ', resolution = ' + o.resolution;
+ }
+
+ // Main canvas for drawing graph types
+ this.canvas = getCanvas(this.canvas, 'canvas');
+ // Overlay canvas for interactive features
+ this.overlay = getCanvas(this.overlay, 'overlay');
+ this.ctx = getContext(this.canvas);
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ this.octx = getContext(this.overlay);
+ this.octx.clearRect(0, 0, this.overlay.width, this.overlay.height);
+ this.canvasHeight = size.height;
+ this.canvasWidth = size.width;
+ this.textEnabled = !!this.ctx.drawText || !!this.ctx.fillText; // Enable text functions
+
+ function getCanvas(canvas, name){
+ if(!canvas){
+ canvas = D.create('canvas');
+ if (typeof FlashCanvas != "undefined" && typeof canvas.getContext === 'function') {
+ FlashCanvas.initElement(canvas);
+ this.isFlashCanvas = true;
+ }
+ canvas.className = 'flotr-'+name;
+ canvas.style.cssText = 'position:absolute;left:0px;top:0px;';
+ D.insert(el, canvas);
+ }
+ _.each(size, function(size, attribute){
+ D.show(canvas);
+ if (name == 'canvas' && canvas.getAttribute(attribute) === size) {
+ return;
+ }
+ canvas.setAttribute(attribute, size * o.resolution);
+ canvas.style[attribute] = size + 'px';
+ });
+ canvas.context_ = null; // Reset the ExCanvas context
+ return canvas;
+ }
+
+ function getContext(canvas){
+ if(window.G_vmlCanvasManager) window.G_vmlCanvasManager.initElement(canvas); // For ExCanvas
+ var context = canvas.getContext('2d');
+ if(!window.G_vmlCanvasManager) context.scale(o.resolution, o.resolution);
+ return context;
+ }
+ },
+
+ _initPlugins: function(){
+ // TODO Should be moved to flotr and mixed in.
+ _.each(flotr.plugins, function(plugin, name){
+ _.each(plugin.callbacks, function(fn, c){
+ this.observe(this.el, c, _.bind(fn, this));
+ }, this);
+ this[name] = flotr.clone(plugin);
+ _.each(this[name], function(fn, p){
+ if (_.isFunction(fn))
+ this[name][p] = _.bind(fn, this);
+ }, this);
+ }, this);
+ },
+
+ /**
+ * Sets options and initializes some variables and color specific values, used by the constructor.
+ * @param {Object} opts - options object
+ */
+ _initOptions: function(opts){
+ var options = flotr.clone(flotr.defaultOptions);
+ options.x2axis = _.extend(_.clone(options.xaxis), options.x2axis);
+ options.y2axis = _.extend(_.clone(options.yaxis), options.y2axis);
+ this.options = flotr.merge(opts || {}, options);
+
+ if (this.options.grid.minorVerticalLines === null &&
+ this.options.xaxis.scaling === 'logarithmic') {
+ this.options.grid.minorVerticalLines = true;
+ }
+ if (this.options.grid.minorHorizontalLines === null &&
+ this.options.yaxis.scaling === 'logarithmic') {
+ this.options.grid.minorHorizontalLines = true;
+ }
+
+ E.fire(this.el, 'flotr:afterinitoptions', [this]);
+
+ this.axes = flotr.Axis.getAxes(this.options);
+
+ // Initialize some variables used throughout this function.
+ var assignedColors = [],
+ colors = [],
+ ln = this.series.length,
+ neededColors = this.series.length,
+ oc = this.options.colors,
+ usedColors = [],
+ variation = 0,
+ c, i, j, s;
+
+ // Collect user-defined colors from series.
+ for(i = neededColors - 1; i > -1; --i){
+ c = this.series[i].color;
+ if(c){
+ --neededColors;
+ if(_.isNumber(c)) assignedColors.push(c);
+ else usedColors.push(flotr.Color.parse(c));
+ }
+ }
+
+ // Calculate the number of colors that need to be generated.
+ for(i = assignedColors.length - 1; i > -1; --i)
+ neededColors = Math.max(neededColors, assignedColors[i] + 1);
+
+ // Generate needed number of colors.
+ for(i = 0; colors.length < neededColors;){
+ c = (oc.length == i) ? new flotr.Color(100, 100, 100) : flotr.Color.parse(oc[i]);
+
+ // Make sure each serie gets a different color.
+ var sign = variation % 2 == 1 ? -1 : 1,
+ factor = 1 + sign * Math.ceil(variation / 2) * 0.2;
+ c.scale(factor, factor, factor);
+
+ /**
+ * @todo if we're getting too close to something else, we should probably skip this one
+ */
+ colors.push(c);
+
+ if(++i >= oc.length){
+ i = 0;
+ ++variation;
+ }
+ }
+
+ // Fill the options with the generated colors.
+ for(i = 0, j = 0; i < ln; ++i){
+ s = this.series[i];
+
+ // Assign the color.
+ if (!s.color){
+ s.color = colors[j++].toString();
+ }else if(_.isNumber(s.color)){
+ s.color = colors[s.color].toString();
+ }
+
+ // Every series needs an axis
+ if (!s.xaxis) s.xaxis = this.axes.x;
+ if (s.xaxis == 1) s.xaxis = this.axes.x;
+ else if (s.xaxis == 2) s.xaxis = this.axes.x2;
+
+ if (!s.yaxis) s.yaxis = this.axes.y;
+ if (s.yaxis == 1) s.yaxis = this.axes.y;
+ else if (s.yaxis == 2) s.yaxis = this.axes.y2;
+
+ // Apply missing options to the series.
+ for (var t in flotr.graphTypes){
+ s[t] = _.extend(_.clone(this.options[t]), s[t]);
+ }
+ s.mouse = _.extend(_.clone(this.options.mouse), s.mouse);
+
+ if (_.isUndefined(s.shadowSize)) s.shadowSize = this.options.shadowSize;
+ }
+ },
+
+ _setEl: function(el) {
+ if (!el) throw 'The target container doesn\'t exist';
+ else if (el.graph instanceof Graph) el.graph.destroy();
+ else if (!el.clientWidth) throw 'The target container must be visible';
+
+ el.graph = this;
+ this.el = el;
+ }
+};
+
+Flotr.Graph = Graph;
+
+})();
+
+/**
+ * Flotr Axis Library
+ */
+
+(function () {
+
+var
+ _ = Flotr._,
+ LOGARITHMIC = 'logarithmic';
+
+function Axis (o) {
+
+ this.orientation = 1;
+ this.offset = 0;
+ this.datamin = Number.MAX_VALUE;
+ this.datamax = -Number.MAX_VALUE;
+
+ _.extend(this, o);
+}
+
+
+// Prototype
+Axis.prototype = {
+
+ setScale : function () {
+ var
+ length = this.length,
+ max = this.max,
+ min = this.min,
+ offset = this.offset,
+ orientation = this.orientation,
+ options = this.options,
+ logarithmic = options.scaling === LOGARITHMIC,
+ scale;
+
+ if (logarithmic) {
+ scale = length / (log(max, options.base) - log(min, options.base));
+ } else {
+ scale = length / (max - min);
+ }
+ this.scale = scale;
+
+ // Logarithmic?
+ if (logarithmic) {
+ this.d2p = function (dataValue) {
+ return offset + orientation * (log(dataValue, options.base) - log(min, options.base)) * scale;
+ };
+ this.p2d = function (pointValue) {
+ return exp((offset + orientation * pointValue) / scale + log(min, options.base), options.base);
+ };
+ } else {
+ this.d2p = function (dataValue) {
+ return offset + orientation * (dataValue - min) * scale;
+ };
+ this.p2d = function (pointValue) {
+ return (offset + orientation * pointValue) / scale + min;
+ };
+ }
+ },
+
+ calculateTicks : function () {
+ var options = this.options;
+
+ this.ticks = [];
+ this.minorTicks = [];
+
+ // User Ticks
+ if(options.ticks){
+ this._cleanUserTicks(options.ticks, this.ticks);
+ this._cleanUserTicks(options.minorTicks || [], this.minorTicks);
+ }
+ else {
+ if (options.mode == 'time') {
+ this._calculateTimeTicks();
+ } else if (options.scaling === 'logarithmic') {
+ this._calculateLogTicks();
+ } else {
+ this._calculateTicks();
+ }
+ }
+
+ // Ticks to strings
+ _.each(this.ticks, function (tick) { tick.label += ''; });
+ _.each(this.minorTicks, function (tick) { tick.label += ''; });
+ },
+
+ /**
+ * Calculates the range of an axis to apply autoscaling.
+ */
+ calculateRange: function () {
+
+ if (!this.used) return;
+
+ var axis = this,
+ o = axis.options,
+ min = o.min !== null ? o.min : axis.datamin,
+ max = o.max !== null ? o.max : axis.datamax,
+ margin = o.autoscaleMargin;
+
+ if (o.scaling == 'logarithmic') {
+ if (min <= 0) min = axis.datamin;
+
+ // Let it widen later on
+ if (max <= 0) max = min;
+ }
+
+ if (max == min) {
+ var widen = max ? 0.01 : 1.00;
+ if (o.min === null) min -= widen;
+ if (o.max === null) max += widen;
+ }
+
+ if (o.scaling === 'logarithmic') {
+ if (min < 0) min = max / o.base; // Could be the result of widening
+
+ var maxexp = Math.log(max);
+ if (o.base != Math.E) maxexp /= Math.log(o.base);
+ maxexp = Math.ceil(maxexp);
+
+ var minexp = Math.log(min);
+ if (o.base != Math.E) minexp /= Math.log(o.base);
+ minexp = Math.ceil(minexp);
+
+ axis.tickSize = Flotr.getTickSize(o.noTicks, minexp, maxexp, o.tickDecimals === null ? 0 : o.tickDecimals);
+
+ // Try to determine a suitable amount of miniticks based on the length of a decade
+ if (o.minorTickFreq === null) {
+ if (maxexp - minexp > 10)
+ o.minorTickFreq = 0;
+ else if (maxexp - minexp > 5)
+ o.minorTickFreq = 2;
+ else
+ o.minorTickFreq = 5;
+ }
+ } else {
+ axis.tickSize = Flotr.getTickSize(o.noTicks, min, max, o.tickDecimals);
+ }
+
+ axis.min = min;
+ axis.max = max; //extendRange may use axis.min or axis.max, so it should be set before it is caled
+
+ // Autoscaling. @todo This probably fails with log scale. Find a testcase and fix it
+ if(o.min === null && o.autoscale){
+ axis.min -= axis.tickSize * margin;
+ // Make sure we don't go below zero if all values are positive.
+ if(axis.min < 0 && axis.datamin >= 0) axis.min = 0;
+ axis.min = axis.tickSize * Math.floor(axis.min / axis.tickSize);
+ }
+
+ if(o.max === null && o.autoscale){
+ axis.max += axis.tickSize * margin;
+ if(axis.max > 0 && axis.datamax <= 0 && axis.datamax != axis.datamin) axis.max = 0;
+ axis.max = axis.tickSize * Math.ceil(axis.max / axis.tickSize);
+ }
+
+ if (axis.min == axis.max) axis.max = axis.min + 1;
+ },
+
+ calculateTextDimensions : function (T, options) {
+
+ var maxLabel = '',
+ length,
+ i;
+
+ if (this.options.showLabels) {
+ for (i = 0; i < this.ticks.length; ++i) {
+ length = this.ticks[i].label.length;
+ if (length > maxLabel.length){
+ maxLabel = this.ticks[i].label;
+ }
+ }
+ }
+
+ this.maxLabel = T.dimensions(
+ maxLabel,
+ {size:options.fontSize, angle: Flotr.toRad(this.options.labelsAngle)},
+ 'font-size:smaller;',
+ 'flotr-grid-label'
+ );
+
+ this.titleSize = T.dimensions(
+ this.options.title,
+ {size:options.fontSize*1.2, angle: Flotr.toRad(this.options.titleAngle)},
+ 'font-weight:bold;',
+ 'flotr-axis-title'
+ );
+ },
+
+ _cleanUserTicks : function (ticks, axisTicks) {
+
+ var axis = this, options = this.options,
+ v, i, label, tick;
+
+ if(_.isFunction(ticks)) ticks = ticks({min : axis.min, max : axis.max});
+
+ for(i = 0; i < ticks.length; ++i){
+ tick = ticks[i];
+ if(typeof(tick) === 'object'){
+ v = tick[0];
+ label = (tick.length > 1) ? tick[1] : options.tickFormatter(v, {min : axis.min, max : axis.max});
+ } else {
+ v = tick;
+ label = options.tickFormatter(v, {min : this.min, max : this.max});
+ }
+ axisTicks[i] = { v: v, label: label };
+ }
+ },
+
+ _calculateTimeTicks : function () {
+ this.ticks = Flotr.Date.generator(this);
+ },
+
+ _calculateLogTicks : function () {
+
+ var axis = this,
+ o = axis.options,
+ v,
+ decadeStart;
+
+ var max = Math.log(axis.max);
+ if (o.base != Math.E) max /= Math.log(o.base);
+ max = Math.ceil(max);
+
+ var min = Math.log(axis.min);
+ if (o.base != Math.E) min /= Math.log(o.base);
+ min = Math.ceil(min);
+
+ for (i = min; i < max; i += axis.tickSize) {
+ decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i);
+ // Next decade begins here:
+ var decadeEnd = decadeStart * ((o.base == Math.E) ? Math.exp(axis.tickSize) : Math.pow(o.base, axis.tickSize));
+ var stepSize = (decadeEnd - decadeStart) / o.minorTickFreq;
+
+ axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})});
+ for (v = decadeStart + stepSize; v < decadeEnd; v += stepSize)
+ axis.minorTicks.push({v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max})});
+ }
+
+ // Always show the value at the would-be start of next decade (end of this decade)
+ decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i);
+ axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})});
+ },
+
+ _calculateTicks : function () {
+
+ var axis = this,
+ o = axis.options,
+ tickSize = axis.tickSize,
+ min = axis.min,
+ max = axis.max,
+ start = tickSize * Math.ceil(min / tickSize), // Round to nearest multiple of tick size.
+ decimals,
+ minorTickSize,
+ v, v2,
+ i, j;
+
+ if (o.minorTickFreq)
+ minorTickSize = tickSize / o.minorTickFreq;
+
+ // Then store all possible ticks.
+ for (i = 0; (v = v2 = start + i * tickSize) <= max; ++i){
+
+ // Round (this is always needed to fix numerical instability).
+ decimals = o.tickDecimals;
+ if (decimals === null) decimals = 1 - Math.floor(Math.log(tickSize) / Math.LN10);
+ if (decimals < 0) decimals = 0;
+
+ v = v.toFixed(decimals);
+ axis.ticks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) });
+
+ if (o.minorTickFreq) {
+ for (j = 0; j < o.minorTickFreq && (i * tickSize + j * minorTickSize) < max; ++j) {
+ v = v2 + j * minorTickSize;
+ axis.minorTicks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) });
+ }
+ }
+ }
+
+ }
+};
+
+
+// Static Methods
+_.extend(Axis, {
+ getAxes : function (options) {
+ return {
+ x: new Axis({options: options.xaxis, n: 1, length: this.plotWidth}),
+ x2: new Axis({options: options.x2axis, n: 2, length: this.plotWidth}),
+ y: new Axis({options: options.yaxis, n: 1, length: this.plotHeight, offset: this.plotHeight, orientation: -1}),
+ y2: new Axis({options: options.y2axis, n: 2, length: this.plotHeight, offset: this.plotHeight, orientation: -1})
+ };
+ }
+});
+
+
+// Helper Methods
+
+
+function log (value, base) {
+ value = Math.log(Math.max(value, Number.MIN_VALUE));
+ if (base !== Math.E)
+ value /= Math.log(base);
+ return value;
+}
+
+function exp (value, base) {
+ return (base === Math.E) ? Math.exp(value) : Math.pow(base, value);
+}
+
+Flotr.Axis = Axis;
+
+})();
+
+/**
+ * Flotr Series Library
+ */
+
+(function () {
+
+var
+ _ = Flotr._;
+
+function Series (o) {
+ _.extend(this, o);
+}
+
+Series.prototype = {
+
+ getRange: function () {
+
+ var
+ data = this.data,
+ length = data.length,
+ xmin = Number.MAX_VALUE,
+ ymin = Number.MAX_VALUE,
+ xmax = -Number.MAX_VALUE,
+ ymax = -Number.MAX_VALUE,
+ xused = false,
+ yused = false,
+ x, y, i;
+
+ if (length < 0 || this.hide) return false;
+
+ for (i = 0; i < length; i++) {
+ x = data[i][0];
+ y = data[i][1];
+ if (x !== null) {
+ if (x < xmin) { xmin = x; xused = true; }
+ if (x > xmax) { xmax = x; xused = true; }
+ }
+ if (y !== null) {
+ if (y < ymin) { ymin = y; yused = true; }
+ if (y > ymax) { ymax = y; yused = true; }
+ }
+ }
+
+ return {
+ xmin : xmin,
+ xmax : xmax,
+ ymin : ymin,
+ ymax : ymax,
+ xused : xused,
+ yused : yused
+ };
+ }
+};
+
+_.extend(Series, {
+ /**
+ * Collects dataseries from input and parses the series into the right format. It returns an Array
+ * of Objects each having at least the 'data' key set.
+ * @param {Array, Object} data - Object or array of dataseries
+ * @return {Array} Array of Objects parsed into the right format ({(...,) data: [[x1,y1], [x2,y2], ...] (, ...)})
+ */
+ getSeries: function(data){
+ return _.map(data, function(s){
+ var series;
+ if (s.data) {
+ series = new Series();
+ _.extend(series, s);
+ } else {
+ series = new Series({data:s});
+ }
+ return series;
+ });
+ }
+});
+
+Flotr.Series = Series;
+
+})();
+
+/** Lines **/
+Flotr.addType('lines', {
+ options: {
+ show: false, // => setting to true will show lines, false will hide
+ lineWidth: 2, // => line width in pixels
+ fill: false, // => true to fill the area from the line to the x axis, false for (transparent) no fill
+ fillBorder: false, // => draw a border around the fill
+ fillColor: null, // => fill color
+ fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
+ steps: false, // => draw steps
+ stacked: false // => setting to true will show stacked lines, false will show normal lines
+ },
+
+ stack : {
+ values : []
+ },
+
+ /**
+ * Draws lines series in the canvas element.
+ * @param {Object} options
+ */
+ draw : function (options) {
+
+ var
+ context = options.context,
+ lineWidth = options.lineWidth,
+ shadowSize = options.shadowSize,
+ offset;
+
+ context.save();
+ context.lineJoin = 'round';
+
+ if (shadowSize) {
+
+ context.lineWidth = shadowSize / 2;
+ offset = lineWidth / 2 + context.lineWidth / 2;
+
+ // @TODO do this instead with a linear gradient
+ context.strokeStyle = "rgba(0,0,0,0.1)";
+ this.plot(options, offset + shadowSize / 2, false);
+
+ context.strokeStyle = "rgba(0,0,0,0.2)";
+ this.plot(options, offset, false);
+ }
+
+ context.lineWidth = lineWidth;
+ context.strokeStyle = options.color;
+
+ this.plot(options, 0, true);
+
+ context.restore();
+ },
+
+ plot : function (options, shadowOffset, incStack) {
+
+ var
+ context = options.context,
+ width = options.width,
+ height = options.height,
+ xScale = options.xScale,
+ yScale = options.yScale,
+ data = options.data,
+ stack = options.stacked ? this.stack : false,
+ length = data.length - 1,
+ prevx = null,
+ prevy = null,
+ zero = yScale(0),
+ start = null,
+ x1, x2, y1, y2, stack1, stack2, i;
+
+ if (length < 1) return;
+
+ context.beginPath();
+
+ for (i = 0; i < length; ++i) {
+
+ // To allow empty values
+ if (data[i][1] === null || data[i+1][1] === null) {
+ if (options.fill) {
+ if (i > 0 && data[i][1] !== null) {
+ context.stroke();
+ fill();
+ start = null;
+ context.closePath();
+ context.beginPath();
+ }
+ }
+ continue;
+ }
+
+ // Zero is infinity for log scales
+ // TODO handle zero for logarithmic
+ // if (xa.options.scaling === 'logarithmic' && (data[i][0] <= 0 || data[i+1][0] <= 0)) continue;
+ // if (ya.options.scaling === 'logarithmic' && (data[i][1] <= 0 || data[i+1][1] <= 0)) continue;
+
+ x1 = xScale(data[i][0]);
+ x2 = xScale(data[i+1][0]);
+
+ if (start === null) start = data[i];
+
+ if (stack) {
+ stack1 = stack.values[data[i][0]] || 0;
+ stack2 = stack.values[data[i+1][0]] || stack.values[data[i][0]] || 0;
+ y1 = yScale(data[i][1] + stack1);
+ y2 = yScale(data[i+1][1] + stack2);
+ if (incStack) {
+ data[i].y0 = stack1;
+ stack.values[data[i][0]] = data[i][1] + stack1;
+ if (i == length-1) {
+ data[i+1].y0 = stack2;
+ stack.values[data[i+1][0]] = data[i+1][1] + stack2;
+ }
+ }
+ } else {
+ y1 = yScale(data[i][1]);
+ y2 = yScale(data[i+1][1]);
+ }
+
+ if (
+ (y1 > height && y2 > height) ||
+ (y1 < 0 && y2 < 0) ||
+ (x1 < 0 && x2 < 0) ||
+ (x1 > width && x2 > width)
+ ) continue;
+
+ if ((prevx != x1) || (prevy != y1 + shadowOffset)) {
+ context.moveTo(x1, y1 + shadowOffset);
+ }
+
+ prevx = x2;
+ prevy = y2 + shadowOffset;
+ if (options.steps) {
+ context.lineTo(prevx + shadowOffset / 2, y1 + shadowOffset);
+ context.lineTo(prevx + shadowOffset / 2, prevy);
+ } else {
+ context.lineTo(prevx, prevy);
+ }
+ }
+
+ if (!options.fill || options.fill && !options.fillBorder) context.stroke();
+
+ fill();
+
+ function fill () {
+ // TODO stacked lines
+ if(!shadowOffset && options.fill && start){
+ x1 = xScale(start[0]);
+ context.fillStyle = options.fillStyle;
+ context.lineTo(x2, zero);
+ context.lineTo(x1, zero);
+ context.lineTo(x1, yScale(start[1]));
+ context.fill();
+ if (options.fillBorder) {
+ context.stroke();
+ }
+ }
+ }
+
+ context.closePath();
+ },
+
+ // Perform any pre-render precalculations (this should be run on data first)
+ // - Pie chart total for calculating measures
+ // - Stacks for lines and bars
+ // precalculate : function () {
+ // }
+ //
+ //
+ // Get any bounds after pre calculation (axis can fetch this if does not have explicit min/max)
+ // getBounds : function () {
+ // }
+ // getMin : function () {
+ // }
+ // getMax : function () {
+ // }
+ //
+ //
+ // Padding around rendered elements
+ // getPadding : function () {
+ // }
+
+ extendYRange : function (axis, data, options, lines) {
+
+ var o = axis.options;
+
+ // If stacked and auto-min
+ if (options.stacked && ((!o.max && o.max !== 0) || (!o.min && o.min !== 0))) {
+
+ var
+ newmax = axis.max,
+ newmin = axis.min,
+ positiveSums = lines.positiveSums || {},
+ negativeSums = lines.negativeSums || {},
+ x, j;
+
+ for (j = 0; j < data.length; j++) {
+
+ x = data[j][0] + '';
+
+ // Positive
+ if (data[j][1] > 0) {
+ positiveSums[x] = (positiveSums[x] || 0) + data[j][1];
+ newmax = Math.max(newmax, positiveSums[x]);
+ }
+
+ // Negative
+ else {
+ negativeSums[x] = (negativeSums[x] || 0) + data[j][1];
+ newmin = Math.min(newmin, negativeSums[x]);
+ }
+ }
+
+ lines.negativeSums = negativeSums;
+ lines.positiveSums = positiveSums;
+
+ axis.max = newmax;
+ axis.min = newmin;
+ }
+
+ if (options.steps) {
+
+ this.hit = function (options) {
+ var
+ data = options.data,
+ args = options.args,
+ yScale = options.yScale,
+ mouse = args[0],
+ length = data.length,
+ n = args[1],
+ x = options.xInverse(mouse.relX),
+ relY = mouse.relY,
+ i;
+
+ for (i = 0; i < length - 1; i++) {
+ if (x >= data[i][0] && x <= data[i+1][0]) {
+ if (Math.abs(yScale(data[i][1]) - relY) < 8) {
+ n.x = data[i][0];
+ n.y = data[i][1];
+ n.index = i;
+ n.seriesIndex = options.index;
+ }
+ break;
+ }
+ }
+ };
+
+ this.drawHit = function (options) {
+ var
+ context = options.context,
+ args = options.args,
+ data = options.data,
+ xScale = options.xScale,
+ index = args.index,
+ x = xScale(args.x),
+ y = options.yScale(args.y),
+ x2;
+
+ if (data.length - 1 > index) {
+ x2 = options.xScale(data[index + 1][0]);
+ context.save();
+ context.strokeStyle = options.color;
+ context.lineWidth = options.lineWidth;
+ context.beginPath();
+ context.moveTo(x, y);
+ context.lineTo(x2, y);
+ context.stroke();
+ context.closePath();
+ context.restore();
+ }
+ };
+
+ this.clearHit = function (options) {
+ var
+ context = options.context,
+ args = options.args,
+ data = options.data,
+ xScale = options.xScale,
+ width = options.lineWidth,
+ index = args.index,
+ x = xScale(args.x),
+ y = options.yScale(args.y),
+ x2;
+
+ if (data.length - 1 > index) {
+ x2 = options.xScale(data[index + 1][0]);
+ context.clearRect(x - width, y - width, x2 - x + 2 * width, 2 * width);
+ }
+ };
+ }
+ }
+
+});
+
+/** Bars **/
+Flotr.addType('bars', {
+
+ options: {
+ show: false, // => setting to true will show bars, false will hide
+ lineWidth: 2, // => in pixels
+ barWidth: 1, // => in units of the x axis
+ fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
+ fillColor: null, // => fill color
+ fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
+ horizontal: false, // => horizontal bars (x and y inverted)
+ stacked: false, // => stacked bar charts
+ centered: true, // => center the bars to their x axis value
+ topPadding: 0.1, // => top padding in percent
+ grouped: false // => groups bars together which share x value, hit not supported.
+ },
+
+ stack : {
+ positive : [],
+ negative : [],
+ _positive : [], // Shadow
+ _negative : [] // Shadow
+ },
+
+ draw : function (options) {
+ var
+ context = options.context;
+
+ this.current += 1;
+
+ context.save();
+ context.lineJoin = 'miter';
+ // @TODO linewidth not interpreted the right way.
+ context.lineWidth = options.lineWidth;
+ context.strokeStyle = options.color;
+ if (options.fill) context.fillStyle = options.fillStyle;
+
+ this.plot(options);
+
+ context.restore();
+ },
+
+ plot : function (options) {
+
+ var
+ data = options.data,
+ context = options.context,
+ shadowSize = options.shadowSize,
+ i, geometry, left, top, width, height;
+
+ if (data.length < 1) return;
+
+ this.translate(context, options.horizontal);
+
+ for (i = 0; i < data.length; i++) {
+
+ geometry = this.getBarGeometry(data[i][0], data[i][1], options);
+ if (geometry === null) continue;
+
+ left = geometry.left;
+ top = geometry.top;
+ width = geometry.width;
+ height = geometry.height;
+
+ if (options.fill) context.fillRect(left, top, width, height);
+ if (shadowSize) {
+ context.save();
+ context.fillStyle = 'rgba(0,0,0,0.05)';
+ context.fillRect(left + shadowSize, top + shadowSize, width, height);
+ context.restore();
+ }
+ if (options.lineWidth) {
+ context.strokeRect(left, top, width, height);
+ }
+ }
+ },
+
+ translate : function (context, horizontal) {
+ if (horizontal) {
+ context.rotate(-Math.PI / 2);
+ context.scale(-1, 1);
+ }
+ },
+
+ getBarGeometry : function (x, y, options) {
+
+ var
+ horizontal = options.horizontal,
+ barWidth = options.barWidth,
+ centered = options.centered,
+ stack = options.stacked ? this.stack : false,
+ lineWidth = options.lineWidth,
+ bisection = centered ? barWidth / 2 : 0,
+ xScale = horizontal ? options.yScale : options.xScale,
+ yScale = horizontal ? options.xScale : options.yScale,
+ xValue = horizontal ? y : x,
+ yValue = horizontal ? x : y,
+ stackOffset = 0,
+ stackValue, left, right, top, bottom;
+
+ if (options.grouped) {
+ this.current / this.groups;
+ xValue = xValue - bisection;
+ barWidth = barWidth / this.groups;
+ bisection = barWidth / 2;
+ xValue = xValue + barWidth * this.current - bisection;
+ }
+
+ // Stacked bars
+ if (stack) {
+ stackValue = yValue > 0 ? stack.positive : stack.negative;
+ stackOffset = stackValue[xValue] || stackOffset;
+ stackValue[xValue] = stackOffset + yValue;
+ }
+
+ left = xScale(xValue - bisection);
+ right = xScale(xValue + barWidth - bisection);
+ top = yScale(yValue + stackOffset);
+ bottom = yScale(stackOffset);
+
+ // TODO for test passing... probably looks better without this
+ if (bottom < 0) bottom = 0;
+
+ // TODO Skipping...
+ // if (right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) continue;
+
+ return (x === null || y === null) ? null : {
+ x : xValue,
+ y : yValue,
+ xScale : xScale,
+ yScale : yScale,
+ top : top,
+ left : Math.min(left, right) - lineWidth / 2,
+ width : Math.abs(right - left) - lineWidth,
+ height : bottom - top
+ };
+ },
+
+ hit : function (options) {
+ var
+ data = options.data,
+ args = options.args,
+ mouse = args[0],
+ n = args[1],
+ x = options.xInverse(mouse.relX),
+ y = options.yInverse(mouse.relY),
+ hitGeometry = this.getBarGeometry(x, y, options),
+ width = hitGeometry.width / 2,
+ left = hitGeometry.left,
+ height = hitGeometry.y,
+ geometry, i;
+
+ for (i = data.length; i--;) {
+ geometry = this.getBarGeometry(data[i][0], data[i][1], options);
+ if (
+ // Height:
+ (
+ // Positive Bars:
+ (height > 0 && height < geometry.y) ||
+ // Negative Bars:
+ (height < 0 && height > geometry.y)
+ ) &&
+ // Width:
+ (Math.abs(left - geometry.left) < width)
+ ) {
+ n.x = data[i][0];
+ n.y = data[i][1];
+ n.index = i;
+ n.seriesIndex = options.index;
+ }
+ }
+ },
+
+ drawHit : function (options) {
+ // TODO hits for stacked bars; implement using calculateStack option?
+ var
+ context = options.context,
+ args = options.args,
+ geometry = this.getBarGeometry(args.x, args.y, options),
+ left = geometry.left,
+ top = geometry.top,
+ width = geometry.width,
+ height = geometry.height;
+
+ context.save();
+ context.strokeStyle = options.color;
+ context.lineWidth = options.lineWidth;
+ this.translate(context, options.horizontal);
+
+ // Draw highlight
+ context.beginPath();
+ context.moveTo(left, top + height);
+ context.lineTo(left, top);
+ context.lineTo(left + width, top);
+ context.lineTo(left + width, top + height);
+ if (options.fill) {
+ context.fillStyle = options.fillStyle;
+ context.fill();
+ }
+ context.stroke();
+ context.closePath();
+
+ context.restore();
+ },
+
+ clearHit: function (options) {
+ var
+ context = options.context,
+ args = options.args,
+ geometry = this.getBarGeometry(args.x, args.y, options),
+ left = geometry.left,
+ width = geometry.width,
+ top = geometry.top,
+ height = geometry.height,
+ lineWidth = 2 * options.lineWidth;
+
+ context.save();
+ this.translate(context, options.horizontal);
+ context.clearRect(
+ left - lineWidth,
+ Math.min(top, top + height) - lineWidth,
+ width + 2 * lineWidth,
+ Math.abs(height) + 2 * lineWidth
+ );
+ context.restore();
+ },
+
+ extendXRange : function (axis, data, options, bars) {
+ this._extendRange(axis, data, options, bars);
+ this.groups = (this.groups + 1) || 1;
+ this.current = 0;
+ },
+
+ extendYRange : function (axis, data, options, bars) {
+ this._extendRange(axis, data, options, bars);
+ },
+ _extendRange: function (axis, data, options, bars) {
+
+ var
+ max = axis.options.max;
+
+ if (_.isNumber(max) || _.isString(max)) return;
+
+ var
+ newmin = axis.min,
+ newmax = axis.max,
+ horizontal = options.horizontal,
+ orientation = axis.orientation,
+ positiveSums = this.positiveSums || {},
+ negativeSums = this.negativeSums || {},
+ value, datum, index, j;
+
+ // Sides of bars
+ if ((orientation == 1 && !horizontal) || (orientation == -1 && horizontal)) {
+ if (options.centered) {
+ newmax = Math.max(axis.datamax + options.barWidth, newmax);
+ newmin = Math.min(axis.datamin - options.barWidth, newmin);
+ }
+ }
+
+ if (options.stacked &&
+ ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal))){
+
+ for (j = data.length; j--;) {
+ value = data[j][(orientation == 1 ? 1 : 0)]+'';
+ datum = data[j][(orientation == 1 ? 0 : 1)];
+
+ // Positive
+ if (datum > 0) {
+ positiveSums[value] = (positiveSums[value] || 0) + datum;
+ newmax = Math.max(newmax, positiveSums[value]);
+ }
+
+ // Negative
+ else {
+ negativeSums[value] = (negativeSums[value] || 0) + datum;
+ newmin = Math.min(newmin, negativeSums[value]);
+ }
+ }
+ }
+
+ // End of bars
+ if ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal)) {
+ if (options.topPadding && (axis.max === axis.datamax || (options.stacked && this.stackMax !== newmax))) {
+ newmax += options.topPadding * (newmax - newmin);
+ }
+ }
+
+ this.stackMin = newmin;
+ this.stackMax = newmax;
+ this.negativeSums = negativeSums;
+ this.positiveSums = positiveSums;
+
+ axis.max = newmax;
+ axis.min = newmin;
+ }
+
+});
+
+/** Bubbles **/
+Flotr.addType('bubbles', {
+ options: {
+ show: false, // => setting to true will show radar chart, false will hide
+ lineWidth: 2, // => line width in pixels
+ fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
+ fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
+ baseRadius: 2 // => ratio of the radar, against the plot size
+ },
+ draw : function (options) {
+ var
+ context = options.context,
+ shadowSize = options.shadowSize;
+
+ context.save();
+ context.lineWidth = options.lineWidth;
+
+ // Shadows
+ context.fillStyle = 'rgba(0,0,0,0.05)';
+ context.strokeStyle = 'rgba(0,0,0,0.05)';
+ this.plot(options, shadowSize / 2);
+ context.strokeStyle = 'rgba(0,0,0,0.1)';
+ this.plot(options, shadowSize / 4);
+
+ // Chart
+ context.strokeStyle = options.color;
+ context.fillStyle = options.fillStyle;
+ this.plot(options);
+
+ context.restore();
+ },
+ plot : function (options, offset) {
+
+ var
+ data = options.data,
+ context = options.context,
+ geometry,
+ i, x, y, z;
+
+ offset = offset || 0;
+
+ for (i = 0; i < data.length; ++i){
+
+ geometry = this.getGeometry(data[i], options);
+
+ context.beginPath();
+ context.arc(geometry.x + offset, geometry.y + offset, geometry.z, 0, 2 * Math.PI, true);
+ context.stroke();
+ if (options.fill) context.fill();
+ context.closePath();
+ }
+ },
+ getGeometry : function (point, options) {
+ return {
+ x : options.xScale(point[0]),
+ y : options.yScale(point[1]),
+ z : point[2] * options.baseRadius
+ };
+ },
+ hit : function (options) {
+ var
+ data = options.data,
+ args = options.args,
+ mouse = args[0],
+ n = args[1],
+ relX = mouse.relX,
+ relY = mouse.relY,
+ distance,
+ geometry,
+ dx, dy;
+
+ n.best = n.best || Number.MAX_VALUE;
+
+ for (i = data.length; i--;) {
+ geometry = this.getGeometry(data[i], options);
+
+ dx = geometry.x - relX;
+ dy = geometry.y - relY;
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance < geometry.z && geometry.z < n.best) {
+ n.x = data[i][0];
+ n.y = data[i][1];
+ n.index = i;
+ n.seriesIndex = options.index;
+ n.best = geometry.z;
+ }
+ }
+ },
+ drawHit : function (options) {
+
+ var
+ context = options.context,
+ geometry = this.getGeometry(options.data[options.args.index], options);
+
+ context.save();
+ context.lineWidth = options.lineWidth;
+ context.fillStyle = options.fillStyle;
+ context.strokeStyle = options.color;
+ context.beginPath();
+ context.arc(geometry.x, geometry.y, geometry.z, 0, 2 * Math.PI, true);
+ context.fill();
+ context.stroke();
+ context.closePath();
+ context.restore();
+ },
+ clearHit : function (options) {
+
+ var
+ context = options.context,
+ geometry = this.getGeometry(options.data[options.args.index], options),
+ offset = geometry.z + options.lineWidth;
+
+ context.save();
+ context.clearRect(
+ geometry.x - offset,
+ geometry.y - offset,
+ 2 * offset,
+ 2 * offset
+ );
+ context.restore();
+ }
+ // TODO Add a hit calculation method (like pie)
+});
+
+/** Candles **/
+Flotr.addType('candles', {
+ options: {
+ show: false, // => setting to true will show candle sticks, false will hide
+ lineWidth: 1, // => in pixels
+ wickLineWidth: 1, // => in pixels
+ candleWidth: 0.6, // => in units of the x axis
+ fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
+ upFillColor: '#00A8F0',// => up sticks fill color
+ downFillColor: '#CB4B4B',// => down sticks fill color
+ fillOpacity: 0.5, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
+ barcharts: false // => draw as barcharts (not standard bars but financial barcharts)
+ },
+
+ draw : function (options) {
+
+ var
+ context = options.context;
+
+ context.save();
+ context.lineJoin = 'miter';
+ context.lineCap = 'butt';
+ // @TODO linewidth not interpreted the right way.
+ context.lineWidth = options.wickLineWidth || options.lineWidth;
+
+ this.plot(options);
+
+ context.restore();
+ },
+
+ plot : function (options) {
+
+ var
+ data = options.data,
+ context = options.context,
+ xScale = options.xScale,
+ yScale = options.yScale,
+ width = options.candleWidth / 2,
+ shadowSize = options.shadowSize,
+ lineWidth = options.lineWidth,
+ wickLineWidth = options.wickLineWidth,
+ pixelOffset = (wickLineWidth % 2) / 2,
+ color,
+ datum, x, y,
+ open, high, low, close,
+ left, right, bottom, top, bottom2, top2, reverseLines,
+ i;
+
+ if (data.length < 1) return;
+
+ for (i = 0; i < data.length; i++) {
+ datum = data[i];
+ x = datum[0];
+ open = datum[1];
+ high = datum[2];
+ low = datum[3];
+ close = datum[4];
+ left = xScale(x - width);
+ right = xScale(x + width);
+ bottom = yScale(low);
+ top = yScale(high);
+ bottom2 = yScale(Math.min(open, close));
+ top2 = yScale(Math.max(open, close));
+
+ /*
+ // TODO skipping
+ if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)
+ continue;
+ */
+
+ color = options[open > close ? 'downFillColor' : 'upFillColor'];
+
+ // Fill the candle.
+ if (options.fill && !options.barcharts) {
+ context.fillStyle = 'rgba(0,0,0,0.05)';
+ context.fillRect(left + shadowSize, top2 + shadowSize, right - left, bottom2 - top2);
+ context.save();
+ context.globalAlpha = options.fillOpacity;
+ context.fillStyle = color;
+ context.fillRect(left, top2 + lineWidth, right - left, bottom2 - top2);
+ context.restore();
+ }
+
+ // Draw candle outline/border, high, low.
+ if (lineWidth || wickLineWidth) {
+
+ x = Math.floor((left + right) / 2) + pixelOffset;
+
+ context.strokeStyle = color;
+ context.beginPath();
+
+ if (options.barcharts) {
+ context.moveTo(x, Math.floor(top + lineWidth));
+ context.lineTo(x, Math.floor(bottom + lineWidth));
+
+ reverseLines = open < close;
+ context.moveTo(reverseLines ? right : left, Math.floor(top2 + lineWidth));
+ context.lineTo(x, Math.floor(top2 + lineWidth));
+ context.moveTo(x, Math.floor(bottom2 + lineWidth));
+ context.lineTo(reverseLines ? left : right, Math.floor(bottom2 + lineWidth));
+ } else {
+ context.strokeRect(left, top2 + lineWidth, right - left, bottom2 - top2);
+ context.moveTo(x, Math.floor(top2 + lineWidth));
+ context.lineTo(x, Math.floor(top + lineWidth));
+ context.moveTo(x, Math.floor(bottom2 + lineWidth));
+ context.lineTo(x, Math.floor(bottom + lineWidth));
+ }
+
+ context.closePath();
+ context.stroke();
+ }
+ }
+ },
+
+ hit : function (options) {
+ var
+ xScale = options.xScale,
+ yScale = options.yScale,
+ data = options.data,
+ args = options.args,
+ mouse = args[0],
+ width = options.candleWidth / 2,
+ n = args[1],
+ x = mouse.relX,
+ y = mouse.relY,
+ length = data.length,
+ i, datum,
+ high, low,
+ left, right, top, bottom;
+
+ for (i = 0; i < length; i++) {
+ datum = data[i],
+ high = datum[2];
+ low = datum[3];
+ left = xScale(datum[0] - width);
+ right = xScale(datum[0] + width);
+ bottom = yScale(low);
+ top = yScale(high);
+
+ if (x > left && x < right && y > top && y < bottom) {
+ n.x = datum[0];
+ n.index = i;
+ n.seriesIndex = options.index;
+ return;
+ }
+ }
+ },
+
+ drawHit : function (options) {
+ var
+ context = options.context;
+ context.save();
+ this.plot(
+ _.defaults({
+ fill : !!options.fillColor,
+ upFillColor : options.color,
+ downFillColor : options.color,
+ data : [options.data[options.args.index]]
+ }, options)
+ );
+ context.restore();
+ },
+
+ clearHit : function (options) {
+ var
+ args = options.args,
+ context = options.context,
+ xScale = options.xScale,
+ yScale = options.yScale,
+ lineWidth = options.lineWidth,
+ width = options.candleWidth / 2,
+ bar = options.data[args.index],
+ left = xScale(bar[0] - width) - lineWidth,
+ right = xScale(bar[0] + width) + lineWidth,
+ top = yScale(bar[2]),
+ bottom = yScale(bar[3]) + lineWidth;
+ context.clearRect(left, top, right - left, bottom - top);
+ },
+
+ extendXRange: function (axis, data, options) {
+ if (axis.options.max === null) {
+ axis.max = Math.max(axis.datamax + 0.5, axis.max);
+ axis.min = Math.min(axis.datamin - 0.5, axis.min);
+ }
+ }
+});
+
+/** Gantt
+ * Base on data in form [s,y,d] where:
+ * y - executor or simply y value
+ * s - task start value
+ * d - task duration
+ * **/
+Flotr.addType('gantt', {
+ options: {
+ show: false, // => setting to true will show gantt, false will hide
+ lineWidth: 2, // => in pixels
+ barWidth: 1, // => in units of the x axis
+ fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
+ fillColor: null, // => fill color
+ fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
+ centered: true // => center the bars to their x axis value
+ },
+ /**
+ * Draws gantt series in the canvas element.
+ * @param {Object} series - Series with options.gantt.show = true.
+ */
+ draw: function(series) {
+ var ctx = this.ctx,
+ bw = series.gantt.barWidth,
+ lw = Math.min(series.gantt.lineWidth, bw);
+
+ ctx.save();
+ ctx.translate(this.plotOffset.left, this.plotOffset.top);
+ ctx.lineJoin = 'miter';
+
+ /**
+ * @todo linewidth not interpreted the right way.
+ */
+ ctx.lineWidth = lw;
+ ctx.strokeStyle = series.color;
+
+ ctx.save();
+ this.gantt.plotShadows(series, bw, 0, series.gantt.fill);
+ ctx.restore();
+
+ if(series.gantt.fill){
+ var color = series.gantt.fillColor || series.color;
+ ctx.fillStyle = this.processColor(color, {opacity: series.gantt.fillOpacity});
+ }
+
+ this.gantt.plot(series, bw, 0, series.gantt.fill);
+ ctx.restore();
+ },
+ plot: function(series, barWidth, offset, fill){
+ var data = series.data;
+ if(data.length < 1) return;
+
+ var xa = series.xaxis,
+ ya = series.yaxis,
+ ctx = this.ctx, i;
+
+ for(i = 0; i < data.length; i++){
+ var y = data[i][0],
+ s = data[i][1],
+ d = data[i][2],
+ drawLeft = true, drawTop = true, drawRight = true;
+
+ if (s === null || d === null) continue;
+
+ var left = s,
+ right = s + d,
+ bottom = y - (series.gantt.centered ? barWidth/2 : 0),
+ top = y + barWidth - (series.gantt.centered ? barWidth/2 : 0);
+
+ if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)
+ continue;
+
+ if(left < xa.min){
+ left = xa.min;
+ drawLeft = false;
+ }
+
+ if(right > xa.max){
+ right = xa.max;
+ if (xa.lastSerie != series)
+ drawTop = false;
+ }
+
+ if(bottom < ya.min)
+ bottom = ya.min;
+
+ if(top > ya.max){
+ top = ya.max;
+ if (ya.lastSerie != series)
+ drawTop = false;
+ }
+
+ /**
+ * Fill the bar.
+ */
+ if(fill){
+ ctx.beginPath();
+ ctx.moveTo(xa.d2p(left), ya.d2p(bottom) + offset);
+ ctx.lineTo(xa.d2p(left), ya.d2p(top) + offset);
+ ctx.lineTo(xa.d2p(right), ya.d2p(top) + offset);
+ ctx.lineTo(xa.d2p(right), ya.d2p(bottom) + offset);
+ ctx.fill();
+ ctx.closePath();
+ }
+
+ /**
+ * Draw bar outline/border.
+ */
+ if(series.gantt.lineWidth && (drawLeft || drawRight || drawTop)){
+ ctx.beginPath();
+ ctx.moveTo(xa.d2p(left), ya.d2p(bottom) + offset);
+
+ ctx[drawLeft ?'lineTo':'moveTo'](xa.d2p(left), ya.d2p(top) + offset);
+ ctx[drawTop ?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(top) + offset);
+ ctx[drawRight?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(bottom) + offset);
+
+ ctx.stroke();
+ ctx.closePath();
+ }
+ }
+ },
+ plotShadows: function(series, barWidth, offset){
+ var data = series.data;
+ if(data.length < 1) return;
+
+ var i, y, s, d,
+ xa = series.xaxis,
+ ya = series.yaxis,
+ ctx = this.ctx,
+ sw = this.options.shadowSize;
+
+ for(i = 0; i < data.length; i++){
+ y = data[i][0];
+ s = data[i][1];
+ d = data[i][2];
+
+ if (s === null || d === null) continue;
+
+ var left = s,
+ right = s + d,
+ bottom = y - (series.gantt.centered ? barWidth/2 : 0),
+ top = y + barWidth - (series.gantt.centered ? barWidth/2 : 0);
+
+ if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)
+ continue;
+
+ if(left < xa.min) left = xa.min;
+ if(right > xa.max) right = xa.max;
+ if(bottom < ya.min) bottom = ya.min;
+ if(top > ya.max) top = ya.max;
+
+ var width = xa.d2p(right)-xa.d2p(left)-((xa.d2p(right)+sw <= this.plotWidth) ? 0 : sw);
+ var height = ya.d2p(bottom)-ya.d2p(top)-((ya.d2p(bottom)+sw <= this.plotHeight) ? 0 : sw );
+
+ ctx.fillStyle = 'rgba(0,0,0,0.05)';
+ ctx.fillRect(Math.min(xa.d2p(left)+sw, this.plotWidth), Math.min(ya.d2p(top)+sw, this.plotHeight), width, height);
+ }
+ },
+ extendXRange: function(axis) {
+ if(axis.options.max === null){
+ var newmin = axis.min,
+ newmax = axis.max,
+ i, j, x, s, g,
+ stackedSumsPos = {},
+ stackedSumsNeg = {},
+ lastSerie = null;
+
+ for(i = 0; i < this.series.length; ++i){
+ s = this.series[i];
+ g = s.gantt;
+
+ if(g.show && s.xaxis == axis) {
+ for (j = 0; j < s.data.length; j++) {
+ if (g.show) {
+ y = s.data[j][0]+'';
+ stackedSumsPos[y] = Math.max((stackedSumsPos[y] || 0), s.data[j][1]+s.data[j][2]);
+ lastSerie = s;
+ }
+ }
+ for (j in stackedSumsPos) {
+ newmax = Math.max(stackedSumsPos[j], newmax);
+ }
+ }
+ }
+ axis.lastSerie = lastSerie;
+ axis.max = newmax;
+ axis.min = newmin;
+ }
+ },
+ extendYRange: function(axis){
+ if(axis.options.max === null){
+ var newmax = Number.MIN_VALUE,
+ newmin = Number.MAX_VALUE,
+ i, j, s, g,
+ stackedSumsPos = {},
+ stackedSumsNeg = {},
+ lastSerie = null;
+
+ for(i = 0; i < this.series.length; ++i){
+ s = this.series[i];
+ g = s.gantt;
+
+ if (g.show && !s.hide && s.yaxis == axis) {
+ var datamax = Number.MIN_VALUE, datamin = Number.MAX_VALUE;
+ for(j=0; j < s.data.length; j++){
+ datamax = Math.max(datamax,s.data[j][0]);
+ datamin = Math.min(datamin,s.data[j][0]);
+ }
+
+ if (g.centered) {
+ newmax = Math.max(datamax + 0.5, newmax);
+ newmin = Math.min(datamin - 0.5, newmin);
+ }
+ else {
+ newmax = Math.max(datamax + 1, newmax);
+ newmin = Math.min(datamin, newmin);
+ }
+ // For normal horizontal bars
+ if (g.barWidth + datamax > newmax){
+ newmax = axis.max + g.barWidth;
+ }
+ }
+ }
+ axis.lastSerie = lastSerie;
+ axis.max = newmax;
+ axis.min = newmin;
+ axis.tickSize = Flotr.getTickSize(axis.options.noTicks, newmin, newmax, axis.options.tickDecimals);
+ }
+ }
+});
+
+/** Markers **/
+/**
+ * Formats the marker labels.
+ * @param {Object} obj - Marker value Object {x:..,y:..}
+ * @return {String} Formatted marker string
+ */
+(function () {
+
+Flotr.defaultMarkerFormatter = function(obj){
+ return (Math.round(obj.y*100)/100)+'';
+};
+
+Flotr.addType('markers', {
+ options: {
+ show: false, // => setting to true will show markers, false will hide
+ lineWidth: 1, // => line width of the rectangle around the marker
+ color: '#000000', // => text color
+ fill: false, // => fill or not the marekers' rectangles
+ fillColor: "#FFFFFF", // => fill color
+ fillOpacity: 0.4, // => fill opacity
+ stroke: false, // => draw the rectangle around the markers
+ position: 'ct', // => the markers position (vertical align: b, m, t, horizontal align: l, c, r)
+ verticalMargin: 0, // => the margin between the point and the text.
+ labelFormatter: Flotr.defaultMarkerFormatter,
+ fontSize: Flotr.defaultOptions.fontSize,
+ stacked: false, // => true if markers should be stacked
+ stackingType: 'b', // => define staching behavior, (b- bars like, a - area like) (see Issue 125 for details)
+ horizontal: false // => true if markers should be horizontal (For now only in a case on horizontal stacked bars, stacks should be calculated horizontaly)
+ },
+
+ // TODO test stacked markers.
+ stack : {
+ positive : [],
+ negative : [],
+ values : []
+ },
+
+ draw : function (options) {
+
+ var
+ data = options.data,
+ context = options.context,
+ stack = options.stacked ? options.stack : false,
+ stackType = options.stackingType,
+ stackOffsetNeg,
+ stackOffsetPos,
+ stackOffset,
+ i, x, y, label;
+
+ context.save();
+ context.lineJoin = 'round';
+ context.lineWidth = options.lineWidth;
+ context.strokeStyle = 'rgba(0,0,0,0.5)';
+ context.fillStyle = options.fillStyle;
+
+ function stackPos (a, b) {
+ stackOffsetPos = stack.negative[a] || 0;
+ stackOffsetNeg = stack.positive[a] || 0;
+ if (b > 0) {
+ stack.positive[a] = stackOffsetPos + b;
+ return stackOffsetPos + b;
+ } else {
+ stack.negative[a] = stackOffsetNeg + b;
+ return stackOffsetNeg + b;
+ }
+ }
+
+ for (i = 0; i < data.length; ++i) {
+
+ x = data[i][0];
+ y = data[i][1];
+
+ if (stack) {
+ if (stackType == 'b') {
+ if (options.horizontal) y = stackPos(y, x);
+ else x = stackPos(x, y);
+ } else if (stackType == 'a') {
+ stackOffset = stack.values[x] || 0;
+ stack.values[x] = stackOffset + y;
+ y = stackOffset + y;
+ }
+ }
+
+ label = options.labelFormatter({x: x, y: y, index: i, data : data});
+ this.plot(options.xScale(x), options.yScale(y), label, options);
+ }
+ context.restore();
+ },
+ plot: function(x, y, label, options) {
+ var context = options.context;
+ if (isImage(label) && !label.complete) {
+ throw 'Marker image not loaded.';
+ } else {
+ this._plot(x, y, label, options);
+ }
+ },
+
+ _plot: function(x, y, label, options) {
+ var context = options.context,
+ margin = 2,
+ left = x,
+ top = y,
+ dim;
+
+ if (isImage(label))
+ dim = {height : label.height, width: label.width};
+ else
+ dim = options.text.canvas(label);
+
+ dim.width = Math.floor(dim.width+margin*2);
+ dim.height = Math.floor(dim.height+margin*2);
+
+ if (options.position.indexOf('c') != -1) left -= dim.width/2 + margin;
+ else if (options.position.indexOf('l') != -1) left -= dim.width;
+
+ if (options.position.indexOf('m') != -1) top -= dim.height/2 + margin;
+ else if (options.position.indexOf('t') != -1) top -= dim.height + options.verticalMargin;
+ else top += options.verticalMargin;
+
+ left = Math.floor(left)+0.5;
+ top = Math.floor(top)+0.5;
+
+ if(options.fill)
+ context.fillRect(left, top, dim.width, dim.height);
+
+ if(options.stroke)
+ context.strokeRect(left, top, dim.width, dim.height);
+
+ if (isImage(label))
+ context.drawImage(label, parseInt(left+margin, 10), parseInt(top+margin, 10));
+ else
+ Flotr.drawText(context, label, left+margin, top+margin, {textBaseline: 'top', textAlign: 'left', size: options.fontSize, color: options.color});
+ }
+});
+
+function isImage (i) {
+ return typeof i === 'object' && i.constructor && (Image ? true : i.constructor === Image);
+}
+
+})();
+
+/**
+ * Pie
+ *
+ * Formats the pies labels.
+ * @param {Object} slice - Slice object
+ * @return {String} Formatted pie label string
+ */
+(function () {
+
+var
+ _ = Flotr._;
+
+Flotr.defaultPieLabelFormatter = function (total, value) {
+ return (100 * value / total).toFixed(2)+'%';
+};
+
+Flotr.addType('pie', {
+ options: {
+ show: false, // => setting to true will show bars, false will hide
+ lineWidth: 1, // => in pixels
+ fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
+ fillColor: null, // => fill color
+ fillOpacity: 0.6, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
+ explode: 6, // => the number of pixels the splices will be far from the center
+ sizeRatio: 0.6, // => the size ratio of the pie relative to the plot
+ startAngle: Math.PI/4, // => the first slice start angle
+ labelFormatter: Flotr.defaultPieLabelFormatter,
+ pie3D: false, // => whether to draw the pie in 3 dimenstions or not (ineffective)
+ pie3DviewAngle: (Math.PI/2 * 0.8),
+ pie3DspliceThickness: 20,
+ epsilon: 0.1 // => how close do you have to get to hit empty slice
+ },
+
+ draw : function (options) {
+
+ // TODO 3D charts what?
+ var
+ data = options.data,
+ context = options.context,
+ lineWidth = options.lineWidth,
+ shadowSize = options.shadowSize,
+ sizeRatio = options.sizeRatio,
+ height = options.height,
+ width = options.width,
+ explode = options.explode,
+ color = options.color,
+ fill = options.fill,
+ fillStyle = options.fillStyle,
+ radius = Math.min(width, height) * sizeRatio / 2,
+ value = data[0][1],
+ html = [],
+ vScale = 1,//Math.cos(series.pie.viewAngle);
+ measure = Math.PI * 2 * value / this.total,
+ startAngle = this.startAngle || (2 * Math.PI * options.startAngle), // TODO: this initial startAngle is already in radians (fixing will be test-unstable)
+ endAngle = startAngle + measure,
+ bisection = startAngle + measure / 2,
+ label = options.labelFormatter(this.total, value),
+ //plotTickness = Math.sin(series.pie.viewAngle)*series.pie.spliceThickness / vScale;
+ explodeCoeff = explode + radius + 4,
+ distX = Math.cos(bisection) * explodeCoeff,
+ distY = Math.sin(bisection) * explodeCoeff,
+ textAlign = distX < 0 ? 'right' : 'left',
+ textBaseline = distY > 0 ? 'top' : 'bottom',
+ style,
+ x, y;
+
+ context.save();
+ context.translate(width / 2, height / 2);
+ context.scale(1, vScale);
+
+ x = Math.cos(bisection) * explode;
+ y = Math.sin(bisection) * explode;
+
+ // Shadows
+ if (shadowSize > 0) {
+ this.plotSlice(x + shadowSize, y + shadowSize, radius, startAngle, endAngle, context);
+ if (fill) {
+ context.fillStyle = 'rgba(0,0,0,0.1)';
+ context.fill();
+ }
+ }
+
+ this.plotSlice(x, y, radius, startAngle, endAngle, context);
+ if (fill) {
+ context.fillStyle = fillStyle;
+ context.fill();
+ }
+ context.lineWidth = lineWidth;
+ context.strokeStyle = color;
+ context.stroke();
+
+ style = {
+ size : options.fontSize * 1.2,
+ color : options.fontColor,
+ weight : 1.5
+ };
+
+ if (label) {
+ if (options.htmlText || !options.textEnabled) {
+ divStyle = 'position:absolute;' + textBaseline + ':' + (height / 2 + (textBaseline === 'top' ? distY : -distY)) + 'px;';
+ divStyle += textAlign + ':' + (width / 2 + (textAlign === 'right' ? -distX : distX)) + 'px;';
+ html.push('<div style="', divStyle, '" class="flotr-grid-label">', label, '</div>');
+ }
+ else {
+ style.textAlign = textAlign;
+ style.textBaseline = textBaseline;
+ Flotr.drawText(context, label, distX, distY, style);
+ }
+ }
+
+ if (options.htmlText || !options.textEnabled) {
+ var div = Flotr.DOM.node('<div style="color:' + options.fontColor + '" class="flotr-labels"></div>');
+ Flotr.DOM.insert(div, html.join(''));
+ Flotr.DOM.insert(options.element, div);
+ }
+
+ context.restore();
+
+ // New start angle
+ this.startAngle = endAngle;
+ this.slices = this.slices || [];
+ this.slices.push({
+ radius : radius,
+ x : x,
+ y : y,
+ explode : explode,
+ start : startAngle,
+ end : endAngle
+ });
+ },
+ plotSlice : function (x, y, radius, startAngle, endAngle, context) {
+ context.beginPath();
+ context.moveTo(x, y);
+ context.arc(x, y, radius, startAngle, endAngle, false);
+ context.lineTo(x, y);
+ context.closePath();
+ },
+ hit : function (options) {
+
+ var
+ data = options.data[0],
+ args = options.args,
+ index = options.index,
+ mouse = args[0],
+ n = args[1],
+ slice = this.slices[index],
+ x = mouse.relX - options.width / 2,
+ y = mouse.relY - options.height / 2,
+ r = Math.sqrt(x * x + y * y),
+ theta = Math.atan(y / x),
+ circle = Math.PI * 2,
+ explode = slice.explode || options.explode,
+ start = slice.start % circle,
+ end = slice.end % circle,
+ epsilon = options.epsilon;
+
+ if (x < 0) {
+ theta += Math.PI;
+ } else if (x > 0 && y < 0) {
+ theta += circle;
+ }
+
+ if (r < slice.radius + explode && r > explode) {
+ if (
+ (theta > start && theta < end) || // Normal Slice
+ (start > end && (theta < end || theta > start)) || // First slice
+ // TODO: Document the two cases at the end:
+ (start === end && ((slice.start === slice.end && Math.abs(theta - start) < epsilon) || (slice.start !== slice.end && Math.abs(theta-start) > epsilon)))
+ ) {
+
+ // TODO Decouple this from hit plugin (chart shouldn't know what n means)
+ n.x = data[0];
+ n.y = data[1];
+ n.sAngle = start;
+ n.eAngle = end;
+ n.index = 0;
+ n.seriesIndex = index;
+ n.fraction = data[1] / this.total;
+ }
+ }
+ },
+ drawHit: function (options) {
+ var
+ context = options.context,
+ slice = this.slices[options.args.seriesIndex];
+
+ context.save();
+ context.translate(options.width / 2, options.height / 2);
+ this.plotSlice(slice.x, slice.y, slice.radius, slice.start, slice.end, context);
+ context.stroke();
+ context.restore();
+ },
+ clearHit : function (options) {
+ var
+ context = options.context,
+ slice = this.slices[options.args.seriesIndex],
+ padding = 2 * options.lineWidth,
+ radius = slice.radius + padding;
+
+ context.save();
+ context.translate(options.width / 2, options.height / 2);
+ context.clearRect(
+ slice.x - radius,
+ slice.y - radius,
+ 2 * radius + padding,
+ 2 * radius + padding
+ );
+ context.restore();
+ },
+ extendYRange : function (axis, data) {
+ this.total = (this.total || 0) + data[0][1];
+ }
+});
+})();
+
+/** Points **/
+Flotr.addType('points', {
+ options: {
+ show: false, // => setting to true will show points, false will hide
+ radius: 3, // => point radius (pixels)
+ lineWidth: 2, // => line width in pixels
+ fill: true, // => true to fill the points with a color, false for (transparent) no fill
+ fillColor: '#FFFFFF', // => fill color. Null to use series color.
+ fillOpacity: 1, // => opacity of color inside the points
+ hitRadius: null // => override for points hit radius
+ },
+
+ draw : function (options) {
+ var
+ context = options.context,
+ lineWidth = options.lineWidth,
+ shadowSize = options.shadowSize;
+
+ context.save();
+
+ if (shadowSize > 0) {
+ context.lineWidth = shadowSize / 2;
+
+ context.strokeStyle = 'rgba(0,0,0,0.1)';
+ this.plot(options, shadowSize / 2 + context.lineWidth / 2);
+
+ context.strokeStyle = 'rgba(0,0,0,0.2)';
+ this.plot(options, context.lineWidth / 2);
+ }
+
+ context.lineWidth = options.lineWidth;
+ context.strokeStyle = options.color;
+ if (options.fill) context.fillStyle = options.fillStyle;
+
+ this.plot(options);
+ context.restore();
+ },
+
+ plot : function (options, offset) {
+ var
+ data = options.data,
+ context = options.context,
+ xScale = options.xScale,
+ yScale = options.yScale,
+ i, x, y;
+
+ for (i = data.length - 1; i > -1; --i) {
+ y = data[i][1];
+ if (y === null) continue;
+
+ x = xScale(data[i][0]);
+ y = yScale(y);
+
+ if (x < 0 || x > options.width || y < 0 || y > options.height) continue;
+
+ context.beginPath();
+ if (offset) {
+ context.arc(x, y + offset, options.radius, 0, Math.PI, false);
+ } else {
+ context.arc(x, y, options.radius, 0, 2 * Math.PI, true);
+ if (options.fill) context.fill();
+ }
+ context.stroke();
+ context.closePath();
+ }
+ }
+});
+
+/** Radar **/
+Flotr.addType('radar', {
+ options: {
+ show: false, // => setting to true will show radar chart, false will hide
+ lineWidth: 2, // => line width in pixels
+ fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill
+ fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
+ radiusRatio: 0.90, // => ratio of the radar, against the plot size
+ sensibility: 2 // => the lower this number, the more precise you have to aim to show a value.
+ },
+ draw : function (options) {
+ var
+ context = options.context,
+ shadowSize = options.shadowSize;
+
+ context.save();
+ context.translate(options.width / 2, options.height / 2);
+ context.lineWidth = options.lineWidth;
+
+ // Shadow
+ context.fillStyle = 'rgba(0,0,0,0.05)';
+ context.strokeStyle = 'rgba(0,0,0,0.05)';
+ this.plot(options, shadowSize / 2);
+ context.strokeStyle = 'rgba(0,0,0,0.1)';
+ this.plot(options, shadowSize / 4);
+
+ // Chart
+ context.strokeStyle = options.color;
+ context.fillStyle = options.fillStyle;
+ this.plot(options);
+
+ context.restore();
+ },
+ plot : function (options, offset) {
+ var
+ data = options.data,
+ context = options.context,
+ radius = Math.min(options.height, options.width) * options.radiusRatio / 2,
+ step = 2 * Math.PI / data.length,
+ angle = -Math.PI / 2,
+ i, ratio;
+
+ offset = offset || 0;
+
+ context.beginPath();
+ for (i = 0; i < data.length; ++i) {
+ ratio = data[i][1] / this.max;
+
+ context[i === 0 ? 'moveTo' : 'lineTo'](
+ Math.cos(i * step + angle) * radius * ratio + offset,
+ Math.sin(i * step + angle) * radius * ratio + offset
+ );
+ }
+ context.closePath();
+ if (options.fill) context.fill();
+ context.stroke();
+ },
+ getGeometry : function (point, options) {
+ var
+ radius = Math.min(options.height, options.width) * options.radiusRatio / 2,
+ step = 2 * Math.PI / options.data.length,
+ angle = -Math.PI / 2,
+ ratio = point[1] / this.max;
+
+ return {
+ x : (Math.cos(point[0] * step + angle) * radius * ratio) + options.width / 2,
+ y : (Math.sin(point[0] * step + angle) * radius * ratio) + options.height / 2
+ };
+ },
+ hit : function (options) {
+ var
+ args = options.args,
+ mouse = args[0],
+ n = args[1],
+ relX = mouse.relX,
+ relY = mouse.relY,
+ distance,
+ geometry,
+ dx, dy;
+
+ for (var i = 0; i < n.series.length; i++) {
+ var serie = n.series[i];
+ var data = serie.data;
+
+ for (var j = data.length; j--;) {
+ geometry = this.getGeometry(data[j], options);
+
+ dx = geometry.x - relX;
+ dy = geometry.y - relY;
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance < options.sensibility*2) {
+ n.x = data[j][0];
+ n.y = data[j][1];
+ n.index = j;
+ n.seriesIndex = i;
+ return n;
+ }
+ }
+ }
+ },
+ drawHit : function (options) {
+ var step = 2 * Math.PI / options.data.length;
+ var angle = -Math.PI / 2;
+ var radius = Math.min(options.height, options.width) * options.radiusRatio / 2;
+
+ var s = options.args.series;
+ var point_radius = s.points.hitRadius || s.points.radius || s.mouse.radius;
+
+ var context = options.context;
+
+ context.translate(options.width / 2, options.height / 2);
+
+ var j = options.args.index;
+ var ratio = options.data[j][1] / this.max;
+ var x = Math.cos(j * step + angle) * radius * ratio;
+ var y = Math.sin(j * step + angle) * radius * ratio;
+ context.beginPath();
+ context.arc(x, y, point_radius , 0, 2 * Math.PI, true);
+ context.closePath();
+ context.stroke();
+ },
+ clearHit : function (options) {
+ var step = 2 * Math.PI / options.data.length;
+ var angle = -Math.PI / 2;
+ var radius = Math.min(options.height, options.width) * options.radiusRatio / 2;
+
+ var context = options.context;
+
+ var
+ s = options.args.series,
+ lw = (s.points ? s.points.lineWidth : 1);
+ offset = (s.points.hitRadius || s.points.radius || s.mouse.radius) + lw;
+
+ context.translate(options.width / 2, options.height / 2);
+
+ var j = options.args.index;
+ var ratio = options.data[j][1] / this.max;
+ var x = Math.cos(j * step + angle) * radius * ratio;
+ var y = Math.sin(j * step + angle) * radius * ratio;
+ context.clearRect(x-offset,y-offset,offset*2,offset*2);
+ },
+ extendYRange : function (axis, data) {
+ this.max = Math.max(axis.max, this.max || -Number.MAX_VALUE);
+ }
+});
+
+Flotr.addType('timeline', {
+ options: {
+ show: false,
+ lineWidth: 1,
+ barWidth: 0.2,
+ fill: true,
+ fillColor: null,
+ fillOpacity: 0.4,
+ centered: true
+ },
+
+ draw : function (options) {
+
+ var
+ context = options.context;
+
+ context.save();
+ context.lineJoin = 'miter';
+ context.lineWidth = options.lineWidth;
+ context.strokeStyle = options.color;
+ context.fillStyle = options.fillStyle;
+
+ this.plot(options);
+
+ context.restore();
+ },
+
+ plot : function (options) {
+
+ var
+ data = options.data,
+ context = options.context,
+ xScale = options.xScale,
+ yScale = options.yScale,
+ barWidth = options.barWidth,
+ lineWidth = options.lineWidth,
+ i;
+
+ Flotr._.each(data, function (timeline) {
+
+ var
+ x = timeline[0],
+ y = timeline[1],
+ w = timeline[2],
+ h = barWidth,
+
+ xt = Math.ceil(xScale(x)),
+ wt = Math.ceil(xScale(x + w)) - xt,
+ yt = Math.round(yScale(y)),
+ ht = Math.round(yScale(y - h)) - yt,
+
+ x0 = xt - lineWidth / 2,
+ y0 = Math.round(yt - ht / 2) - lineWidth / 2;
+
+ context.strokeRect(x0, y0, wt, ht);
+ context.fillRect(x0, y0, wt, ht);
+
+ });
+ },
+
+ extendRange : function (series) {
+
+ var
+ data = series.data,
+ xa = series.xaxis,
+ ya = series.yaxis,
+ w = series.timeline.barWidth;
+
+ if (xa.options.min === null)
+ xa.min = xa.datamin - w / 2;
+
+ if (xa.options.max === null) {
+
+ var
+ max = xa.max;
+
+ Flotr._.each(data, function (timeline) {
+ max = Math.max(max, timeline[0] + timeline[2]);
+ }, this);
+
+ xa.max = max + w / 2;
+ }
+
+ if (ya.options.min === null)
+ ya.min = ya.datamin - w;
+ if (ya.options.min === null)
+ ya.max = ya.datamax + w;
+ }
+
+});
+
+(function () {
+
+var D = Flotr.DOM;
+
+Flotr.addPlugin('crosshair', {
+ options: {
+ mode: null, // => one of null, 'x', 'y' or 'xy'
+ color: '#FF0000', // => crosshair color
+ hideCursor: true // => hide the cursor when the crosshair is shown
+ },
+ callbacks: {
+ 'flotr:mousemove': function(e, pos) {
+ if (this.options.crosshair.mode) {
+ this.crosshair.clearCrosshair();
+ this.crosshair.drawCrosshair(pos);
+ }
+ }
+ },
+ /**
+ * Draws the selection box.
+ */
+ drawCrosshair: function(pos) {
+ var octx = this.octx,
+ options = this.options.crosshair,
+ plotOffset = this.plotOffset,
+ x = plotOffset.left + Math.round(pos.relX) + 0.5,
+ y = plotOffset.top + Math.round(pos.relY) + 0.5;
+
+ if (pos.relX < 0 || pos.relY < 0 || pos.relX > this.plotWidth || pos.relY > this.plotHeight) {
+ this.el.style.cursor = null;
+ D.removeClass(this.el, 'flotr-crosshair');
+ return;
+ }
+
+ if (options.hideCursor) {
+ this.el.style.cursor = 'none';
+ D.addClass(this.el, 'flotr-crosshair');
+ }
+
+ octx.save();
+ octx.strokeStyle = options.color;
+ octx.lineWidth = 1;
+ octx.beginPath();
+
+ if (options.mode.indexOf('x') != -1) {
+ octx.moveTo(x, plotOffset.top);
+ octx.lineTo(x, plotOffset.top + this.plotHeight);
+ }
+
+ if (options.mode.indexOf('y') != -1) {
+ octx.moveTo(plotOffset.left, y);
+ octx.lineTo(plotOffset.left + this.plotWidth, y);
+ }
+
+ octx.stroke();
+ octx.restore();
+ },
+ /**
+ * Removes the selection box from the overlay canvas.
+ */
+ clearCrosshair: function() {
+
+ var
+ plotOffset = this.plotOffset,
+ position = this.lastMousePos,
+ context = this.octx;
+
+ if (position) {
+ context.clearRect(
+ Math.round(position.relX) + plotOffset.left,
+ plotOffset.top,
+ 1,
+ this.plotHeight + 1
+ );
+ context.clearRect(
+ plotOffset.left,
+ Math.round(position.relY) + plotOffset.top,
+ this.plotWidth + 1,
+ 1
+ );
+ }
+ }
+});
+})();
+
+(function() {
+
+var
+ D = Flotr.DOM,
+ _ = Flotr._;
+
+function getImage (type, canvas, context, width, height, background) {
+
+ // TODO add scaling for w / h
+ var
+ mime = 'image/'+type,
+ data = context.getImageData(0, 0, width, height),
+ image = new Image();
+
+ context.save();
+ context.globalCompositeOperation = 'destination-over';
+ context.fillStyle = background;
+ context.fillRect(0, 0, width, height);
+ image.src = canvas.toDataURL(mime);
+ context.restore();
+
+ context.clearRect(0, 0, width, height);
+ context.putImageData(data, 0, 0);
+
+ return image;
+}
+
+Flotr.addPlugin('download', {
+
+ saveImage: function (type, width, height, replaceCanvas) {
+ var
+ grid = this.options.grid,
+ image;
+
+ if (Flotr.isIE && Flotr.isIE < 9) {
+ image = '<html><body>'+this.canvas.firstChild.innerHTML+'</body></html>';
+ return window.open().document.write(image);
+ }
+
+ if (type !== 'jpeg' && type !== 'png') return;
+
+ image = getImage(
+ type, this.canvas, this.ctx,
+ this.canvasWidth, this.canvasHeight,
+ grid && grid.backgroundColor || '#ffffff'
+ );
+
+ if (_.isElement(image) && replaceCanvas) {
+ this.download.restoreCanvas();
+ D.hide(this.canvas);
+ D.hide(this.overlay);
+ D.setStyles({position: 'absolute'});
+ D.insert(this.el, image);
+ this.saveImageElement = image;
+ } else {
+ return window.open(image.src);
+ }
+ },
+
+ restoreCanvas: function() {
+ D.show(this.canvas);
+ D.show(this.overlay);
+ if (this.saveImageElement) this.el.removeChild(this.saveImageElement);
+ this.saveImageElement = null;
+ }
+});
+
+})();
+
+(function () {
+
+var E = Flotr.EventAdapter,
+ _ = Flotr._;
+
+Flotr.addPlugin('graphGrid', {
+
+ callbacks: {
+ 'flotr:beforedraw' : function () {
+ this.graphGrid.drawGrid();
+ },
+ 'flotr:afterdraw' : function () {
+ this.graphGrid.drawOutline();
+ }
+ },
+
+ drawGrid: function(){
+
+ var
+ ctx = this.ctx,
+ options = this.options,
+ grid = options.grid,
+ verticalLines = grid.verticalLines,
+ horizontalLines = grid.horizontalLines,
+ minorVerticalLines = grid.minorVerticalLines,
+ minorHorizontalLines = grid.minorHorizontalLines,
+ plotHeight = this.plotHeight,
+ plotWidth = this.plotWidth,
+ a, v, i, j;
+
+ if(verticalLines || minorVerticalLines ||
+ horizontalLines || minorHorizontalLines){
+ E.fire(this.el, 'flotr:beforegrid', [this.axes.x, this.axes.y, options, this]);
+ }
+ ctx.save();
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = grid.tickColor;
+
+ function circularHorizontalTicks (ticks) {
+ for(i = 0; i < ticks.length; ++i){
+ var ratio = ticks[i].v / a.max;
+ for(j = 0; j <= sides; ++j){
+ ctx[j === 0 ? 'moveTo' : 'lineTo'](
+ Math.cos(j*coeff+angle)*radius*ratio,
+ Math.sin(j*coeff+angle)*radius*ratio
+ );
+ }
+ }
+ }
+ function drawGridLines (ticks, callback) {
+ _.each(_.pluck(ticks, 'v'), function(v){
+ // Don't show lines on upper and lower bounds.
+ if ((v <= a.min || v >= a.max) ||
+ (v == a.min || v == a.max) && grid.outlineWidth)
+ return;
+ callback(Math.floor(a.d2p(v)) + ctx.lineWidth/2);
+ });
+ }
+ function drawVerticalLines (x) {
+ ctx.moveTo(x, 0);
+ ctx.lineTo(x, plotHeight);
+ }
+ function drawHorizontalLines (y) {
+ ctx.moveTo(0, y);
+ ctx.lineTo(plotWidth, y);
+ }
+
+ if (grid.circular) {
+ ctx.translate(this.plotOffset.left+plotWidth/2, this.plotOffset.top+plotHeight/2);
+ var radius = Math.min(plotHeight, plotWidth)*options.radar.radiusRatio/2,
+ sides = this.axes.x.ticks.length,
+ coeff = 2*(Math.PI/sides),
+ angle = -Math.PI/2;
+
+ // Draw grid lines in vertical direction.
+ ctx.beginPath();
+
+ a = this.axes.y;
+
+ if(horizontalLines){
+ circularHorizontalTicks(a.ticks);
+ }
+ if(minorHorizontalLines){
+ circularHorizontalTicks(a.minorTicks);
+ }
+
+ if(verticalLines){
+ _.times(sides, function(i){
+ ctx.moveTo(0, 0);
+ ctx.lineTo(Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius);
+ });
+ }
+ ctx.stroke();
+ }
+ else {
+ ctx.translate(this.plotOffset.left, this.plotOffset.top);
+
+ // Draw grid background, if present in options.
+ if(grid.backgroundColor){
+ ctx.fillStyle = this.processColor(grid.backgroundColor, {x1: 0, y1: 0, x2: plotWidth, y2: plotHeight});
+ ctx.fillRect(0, 0, plotWidth, plotHeight);
+ }
+
+ ctx.beginPath();
+
+ a = this.axes.x;
+ if (verticalLines) drawGridLines(a.ticks, drawVerticalLines);
+ if (minorVerticalLines) drawGridLines(a.minorTicks, drawVerticalLines);
+
+ a = this.axes.y;
+ if (horizontalLines) drawGridLines(a.ticks, drawHorizontalLines);
+ if (minorHorizontalLines) drawGridLines(a.minorTicks, drawHorizontalLines);
+
+ ctx.stroke();
+ }
+
+ ctx.restore();
+ if(verticalLines || minorVerticalLines ||
+ horizontalLines || minorHorizontalLines){
+ E.fire(this.el, 'flotr:aftergrid', [this.axes.x, this.axes.y, options, this]);
+ }
+ },
+
+ drawOutline: function(){
+ var
+ that = this,
+ options = that.options,
+ grid = options.grid,
+ outline = grid.outline,
+ ctx = that.ctx,
+ backgroundImage = grid.backgroundImage,
+ plotOffset = that.plotOffset,
+ leftOffset = plotOffset.left,
+ topOffset = plotOffset.top,
+ plotWidth = that.plotWidth,
+ plotHeight = that.plotHeight,
+ v, img, src, left, top, globalAlpha;
+
+ if (!grid.outlineWidth) return;
+
+ ctx.save();
+
+ if (grid.circular) {
+ ctx.translate(leftOffset + plotWidth / 2, topOffset + plotHeight / 2);
+ var radius = Math.min(plotHeight, plotWidth) * options.radar.radiusRatio / 2,
+ sides = this.axes.x.ticks.length,
+ coeff = 2*(Math.PI/sides),
+ angle = -Math.PI/2;
+
+ // Draw axis/grid border.
+ ctx.beginPath();
+ ctx.lineWidth = grid.outlineWidth;
+ ctx.strokeStyle = grid.color;
+ ctx.lineJoin = 'round';
+
+ for(i = 0; i <= sides; ++i){
+ ctx[i === 0 ? 'moveTo' : 'lineTo'](Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius);
+ }
+ //ctx.arc(0, 0, radius, 0, Math.PI*2, true);
+
+ ctx.stroke();
+ }
+ else {
+ ctx.translate(leftOffset, topOffset);
+
+ // Draw axis/grid border.
+ var lw = grid.outlineWidth,
+ orig = 0.5-lw+((lw+1)%2/2),
+ lineTo = 'lineTo',
+ moveTo = 'moveTo';
+ ctx.lineWidth = lw;
+ ctx.strokeStyle = grid.color;
+ ctx.lineJoin = 'miter';
+ ctx.beginPath();
+ ctx.moveTo(orig, orig);
+ plotWidth = plotWidth - (lw / 2) % 1;
+ plotHeight = plotHeight + lw / 2;
+ ctx[outline.indexOf('n') !== -1 ? lineTo : moveTo](plotWidth, orig);
+ ctx[outline.indexOf('e') !== -1 ? lineTo : moveTo](plotWidth, plotHeight);
+ ctx[outline.indexOf('s') !== -1 ? lineTo : moveTo](orig, plotHeight);
+ ctx[outline.indexOf('w') !== -1 ? lineTo : moveTo](orig, orig);
+ ctx.stroke();
+ ctx.closePath();
+ }
+
+ ctx.restore();
+
+ if (backgroundImage) {
+
+ src = backgroundImage.src || backgroundImage;
+ left = (parseInt(backgroundImage.left, 10) || 0) + plotOffset.left;
+ top = (parseInt(backgroundImage.top, 10) || 0) + plotOffset.top;
+ img = new Image();
+
+ img.onload = function() {
+ ctx.save();
+ if (backgroundImage.alpha) ctx.globalAlpha = backgroundImage.alpha;
+ ctx.globalCompositeOperation = 'destination-over';
+ ctx.drawImage(img, 0, 0, img.width, img.height, left, top, plotWidth, plotHeight);
+ ctx.restore();
+ };
+
+ img.src = src;
+ }
+ }
+});
+
+})();
+
+(function () {
+
+var
+ D = Flotr.DOM,
+ _ = Flotr._,
+ flotr = Flotr,
+ S_MOUSETRACK = 'opacity:0.7;background-color:#000;color:#fff;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;';
+
+Flotr.addPlugin('hit', {
+ callbacks: {
+ 'flotr:mousemove': function(e, pos) {
+ this.hit.track(pos);
+ },
+ 'flotr:click': function(pos) {
+ var
+ hit = this.hit.track(pos);
+ if (hit && !_.isUndefined(hit.index)) pos.hit = hit;
+ },
+ 'flotr:mouseout': function(e) {
+ if (e.relatedTarget !== this.mouseTrack) {
+ this.hit.clearHit();
+ }
+ },
+ 'flotr:destroy': function() {
+ if (this.options.mouse.container) {
+ D.remove(this.mouseTrack);
+ }
+ this.mouseTrack = null;
+ }
+ },
+ track : function (pos) {
+ if (this.options.mouse.track || _.any(this.series, function(s){return s.mouse && s.mouse.track;})) {
+ return this.hit.hit(pos);
+ }
+ },
+ /**
+ * Try a method on a graph type. If the method exists, execute it.
+ * @param {Object} series
+ * @param {String} method Method name.
+ * @param {Array} args Arguments applied to method.
+ * @return executed successfully or failed.
+ */
+ executeOnType: function(s, method, args){
+ var
+ success = false,
+ options;
+
+ if (!_.isArray(s)) s = [s];
+
+ function e(s, index) {
+ _.each(_.keys(flotr.graphTypes), function (type) {
+ if (s[type] && s[type].show && !s.hide && this[type][method]) {
+ options = this.getOptions(s, type);
+
+ options.fill = !!s.mouse.fillColor;
+ options.fillStyle = this.processColor(s.mouse.fillColor || '#ffffff', {opacity: s.mouse.fillOpacity});
+ options.color = s.mouse.lineColor;
+ options.context = this.octx;
+ options.index = index;
+
+ if (args) options.args = args;
+ this[type][method].call(this[type], options);
+ success = true;
+ }
+ }, this);
+ }
+ _.each(s, e, this);
+
+ return success;
+ },
+ /**
+ * Updates the mouse tracking point on the overlay.
+ */
+ drawHit: function(n){
+ var octx = this.octx,
+ s = n.series;
+
+ if (s.mouse.lineColor) {
+ octx.save();
+ octx.lineWidth = (s.points ? s.points.lineWidth : 1);
+ octx.strokeStyle = s.mouse.lineColor;
+ octx.fillStyle = this.processColor(s.mouse.fillColor || '#ffffff', {opacity: s.mouse.fillOpacity});
+ octx.translate(this.plotOffset.left, this.plotOffset.top);
+
+ if (!this.hit.executeOnType(s, 'drawHit', n)) {
+ var
+ xa = n.xaxis,
+ ya = n.yaxis;
+
+ octx.beginPath();
+ // TODO fix this (points) should move to general testable graph mixin
+ octx.arc(xa.d2p(n.x), ya.d2p(n.y), s.points.hitRadius || s.points.radius || s.mouse.radius, 0, 2 * Math.PI, true);
+ octx.fill();
+ octx.stroke();
+ octx.closePath();
+ }
+ octx.restore();
+ this.clip(octx);
+ }
+ this.prevHit = n;
+ },
+ /**
+ * Removes the mouse tracking point from the overlay.
+ */
+ clearHit: function(){
+ var prev = this.prevHit,
+ octx = this.octx,
+ plotOffset = this.plotOffset;
+ octx.save();
+ octx.translate(plotOffset.left, plotOffset.top);
+ if (prev) {
+ if (!this.hit.executeOnType(prev.series, 'clearHit', this.prevHit)) {
+ // TODO fix this (points) should move to general testable graph mixin
+ var
+ s = prev.series,
+ lw = (s.points ? s.points.lineWidth : 1);
+ offset = (s.points.hitRadius || s.points.radius || s.mouse.radius) + lw;
+ octx.clearRect(
+ prev.xaxis.d2p(prev.x) - offset,
+ prev.yaxis.d2p(prev.y) - offset,
+ offset*2,
+ offset*2
+ );
+ }
+ D.hide(this.mouseTrack);
+ this.prevHit = null;
+ }
+ octx.restore();
+ },
+ /**
+ * Retrieves the nearest data point from the mouse cursor. If it's within
+ * a certain range, draw a point on the overlay canvas and display the x and y
+ * value of the data.
+ * @param {Object} mouse - Object that holds the relative x and y coordinates of the cursor.
+ */
+ hit : function (mouse) {
+
+ var
+ options = this.options,
+ prevHit = this.prevHit,
+ closest, sensibility, dataIndex, seriesIndex, series, value, xaxis, yaxis, n;
+
+ if (this.series.length === 0) return;
+
+ // Nearest data element.
+ // dist, x, y, relX, relY, absX, absY, sAngle, eAngle, fraction, mouse,
+ // xaxis, yaxis, series, index, seriesIndex
+ n = {
+ relX : mouse.relX,
+ relY : mouse.relY,
+ absX : mouse.absX,
+ absY : mouse.absY,
+ series: this.series
+ };
+
+ if (options.mouse.trackY &&
+ !options.mouse.trackAll &&
+ this.hit.executeOnType(this.series, 'hit', [mouse, n]) &&
+ !_.isUndefined(n.seriesIndex))
+ {
+ series = this.series[n.seriesIndex];
+ n.series = series;
+ n.mouse = series.mouse;
+ n.xaxis = series.xaxis;
+ n.yaxis = series.yaxis;
+ } else {
+
+ closest = this.hit.closest(mouse);
+
+ if (closest) {
+
+ closest = options.mouse.trackY ? closest.point : closest.x;
+ seriesIndex = closest.seriesIndex;
+ series = this.series[seriesIndex];
+ xaxis = series.xaxis;
+ yaxis = series.yaxis;
+ sensibility = 2 * series.mouse.sensibility;
+
+ if
+ (options.mouse.trackAll ||
+ (closest.distanceX < sensibility / xaxis.scale &&
+ (!options.mouse.trackY || closest.distanceY < sensibility / yaxis.scale)))
+ {
+ n.series = series;
+ n.xaxis = series.xaxis;
+ n.yaxis = series.yaxis;
+ n.mouse = series.mouse;
+ n.x = closest.x;
+ n.y = closest.y;
+ n.dist = closest.distance;
+ n.index = closest.dataIndex;
+ n.seriesIndex = seriesIndex;
+ }
+ }
+ }
+
+ if (!prevHit || (prevHit.index !== n.index || prevHit.seriesIndex !== n.seriesIndex)) {
+ this.hit.clearHit();
+ if (n.series && n.mouse && n.mouse.track) {
+ this.hit.drawMouseTrack(n);
+ this.hit.drawHit(n);
+ Flotr.EventAdapter.fire(this.el, 'flotr:hit', [n, this]);
+ }
+ }
+
+ return n;
+ },
+
+ closest : function (mouse) {
+
+ var
+ series = this.series,
+ options = this.options,
+ relX = mouse.relX,
+ relY = mouse.relY,
+ compare = Number.MAX_VALUE,
+ compareX = Number.MAX_VALUE,
+ closest = {},
+ closestX = {},
+ check = false,
+ serie, data,
+ distance, distanceX, distanceY,
+ mouseX, mouseY,
+ x, y, i, j;
+
+ function setClosest (o) {
+ o.distance = distance;
+ o.distanceX = distanceX;
+ o.distanceY = distanceY;
+ o.seriesIndex = i;
+ o.dataIndex = j;
+ o.x = x;
+ o.y = y;
+ check = true;
+ }
+
+ for (i = 0; i < series.length; i++) {
+
+ serie = series[i];
+ data = serie.data;
+ mouseX = serie.xaxis.p2d(relX);
+ mouseY = serie.yaxis.p2d(relY);
+
+ if (serie.hide) continue;
+
+ for (j = data.length; j--;) {
+
+ x = data[j][0];
+ y = data[j][1];
+ // Add stack offset if exists
+ if (data[j].y0) y += data[j].y0;
+
+ if (x === null || y === null) continue;
+
+ // don't check if the point isn't visible in the current range
+ if (x < serie.xaxis.min || x > serie.xaxis.max) continue;
+
+ distanceX = Math.abs(x - mouseX);
+ distanceY = Math.abs(y - mouseY);
+
+ // Skip square root for speed
+ distance = distanceX * distanceX + distanceY * distanceY;
+
+ if (distance < compare) {
+ compare = distance;
+ setClosest(closest);
+ }
+
+ if (distanceX < compareX) {
+ compareX = distanceX;
+ setClosest(closestX);
+ }
+ }
+ }
+
+ return check ? {
+ point : closest,
+ x : closestX
+ } : false;
+ },
+
+ drawMouseTrack : function (n) {
+
+ var
+ pos = '',
+ s = n.series,
+ p = n.mouse.position,
+ m = n.mouse.margin,
+ x = n.x,
+ y = n.y,
+ elStyle = S_MOUSETRACK,
+ mouseTrack = this.mouseTrack,
+ plotOffset = this.plotOffset,
+ left = plotOffset.left,
+ right = plotOffset.right,
+ bottom = plotOffset.bottom,
+ top = plotOffset.top,
+ decimals = n.mouse.trackDecimals,
+ options = this.options,
+ container = options.mouse.container,
+ oTop = 0,
+ oLeft = 0,
+ offset, size, content;
+
+ // Create
+ if (!mouseTrack) {
+ mouseTrack = D.node('<div class="flotr-mouse-value" style="'+elStyle+'"></div>');
+ this.mouseTrack = mouseTrack;
+ D.insert(container || this.el, mouseTrack);
+ }
+
+ // Fill tracker:
+ if (!decimals || decimals < 0) decimals = 0;
+ if (x && x.toFixed) x = x.toFixed(decimals);
+ if (y && y.toFixed) y = y.toFixed(decimals);
+ content = n.mouse.trackFormatter({
+ x: x,
+ y: y,
+ series: n.series,
+ index: n.index,
+ nearest: n,
+ fraction: n.fraction
+ });
+ if (_.isNull(content) || _.isUndefined(content)) {
+ D.hide(mouseTrack);
+ return;
+ } else {
+ mouseTrack.innerHTML = content;
+ D.show(mouseTrack);
+ }
+
+ // Positioning
+ if (!p) {
+ return;
+ }
+ size = D.size(mouseTrack);
+ if (container) {
+ offset = D.position(this.el);
+ oTop = offset.top;
+ oLeft = offset.left;
+ }
+
+ if (!n.mouse.relative) { // absolute to the canvas
+ pos += 'top:';
+ if (p.charAt(0) == 'n') pos += (oTop + m + top);
+ else if (p.charAt(0) == 's') pos += (oTop - m + top + this.plotHeight - size.height);
+ pos += 'px;bottom:auto;left:';
+ if (p.charAt(1) == 'e') pos += (oLeft - m + left + this.plotWidth - size.width);
+ else if (p.charAt(1) == 'w') pos += (oLeft + m + left);
+ pos += 'px;right:auto;';
+
+ // Pie
+ } else if (s.pie && s.pie.show) {
+ var center = {
+ x: (this.plotWidth)/2,
+ y: (this.plotHeight)/2
+ },
+ radius = (Math.min(this.canvasWidth, this.canvasHeight) * s.pie.sizeRatio) / 2,
+ bisection = n.sAngle<n.eAngle ? (n.sAngle + n.eAngle) / 2: (n.sAngle + n.eAngle + 2* Math.PI) / 2;
+
+ pos += 'bottom:' + (m - top - center.y - Math.sin(bisection) * radius/2 + this.canvasHeight) + 'px;top:auto;';
+ pos += 'left:' + (m + left + center.x + Math.cos(bisection) * radius/2) + 'px;right:auto;';
+
+ // Default
+ } else {
+ pos += 'top:';
+ if (/n/.test(p)) pos += (oTop - m + top + n.yaxis.d2p(n.y) - size.height);
+ else pos += (oTop + m + top + n.yaxis.d2p(n.y));
+ pos += 'px;bottom:auto;left:';
+ if (/w/.test(p)) pos += (oLeft - m + left + n.xaxis.d2p(n.x) - size.width);
+ else pos += (oLeft + m + left + n.xaxis.d2p(n.x));
+ pos += 'px;right:auto;';
+ }
+
+ // Set position
+ mouseTrack.style.cssText = elStyle + pos;
+
+ if (n.mouse.relative) {
+ if (!/[ew]/.test(p)) {
+ // Center Horizontally
+ mouseTrack.style.left =
+ (oLeft + left + n.xaxis.d2p(n.x) - D.size(mouseTrack).width / 2) + 'px';
+ } else
+ if (!/[ns]/.test(p)) {
+ // Center Vertically
+ mouseTrack.style.top =
+ (oTop + top + n.yaxis.d2p(n.y) - D.size(mouseTrack).height / 2) + 'px';
+ }
+ }
+ }
+
+});
+})();
+
+/**
+ * Selection Handles Plugin
+ *
+ *
+ * Options
+ * show - True enables the handles plugin.
+ * drag - Left and Right drag handles
+ * scroll - Scrolling handle
+ */
+(function () {
+
+function isLeftClick (e, type) {
+ return (e.which ? (e.which === 1) : (e.button === 0 || e.button === 1));
+}
+
+function boundX(x, graph) {
+ return Math.min(Math.max(0, x), graph.plotWidth - 1);
+}
+
+function boundY(y, graph) {
+ return Math.min(Math.max(0, y), graph.plotHeight);
+}
+
+var
+ D = Flotr.DOM,
+ E = Flotr.EventAdapter,
+ _ = Flotr._;
+
+
+Flotr.addPlugin('selection', {
+
+ options: {
+ pinchOnly: null, // Only select on pinch
+ mode: null, // => one of null, 'x', 'y' or 'xy'
+ color: '#B6D9FF', // => selection box color
+ fps: 20 // => frames-per-second
+ },
+
+ callbacks: {
+ 'flotr:mouseup' : function (event) {
+
+ var
+ options = this.options.selection,
+ selection = this.selection,
+ pointer = this.getEventPosition(event);
+
+ if (!options || !options.mode) return;
+ if (selection.interval) clearInterval(selection.interval);
+
+ if (this.multitouches) {
+ selection.updateSelection();
+ } else
+ if (!options.pinchOnly) {
+ selection.setSelectionPos(selection.selection.second, pointer);
+ }
+ selection.clearSelection();
+
+ if(selection.selecting && selection.selectionIsSane()){
+ selection.drawSelection();
+ selection.fireSelectEvent();
+ this.ignoreClick = true;
+ }
+ },
+ 'flotr:mousedown' : function (event) {
+
+ var
+ options = this.options.selection,
+ selection = this.selection,
+ pointer = this.getEventPosition(event);
+
+ if (!options || !options.mode) return;
+ if (!options.mode || (!isLeftClick(event) && _.isUndefined(event.touches))) return;
+ if (!options.pinchOnly) selection.setSelectionPos(selection.selection.first, pointer);
+ if (selection.interval) clearInterval(selection.interval);
+
+ this.lastMousePos.pageX = null;
+ selection.selecting = false;
+ selection.interval = setInterval(
+ _.bind(selection.updateSelection, this),
+ 1000 / options.fps
+ );
+ },
+ 'flotr:destroy' : function (event) {
+ clearInterval(this.selection.interval);
+ }
+ },
+
+ // TODO This isn't used. Maybe it belongs in the draw area and fire select event methods?
+ getArea: function() {
+
+ var
+ s = this.selection.selection,
+ a = this.axes,
+ first = s.first,
+ second = s.second,
+ x1, x2, y1, y2;
+
+ x1 = a.x.p2d(s.first.x);
+ x2 = a.x.p2d(s.second.x);
+ y1 = a.y.p2d(s.first.y);
+ y2 = a.y.p2d(s.second.y);
+
+ return {
+ x1 : Math.min(x1, x2),
+ y1 : Math.min(y1, y2),
+ x2 : Math.max(x1, x2),
+ y2 : Math.max(y1, y2),
+ xfirst : x1,
+ xsecond : x2,
+ yfirst : y1,
+ ysecond : y2
+ };
+ },
+
+ selection: {first: {x: -1, y: -1}, second: {x: -1, y: -1}},
+ prevSelection: null,
+ interval: null,
+
+ /**
+ * Fires the 'flotr:select' event when the user made a selection.
+ */
+ fireSelectEvent: function(name){
+ var
+ area = this.selection.getArea();
+ name = name || 'select';
+ area.selection = this.selection.selection;
+ E.fire(this.el, 'flotr:'+name, [area, this]);
+ },
+
+ /**
+ * Allows the user the manually select an area.
+ * @param {Object} area - Object with coordinates to select.
+ */
+ setSelection: function(area, preventEvent){
+ var options = this.options,
+ xa = this.axes.x,
+ ya = this.axes.y,
+ vertScale = ya.scale,
+ hozScale = xa.scale,
+ selX = options.selection.mode.indexOf('x') != -1,
+ selY = options.selection.mode.indexOf('y') != -1,
+ s = this.selection.selection;
+
+ this.selection.clearSelection();
+
+ s.first.y = boundY((selX && !selY) ? 0 : (ya.max - area.y1) * vertScale, this);
+ s.second.y = boundY((selX && !selY) ? this.plotHeight - 1: (ya.max - area.y2) * vertScale, this);
+ s.first.x = boundX((selY && !selX) ? 0 : (area.x1 - xa.min) * hozScale, this);
+ s.second.x = boundX((selY && !selX) ? this.plotWidth : (area.x2 - xa.min) * hozScale, this);
+
+ this.selection.drawSelection();
+ if (!preventEvent)
+ this.selection.fireSelectEvent();
+ },
+
+ /**
+ * Calculates the position of the selection.
+ * @param {Object} pos - Position object.
+ * @param {Event} event - Event object.
+ */
+ setSelectionPos: function(pos, pointer) {
+ var mode = this.options.selection.mode,
+ selection = this.selection.selection;
+
+ if(mode.indexOf('x') == -1) {
+ pos.x = (pos == selection.first) ? 0 : this.plotWidth;
+ }else{
+ pos.x = boundX(pointer.relX, this);
+ }
+
+ if (mode.indexOf('y') == -1) {
+ pos.y = (pos == selection.first) ? 0 : this.plotHeight - 1;
+ }else{
+ pos.y = boundY(pointer.relY, this);
+ }
+ },
+ /**
+ * Draws the selection box.
+ */
+ drawSelection: function() {
+
+ this.selection.fireSelectEvent('selecting');
+
+ var s = this.selection.selection,
+ octx = this.octx,
+ options = this.options,
+ plotOffset = this.plotOffset,
+ prevSelection = this.selection.prevSelection;
+
+ if (prevSelection &&
+ s.first.x == prevSelection.first.x &&
+ s.first.y == prevSelection.first.y &&
+ s.second.x == prevSelection.second.x &&
+ s.second.y == prevSelection.second.y) {
+ return;
+ }
+
+ octx.save();
+ octx.strokeStyle = this.processColor(options.selection.color, {opacity: 0.8});
+ octx.lineWidth = 1;
+ octx.lineJoin = 'miter';
+ octx.fillStyle = this.processColor(options.selection.color, {opacity: 0.4});
+
+ this.selection.prevSelection = {
+ first: { x: s.first.x, y: s.first.y },
+ second: { x: s.second.x, y: s.second.y }
+ };
+
+ var x = Math.min(s.first.x, s.second.x),
+ y = Math.min(s.first.y, s.second.y),
+ w = Math.abs(s.second.x - s.first.x),
+ h = Math.abs(s.second.y - s.first.y);
+
+ octx.fillRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h);
+ octx.strokeRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h);
+ octx.restore();
+ },
+
+ /**
+ * Updates (draws) the selection box.
+ */
+ updateSelection: function(){
+ if (!this.lastMousePos.pageX) return;
+
+ this.selection.selecting = true;
+
+ if (this.multitouches) {
+ this.selection.setSelectionPos(this.selection.selection.first, this.getEventPosition(this.multitouches[0]));
+ this.selection.setSelectionPos(this.selection.selection.second, this.getEventPosition(this.multitouches[1]));
+ } else
+ if (this.options.selection.pinchOnly) {
+ return;
+ } else {
+ this.selection.setSelectionPos(this.selection.selection.second, this.lastMousePos);
+ }
+
+ this.selection.clearSelection();
+
+ if(this.selection.selectionIsSane()) {
+ this.selection.drawSelection();
+ }
+ },
+
+ /**
+ * Removes the selection box from the overlay canvas.
+ */
+ clearSelection: function() {
+ if (!this.selection.prevSelection) return;
+
+ var prevSelection = this.selection.prevSelection,
+ lw = 1,
+ plotOffset = this.plotOffset,
+ x = Math.min(prevSelection.first.x, prevSelection.second.x),
+ y = Math.min(prevSelection.first.y, prevSelection.second.y),
+ w = Math.abs(prevSelection.second.x - prevSelection.first.x),
+ h = Math.abs(prevSelection.second.y - prevSelection.first.y);
+
+ this.octx.clearRect(x + plotOffset.left - lw + 0.5,
+ y + plotOffset.top - lw,
+ w + 2 * lw + 0.5,
+ h + 2 * lw + 0.5);
+
+ this.selection.prevSelection = null;
+ },
+ /**
+ * Determines whether or not the selection is sane and should be drawn.
+ * @return {Boolean} - True when sane, false otherwise.
+ */
+ selectionIsSane: function(){
+ var s = this.selection.selection;
+ return Math.abs(s.second.x - s.first.x) >= 5 ||
+ Math.abs(s.second.y - s.first.y) >= 5;
+ }
+
+});
+
+})();
+
+(function () {
+
+var D = Flotr.DOM;
+
+Flotr.addPlugin('labels', {
+
+ callbacks : {
+ 'flotr:afterdraw' : function () {
+ this.labels.draw();
+ }
+ },
+
+ draw: function(){
+ // Construct fixed width label boxes, which can be styled easily.
+ var
+ axis, tick, left, top, xBoxWidth,
+ radius, sides, coeff, angle,
+ div, i, html = '',
+ noLabels = 0,
+ options = this.options,
+ ctx = this.ctx,
+ a = this.axes,
+ style = { size: options.fontSize };
+
+ for (i = 0; i < a.x.ticks.length; ++i){
+ if (a.x.ticks[i].label) { ++noLabels; }
+ }
+ xBoxWidth = this.plotWidth / noLabels;
+
+ if (options.grid.circular) {
+ ctx.save();
+ ctx.translate(this.plotOffset.left + this.plotWidth / 2,
+ this.plotOffset.top + this.plotHeight / 2);
+
+ radius = this.plotHeight * options.radar.radiusRatio / 2 + options.fontSize;
+ sides = this.axes.x.ticks.length;
+ coeff = 2 * (Math.PI / sides);
+ angle = -Math.PI / 2;
+
+ drawLabelCircular(this, a.x, false);
+ drawLabelCircular(this, a.x, true);
+ drawLabelCircular(this, a.y, false);
+ drawLabelCircular(this, a.y, true);
+ ctx.restore();
+ }
+
+ if (!options.HtmlText && this.textEnabled) {
+ drawLabelNoHtmlText(this, a.x, 'center', 'top');
+ drawLabelNoHtmlText(this, a.x2, 'center', 'bottom');
+ drawLabelNoHtmlText(this, a.y, 'right', 'middle');
+ drawLabelNoHtmlText(this, a.y2, 'left', 'middle');
+
+ } else if ((
+ a.x.options.showLabels ||
+ a.x2.options.showLabels ||
+ a.y.options.showLabels ||
+ a.y2.options.showLabels) &&
+ !options.grid.circular
+ ) {
+
+ html = '';
+
+ drawLabelHtml(this, a.x);
+ drawLabelHtml(this, a.x2);
+ drawLabelHtml(this, a.y);
+ drawLabelHtml(this, a.y2);
+
+ ctx.stroke();
+ ctx.restore();
+ div = D.create('div');
+ D.setStyles(div, {
+ fontSize: 'smaller',
+ color: options.grid.color
+ });
+ div.className = 'flotr-labels';
+ D.insert(this.el, div);
+ D.insert(div, html);
+ }
+
+ function drawLabelCircular (graph, axis, minorTicks) {
+ var
+ ticks = minorTicks ? axis.minorTicks : axis.ticks,
+ isX = axis.orientation === 1,
+ isFirst = axis.n === 1,
+ style, offset;
+
+ style = {
+ color : axis.options.color || options.grid.color,
+ angle : Flotr.toRad(axis.options.labelsAngle),
+ textBaseline : 'middle'
+ };
+
+ for (i = 0; i < ticks.length &&
+ (minorTicks ? axis.options.showMinorLabels : axis.options.showLabels); ++i){
+ tick = ticks[i];
+ tick.label += '';
+ if (!tick.label || !tick.label.length) { continue; }
+
+ x = Math.cos(i * coeff + angle) * radius;
+ y = Math.sin(i * coeff + angle) * radius;
+
+ style.textAlign = isX ? (Math.abs(x) < 0.1 ? 'center' : (x < 0 ? 'right' : 'left')) : 'left';
+
+ Flotr.drawText(
+ ctx, tick.label,
+ isX ? x : 3,
+ isX ? y : -(axis.ticks[i].v / axis.max) * (radius - options.fontSize),
+ style
+ );
+ }
+ }
+
+ function drawLabelNoHtmlText (graph, axis, textAlign, textBaseline) {
+ var
+ isX = axis.orientation === 1,
+ isFirst = axis.n === 1,
+ style, offset;
+
+ style = {
+ color : axis.options.color || options.grid.color,
+ textAlign : textAlign,
+ textBaseline : textBaseline,
+ angle : Flotr.toRad(axis.options.labelsAngle)
+ };
+ style = Flotr.getBestTextAlign(style.angle, style);
+
+ for (i = 0; i < axis.ticks.length && continueShowingLabels(axis); ++i) {
+
+ tick = axis.ticks[i];
+ if (!tick.label || !tick.label.length) { continue; }
+
+ offset = axis.d2p(tick.v);
+ if (offset < 0 ||
+ offset > (isX ? graph.plotWidth : graph.plotHeight)) { continue; }
+
+ Flotr.drawText(
+ ctx, tick.label,
+ leftOffset(graph, isX, isFirst, offset),
+ topOffset(graph, isX, isFirst, offset),
+ style
+ );
+
+ // Only draw on axis y2
+ if (!isX && !isFirst) {
+ ctx.save();
+ ctx.strokeStyle = style.color;
+ ctx.beginPath();
+ ctx.moveTo(graph.plotOffset.left + graph.plotWidth - 8, graph.plotOffset.top + axis.d2p(tick.v));
+ ctx.lineTo(graph.plotOffset.left + graph.plotWidth, graph.plotOffset.top + axis.d2p(tick.v));
+ ctx.stroke();
+ ctx.restore();
+ }
+ }
+
+ function continueShowingLabels (axis) {
+ return axis.options.showLabels && axis.used;
+ }
+ function leftOffset (graph, isX, isFirst, offset) {
+ return graph.plotOffset.left +
+ (isX ? offset :
+ (isFirst ?
+ -options.grid.labelMargin :
+ options.grid.labelMargin + graph.plotWidth));
+ }
+ function topOffset (graph, isX, isFirst, offset) {
+ return graph.plotOffset.top +
+ (isX ? options.grid.labelMargin : offset) +
+ ((isX && isFirst) ? graph.plotHeight : 0);
+ }
+ }
+
+ function drawLabelHtml (graph, axis) {
+ var
+ isX = axis.orientation === 1,
+ isFirst = axis.n === 1,
+ name = '',
+ left, style, top,
+ offset = graph.plotOffset;
+
+ if (!isX && !isFirst) {
+ ctx.save();
+ ctx.strokeStyle = axis.options.color || options.grid.color;
+ ctx.beginPath();
+ }
+
+ if (axis.options.showLabels && (isFirst ? true : axis.used)) {
+ for (i = 0; i < axis.ticks.length; ++i) {
+ tick = axis.ticks[i];
+ if (!tick.label || !tick.label.length ||
+ ((isX ? offset.left : offset.top) + axis.d2p(tick.v) < 0) ||
+ ((isX ? offset.left : offset.top) + axis.d2p(tick.v) > (isX ? graph.canvasWidth : graph.canvasHeight))) {
+ continue;
+ }
+ top = offset.top +
+ (isX ?
+ ((isFirst ? 1 : -1 ) * (graph.plotHeight + options.grid.labelMargin)) :
+ axis.d2p(tick.v) - axis.maxLabel.height / 2);
+ left = isX ? (offset.left + axis.d2p(tick.v) - xBoxWidth / 2) : 0;
+
+ name = '';
+ if (i === 0) {
+ name = ' first';
+ } else if (i === axis.ticks.length - 1) {
+ name = ' last';
+ }
+ name += isX ? ' flotr-grid-label-x' : ' flotr-grid-label-y';
+
+ html += [
+ '<div style="position:absolute; text-align:' + (isX ? 'center' : 'right') + '; ',
+ 'top:' + top + 'px; ',
+ ((!isX && !isFirst) ? 'right:' : 'left:') + left + 'px; ',
+ 'width:' + (isX ? xBoxWidth : ((isFirst ? offset.left : offset.right) - options.grid.labelMargin)) + 'px; ',
+ axis.options.color ? ('color:' + axis.options.color + '; ') : ' ',
+ '" class="flotr-grid-label' + name + '">' + tick.label + '</div>'
+ ].join(' ');
+
+ if (!isX && !isFirst) {
+ ctx.moveTo(offset.left + graph.plotWidth - 8, offset.top + axis.d2p(tick.v));
+ ctx.lineTo(offset.left + graph.plotWidth, offset.top + axis.d2p(tick.v));
+ }
+ }
+ }
+ }
+ }
+
+});
+})();
+
+(function () {
+
+var
+ D = Flotr.DOM,
+ _ = Flotr._;
+
+Flotr.addPlugin('legend', {
+ options: {
+ show: true, // => setting to true will show the legend, hide otherwise
+ noColumns: 1, // => number of colums in legend table // @todo: doesn't work for HtmlText = false
+ labelFormatter: function(v){return v;}, // => fn: string -> string
+ labelBoxBorderColor: '#CCCCCC', // => border color for the little label boxes
+ labelBoxWidth: 14,
+ labelBoxHeight: 10,
+ labelBoxMargin: 5,
+ container: null, // => container (as jQuery object) to put legend in, null means default on top of graph
+ position: 'nw', // => position of default legend container within plot
+ margin: 5, // => distance from grid edge to default legend container within plot
+ backgroundColor: '#F0F0F0', // => Legend background color.
+ backgroundOpacity: 0.85// => set to 0 to avoid background, set to 1 for a solid background
+ },
+ callbacks: {
+ 'flotr:afterinit': function() {
+ this.legend.insertLegend();
+ },
+ 'flotr:destroy': function() {
+ var markup = this.legend.markup;
+ if (markup) {
+ this.legend.markup = null;
+ D.remove(markup);
+ }
+ }
+ },
+ /**
+ * Adds a legend div to the canvas container or draws it on the canvas.
+ */
+ insertLegend: function(){
+
+ if(!this.options.legend.show)
+ return;
+
+ var series = this.series,
+ plotOffset = this.plotOffset,
+ options = this.options,
+ legend = options.legend,
+ fragments = [],
+ rowStarted = false,
+ ctx = this.ctx,
+ itemCount = _.filter(series, function(s) {return (s.label && !s.hide);}).length,
+ p = legend.position,
+ m = legend.margin,
+ opacity = legend.backgroundOpacity,
+ i, label, color;
+
+ if (itemCount) {
+
+ var lbw = legend.labelBoxWidth,
+ lbh = legend.labelBoxHeight,
+ lbm = legend.labelBoxMargin,
+ offsetX = plotOffset.left + m,
+ offsetY = plotOffset.top + m,
+ labelMaxWidth = 0,
+ style = {
+ size: options.fontSize*1.1,
+ color: options.grid.color
+ };
+
+ // We calculate the labels' max width
+ for(i = series.length - 1; i > -1; --i){
+ if(!series[i].label || series[i].hide) continue;
+ label = legend.labelFormatter(series[i].label,series[i]);
+ labelMaxWidth = Math.max(labelMaxWidth, this._text.measureText(label, style).width);
+ }
+
+ var legendWidth = Math.round(lbw + lbm*3 + labelMaxWidth),
+ legendHeight = Math.round(itemCount*(lbm+lbh) + lbm);
+
+ // Default Opacity
+ if (!opacity && opacity !== 0) {
+ opacity = 0.1;
+ }
+
+ if (!options.HtmlText && this.textEnabled && !legend.container) {
+
+ if(p.charAt(0) == 's') offsetY = plotOffset.top + this.plotHeight - (m + legendHeight);
+ if(p.charAt(0) == 'c') offsetY = plotOffset.top + (this.plotHeight/2) - (m + (legendHeight/2));
+ if(p.charAt(1) == 'e') offsetX = plotOffset.left + this.plotWidth - (m + legendWidth);
+
+ // Legend box
+ color = this.processColor(legend.backgroundColor, { opacity : opacity });
+
+ ctx.fillStyle = color;
+ ctx.fillRect(offsetX, offsetY, legendWidth, legendHeight);
+ ctx.strokeStyle = legend.labelBoxBorderColor;
+ ctx.strokeRect(Flotr.toPixel(offsetX), Flotr.toPixel(offsetY), legendWidth, legendHeight);
+
+ // Legend labels
+ var x = offsetX + lbm;
+ var y = offsetY + lbm;
+ for(i = 0; i < series.length; i++){
+ if(!series[i].label || series[i].hide) continue;
+ label = legend.labelFormatter(series[i].label,series[i]);
+
+ ctx.fillStyle = series[i].color;
+ ctx.fillRect(x, y, lbw-1, lbh-1);
+
+ ctx.strokeStyle = legend.labelBoxBorderColor;
+ ctx.lineWidth = 1;
+ ctx.strokeRect(Math.ceil(x)-1.5, Math.ceil(y)-1.5, lbw+2, lbh+2);
+
+ // Legend text
+ Flotr.drawText(ctx, label, x + lbw + lbm, y + lbh, style);
+
+ y += lbh + lbm;
+ }
+ }
+ else {
+ for(i = 0; i < series.length; ++i){
+ if(!series[i].label || series[i].hide) continue;
+
+ if(i % legend.noColumns === 0){
+ fragments.push(rowStarted ? '</tr><tr>' : '<tr>');
+ rowStarted = true;
+ }
+
+ var s = series[i],
+ boxWidth = legend.labelBoxWidth,
+ boxHeight = legend.labelBoxHeight;
+
+ label = legend.labelFormatter(s.label,s);
+ color = 'background-color:' + ((s.bars && s.bars.show && s.bars.fillColor && s.bars.fill) ? s.bars.fillColor : s.color) + ';';
+
+ fragments.push(
+ '<td class="flotr-legend-color-box">',
+ '<div style="border:1px solid ', legend.labelBoxBorderColor, ';padding:1px">',
+ '<div style="width:', (boxWidth-1), 'px;height:', (boxHeight-1), 'px;border:1px solid ', series[i].color, '">', // Border
+ '<div style="width:', boxWidth, 'px;height:', boxHeight, 'px;', color, '"></div>', // Background
+ '</div>',
+ '</div>',
+ '</td>',
+ '<td class="flotr-legend-label">', label, '</td>'
+ );
+ }
+ if(rowStarted) fragments.push('</tr>');
+
+ if(fragments.length > 0){
+ var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join('') + '</table>';
+ if(legend.container){
+ table = D.node(table);
+ this.legend.markup = table;
+ D.insert(legend.container, table);
+ }
+ else {
+ var styles = {position: 'absolute', 'zIndex': '2', 'border' : '1px solid ' + legend.labelBoxBorderColor};
+
+ if(p.charAt(0) == 'n') { styles.top = (m + plotOffset.top) + 'px'; styles.bottom = 'auto'; }
+ else if(p.charAt(0) == 'c') { styles.top = (m + (this.plotHeight - legendHeight) / 2) + 'px'; styles.bottom = 'auto'; }
+ else if(p.charAt(0) == 's') { styles.bottom = (m + plotOffset.bottom) + 'px'; styles.top = 'auto'; }
+ if(p.charAt(1) == 'e') { styles.right = (m + plotOffset.right) + 'px'; styles.left = 'auto'; }
+ else if(p.charAt(1) == 'w') { styles.left = (m + plotOffset.left) + 'px'; styles.right = 'auto'; }
+
+ var div = D.create('div'), size;
+ div.className = 'flotr-legend';
+ D.setStyles(div, styles);
+ D.insert(div, table);
+ D.insert(this.el, div);
+
+ if (!opacity) return;
+
+ var c = legend.backgroundColor || options.grid.backgroundColor || '#ffffff';
+
+ _.extend(styles, D.size(div), {
+ 'backgroundColor': c,
+ 'zIndex' : '',
+ 'border' : ''
+ });
+ styles.width += 'px';
+ styles.height += 'px';
+
+ // Put in the transparent background separately to avoid blended labels and
+ div = D.create('div');
+ div.className = 'flotr-legend-bg';
+ D.setStyles(div, styles);
+ D.opacity(div, opacity);
+ D.insert(div, ' ');
+ D.insert(this.el, div);
+ }
+ }
+ }
+ }
+ }
+});
+})();
+
+/** Spreadsheet **/
+(function() {
+
+function getRowLabel(value){
+ if (this.options.spreadsheet.tickFormatter){
+ //TODO maybe pass the xaxis formatter to the custom tick formatter as an opt-out?
+ return this.options.spreadsheet.tickFormatter(value);
+ }
+ else {
+ var t = _.find(this.axes.x.ticks, function(t){return t.v == value;});
+ if (t) {
+ return t.label;
+ }
+ return value;
+ }
+}
+
+var
+ D = Flotr.DOM,
+ _ = Flotr._;
+
+Flotr.addPlugin('spreadsheet', {
+ options: {
+ show: false, // => show the data grid using two tabs
+ tabGraphLabel: 'Graph',
+ tabDataLabel: 'Data',
+ toolbarDownload: 'Download CSV', // @todo: add better language support
+ toolbarSelectAll: 'Select all',
+ csvFileSeparator: ',',
+ decimalSeparator: '.',
+ tickFormatter: null,
+ initialTab: 'graph'
+ },
+ /**
+ * Builds the tabs in the DOM
+ */
+ callbacks: {
+ 'flotr:afterconstruct': function(){
+ // @TODO necessary?
+ //this.el.select('.flotr-tabs-group,.flotr-datagrid-container').invoke('remove');
+
+ if (!this.options.spreadsheet.show) return;
+
+ var ss = this.spreadsheet,
+ container = D.node('<div class="flotr-tabs-group" style="position:absolute;left:0px;width:'+this.canvasWidth+'px"></div>'),
+ graph = D.node('<div style="float:left" class="flotr-tab selected">'+this.options.spreadsheet.tabGraphLabel+'</div>'),
+ data = D.node('<div style="float:left" class="flotr-tab">'+this.options.spreadsheet.tabDataLabel+'</div>'),
+ offset;
+
+ ss.tabsContainer = container;
+ ss.tabs = { graph : graph, data : data };
+
+ D.insert(container, graph);
+ D.insert(container, data);
+ D.insert(this.el, container);
+
+ offset = D.size(data).height + 2;
+ this.plotOffset.bottom += offset;
+
+ D.setStyles(container, {top: this.canvasHeight-offset+'px'});
+
+ this.
+ observe(graph, 'click', function(){ss.showTab('graph');}).
+ observe(data, 'click', function(){ss.showTab('data');});
+ if (this.options.spreadsheet.initialTab !== 'graph'){
+ ss.showTab(this.options.spreadsheet.initialTab);
+ }
+ }
+ },
+ /**
+ * Builds a matrix of the data to make the correspondance between the x values and the y values :
+ * X value => Y values from the axes
+ * @return {Array} The data grid
+ */
+ loadDataGrid: function(){
+ if (this.seriesData) return this.seriesData;
+
+ var s = this.series,
+ rows = {};
+
+ /* The data grid is a 2 dimensions array. There is a row for each X value.
+ * Each row contains the x value and the corresponding y value for each serie ('undefined' if there isn't one)
+ **/
+ _.each(s, function(serie, i){
+ _.each(serie.data, function (v) {
+ var x = v[0],
+ y = v[1],
+ r = rows[x];
+ if (r) {
+ r[i+1] = y;
+ } else {
+ var newRow = [];
+ newRow[0] = x;
+ newRow[i+1] = y;
+ rows[x] = newRow;
+ }
+ });
+ });
+
+ // The data grid is sorted by x value
+ this.seriesData = _.sortBy(rows, function(row, x){
+ return parseInt(x, 10);
+ });
+ return this.seriesData;
+ },
+ /**
+ * Constructs the data table for the spreadsheet
+ * @todo make a spreadsheet manager (Flotr.Spreadsheet)
+ * @return {Element} The resulting table element
+ */
+ constructDataGrid: function(){
+ // If the data grid has already been built, nothing to do here
+ if (this.spreadsheet.datagrid) return this.spreadsheet.datagrid;
+
+ var s = this.series,
+ datagrid = this.spreadsheet.loadDataGrid(),
+ colgroup = ['<colgroup><col />'],
+ buttonDownload, buttonSelect, t;
+
+ // First row : series' labels
+ var html = ['<table class="flotr-datagrid"><tr class="first-row">'];
+ html.push('<th>&nbsp;</th>');
+ _.each(s, function(serie,i){
+ html.push('<th scope="col">'+(serie.label || String.fromCharCode(65+i))+'</th>');
+ colgroup.push('<col />');
+ });
+ html.push('</tr>');
+ // Data rows
+ _.each(datagrid, function(row){
+ html.push('<tr>');
+ _.times(s.length+1, function(i){
+ var tag = 'td',
+ value = row[i],
+ // TODO: do we really want to handle problems with floating point
+ // precision here?
+ content = (!_.isUndefined(value) ? Math.round(value*100000)/100000 : '');
+ if (i === 0) {
+ tag = 'th';
+ var label = getRowLabel.call(this, content);
+ if (label) content = label;
+ }
+
+ html.push('<'+tag+(tag=='th'?' scope="row"':'')+'>'+content+'</'+tag+'>');
+ }, this);
+ html.push('</tr>');
+ }, this);
+ colgroup.push('</colgroup>');
+ t = D.node(html.join(''));
+
+ /**
+ * @TODO disabled this
+ if (!Flotr.isIE || Flotr.isIE == 9) {
+ function handleMouseout(){
+ t.select('colgroup col.hover, th.hover').invoke('removeClassName', 'hover');
+ }
+ function handleMouseover(e){
+ var td = e.element(),
+ siblings = td.previousSiblings();
+ t.select('th[scope=col]')[siblings.length-1].addClassName('hover');
+ t.select('colgroup col')[siblings.length].addClassName('hover');
+ }
+ _.each(t.select('td'), function(td) {
+ Flotr.EventAdapter.
+ observe(td, 'mouseover', handleMouseover).
+ observe(td, 'mouseout', handleMouseout);
+ });
+ }
+ */
+
+ buttonDownload = D.node(
+ '<button type="button" class="flotr-datagrid-toolbar-button">' +
+ this.options.spreadsheet.toolbarDownload +
+ '</button>');
+
+ buttonSelect = D.node(
+ '<button type="button" class="flotr-datagrid-toolbar-button">' +
+ this.options.spreadsheet.toolbarSelectAll+
+ '</button>');
+
+ this.
+ observe(buttonDownload, 'click', _.bind(this.spreadsheet.downloadCSV, this)).
+ observe(buttonSelect, 'click', _.bind(this.spreadsheet.selectAllData, this));
+
+ var toolbar = D.node('<div class="flotr-datagrid-toolbar"></div>');
+ D.insert(toolbar, buttonDownload);
+ D.insert(toolbar, buttonSelect);
+
+ var containerHeight =this.canvasHeight - D.size(this.spreadsheet.tabsContainer).height-2,
+ container = D.node('<div class="flotr-datagrid-container" style="position:absolute;left:0px;top:0px;width:'+
+ this.canvasWidth+'px;height:'+containerHeight+'px;overflow:auto;z-index:10"></div>');
+
+ D.insert(container, toolbar);
+ D.insert(container, t);
+ D.insert(this.el, container);
+ this.spreadsheet.datagrid = t;
+ this.spreadsheet.container = container;
+
+ return t;
+ },
+ /**
+ * Shows the specified tab, by its name
+ * @todo make a tab manager (Flotr.Tabs)
+ * @param {String} tabName - The tab name
+ */
+ showTab: function(tabName){
+ if (this.spreadsheet.activeTab === tabName){
+ return;
+ }
+ switch(tabName) {
+ case 'graph':
+ D.hide(this.spreadsheet.container);
+ D.removeClass(this.spreadsheet.tabs.data, 'selected');
+ D.addClass(this.spreadsheet.tabs.graph, 'selected');
+ break;
+ case 'data':
+ if (!this.spreadsheet.datagrid)
+ this.spreadsheet.constructDataGrid();
+ D.show(this.spreadsheet.container);
+ D.addClass(this.spreadsheet.tabs.data, 'selected');
+ D.removeClass(this.spreadsheet.tabs.graph, 'selected');
+ break;
+ default:
+ throw 'Illegal tab name: ' + tabName;
+ }
+ this.spreadsheet.activeTab = tabName;
+ },
+ /**
+ * Selects the data table in the DOM for copy/paste
+ */
+ selectAllData: function(){
+ if (this.spreadsheet.tabs) {
+ var selection, range, doc, win, node = this.spreadsheet.constructDataGrid();
+
+ this.spreadsheet.showTab('data');
+
+ // deferred to be able to select the table
+ setTimeout(function () {
+ if ((doc = node.ownerDocument) && (win = doc.defaultView) &&
+ win.getSelection && doc.createRange &&
+ (selection = window.getSelection()) &&
+ selection.removeAllRanges) {
+ range = doc.createRange();
+ range.selectNode(node);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ else if (document.body && document.body.createTextRange &&
+ (range = document.body.createTextRange())) {
+ range.moveToElementText(node);
+ range.select();
+ }
+ }, 0);
+ return true;
+ }
+ else return false;
+ },
+ /**
+ * Converts the data into CSV in order to download a file
+ */
+ downloadCSV: function(){
+ var csv = '',
+ series = this.series,
+ options = this.options,
+ dg = this.spreadsheet.loadDataGrid(),
+ separator = encodeURIComponent(options.spreadsheet.csvFileSeparator);
+
+ if (options.spreadsheet.decimalSeparator === options.spreadsheet.csvFileSeparator) {
+ throw "The decimal separator is the same as the column separator ("+options.spreadsheet.decimalSeparator+")";
+ }
+
+ // The first row
+ _.each(series, function(serie, i){
+ csv += separator+'"'+(serie.label || String.fromCharCode(65+i)).replace(/\"/g, '\\"')+'"';
+ });
+
+ csv += "%0D%0A"; // \r\n
+
+ // For each row
+ csv += _.reduce(dg, function(memo, row){
+ var rowLabel = getRowLabel.call(this, row[0]) || '';
+ rowLabel = '"'+(rowLabel+'').replace(/\"/g, '\\"')+'"';
+ var numbers = row.slice(1).join(separator);
+ if (options.spreadsheet.decimalSeparator !== '.') {
+ numbers = numbers.replace(/\./g, options.spreadsheet.decimalSeparator);
+ }
+ return memo + rowLabel+separator+numbers+"%0D%0A"; // \t and \r\n
+ }, '', this);
+
+ if (Flotr.isIE && Flotr.isIE < 9) {
+ csv = csv.replace(new RegExp(separator, 'g'), decodeURIComponent(separator)).replace(/%0A/g, '\n').replace(/%0D/g, '\r');
+ window.open().document.write(csv);
+ }
+ else window.open('data:text/csv,'+csv);
+ }
+});
+})();
+
+(function () {
+
+var D = Flotr.DOM;
+
+Flotr.addPlugin('titles', {
+ callbacks: {
+ 'flotr:afterdraw': function() {
+ this.titles.drawTitles();
+ }
+ },
+ /**
+ * Draws the title and the subtitle
+ */
+ drawTitles : function () {
+ var html,
+ options = this.options,
+ margin = options.grid.labelMargin,
+ ctx = this.ctx,
+ a = this.axes;
+
+ if (!options.HtmlText && this.textEnabled) {
+ var style = {
+ size: options.fontSize,
+ color: options.grid.color,
+ textAlign: 'center'
+ };
+
+ // Add subtitle
+ if (options.subtitle){
+ Flotr.drawText(
+ ctx, options.subtitle,
+ this.plotOffset.left + this.plotWidth/2,
+ this.titleHeight + this.subtitleHeight - 2,
+ style
+ );
+ }
+
+ style.weight = 1.5;
+ style.size *= 1.5;
+
+ // Add title
+ if (options.title){
+ Flotr.drawText(
+ ctx, options.title,
+ this.plotOffset.left + this.plotWidth/2,
+ this.titleHeight - 2,
+ style
+ );
+ }
+
+ style.weight = 1.8;
+ style.size *= 0.8;
+
+ // Add x axis title
+ if (a.x.options.title && a.x.used){
+ style.textAlign = a.x.options.titleAlign || 'center';
+ style.textBaseline = 'top';
+ style.angle = Flotr.toRad(a.x.options.titleAngle);
+ style = Flotr.getBestTextAlign(style.angle, style);
+ Flotr.drawText(
+ ctx, a.x.options.title,
+ this.plotOffset.left + this.plotWidth/2,
+ this.plotOffset.top + a.x.maxLabel.height + this.plotHeight + 2 * margin,
+ style
+ );
+ }
+
+ // Add x2 axis title
+ if (a.x2.options.title && a.x2.used){
+ style.textAlign = a.x2.options.titleAlign || 'center';
+ style.textBaseline = 'bottom';
+ style.angle = Flotr.toRad(a.x2.options.titleAngle);
+ style = Flotr.getBestTextAlign(style.angle, style);
+ Flotr.drawText(
+ ctx, a.x2.options.title,
+ this.plotOffset.left + this.plotWidth/2,
+ this.plotOffset.top - a.x2.maxLabel.height - 2 * margin,
+ style
+ );
+ }
+
+ // Add y axis title
+ if (a.y.options.title && a.y.used){
+ style.textAlign = a.y.options.titleAlign || 'right';
+ style.textBaseline = 'middle';
+ style.angle = Flotr.toRad(a.y.options.titleAngle);
+ style = Flotr.getBestTextAlign(style.angle, style);
+ Flotr.drawText(
+ ctx, a.y.options.title,
+ this.plotOffset.left - a.y.maxLabel.width - 2 * margin,
+ this.plotOffset.top + this.plotHeight / 2,
+ style
+ );
+ }
+
+ // Add y2 axis title
+ if (a.y2.options.title && a.y2.used){
+ style.textAlign = a.y2.options.titleAlign || 'left';
+ style.textBaseline = 'middle';
+ style.angle = Flotr.toRad(a.y2.options.titleAngle);
+ style = Flotr.getBestTextAlign(style.angle, style);
+ Flotr.drawText(
+ ctx, a.y2.options.title,
+ this.plotOffset.left + this.plotWidth + a.y2.maxLabel.width + 2 * margin,
+ this.plotOffset.top + this.plotHeight / 2,
+ style
+ );
+ }
+ }
+ else {
+ html = [];
+
+ // Add title
+ if (options.title)
+ html.push(
+ '<div style="position:absolute;top:0;left:',
+ this.plotOffset.left, 'px;font-size:1em;font-weight:bold;text-align:center;width:',
+ this.plotWidth,'px;" class="flotr-title">', options.title, '</div>'
+ );
+
+ // Add subtitle
+ if (options.subtitle)
+ html.push(
+ '<div style="position:absolute;top:', this.titleHeight, 'px;left:',
+ this.plotOffset.left, 'px;font-size:smaller;text-align:center;width:',
+ this.plotWidth, 'px;" class="flotr-subtitle">', options.subtitle, '</div>'
+ );
+
+ html.push('</div>');
+
+ html.push('<div class="flotr-axis-title" style="font-weight:bold;">');
+
+ // Add x axis title
+ if (a.x.options.title && a.x.used)
+ html.push(
+ '<div style="position:absolute;top:',
+ (this.plotOffset.top + this.plotHeight + options.grid.labelMargin + a.x.titleSize.height),
+ 'px;left:', this.plotOffset.left, 'px;width:', this.plotWidth,
+ 'px;text-align:', a.x.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-x1">', a.x.options.title, '</div>'
+ );
+
+ // Add x2 axis title
+ if (a.x2.options.title && a.x2.used)
+ html.push(
+ '<div style="position:absolute;top:0;left:', this.plotOffset.left, 'px;width:',
+ this.plotWidth, 'px;text-align:', a.x2.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-x2">', a.x2.options.title, '</div>'
+ );
+
+ // Add y axis title
+ if (a.y.options.title && a.y.used)
+ html.push(
+ '<div style="position:absolute;top:',
+ (this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2),
+ 'px;left:0;text-align:', a.y.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-y1">', a.y.options.title, '</div>'
+ );
+
+ // Add y2 axis title
+ if (a.y2.options.title && a.y2.used)
+ html.push(
+ '<div style="position:absolute;top:',
+ (this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2),
+ 'px;right:0;text-align:', a.y2.options.titleAlign, ';" class="flotr-axis-title flotr-axis-title-y2">', a.y2.options.title, '</div>'
+ );
+
+ html = html.join('');
+
+ var div = D.create('div');
+ D.setStyles({
+ color: options.grid.color
+ });
+ div.className = 'flotr-titles';
+ D.insert(this.el, div);
+ D.insert(div, html);
+ }
+ }
+});
+})();
diff --git a/library/flotr2/flotr2.min.js b/library/flotr2/flotr2.min.js
new file mode 100644
index 0000000..c3601e8
--- /dev/null
+++ b/library/flotr2/flotr2.min.js
@@ -0,0 +1,27 @@
+/*!
+ * bean.js - copyright Jacob Thornton 2011
+ * https://github.com/fat/bean
+ * MIT License
+ * special thanks to:
+ * dean edwards: http://dean.edwards.name/
+ * dperini: https://github.com/dperini/nwevents
+ * the entire mootools team: github.com/mootools/mootools-core
+ *//*global module:true, define:true*/
+!function(e,t,n){typeof module!="undefined"?module.exports=n(e,t):typeof define=="function"&&typeof define.amd=="object"?define(n):t[e]=n(e,t)}("bean",this,function(e,t){var n=window,r=t[e],i=/over|out/,s=/[^\.]*(?=\..*)\.|.*/,o=/\..*/,u="addEventListener",a="attachEvent",f="removeEventListener",l="detachEvent",c=document||{},h=c.documentElement||{},p=h[u],d=p?u:a,v=Array.prototype.slice,m=/click|mouse|menu|drag|drop/i,g=/^touch|^gesture/i,y={one:1},b=function(e,t,n){for(n=0;n<t.length;n++)e[t[n]]=1;return e}({},("click dblclick mouseup mousedown contextmenu mousewheel DOMMouseScroll mouseover mouseout mousemove selectstart selectend keydown keypress keyup orientationchange focus blur change reset select submit load unload beforeunload resize move DOMContentLoaded readystatechange error abort scroll "+(p?"show input invalid touchstart touchmove touchend touchcancel gesturestart gesturechange gestureend message readystatechange pageshow pagehide popstate hashchange offline online afterprint beforeprint dragstart dragenter dragover dragleave drag drop dragend loadstart progress suspend emptied stalled loadmetadata loadeddata canplay canplaythrough playing waiting seeking seeked ended durationchange timeupdate play pause ratechange volumechange cuechange checking noupdate downloading cached updateready obsolete ":"")).split(" ")),w=function(){function e(e,t){while((t=t.parentNode)!==null)if(t===e)return!0;return!1}function t(t){var n=t.relatedTarget;return n?n!==this&&n.prefix!=="xul"&&!/document/.test(this.toString())&&!e(this,n):n===null}return{mouseenter:{base:"mouseover",condition:t},mouseleave:{base:"mouseout",condition:t},mousewheel:{base:/Firefox/.test(navigator.userAgent)?"DOMMouseScroll":"mousewheel"}}}(),E=function(){var e="altKey attrChange attrName bubbles cancelable ctrlKey currentTarget detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey srcElement target timeStamp type view which".split(" "),t=e.concat("button buttons clientX clientY dataTransfer fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" ")),n=e.concat("char charCode key keyCode".split(" ")),r=e.concat("touches targetTouches changedTouches scale rotation".split(" ")),s="preventDefault",o=function(e){return function(){e[s]?e[s]():e.returnValue=!1}},u="stopPropagation",a=function(e){return function(){e[u]?e[u]():e.cancelBubble=!0}},f=function(e){return function(){e[s](),e[u](),e.stopped=!0}},l=function(e,t,n){var r,i;for(r=n.length;r--;)i=n[r],!(i in t)&&i in e&&(t[i]=e[i])};return function(p,d){var v={originalEvent:p,isNative:d};if(!p)return v;var y,b=p.type,w=p.target||p.srcElement;v[s]=o(p),v[u]=a(p),v.stop=f(v),v.target=w&&w.nodeType===3?w.parentNode:w;if(d){if(b.indexOf("key")!==-1)y=n,v.keyCode=p.which||p.keyCode;else if(m.test(b)){y=t,v.rightClick=p.which===3||p.button===2,v.pos={x:0,y:0};if(p.pageX||p.pageY)v.clientX=p.pageX,v.clientY=p.pageY;else if(p.clientX||p.clientY)v.clientX=p.clientX+c.body.scrollLeft+h.scrollLeft,v.clientY=p.clientY+c.body.scrollTop+h.scrollTop;i.test(b)&&(v.relatedTarget=p.relatedTarget||p[(b==="mouseover"?"from":"to")+"Element"])}else g.test(b)&&(y=r);l(p,v,y||e)}return v}}(),S=function(e,t){return!p&&!t&&(e===c||e===n)?h:e},x=function(){function e(e,t,n,r,i){this.element=e,this.type=t,this.handler=n,this.original=r,this.namespaces=i,this.custom=w[t],this.isNative=b[t]&&e[d],this.eventType=p||this.isNative?t:"propertychange",this.customType=!p&&!this.isNative&&t,this.target=S(e,this.isNative),this.eventSupport=this.target[d]}return e.prototype={inNamespaces:function(e){var t,n;if(!e)return!0;if(!this.namespaces)return!1;for(t=e.length;t--;)for(n=this.namespaces.length;n--;)if(e[t]===this.namespaces[n])return!0;return!1},matches:function(e,t,n){return this.element===e&&(!t||this.original===t)&&(!n||this.handler===n)}},e}(),T=function(){var e={},t=function(n,r,i,s,o){if(!r||r==="*")for(var u in e)u.charAt(0)==="$"&&t(n,u.substr(1),i,s,o);else{var a=0,f,l=e["$"+r],c=n==="*";if(!l)return;for(f=l.length;a<f;a++)if(c||l[a].matches(n,i,s))if(!o(l[a],l,a,r))return}},n=function(t,n,r){var i,s=e["$"+n];if(s)for(i=s.length;i--;)if(s[i].matches(t,r,null))return!0;return!1},r=function(e,n,r){var i=[];return t(e,n,r,null,function(e){return i.push(e)}),i},i=function(t){return(e["$"+t.type]||(e["$"+t.type]=[])).push(t),t},s=function(n){t(n.element,n.type,null,n.handler,function(t,n,r){return n.splice(r,1),n.length===0&&delete e["$"+t.type],!1})},o=function(){var t,n=[];for(t in e)t.charAt(0)==="$"&&(n=n.concat(e[t]));return n};return{has:n,get:r,put:i,del:s,entries:o}}(),N=p?function(e,t,n,r){e[r?u:f](t,n,!1)}:function(e,t,n,r,i){i&&r&&e["_on"+i]===null&&(e["_on"+i]=0),e[r?a:l]("on"+t,n)},C=function(e,t,r){return function(i){return i=E(i||((this.ownerDocument||this.document||this).parentWindow||n).event,!0),t.apply(e,[i].concat(r))}},k=function(e,t,r,i,s,o){return function(u){if(i?i.apply(this,arguments):p?!0:u&&u.propertyName==="_on"+r||!u)u&&(u=E(u||((this.ownerDocument||this.document||this).parentWindow||n).event,o)),t.apply(e,u&&(!s||s.length===0)?arguments:v.call(arguments,u?0:1).concat(s))}},L=function(e,t,n,r,i){return function(){e(t,n,i),r.apply(this,arguments)}},A=function(e,t,n,r){var i,s,u,a=t&&t.replace(o,""),f=T.get(e,a,n);for(i=0,s=f.length;i<s;i++)f[i].inNamespaces(r)&&((u=f[i]).eventSupport&&N(u.target,u.eventType,u.handler,!1,u.type),T.del(u))},O=function(e,t,n,r,i){var u,a=t.replace(o,""),f=t.replace(s,"").split(".");if(T.has(e,a,n))return e;a==="unload"&&(n=L(A,e,a,n,r)),w[a]&&(w[a].condition&&(n=k(e,n,a,w[a].condition,!0)),a=w[a].base||a),u=T.put(new x(e,a,n,r,f[0]&&f)),u.handler=u.isNative?C(e,u.handler,i):k(e,u.handler,a,!1,i,!1),u.eventSupport&&N(u.target,u.eventType,u.handler,!0,u.customType)},M=function(e,t,n){return function(r){var i,s,o=typeof e=="string"?n(e,this):e;for(i=r.target;i&&i!==this;i=i.parentNode)for(s=o.length;s--;)if(o[s]===i)return t.apply(i,arguments)}},_=function(e,t,n){var r,i,u,a,f,l=A,c=t&&typeof t=="string";if(c&&t.indexOf(" ")>0){t=t.split(" ");for(f=t.length;f--;)_(e,t[f],n);return e}u=c&&t.replace(o,""),u&&w[u]&&(u=w[u].type);if(!t||c){if(a=c&&t.replace(s,""))a=a.split(".");l(e,u,n,a)}else if(typeof t=="function")l(e,null,t);else for(r in t)t.hasOwnProperty(r)&&_(e,r,t[r]);return e},D=function(e,t,n,r,i){var s,o,u,a,f=n,l=n&&typeof n=="string";if(t&&!n&&typeof t=="object")for(s in t)t.hasOwnProperty(s)&&D.apply(this,[e,s,t[s]]);else{a=arguments.length>3?v.call(arguments,3):[],o=(l?n:t).split(" "),l&&(n=M(t,f=r,i))&&(a=v.call(a,1)),this===y&&(n=L(_,e,t,n,f));for(u=o.length;u--;)O(e,o[u],n,f,a)}return e},P=function(){return D.apply(y,arguments)},H=p?function(e,t,r){var i=c.createEvent(e?"HTMLEvents":"UIEvents");i[e?"initEvent":"initUIEvent"](t,!0,!0,n,1),r.dispatchEvent(i)}:function(e,t,n){n=S(n,e),e?n.fireEvent("on"+t,c.createEventObject()):n["_on"+t]++},B=function(e,t,n){var r,i,u,a,f,l=t.split(" ");for(r=l.length;r--;){t=l[r].replace(o,"");if(a=l[r].replace(s,""))a=a.split(".");if(!a&&!n&&e[d])H(b[t],t,e);else{f=T.get(e,t),n=[!1].concat(n);for(i=0,u=f.length;i<u;i++)f[i].inNamespaces(a)&&f[i].handler.apply(e,n)}}return e},j=function(e,t,n){var r=0,i=T.get(t,n),s=i.length;for(;r<s;r++)i[r].original&&D(e,i[r].type,i[r].original);return e},F={add:D,one:P,remove:_,clone:j,fire:B,noConflict:function(){return t[e]=r,this}};if(n[a]){var I=function(){var e,t=T.entries();for(e in t)t[e].type&&t[e].type!=="unload"&&_(t[e].element,t[e].type);n[l]("onunload",I),n.CollectGarbage&&n.CollectGarbage()};n[a]("onunload",I)}return F});
+// Underscore.js 1.1.7
+// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
+// Underscore is freely distributable under the MIT license.
+// Portions of Underscore are inspired or borrowed from Prototype,
+// Oliver Steele's Functional, and John Resig's Micro-Templating.
+// For all details and documentation:
+// http://documentcloud.github.com/underscore
+
+(function(){var e=this,t=e._,n={},r=Array.prototype,i=Object.prototype,s=Function.prototype,o=r.slice,u=r.unshift,a=i.toString,f=i.hasOwnProperty,l=r.forEach,c=r.map,h=r.reduce,p=r.reduceRight,d=r.filter,v=r.every,m=r.some,g=r.indexOf,y=r.lastIndexOf,b=Array.isArray,w=Object.keys,E=s.bind,S=function(e){return new k(e)};typeof module!="undefined"&&module.exports?(module.exports=S,S._=S):e._=S,S.VERSION="1.1.7";var x=S.each=S.forEach=function(e,t,r){if(e==null)return;if(l&&e.forEach===l)e.forEach(t,r);else if(e.length===+e.length){for(var i=0,s=e.length;i<s;i++)if(i in e&&t.call(r,e[i],i,e)===n)return}else for(var o in e)if(f.call(e,o)&&t.call(r,e[o],o,e)===n)return};S.map=function(e,t,n){var r=[];return e==null?r:c&&e.map===c?e.map(t,n):(x(e,function(e,i,s){r[r.length]=t.call(n,e,i,s)}),r)},S.reduce=S.foldl=S.inject=function(e,t,n,r){var i=n!==void 0;e==null&&(e=[]);if(h&&e.reduce===h)return r&&(t=S.bind(t,r)),i?e.reduce(t,n):e.reduce(t);x(e,function(e,s,o){i?n=t.call(r,n,e,s,o):(n=e,i=!0)});if(!i)throw new TypeError("Reduce of empty array with no initial value");return n},S.reduceRight=S.foldr=function(e,t,n,r){e==null&&(e=[]);if(p&&e.reduceRight===p)return r&&(t=S.bind(t,r)),n!==void 0?e.reduceRight(t,n):e.reduceRight(t);var i=(S.isArray(e)?e.slice():S.toArray(e)).reverse();return S.reduce(i,t,n,r)},S.find=S.detect=function(e,t,n){var r;return T(e,function(e,i,s){if(t.call(n,e,i,s))return r=e,!0}),r},S.filter=S.select=function(e,t,n){var r=[];return e==null?r:d&&e.filter===d?e.filter(t,n):(x(e,function(e,i,s){t.call(n,e,i,s)&&(r[r.length]=e)}),r)},S.reject=function(e,t,n){var r=[];return e==null?r:(x(e,function(e,i,s){t.call(n,e,i,s)||(r[r.length]=e)}),r)},S.every=S.all=function(e,t,r){var i=!0;return e==null?i:v&&e.every===v?e.every(t,r):(x(e,function(e,s,o){if(!(i=i&&t.call(r,e,s,o)))return n}),i)};var T=S.some=S.any=function(e,t,r){t=t||S.identity;var i=!1;return e==null?i:m&&e.some===m?e.some(t,r):(x(e,function(e,s,o){if(i|=t.call(r,e,s,o))return n}),!!i)};S.include=S.contains=function(e,t){var n=!1;return e==null?n:g&&e.indexOf===g?e.indexOf(t)!=-1:(T(e,function(e){if(n=e===t)return!0}),n)},S.invoke=function(e,t){var n=o.call(arguments,2);return S.map(e,function(e){return(t.call?t||e:e[t]).apply(e,n)})},S.pluck=function(e,t){return S.map(e,function(e){return e[t]})},S.max=function(e,t,n){if(!t&&S.isArray(e))return Math.max.apply(Math,e);var r={computed:-Infinity};return x(e,function(e,i,s){var o=t?t.call(n,e,i,s):e;o>=r.computed&&(r={value:e,computed:o})}),r.value},S.min=function(e,t,n){if(!t&&S.isArray(e))return Math.min.apply(Math,e);var r={computed:Infinity};return x(e,function(e,i,s){var o=t?t.call(n,e,i,s):e;o<r.computed&&(r={value:e,computed:o})}),r.value},S.sortBy=function(e,t,n){return S.pluck(S.map(e,function(e,r,i){return{value:e,criteria:t.call(n,e,r,i)}}).sort(function(e,t){var n=e.criteria,r=t.criteria;return n<r?-1:n>r?1:0}),"value")},S.groupBy=function(e,t){var n={};return x(e,function(e,r){var i=t(e,r);(n[i]||(n[i]=[])).push(e)}),n},S.sortedIndex=function(e,t,n){n||(n=S.identity);var r=0,i=e.length;while(r<i){var s=r+i>>1;n(e[s])<n(t)?r=s+1:i=s}return r},S.toArray=function(e){return e?e.toArray?e.toArray():S.isArray(e)?o.call(e):S.isArguments(e)?o.call(e):S.values(e):[]},S.size=function(e){return S.toArray(e).length},S.first=S.head=function(e,t,n){return t!=null&&!n?o.call(e,0,t):e[0]},S.rest=S.tail=function(e,t,n){return o.call(e,t==null||n?1:t)},S.last=function(e){return e[e.length-1]},S.compact=function(e){return S.filter(e,function(e){return!!e})},S.flatten=function(e){return S.reduce(e,function(e,t){return S.isArray(t)?e.concat(S.flatten(t)):(e[e.length]=t,e)},[])},S.without=function(e){return S.difference(e,o.call(arguments,1))},S.uniq=S.unique=function(e,t){return S.reduce(e,function(e,n,r){if(0==r||(t===!0?S.last(e)!=n:!S.include(e,n)))e[e.length]=n;return e},[])},S.union=function(){return S.uniq(S.flatten(arguments))},S.intersection=S.intersect=function(e){var t=o.call(arguments,1);return S.filter(S.uniq(e),function(e){return S.every(t,function(t){return S.indexOf(t,e)>=0})})},S.difference=function(e,t){return S.filter(e,function(e){return!S.include(t,e)})},S.zip=function(){var e=o.call(arguments),t=S.max(S.pluck(e,"length")),n=new Array(t);for(var r=0;r<t;r++)n[r]=S.pluck(e,""+r);return n},S.indexOf=function(e,t,n){if(e==null)return-1;var r,i;if(n)return r=S.sortedIndex(e,t),e[r]===t?r:-1;if(g&&e.indexOf===g)return e.indexOf(t);for(r=0,i=e.length;r<i;r++)if(e[r]===t)return r;return-1},S.lastIndexOf=function(e,t){if(e==null)return-1;if(y&&e.lastIndexOf===y)return e.lastIndexOf(t);var n=e.length;while(n--)if(e[n]===t)return n;return-1},S.range=function(e,t,n){arguments.length<=1&&(t=e||0,e=0),n=arguments[2]||1;var r=Math.max(Math.ceil((t-e)/n),0),i=0,s=new Array(r);while(i<r)s[i++]=e,e+=n;return s},S.bind=function(e,t){if(e.bind===E&&E)return E.apply(e,o.call(arguments,1));var n=o.call(arguments,2);return function(){return e.apply(t,n.concat(o.call(arguments)))}},S.bindAll=function(e){var t=o.call(arguments,1);return t.length==0&&(t=S.functions(e)),x(t,function(t){e[t]=S.bind(e[t],e)}),e},S.memoize=function(e,t){var n={};return t||(t=S.identity),function(){var r=t.apply(this,arguments);return f.call(n,r)?n[r]:n[r]=e.apply(this,arguments)}},S.delay=function(e,t){var n=o.call(arguments,2);return setTimeout(function(){return e.apply(e,n)},t)},S.defer=function(e){return S.delay.apply(S,[e,1].concat(o.call(arguments,1)))};var N=function(e,t,n){var r;return function(){var i=this,s=arguments,o=function(){r=null,e.apply(i,s)};n&&clearTimeout(r);if(n||!r)r=setTimeout(o,t)}};S.throttle=function(e,t){return N(e,t,!1)},S.debounce=function(e,t){return N(e,t,!0)},S.once=function(e){var t=!1,n;return function(){return t?n:(t=!0,n=e.apply(this,arguments))}},S.wrap=function(e,t){return function(){var n=[e].concat(o.call(arguments));return t.apply(this,n)}},S.compose=function(){var e=o.call(arguments);return function(){var t=o.call(arguments);for(var n=e.length-1;n>=0;n--)t=[e[n].apply(this,t)];return t[0]}},S.after=function(e,t){return function(){if(--e<1)return t.apply(this,arguments)}},S.keys=w||function(e){if(e!==Object(e))throw new TypeError("Invalid object");var t=[];for(var n in e)f.call(e,n)&&(t[t.length]=n);return t},S.values=function(e){return S.map(e,S.identity)},S.functions=S.methods=function(e){var t=[];for(var n in e)S.isFunction(e[n])&&t.push(n);return t.sort()},S.extend=function(e){return x(o.call(arguments,1),function(t){for(var n in t)t[n]!==void 0&&(e[n]=t[n])}),e},S.defaults=function(e){return x(o.call(arguments,1),function(t){for(var n in t)e[n]==null&&(e[n]=t[n])}),e},S.clone=function(e){return S.isArray(e)?e.slice():S.extend({},e)},S.tap=function(e,t){return t(e),e},S.isEqual=function(e,t){if(e===t)return!0;var n=typeof e,r=typeof t;if(n!=r)return!1;if(e==t)return!0;if(!e&&t||e&&!t)return!1;e._chain&&(e=e._wrapped),t._chain&&(t=t._wrapped);if(e.isEqual)return e.isEqual(t);if(t.isEqual)return t.isEqual(e);if(S.isDate(e)&&S.isDate(t))return e.getTime()===t.getTime();if(S.isNaN(e)&&S.isNaN(t))return!1;if(S.isRegExp(e)&&S.isRegExp(t))return e.source===t.source&&e.global===t.global&&e.ignoreCase===t.ignoreCase&&e.multiline===t.multiline;if(n!=="object")return!1;if(e.length&&e.length!==t.length)return!1;var i=S.keys(e),s=S.keys(t);if(i.length!=s.length)return!1;for(var o in e)if(!(o in t)||!S.isEqual(e[o],t[o]))return!1;return!0},S.isEmpty=function(e){if(S.isArray(e)||S.isString(e))return e.length===0;for(var t in e)if(f.call(e,t))return!1;return!0},S.isElement=function(e){return!!e&&e.nodeType==1},S.isArray=b||function(e){return a.call(e)==="[object Array]"},S.isObject=function(e){return e===Object(e)},S.isArguments=function(e){return!!e&&!!f.call(e,"callee")},S.isFunction=function(e){return!!(e&&e.constructor&&e.call&&e.apply)},S.isString=function(e){return!!(e===""||e&&e.charCodeAt&&e.substr)},S.isNumber=function(e){return!!(e===0||e&&e.toExponential&&e.toFixed)},S.isNaN=function(e){return e!==e},S.isBoolean=function(e){return e===!0||e===!1},S.isDate=function(e){return!!(e&&e.getTimezoneOffset&&e.setUTCFullYear)},S.isRegExp=function(e){return!(!(e&&e.test&&e.exec)||!e.ignoreCase&&e.ignoreCase!==!1)},S.isNull=function(e){return e===null},S.isUndefined=function(e){return e===void 0},S.noConflict=function(){return e._=t,this},S.identity=function(e){return e},S.times=function(e,t,n){for(var r=0;r<e;r++)t.call(n,r)},S.mixin=function(e){x(S.functions(e),function(t){A(t,S[t]=e[t])})};var C=0;S.uniqueId=function(e){var t=C++;return e?e+t:t},S.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g},S.template=function(e,t){var n=S.templateSettings,r="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+e.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(n.interpolate,function(e,t){return"',"+t.replace(/\\'/g,"'")+",'"}).replace(n.evaluate||null,function(e,t){return"');"+t.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",i=new Function("obj",r);return t?i(t):i};var k=function(e){this._wrapped=e};S.prototype=k.prototype;var L=function(e,t){return t?S(e).chain():e},A=function(e,t){k.prototype[e]=function(){var e=o.call(arguments);return u.call(e,this._wrapped),L(t.apply(S,e),this._chain)}};S.mixin(S),x(["pop","push","reverse","shift","sort","splice","unshift"],function(e){var t=r[e];k.prototype[e]=function(){return t.apply(this._wrapped,arguments),L(this._wrapped,this._chain)}}),x(["concat","join","slice"],function(e){var t=r[e];k.prototype[e]=function(){return L(t.apply(this._wrapped,arguments),this._chain)}}),k.prototype.chain=function(){return this._chain=!0,this},k.prototype.value=function(){return this._wrapped}})();
+/**
+ * Flotr2 (c) 2012 Carl Sutherland
+ * MIT License
+ * Special thanks to:
+ * Flotr: http://code.google.com/p/flotr/ (fork)
+ * Flot: https://github.com/flot/flot (original fork)
+ */
+(function(){var e=this,t=this.Flotr,n;n={_:_,bean:bean,isIphone:/iphone/i.test(navigator.userAgent),isIE:navigator.appVersion.indexOf("MSIE")!=-1?parseFloat(navigator.appVersion.split("MSIE")[1]):!1,graphTypes:{},plugins:{},addType:function(e,t){n.graphTypes[e]=t,n.defaultOptions[e]=t.options||{},n.defaultOptions.defaultType=n.defaultOptions.defaultType||e},addPlugin:function(e,t){n.plugins[e]=t,n.defaultOptions[e]=t.options||{}},draw:function(e,t,r,i){return i=i||n.Graph,new i(e,t,r)},merge:function(e,t){var r,i,s=t||{};for(r in e)i=e[r],i&&typeof i=="object"?i.constructor===Array?s[r]=this._.clone(i):i.constructor!==RegExp&&!this._.isElement(i)&&!i.jquery?s[r]=n.merge(i,t?t[r]:undefined):s[r]=i:s[r]=i;return s},clone:function(e){return n.merge(e,{})},getTickSize:function(e,t,r,i){var s=(r-t)/e,o=n.getMagnitude(s),u=10,a=s/o;return a<1.5?u=1:a<2.25?u=2:a<3?u=i===0?2:2.5:a<7.5&&(u=5),u*o},defaultTickFormatter:function(e,t){return e+""},defaultTrackFormatter:function(e){return"("+e.x+", "+e.y+")"},engineeringNotation:function(e,t,n){var r=["Y","Z","E","P","T","G","M","k",""],i=["y","z","a","f","p","n","µ","m",""],s=r.length;n=n||1e3,t=Math.pow(10,t||2);if(e===0)return 0;if(e>1)while(s--&&e>=n)e/=n;else{r=i,s=r.length;while(s--&&e<1)e*=n}return Math.round(e*t)/t+r[s]},getMagnitude:function(e){return Math.pow(10,Math.floor(Math.log(e)/Math.LN10))},toPixel:function(e){return Math.floor(e)+.5},toRad:function(e){return-e*(Math.PI/180)},floorInBase:function(e,t){return t*Math.floor(e/t)},drawText:function(e,t,r,i,s){if(!e.fillText){e.drawText(t,r,i,s);return}s=this._.extend({size:n.defaultOptions.fontSize,color:"#000000",textAlign:"left",textBaseline:"bottom",weight:1,angle:0},s),e.save(),e.translate(r,i),e.rotate(s.angle),e.fillStyle=s.color,e.font=(s.weight>1?"bold ":"")+s.size*1.3+"px sans-serif",e.textAlign=s.textAlign,e.textBaseline=s.textBaseline,e.fillText(t,0,0),e.restore()},getBestTextAlign:function(e,t){return t=t||{textAlign:"center",textBaseline:"middle"},e+=n.getTextAngleFromAlign(t),Math.abs(Math.cos(e))>.01&&(t.textAlign=Math.cos(e)>0?"right":"left"),Math.abs(Math.sin(e))>.01&&(t.textBaseline=Math.sin(e)>0?"top":"bottom"),t},alignTable:{"right middle":0,"right top":Math.PI/4,"center top":Math.PI/2,"left top":3*(Math.PI/4),"left middle":Math.PI,"left bottom":-3*(Math.PI/4),"center bottom":-Math.PI/2,"right bottom":-Math.PI/4,"center middle":0},getTextAngleFromAlign:function(e){return n.alignTable[e.textAlign+" "+e.textBaseline]||0},noConflict:function(){return e.Flotr=t,this}},e.Flotr=n})(),Flotr.defaultOptions={colors:["#00A8F0","#C0D800","#CB4B4B","#4DA74D","#9440ED"],ieBackgroundColor:"#FFFFFF",title:null,subtitle:null,shadowSize:4,defaultType:null,HtmlText:!0,fontColor:"#545454",fontSize:7.5,resolution:1,parseFloat:!0,preventDefault:!0,xaxis:{ticks:null,minorTicks:null,showLabels:!0,showMinorLabels:!1,labelsAngle:0,title:null,titleAngle:0,noTicks:5,minorTickFreq:null,tickFormatter:Flotr.defaultTickFormatter,tickDecimals:null,min:null,max:null,autoscale:!1,autoscaleMargin:0,color:null,mode:"normal",timeFormat:null,timeMode:"UTC",timeUnit:"millisecond",scaling:"linear",base:Math.E,titleAlign:"center",margin:!0},x2axis:{},yaxis:{ticks:null,minorTicks:null,showLabels:!0,showMinorLabels:!1,labelsAngle:0,title:null,titleAngle:90,noTicks:5,minorTickFreq:null,tickFormatter:Flotr.defaultTickFormatter,tickDecimals:null,min:null,max:null,autoscale:!1,autoscaleMargin:0,color:null,scaling:"linear",base:Math.E,titleAlign:"center",margin:!0},y2axis:{titleAngle:270},grid:{color:"#545454",backgroundColor:null,backgroundImage:null,watermarkAlpha:.4,tickColor:"#DDDDDD",labelMargin:3,verticalLines:!0,minorVerticalLines:null,horizontalLines:!0,minorHorizontalLines:null,outlineWidth:1,outline:"nsew",circular:!1},mouse:{track:!1,trackAll:!1,position:"se",relative:!1,trackFormatter:Flotr.defaultTrackFormatter,margin:5,lineColor:"#FF3F19",trackDecimals:1,sensibility:2,trackY:!0,radius:3,fillColor:null,fillOpacity:.4}},function(){function t(e,t,n,r){this.rgba=["r","g","b","a"];var i=4;while(-1<--i)this[this.rgba[i]]=arguments[i]||(i==3?1:0);this.normalize()}var e=Flotr._,n={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]};t.prototype={scale:function(t,n,r,i){var s=4;while(-1<--s)e.isUndefined(arguments[s])||(this[this.rgba[s]]*=arguments[s]);return this.normalize()},alpha:function(t){return!e.isUndefined(t)&&!e.isNull(t)&&(this.a=t),this.normalize()},clone:function(){return new t(this.r,this.b,this.g,this.a)},limit:function(e,t,n){return Math.max(Math.min(e,n),t)},normalize:function(){var e=this.limit;return this.r=e(parseInt(this.r,10),0,255),this.g=e(parseInt(this.g,10),0,255),this.b=e(parseInt(this.b,10),0,255),this.a=e(this.a,0,1),this},distance:function(e){if(!e)return;e=new t.parse(e);var n=0,r=3;while(-1<--r)n+=Math.abs(this[this.rgba[r]]-e[this.rgba[r]]);return n},toString:function(){return this.a>=1?"rgb("+[this.r,this.g,this.b].join(",")+")":"rgba("+[this.r,this.g,this.b,this.a].join(",")+")"},contrast:function(){var e=1-(.299*this.r+.587*this.g+.114*this.b)/255;return e<.5?"#000000":"#ffffff"}},e.extend(t,{parse:function(e){if(e instanceof t)return e;var r;if(r=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(e))return new t(parseInt(r[1],16),parseInt(r[2],16),parseInt(r[3],16));if(r=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(e))return new t(parseInt(r[1],10),parseInt(r[2],10),parseInt(r[3],10));if(r=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(e))return new t(parseInt(r[1]+r[1],16),parseInt(r[2]+r[2],16),parseInt(r[3]+r[3],16));if(r=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(e))return new t(parseInt(r[1],10),parseInt(r[2],10),parseInt(r[3],10),parseFloat(r[4]));if(r=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(e))return new t(parseFloat(r[1])*2.55,parseFloat(r[2])*2.55,parseFloat(r[3])*2.55);if(r=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(e))return new t(parseFloat(r[1])*2.55,parseFloat(r[2])*2.55,parseFloat(r[3])*2.55,parseFloat(r[4]));var i=(e+"").replace(/^\s*([\S\s]*?)\s*$/,"$1").toLowerCase();return i=="transparent"?new t(255,255,255,0):(r=n[i])?new t(r[0],r[1],r[2]):new t(0,0,0,0)},processColor:function(n,r){var i=r.opacity;if(!n)return"rgba(0, 0, 0, 0)";if(n instanceof t)return n.alpha(i).toString();if(e.isString(n))return t.parse(n).alpha(i).toString();var s=n.colors?n:{colors:n};if(!r.ctx)return e.isArray(s.colors)?t.parse(e.isArray(s.colors[0])?s.colors[0][1]:s.colors[0]).alpha(i).toString():"rgba(0, 0, 0, 0)";s=e.extend({start:"top",end:"bottom"},s),/top/i.test(s.start)&&(r.x1=0),/left/i.test(s.start)&&(r.y1=0),/bottom/i.test(s.end)&&(r.x2=0),/right/i.test(s.end)&&(r.y2=0);var o,u,a,f=r.ctx.createLinearGradient(r.x1,r.y1,r.x2,r.y2);for(o=0;o<s.colors.length;o++)u=s.colors[o],e.isArray(u)?(a=u[0],u=u[1]):a=o/(s.colors.length-1),f.addColorStop(a,t.parse(u).alpha(i));return f}}),Flotr.Color=t}(),Flotr.Date={set:function(e,t,n,r){n=n||"UTC",t="set"+(n==="UTC"?"UTC":"")+t,e[t](r)},get:function(e,t,n){return n=n||"UTC",t="get"+(n==="UTC"?"UTC":"")+t,e[t]()},format:function(e,t,n){function s(e){return e+="",e.length==1?"0"+e:e}if(!e)return;var r=this.get,i={h:r(e,"Hours",n).toString(),H:s(r(e,"Hours",n)),M:s(r(e,"Minutes",n)),S:s(r(e,"Seconds",n)),s:r(e,"Milliseconds",n),d:r(e,"Date",n).toString(),m:(r(e,"Month",n)+1).toString(),y:r(e,"FullYear",n).toString(),b:Flotr.Date.monthNames[r(e,"Month",n)]},o=[],u,a=!1;for(var f=0;f<t.length;++f)u=t.charAt(f),a?(o.push(i[u]||u),a=!1):u=="%"?a=!0:o.push(u);return o.join("")},getFormat:function(e,t){var n=Flotr.Date.timeUnits;return e<n.second?"%h:%M:%S.%s":e<n.minute?"%h:%M:%S":e<n.day?t<2*n.day?"%h:%M":"%b %d %h:%M":e<n.month?"%b %d":e<n.year?t<n.year?"%b":"%b %y":"%y"},formatter:function(e,t){var n=t.options,r=Flotr.Date.timeUnits[n.timeUnit],i=new Date(e*r);if(t.options.timeFormat)return Flotr.Date.format(i,n.timeFormat,n.timeMode);var s=(t.max-t.min)*r,o=t.tickSize*Flotr.Date.timeUnits[t.tickUnit];return Flotr.Date.format(i,Flotr.Date.getFormat(o,s),n.timeMode)},generator:function(e){function y(e){t(m,e,o,Flotr.floorInBase(n(m,e,o),h))}var t=this.set,n=this.get,r=this.timeUnits,i=this.spec,s=e.options,o=s.timeMode,u=r[s.timeUnit],a=e.min*u,f=e.max*u,l=(f-a)/s.noTicks,c=[],h=e.tickSize,p,d,v;d=s.tickFormatter===Flotr.defaultTickFormatter?this.formatter:s.tickFormatter;for(v=0;v<i.length-1;++v){var m=i[v][0]*r[i[v][1]];if(l<(m+i[v+1][0]*r[i[v+1][1]])/2&&m>=h)break}h=i[v][0],p=i[v][1],p=="year"&&(h=Flotr.getTickSize(s.noTicks*r.year,a,f,0),h==.5&&(p="month",h=6)),e.tickUnit=p,e.tickSize=h;var g=h*r[p];m=new Date(a);switch(p){case"millisecond":y("Milliseconds");break;case"second":y("Seconds");break;case"minute":y("Minutes");break;case"hour":y("Hours");break;case"month":y("Month");break;case"year":y("FullYear")}g>=r.second&&t(m,"Milliseconds",o,0),g>=r.minute&&t(m,"Seconds",o,0),g>=r.hour&&t(m,"Minutes",o,0),g>=r.day&&t(m,"Hours",o,0),g>=r.day*4&&t(m,"Date",o,1),g>=r.year&&t(m,"Month",o,0);var b=0,w=NaN,E;do{E=w,w=m.getTime(),c.push({v:w/u,label:d(w/u,e)});if(p=="month")if(h<1){t(m,"Date",o,1);var S=m.getTime();t(m,"Month",o,n(m,"Month",o)+1);var x=m.getTime();m.setTime(w+b*r.hour+(x-S)*h),b=n(m,"Hours",o),t(m,"Hours",o,0)}else t(m,"Month",o,n(m,"Month",o)+h);else p=="year"?t(m,"FullYear",o,n(m,"FullYear",o)+h):m.setTime(w+g)}while(w<f&&w!=E);return c},timeUnits:{millisecond:1,second:1e3,minute:6e4,hour:36e5,day:864e5,month:2592e6,year:31556952e3},spec:[[1,"millisecond"],[20,"millisecond"],[50,"millisecond"],[100,"millisecond"],[200,"millisecond"],[500,"millisecond"],[1,"second"],[2,"second"],[5,"second"],[10,"second"],[30,"second"],[1,"minute"],[2,"minute"],[5,"minute"],[10,"minute"],[30,"minute"],[1,"hour"],[2,"hour"],[4,"hour"],[8,"hour"],[12,"hour"],[1,"day"],[2,"day"],[3,"day"],[.25,"month"],[.5,"month"],[1,"month"],[2,"month"],[3,"month"],[6,"month"],[1,"year"]],monthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]},function(){function t(e){return e&&e.jquery?e[0]:e}var e=Flotr._;Flotr.DOM={addClass:function(n,r){n=t(n);var i=n.className?n.className:"";if(e.include(i.split(/\s+/g),r))return;n.className=(i?i+" ":"")+r},create:function(e){return document.createElement(e)},node:function(e){var t=Flotr.DOM.create("div"),n;return t.innerHTML=e,n=t.children[0],t.innerHTML="",n},empty:function(e){e=t(e),e.innerHTML=""},remove:function(e){e=t(e),e.parentNode.removeChild(e)},hide:function(e){e=t(e),Flotr.DOM.setStyles(e,{display:"none"})},insert:function(n,r){n=t(n),e.isString(r)?n.innerHTML+=r:e.isElement(r)&&n.appendChild(r)},opacity:function(e,n){e=t(e),e.style.opacity=n},position:function(e,n){return e=t(e),e.offsetParent?(n=this.position(e.offsetParent),n.left+=e.offsetLeft,n.top+=e.offsetTop,n):{left:e.offsetLeft||0,top:e.offsetTop||0}},removeClass:function(n,r){var i=n.className?n.className:"";n=t(n),n.className=e.filter(i.split(/\s+/g),function(e){if(e!=r)return!0}).join(" ")},setStyles:function(n,r){n=t(n),e.each(r,function(e,t){n.style[t]=e})},show:function(e){e=t(e),Flotr.DOM.setStyles(e,{display:""})},size:function(e){return e=t(e),{height:e.offsetHeight,width:e.offsetWidth}}}}(),function(){var e=Flotr,t=e.bean;e.EventAdapter={observe:function(e,n,r){return t.add(e,n,r),this},fire:function(e,n,r){return t.fire(e,n,r),typeof Prototype!="undefined"&&Event.fire(e,n,r),this},stopObserving:function(e,n,r){return t.remove(e,n,r),this},eventPointer:function(t){if(!e._.isUndefined(t.touches)&&t.touches.length>0)return{x:t.touches[0].pageX,y:t.touches[0].pageY};if(!e._.isUndefined(t.changedTouches)&&t.changedTouches.length>0)return{x:t.changedTouches[0].pageX,y:t.changedTouches[0].pageY};if(t.pageX||t.pageY)return{x:t.pageX,y:t.pageY};if(t.clientX||t.clientY){var n=document,r=n.body,i=n.documentElement;return{x:t.clientX+r.scrollLeft+i.scrollLeft,y:t.clientY+r.scrollTop+i.scrollTop}}}}}(),function(){var e=Flotr,t=e.DOM,n=e._,r=function(e){this.o=e};r.prototype={dimensions:function(e,t,n,r){return e?this.o.html?this.html(e,this.o.element,n,r):this.canvas(e,t):{width:0,height:0}},canvas:function(t,n){if(!this.o.textEnabled)return;n=n||{};var r=this.measureText(t,n),i=r.width,s=n.size||e.defaultOptions.fontSize,o=n.angle||0,u=Math.cos(o),a=Math.sin(o),f=2,l=6,c;return c={width:Math.abs(u*i)+Math.abs(a*s)+f,height:Math.abs(a*i)+Math.abs(u*s)+l},c},html:function(e,n,r,i){var s=t.create("div");return t.setStyles(s,{position:"absolute",top:"-10000px"}),t.insert(s,'<div style="'+r+'" class="'+i+' flotr-dummy-div">'+e+"</div>"),t.insert(this.o.element,s),t.size(s)},measureText:function(t,r){var i=this.o.ctx,s;return!i.fillText||e.isIphone&&i.measure?{width:i.measure(t,r)}:(r=n.extend({size:e.defaultOptions.fontSize,weight:1,angle:0},r),i.save(),i.font=(r.weight>1?"bold ":"")+r.size*1.3+"px sans-serif",s=i.measureText(t),i.restore(),s)}},Flotr.Text=r}(),function(){function i(e,n,r){return t.observe.apply(this,arguments),this._handles.push(arguments),this}var e=Flotr.DOM,t=Flotr.EventAdapter,n=Flotr._,r=Flotr;Graph=function(e,i,s){this._setEl(e),this._initMembers(),this._initPlugins(),t.fire(this.el,"flotr:beforeinit",[this]),this.data=i,this.series=r.Series.getSeries(i),this._initOptions(s),this._initGraphTypes(),this._initCanvas(),this._text=new r.Text({element:this.el,ctx:this.ctx,html:this.options.HtmlText,textEnabled:this.textEnabled}),t.fire(this.el,"flotr:afterconstruct",[this]),this._initEvents(),this.findDataRanges(),this.calculateSpacing(),this.draw(n.bind(function(){t.fire(this.el,"flotr:afterinit",[this])},this))},Graph.prototype={destroy:function(){t.fire(this.el,"flotr:destroy"),n.each(this._handles,function(e){t.stopObserving.apply(this,e)}),this._handles=[],this.el.graph=null},observe:i,_observe:i,processColor:function(e,t){var i={x1:0,y1:0,x2:this.plotWidth,y2:this.plotHeight,opacity:1,ctx:this.ctx};return n.extend(i,t),r.Color.processColor(e,i)},findDataRanges:function(){var e=this.axes,t,i,s;n.each(this.series,function(e){s=e.getRange(),s&&(t=e.xaxis,i=e.yaxis,t.datamin=Math.min(s.xmin,t.datamin),t.datamax=Math.max(s.xmax,t.datamax),i.datamin=Math.min(s.ymin,i.datamin),i.datamax=Math.max(s.ymax,i.datamax),t.used=t.used||s.xused,i.used=i.used||s.yused)},this),!e.x.used&&!e.x2.used&&(e.x.used=!0),!e.y.used&&!e.y2.used&&(e.y.used=!0),n.each(e,function(e){e.calculateRange()});var o=n.keys(r.graphTypes),u=!1;n.each(this.series,function(e){if(e.hide)return;n.each(o,function(t){e[t]&&e[t].show&&(this.extendRange(t,e),u=!0)},this),u||this.extendRange(this.options.defaultType,e)},this)},extendRange:function(e,t){this[e].extendRange&&this[e].extendRange(t,t.data,t[e],this[e]),this[e].extendYRange&&this[e].extendYRange(t.yaxis,t.data,t[e],this[e]),this[e].extendXRange&&this[e].extendXRange(t.xaxis,t.data,t[e],this[e])},calculateSpacing:function(){var e=this.axes,t=this.options,r=this.series,i=t.grid.labelMargin,s=this._text,o=e.x,u=e.x2,a=e.y,f=e.y2,l=t.grid.outlineWidth,c,h,p,d;n.each(e,function(e){e.calculateTicks(),e.calculateTextDimensions(s,t)}),d=s.dimensions(t.title,{size:t.fontSize*1.5},"font-size:1em;font-weight:bold;","flotr-title"),this.titleHeight=d.height,d=s.dimensions(t.subtitle,{size:t.fontSize},"font-size:smaller;","flotr-subtitle"),this.subtitleHeight=d.height;for(h=0;h<t.length;++h)r[h].points.show&&(l=Math.max(l,r[h].points.radius+r[h].points.lineWidth/2));var v=this.plotOffset;o.options.margin===!1?(v.bottom=0,v.top=0):o.options.margin===!0?(v.bottom+=(t.grid.circular?0:o.used&&o.options.showLabels?o.maxLabel.height+i:0)+(o.used&&o.options.title?o.titleSize.height+i:0)+l,v.top+=(t.grid.circular?0:u.used&&u.options.showLabels?u.maxLabel.height+i:0)+(u.used&&u.options.title?u.titleSize.height+i:0)+this.subtitleHeight+this.titleHeight+l):(v.bottom=o.options.margin,v.top=o.options.margin),a.options.margin===!1?(v.left=0,v.right=0):a.options.margin===!0?(v.left+=(t.grid.circular?0:a.used&&a.options.showLabels?a.maxLabel.width+i:0)+(a.used&&a.options.title?a.titleSize.width+i:0)+l,v.right+=(t.grid.circular?0:f.used&&f.options.showLabels?f.maxLabel.width+i:0)+(f.used&&f.options.title?f.titleSize.width+i:0)+l):(v.left=a.options.margin,v.right=a.options.margin),v.top=Math.floor(v.top),this.plotWidth=this.canvasWidth-v.left-v.right,this.plotHeight=this.canvasHeight-v.bottom-v.top,o.length=u.length=this.plotWidth,a.length=f.length=this.plotHeight,a.offset=f.offset=this.plotHeight,o.setScale(),u.setScale(),a.setScale(),f.setScale()},draw:function(e){var n=this.ctx,r;t.fire(this.el,"flotr:beforedraw",[this.series,this]);if(this.series.length){n.save(),n.translate(this.plotOffset.left,this.plotOffset.top);for(r=0;r<this.series.length;r++)this.series[r].hide||this.drawSeries(this.series[r]);n.restore(),this.clip()}t.fire(this.el,"flotr:afterdraw",[this.series,this]),e&&e()},drawSeries:function(e){function t(e,t){var n=this.getOptions(e,t);this[t].draw(n)}var i=!1;e=e||this.series,n.each(r.graphTypes,function(n,r){e[r]&&e[r].show&&this[r]&&(i=!0,t.call(this,e,r))},this),i||t.call(this,e,this.options.defaultType)},getOptions:function(e,t){var n=e[t],i=this[t],s=e.xaxis,o=e.yaxis,u={context:this.ctx,width:this.plotWidth,height:this.plotHeight,fontSize:this.options.fontSize,fontColor:this.options.fontColor,textEnabled:this.textEnabled,htmlText:this.options.HtmlText,text:this._text,element:this.el,data:e.data,color:e.color,shadowSize:e.shadowSize,xScale:s.d2p,yScale:o.d2p,xInverse:s.p2d,yInverse:o.p2d};return u=r.merge(n,u),u.fillStyle=this.processColor(n.fillColor||e.color,{opacity:n.fillOpacity}),u},getEventPosition:function(n){var r=document,i=r.body,s=r.documentElement,o=this.axes,u=this.plotOffset,a=this.lastMousePos,f=t.eventPointer(n),l=f.x-a.pageX,c=f.y-a.pageY,h,p,d;return"ontouchstart"in this.el?(h=e.position(this.overlay),p=f.x-h.left-u.left,d=f.y-h.top-u.top):(h=this.overlay.getBoundingClientRect(),p=n.clientX-h.left-u.left-i.scrollLeft-s.scrollLeft,d=n.clientY-h.top-u.top-i.scrollTop-s.scrollTop),{x:o.x.p2d(p),x2:o.x2.p2d(p),y:o.y.p2d(d),y2:o.y2.p2d(d),relX:p,relY:d,dX:l,dY:c,absX:f.x,absY:f.y,pageX:f.x,pageY:f.y}},clickHandler:function(e){if(this.ignoreClick)return this.ignoreClick=!1,this.ignoreClick;t.fire(this.el,"flotr:click",[this.getEventPosition(e),this])},mouseMoveHandler:function(e){if(this.mouseDownMoveHandler)return;var n=this.getEventPosition(e);t.fire(this.el,"flotr:mousemove",[e,n,this]),this.lastMousePos=n},mouseDownHandler:function(e){if(this.mouseUpHandler)return;this.mouseUpHandler=n.bind(function(e){t.stopObserving(document,"mouseup",this.mouseUpHandler),t.stopObserving(document,"mousemove",this.mouseDownMoveHandler),this.mouseDownMoveHandler=null,this.mouseUpHandler=null,t.fire(this.el,"flotr:mouseup",[e,this])},this),this.mouseDownMoveHandler=n.bind(function(n){var r=this.getEventPosition(n);t.fire(this.el,"flotr:mousemove",[e,r,this]),this.lastMousePos=r},this),t.observe(document,"mouseup",this.mouseUpHandler),t.observe(document,"mousemove",this.mouseDownMoveHandler),t.fire(this.el,"flotr:mousedown",[e,this]),this.ignoreClick=!1},drawTooltip:function(t,n,r,i){var s=this.getMouseTrack(),o="opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;",u=i.position,a=i.margin,f=this.plotOffset;n!==null&&r!==null?(i.relative?(u.charAt(0)=="n"?o+="bottom:"+(a-f.top-r+this.canvasHeight)+"px;top:auto;":u.charAt(0)=="s"&&(o+="top:"+(a+f.top+r)+"px;bottom:auto;"),u.charAt(1)=="e"?o+="left:"+(a+f.left+n)+"px;right:auto;":u.charAt(1)=="w"&&(o+="right:"+(a-f.left-n+this.canvasWidth)+"px;left:auto;")):(u.charAt(0)=="n"?o+="top:"+(a+f.top)+"px;bottom:auto;":u.charAt(0)=="s"&&(o+="bottom:"+(a+f.bottom)+"px;top:auto;"),u.charAt(1)=="e"?o+="right:"+(a+f.right)+"px;left:auto;":u.charAt(1)=="w"&&(o+="left:"+(a+f.left)+"px;right:auto;")),s.style.cssText=o,e.empty(s),e.insert(s,t),e.show(s)):e.hide(s)},clip:function(e){var t=this.plotOffset,n=this.canvasWidth,i=this.canvasHeight;e=e||this.ctx;if(r.isIE&&r.isIE<9&&!r.isFlashCanvas){if(e===this.octx)return;e.save(),e.fillStyle=this.processColor(this.options.ieBackgroundColor),e.fillRect(0,0,n,t.top),e.fillRect(0,0,t.left,i),e.fillRect(0,i-t.bottom,n,t.bottom),e.fillRect(n-t.right,0,t.right,i),e.restore()}else e.clearRect(0,0,n,t.top),e.clearRect(0,0,t.left,i),e.clearRect(0,i-t.bottom,n,t.bottom),e.clearRect(n-t.right,0,t.right,i)},_initMembers:function(){this._handles=[],this.lastMousePos={pageX:null,pageY:null},this.plotOffset={left:0,right:0,top:0,bottom:0},this.ignoreClick=!0,this.prevHit=null},_initGraphTypes:function(){n.each(r.graphTypes,function(e,t){this[t]=r.clone(e)},this)},_initEvents:function(){var e=this.el,r,i,s;"ontouchstart"in e?(r=n.bind(function(n){s=!0,t.stopObserving(document,"touchend",r),t.fire(e,"flotr:mouseup",[event,this]),this.multitouches=null,i||this.clickHandler(n)},this),this.observe(this.overlay,"touchstart",n.bind(function(n){i=!1,s=!1,this.ignoreClick=!1,n.touches&&n.touches.length>1&&(this.multitouches=n.touches),t.fire(e,"flotr:mousedown",[event,this]),this.observe(document,"touchend",r)},this)),this.observe(this.overlay,"touchmove",n.bind(function(n){var r=this.getEventPosition(n);this.options.preventDefault&&n.preventDefault(),i=!0,this.multitouches||n.touches&&n.touches.length>1?this.multitouches=n.touches:s||t.fire(e,"flotr:mousemove",[event,r,this]),this.lastMousePos=r},this))):this.observe(this.overlay,"mousedown",n.bind(this.mouseDownHandler,this)).observe(e,"mousemove",n.bind(this.mouseMoveHandler,this)).observe(this.overlay,"click",n.bind(this.clickHandler,this)).observe(e,"mouseout",function(n){t.fire(e,"flotr:mouseout",n)})},_initCanvas:function(){function l(i,s){return i||(i=e.create("canvas"),typeof FlashCanvas!="undefined"&&typeof i.getContext=="function"&&(FlashCanvas.initElement(i),this.isFlashCanvas=!0),i.className="flotr-"+s,i.style.cssText="position:absolute;left:0px;top:0px;",e.insert(t,i)),n.each(a,function(t,n){e.show(i);if(s=="canvas"&&i.getAttribute(n)===t)return;i.setAttribute(n,t*r.resolution),i.style[n]=t+"px"}),i.context_=null,i}function c(e){window.G_vmlCanvasManager&&window.G_vmlCanvasManager.initElement(e);var t=e.getContext("2d");return window.G_vmlCanvasManager||t.scale(r.resolution,r.resolution),t}var t=this.el,r=this.options,i=t.children,s=[],o,u,a,f;for(u=i.length;u--;)o=i[u],!this.canvas&&o.className==="flotr-canvas"?this.canvas=o:!this.overlay&&o.className==="flotr-overlay"?this.overlay=o:s.push(o);for(u=s.length;u--;)t.removeChild(s[u]);e.setStyles(t,{position:"relative"}),a={},a.width=t.clientWidth,a.height=t.clientHeight;if(a.width<=0||a.height<=0||r.resolution<=0)throw"Invalid dimensions for plot, width = "+a.width+", height = "+a.height+", resolution = "+r.resolution;this.canvas=l(this.canvas,"canvas"),this.overlay=l(this.overlay,"overlay"),this.ctx=c(this.canvas),this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.octx=c(this.overlay),this.octx.clearRect(0,0,this.overlay.width,this.overlay.height),this.canvasHeight=a.height,this.canvasWidth=a.width,this.textEnabled=!!this.ctx.drawText||!!this.ctx.fillText},_initPlugins:function(){n.each(r.plugins,function(e,t){n.each(e.callbacks,function(e,t){this.observe(this.el,t,n.bind(e,this))},this),this[t]=r.clone(e),n.each(this[t],function(e,r){n.isFunction(e)&&(this[t][r]=n.bind(e,this))},this)},this)},_initOptions:function(e){var i=r.clone(r.defaultOptions);i.x2axis=n.extend(n.clone(i.xaxis),i.x2axis),i.y2axis=n.extend(n.clone(i.yaxis),i.y2axis),this.options=r.merge(e||{},i),this.options.grid.minorVerticalLines===null&&this.options.xaxis.scaling==="logarithmic"&&(this.options.grid.minorVerticalLines=!0),this.options.grid.minorHorizontalLines===null&&this.options.yaxis.scaling==="logarithmic"&&(this.options.grid.minorHorizontalLines=!0),t.fire(this.el,"flotr:afterinitoptions",[this]),this.axes=r.Axis.getAxes(this.options);var s=[],o=[],u=this.series.length,a=this.series.length,f=this.options.colors,l=[],c=0,h,p,d,v;for(p=a-1;p>-1;--p)h=this.series[p].color,h&&(--a,n.isNumber(h)?s.push(h):l.push(r.Color.parse(h)));for(p=s.length-1;p>-1;--p)a=Math.max(a,s[p]+1);for(p=0;o.length<a;){h=f.length==p?new r.Color(100,100,100):r.Color.parse(f[p]);var m=c%2==1?-1:1,g=1+m*Math.ceil(c/2)*.2;h.scale(g,g,g),o.push(h),++p>=f.length&&(p=0,++c)}for(p=0,d=0;p<u;++p){v=this.series[p],v.color?n.isNumber(v.color)&&(v.color=o[v.color].toString()):v.color=o[d++].toString(),v.xaxis||(v.xaxis=this.axes.x),v.xaxis==1?v.xaxis=this.axes.x:v.xaxis==2&&(v.xaxis=this.axes.x2),v.yaxis||(v.yaxis=this.axes.y),v.yaxis==1?v.yaxis=this.axes.y:v.yaxis==2&&(v.yaxis=this.axes.y2);for(var y in r.graphTypes)v[y]=n.extend(n.clone(this.options[y]),v[y]);v.mouse=n.extend(n.clone(this.options.mouse),v.mouse),n.isUndefined(v.shadowSize)&&(v.shadowSize=this.options.shadowSize)}},_setEl:function(e){if(!e)throw"The target container doesn't exist";if(e.graph instanceof Graph)e.graph.destroy();else if(!e.clientWidth)throw"The target container must be visible";e.graph=this,this.el=e}},Flotr.Graph=Graph}(),function(){function n(t){this.orientation=1,this.offset=0,this.datamin=Number.MAX_VALUE,this.datamax=-Number.MAX_VALUE,e.extend(this,t)}function r(e,t){return e=Math.log(Math.max(e,Number.MIN_VALUE)),t!==Math.E&&(e/=Math.log(t)),e}function s(e,t){return t===Math.E?Math.exp(e):Math.pow(t,e)}var e=Flotr._,t="logarithmic";n.prototype={setScale:function(){var e=this.length,n=this.max,i=this.min,o=this.offset,u=this.orientation,a=this.options,f=a.scaling===t,l;f?l=e/(r(n,a.base)-r(i,a.base)):l=e/(n-i),this.scale=l,f?(this.d2p=function(e){return o+u*(r(e,a.base)-r(i,a.base))*l},this.p2d=function(e){return s((o+u*e)/l+r(i,a.base),a.base)}):(this.d2p=function(e){return o+u*(e-i)*l},this.p2d=function(e){return(o+u*e)/l+i})},calculateTicks:function(){var t=this.options;this.ticks=[],this.minorTicks=[],t.ticks?(this._cleanUserTicks(t.ticks,this.ticks),this._cleanUserTicks(t.minorTicks||[],this.minorTicks)):t.mode=="time"?this._calculateTimeTicks():t.scaling==="logarithmic"?this._calculateLogTicks():this._calculateTicks(),e.each(this.ticks,function(e){e.label+=""}),e.each(this.minorTicks,function(e){e.label+=""})},calculateRange:function(){if(!this.used)return;var e=this,t=e.options,n=t.min!==null?t.min:e.datamin,r=t.max!==null?t.max:e.datamax,i=t.autoscaleMargin;t.scaling=="logarithmic"&&(n<=0&&(n=e.datamin),r<=0&&(r=n));if(r==n){var s=r?.01:1;t.min===null&&(n-=s),t.max===null&&(r+=s)}if(t.scaling==="logarithmic"){n<0&&(n=r/t.base);var o=Math.log(r);t.base!=Math.E&&(o/=Math.log(t.base)),o=Math.ceil(o);var u=Math.log(n);t.base!=Math.E&&(u/=Math.log(t.base)),u=Math.ceil(u),e.tickSize=Flotr.getTickSize(t.noTicks,u,o,t.tickDecimals===null?0:t.tickDecimals),t.minorTickFreq===null&&(o-u>10?t.minorTickFreq=0:o-u>5?t.minorTickFreq=2:t.minorTickFreq=5)}else e.tickSize=Flotr.getTickSize(t.noTicks,n,r,t.tickDecimals);e.min=n,e.max=r,t.min===null&&t.autoscale&&(e.min-=e.tickSize*i,e.min<0&&e.datamin>=0&&(e.min=0),e.min=e.tickSize*Math.floor(e.min/e.tickSize)),t.max===null&&t.autoscale&&(e.max+=e.tickSize*i,e.max>0&&e.datamax<=0&&e.datamax!=e.datamin&&(e.max=0),e.max=e.tickSize*Math.ceil(e.max/e.tickSize)),e.min==e.max&&(e.max=e.min+1)},calculateTextDimensions:function(e,t){var n="",r,i;if(this.options.showLabels)for(i=0;i<this.ticks.length;++i)r=this.ticks[i].label.length,r>n.length&&(n=this.ticks[i].label);this.maxLabel=e.dimensions(n,{size:t.fontSize,angle:Flotr.toRad(this.options.labelsAngle)},"font-size:smaller;","flotr-grid-label"),this.titleSize=e.dimensions(this.options.title,{size:t.fontSize*1.2,angle:Flotr.toRad(this.options.titleAngle)},"font-weight:bold;","flotr-axis-title")},_cleanUserTicks:function(t,n){var r=this,i=this.options,s,o,u,a;e.isFunction(t)&&(t=t({min:r.min,max:r.max}));for(o=0;o<t.length;++o)a=t[o],typeof a=="object"?(s=a[0],u=a.length>1?a[1]:i.tickFormatter(s,{min:r.min,max:r.max})):(s=a,u=i.tickFormatter(s,{min:this.min,max:this.max})),n[o]={v:s,label:u}},_calculateTimeTicks:function(){this.ticks=Flotr.Date.generator(this)},_calculateLogTicks:function(){var e=this,t=e.options,n,r,s=Math.log(e.max);t.base!=Math.E&&(s/=Math.log(t.base)),s=Math.ceil(s);var o=Math.log(e.min);t.base!=Math.E&&(o/=Math.log(t.base)),o=Math.ceil(o);for(i=o;i<s;i+=e.tickSize){r=t.base==Math.E?Math.exp(i):Math.pow(t.base,i);var u=r*(t.base==Math.E?Math.exp(e.tickSize):Math.pow(t.base,e.tickSize)),a=(u-r)/t.minorTickFreq;e.ticks.push({v:r,label:t.tickFormatter(r,{min:e.min,max:e.max})});for(n=r+a;n<u;n+=a)e.minorTicks.push({v:n,label:t.tickFormatter(n,{min:e.min,max:e.max})})}r=t.base==Math.E?Math.exp(i):Math.pow(t.base,i),e.ticks.push({v:r,label:t.tickFormatter(r,{min:e.min,max:e.max})})},_calculateTicks:function(){var e=this,t=e.options,n=e.tickSize,r=e.min,i=e.max,s=n*Math.ceil(r/n),o,u,a,f,l,c;t.minorTickFreq&&(u=n/t.minorTickFreq);for(l=0;(a=f=s+l*n)<=i;++l){o=t.tickDecimals,o===null&&(o=1-Math.floor(Math.log(n)/Math.LN10)),o<0&&(o=0),a=a.toFixed(o),e.ticks.push({v:a,label:t.tickFormatter(a,{min:e.min,max:e.max})});if(t.minorTickFreq)for(c=0;c<t.minorTickFreq&&l*n+c*u<i;++c)a=f+c*u,e.minorTicks.push({v:a,label:t.tickFormatter(a,{min:e.min,max:e.max})})}}},e.extend(n,{getAxes:function(e){return{x:new n({options:e.xaxis,n:1,length:this.plotWidth}),x2:new n({options:e.x2axis,n:2,length:this.plotWidth}),y:new n({options:e.yaxis,n:1,length:this.plotHeight,offset:this.plotHeight,orientation:-1}),y2:new n({options:e.y2axis,n:2,length:this.plotHeight,offset:this.plotHeight,orientation:-1})}}}),Flotr.Axis=n}(),function(){function t(t){e.extend(this,t)}var e=Flotr._;t.prototype={getRange:function(){var e=this.data,t=e.length,n=Number.MAX_VALUE,r=Number.MAX_VALUE,i=-Number.MAX_VALUE,s=-Number.MAX_VALUE,o=!1,u=!1,a,f,l;if(t<0||this.hide)return!1;for(l=0;l<t;l++)a=e[l][0],f=e[l][1],a!==null&&(a<n&&(n=a,o=!0),a>i&&(i=a,o=!0)),f!==null&&(f<r&&(r=f,u=!0),f>s&&(s=f,u=!0));return{xmin:n,xmax:i,ymin:r,ymax:s,xused:o,yused:u}}},e.extend(t,{getSeries:function(n){return e.map(n,function(n){var r;return n.data?(r=new t,e.extend(r,n)):r=new t({data:n}),r})}}),Flotr.Series=t}(),Flotr.addType("lines",{options:{show:!1,lineWidth:2,fill:!1,fillBorder:!1,fillColor:null,fillOpacity:.4,steps:!1,stacked:!1},stack:{values:[]},draw:function(e){var t=e.context,n=e.lineWidth,r=e.shadowSize,i;t.save(),t.lineJoin="round",r&&(t.lineWidth=r/2,i=n/2+t.lineWidth/2,t.strokeStyle="rgba(0,0,0,0.1)",this.plot(e,i+r/2,!1),t.strokeStyle="rgba(0,0,0,0.2)",this.plot(e,i,!1)),t.lineWidth=n,t.strokeStyle=e.color,this.plot(e,0,!0),t.restore()},plot:function(e,t,n){function S(){!t&&e.fill&&d&&(v=o(d[0]),r.fillStyle=e.fillStyle,r.lineTo(m,p),r.lineTo(v,p),r.lineTo(v,u(d[1])),r.fill(),e.fillBorder&&r.stroke())}var r=e.context,i=e.width,s=e.height,o=e.xScale,u=e.yScale,a=e.data,f=e.stacked?this.stack:!1,l=a.length-1,c=null,h=null,p=u(0),d=null,v,m,g,y,b,w,E;if(l<1)return;r.beginPath();for(E=0;E<l;++E){if(a[E][1]===null||a[E+1][1]===null){e.fill&&E>0&&a[E][1]!==null&&(r.stroke(),S(),d=null,r.closePath(),r.beginPath());continue}v=o(a[E][0]),m=o(a[E+1][0]),d===null&&(d=a[E]),f?(b=f.values[a[E][0]]||0,w=f.values[a[E+1][0]]||f.values[a[E][0]]||0,g=u(a[E][1]+b),y=u(a[E+1][1]+w),n&&(a[E].y0=b,f.values[a[E][0]]=a[E][1]+b,E==l-1&&(a[E+1].y0=w,f.values[a[E+1][0]]=a[E+1][1]+w))):(g=u(a[E][1]),y=u(a[E+1][1]));if(g>s&&y>s||g<0&&y<0||v<0&&m<0||v>i&&m>i)continue;(c!=v||h!=g+t)&&r.moveTo(v,g+t),c=m,h=y+t,e.steps?(r.lineTo(c+t/2,g+t),r.lineTo(c+t/2,h)):r.lineTo(c,h)}(!e.fill||e.fill&&!e.fillBorder)&&r.stroke(),S(),r.closePath()},extendYRange:function(e,t,n,r){var i=e.options;if(n.stacked&&(!i.max&&i.max!==0||!i.min&&i.min!==0)){var s=e.max,o=e.min,u=r.positiveSums||{},a=r.negativeSums||{},f,l;for(l=0;l<t.length;l++)f=t[l][0]+"",t[l][1]>0?(u[f]=(u[f]||0)+t[l][1],s=Math.max(s,u[f])):(a[f]=(a[f]||0)+t[l][1],o=Math.min(o,a[f]));r.negativeSums=a,r.positiveSums=u,e.max=s,e.min=o}n.steps&&(this.hit=function(e){var t=e.data,n=e.args,r=e.yScale,i=n[0],s=t.length,o=n[1],u=e.xInverse(i.relX),a=i.relY,f;for(f=0;f<s-1;f++)if(u>=t[f][0]&&u<=t[f+1][0]){Math.abs(r(t[f][1])-a)<8&&(o.x=t[f][0],o.y=t[f][1],o.index=f,o.seriesIndex=e.index);break}},this.drawHit=function(e){var t=e.context,n=e.args,r=e.data,i=e.xScale,s=n.index,o=i(n.x),u=e.yScale(n.y),a;r.length-1>s&&(a=e.xScale(r[s+1][0]),t.save(),t.strokeStyle=e.color,t.lineWidth=e.lineWidth,t.beginPath(),t.moveTo(o,u),t.lineTo(a,u),t.stroke(),t.closePath(),t.restore())},this.clearHit=function(e){var t=e.context,n=e.args,r=e.data,i=e.xScale,s=e.lineWidth,o=n.index,u=i(n.x),a=e.yScale(n.y),f;r.length-1>o&&(f=e.xScale(r[o+1][0]),t.clearRect(u-s,a-s,f-u+2*s,2*s))})}}),Flotr.addType("bars",{options:{show:!1,lineWidth:2,barWidth:1,fill:!0,fillColor:null,fillOpacity:.4,horizontal:!1,stacked:!1,centered:!0,topPadding:.1,grouped:!1},stack:{positive:[],negative:[],_positive:[],_negative:[]},draw:function(e){var t=e.context;this.current+=1,t.save(),t.lineJoin="miter",t.lineWidth=e.lineWidth,t.strokeStyle=e.color,e.fill&&(t.fillStyle=e.fillStyle),this.plot(e),t.restore()},plot:function(e){var t=e.data,n=e.context,r=e.shadowSize,i,s,o,u,a,f;if(t.length<1)return;this.translate(n,e.horizontal);for(i=0;i<t.length;i++){s=this.getBarGeometry(t[i][0],t[i][1],e);if(s===null)continue;o=s.left,u=s.top,a=s.width,f=s.height,e.fill&&n.fillRect(o,u,a,f),r&&(n.save(),n.fillStyle="rgba(0,0,0,0.05)",n.fillRect(o+r,u+r,a,f),n.restore()),e.lineWidth&&n.strokeRect(o,u,a,f)}},translate:function(e,t){t&&(e.rotate(-Math.PI/2),e.scale(-1,1))},getBarGeometry:function(e,t,n){var r=n.horizontal,i=n.barWidth,s=n.centered,o=n.stacked?this.stack:!1,u=n.lineWidth,a=s?i/2:0,f=r?n.yScale:n.xScale,l=r?n.xScale:n.yScale,c=r?t:e,h=r?e:t,p=0,d,v,m,g,y;return n.grouped&&(this.current/this.groups,c-=a,i/=this.groups,a=i/2,c=c+i*this.current-a),o&&(d=h>0?o.positive:o.negative,p=d[c]||p,d[c]=p+h),v=f(c-a),m=f(c+i-a),g=l(h+p),y=l(p),y<0&&(y=0),e===null||t===null?null:{x:c,y:h,xScale:f,yScale:l,top:g,left:Math.min(v,m)-u/2,width:Math.abs(m-v)-u,height:y-g}},hit:function(e){var t=e.data,n=e.args,r=n[0],i=n[1],s=e.xInverse(r.relX),o=e.yInverse(r.relY),u=this.getBarGeometry(s,o,e),a=u.width/2,f=u.left,l=u.y,c,h;for(h=t.length;h--;)c=this.getBarGeometry(t[h][0],t[h][1],e),(l>0&&l<c.y||l<0&&l>c.y)&&Math.abs(f-c.left)<a&&(i.x=t[h][0],i.y=t[h][1],i.index=h,i.seriesIndex=e.index)},drawHit:function(e){var t=e.context,n=e.args,r=this.getBarGeometry(n.x,n.y,e),i=r.left,s=r.top,o=r.width,u=r.height;t.save(),t.strokeStyle=e.color,t.lineWidth=e.lineWidth,this.translate(t,e.horizontal),t.beginPath(),t.moveTo(i,s+u),t.lineTo(i,s),t.lineTo(i+o,s),t.lineTo(i+o,s+u),e.fill&&(t.fillStyle=e.fillStyle,t.fill()),t.stroke(),t.closePath(),t.restore()},clearHit:function(e){var t=e.context,n=e.args,r=this.getBarGeometry(n.x,n.y,e),i=r.left,s=r.width,o=r.top,u=r.height,a=2*e.lineWidth;t.save(),this.translate(t,e.horizontal),t.clearRect(i-a,Math.min(o,o+u)-a,s+2*a,Math.abs(u)+2*a),t.restore()},extendXRange:function(e,t,n,r){this._extendRange(e,t,n,r),this.groups=this.groups+1||1,this.current=0},extendYRange:function(e,t,n,r){this._extendRange(e,t,n,r)},_extendRange:function(e,t,n,r){var i=e.options.max;if(_.isNumber(i)||_.isString(i))return;var s=e.min,o=e.max,u=n.horizontal,a=e.orientation,f=this.positiveSums||{},l=this.negativeSums||{},c,h,p,d;(a==1&&!u||a==-1&&u)&&n.centered&&(o=Math.max(e.datamax+n.barWidth,o),s=Math.min(e.datamin-n.barWidth,s));if(n.stacked&&(a==1&&u||a==-1&&!u))for(d=t.length;d--;)c=t[d][a==1?1:0]+"",h=t[d][a==1?0:1],h>0?(f[c]=(f[c]||0)+h,o=Math.max(o,f[c])):(l[c]=(l[c]||0)+h,s=Math.min(s,l[c]));(a==1&&u||a==-1&&!u)&&n.topPadding&&(e.max===e.datamax||n.stacked&&this.stackMax!==o)&&(o+=n.topPadding*(o-s)),this.stackMin=s,this.stackMax=o,this.negativeSums=l,this.positiveSums=f,e.max=o,e.min=s}}),Flotr.addType("bubbles",{options:{show:!1,lineWidth:2,fill:!0,fillOpacity:.4,baseRadius:2},draw:function(e){var t=e.context,n=e.shadowSize;t.save(),t.lineWidth=e.lineWidth,t.fillStyle="rgba(0,0,0,0.05)",t.strokeStyle="rgba(0,0,0,0.05)",this.plot(e,n/2),t.strokeStyle="rgba(0,0,0,0.1)",this.plot(e,n/4),t.strokeStyle=e.color,t.fillStyle=e.fillStyle,this.plot(e),t.restore()},plot:function(e,t){var n=e.data,r=e.context,i,s,o,u,a;t=t||0;for(s=0;s<n.length;++s)i=this.getGeometry(n[s],e),r.beginPath(),r.arc(i.x+t,i.y+t,i.z,0,2*Math.PI,!0),r.stroke(),e.fill&&r.fill(),r.closePath()},getGeometry:function(e,t){return{x:t.xScale(e[0]),y:t.yScale(e[1]),z:e[2]*t.baseRadius}},hit:function(e){var t=e.data,n=e.args,r=n[0],s=n[1],o=r.relX,u=r.relY,a,f,l,c;s.best=s.best||Number.MAX_VALUE;for(i=t.length;i--;)f=this.getGeometry(t[i],e),l=f.x-o,c=f.y-u,a=Math.sqrt(l*l+c*c),a<f.z&&f.z<s.best&&(s.x=t[i][0],s.y=t[i][1],s.index=i,s.seriesIndex=e.index,s.best=f.z)},drawHit:function(e){var t=e.context,n=this.getGeometry(e.data[e.args.index],e);t.save(),t.lineWidth=e.lineWidth,t.fillStyle=e.fillStyle,t.strokeStyle=e.color,t.beginPath(),t.arc(n.x,n.y,n.z,0,2*Math.PI,!0),t.fill(),t.stroke(),t.closePath(),t.restore()},clearHit:function(e){var t=e.context,n=this.getGeometry(e.data[e.args.index],e),r=n.z+e.lineWidth;t.save(),t.clearRect(n.x-r,n.y-r,2*r,2*r),t.restore()}}),Flotr.addType("candles",{options:{show:!1,lineWidth:1,wickLineWidth:1,candleWidth:.6,fill:!0,upFillColor:"#00A8F0",downFillColor:"#CB4B4B",fillOpacity:.5,barcharts:!1},draw:function(e){var t=e.context;t.save(),t.lineJoin="miter",t.lineCap="butt",t.lineWidth=e.wickLineWidth||e.lineWidth,this.plot(e),t.restore()},plot:function(e){var t=e.data,n=e.context,r=e.xScale,i=e.yScale,s=e.candleWidth/2,o=e.shadowSize,u=e.lineWidth,a=e.wickLineWidth,f=a%2/2,l,c,h,p,d,v,m,g,y,b,w,E,S,x,T,N;if(t.length<1)return;for(N=0;N<t.length;N++){c=t[N],h=c[0],d=c[1],v=c[2],m=c[3],g=c[4],y=r(h-s),b=r(h+s),w=i(m),E=i(v),S=i(Math.min(d,g)),x=i(Math.max(d,g)),l=e[d>g?"downFillColor":"upFillColor"],e.fill&&!e.barcharts&&(n.fillStyle="rgba(0,0,0,0.05)",n.fillRect(y+o,x+o,b-y,S-x),n.save(),n.globalAlpha=e.fillOpacity,n.fillStyle=l,n.fillRect(y,x+u,b-y,S-x),n.restore());if(u||a)h=Math.floor((y+b)/2)+f,n.strokeStyle=l,n.beginPath(),e.barcharts?(n.moveTo(h,Math.floor(E+u)),n.lineTo(h,Math.floor(w+u)),T=d<g,n.moveTo(T?b:y,Math.floor(x+u)),n.lineTo(h,Math.floor(x+u)),n.moveTo(h,Math.floor(S+u)),n.lineTo(T?y:b,Math.floor(S+u))):(n.strokeRect(y,x+u,b-y,S-x),n.moveTo(h,Math.floor(x+u)),n.lineTo(h,Math.floor(E+u)),n.moveTo(h,Math.floor(S+u)),n.lineTo(h,Math.floor(w+u))),n.closePath(),n.stroke()}},hit:function(e){var t=e.xScale,n=e.yScale,r=e.data,i=e.args,s=i[0],o=e.candleWidth/2,u=i[1],a=s.relX,f=s.relY,l=r.length,c,h,p,d,v,m,g,y;for(c=0;c<l;c++){h=r[c],p=h[2],d=h[3],v=t(h[0]-o),m=t(h[0]+o),y=n(d),g=n(p);if(a>v&&a<m&&f>g&&f<y){u.x=h[0],u.index=c,u.seriesIndex=e.index;return}}},drawHit:function(e){var t=e.context;t.save(),this.plot(_.defaults({fill:!!e.fillColor,upFillColor:e.color,downFillColor:e.color,data:[e.data[e.args.index]]},e)),t.restore()},clearHit:function(e){var t=e.args,n=e.context,r=e.xScale,i=e.yScale,s=e.lineWidth,o=e.candleWidth/2,u=e.data[t.index],a=r(u[0]-o)-s,f=r(u[0]+o)+s,l=i(u[2]),c=i(u[3])+s;n.clearRect(a,l,f-a,c-l)},extendXRange:function(e,t,n){e.options.max===null&&(e.max=Math.max(e.datamax+.5,e.max),e.min=Math.min(e.datamin-.5,e.min))}}),Flotr.addType("gantt",{options:{show:!1,lineWidth:2,barWidth:1,fill:!0,fillColor:null,fillOpacity:.4,centered:!0},draw:function(e){var t=this.ctx,n=e.gantt.barWidth,r=Math.min(e.gantt.lineWidth,n);t.save(),t.translate(this.plotOffset.left,this.plotOffset.top),t.lineJoin="miter",t.lineWidth=r,t.strokeStyle=e.color,t.save(),this.gantt.plotShadows(e,n,0,e.gantt.fill),t.restore();if(e.gantt.fill){var i=e.gantt.fillColor||e.color;t.fillStyle=this.processColor(i,{opacity:e.gantt.fillOpacity})}this.gantt.plot(e,n,0,e.gantt.fill),t.restore()},plot:function(e,t,n,r){var i=e.data;if(i.length<1)return;var s=e.xaxis,o=e.yaxis,u=this.ctx,a;for(a=0;a<i.length;a++){var f=i[a][0],l=i[a][1],c=i[a][2],h=!0,p=!0,d=!0;if(l===null||c===null)continue;var v=l,m=l+c,g=f-(e.gantt.centered?t/2:0),y=f+t-(e.gantt.centered?t/2:0);if(m<s.min||v>s.max||y<o.min||g>o.max)continue;v<s.min&&(v=s.min,h=!1),m>s.max&&(m=s.max,s.lastSerie!=e&&(p=!1)),g<o.min&&(g=o.min),y>o.max&&(y=o.max,o.lastSerie!=e&&(p=!1)),r&&(u.beginPath(),u.moveTo(s.d2p(v),o.d2p(g)+n),u.lineTo(s.d2p(v),o.d2p(y)+n),u.lineTo(s.d2p(m),o.d2p(y)+n),u.lineTo(s.d2p(m),o.d2p(g)+n),u.fill(),u.closePath()),e.gantt.lineWidth&&(h||d||p)&&(u.beginPath(),u.moveTo(s.d2p(v),o.d2p(g)+n),u[h?"lineTo":"moveTo"](s.d2p(v),o.d2p(y)+n),u[p?"lineTo":"moveTo"](s.d2p(m),o.d2p(y)+n),u[d?"lineTo":"moveTo"](s.d2p(m),o.d2p(g)+n),u.stroke(),u.closePath())}},plotShadows:function(e,t,n){var r=e.data;if(r.length<1)return;var i,s,o,u,a=e.xaxis,f=e.yaxis,l=this.ctx,c=this.options.shadowSize;for(i=0;i<r.length;i++){s=r[i][0],o=r[i][1],u=r[i][2];if(o===null||u===null)continue;var h=o,p=o+u,d=s-(e.gantt.centered?t/2:0),v=s+t-(e.gantt.centered?t/2:0);if(p<a.min||h>a.max||v<f.min||d>f.max)continue;h<a.min&&(h=a.min),p>a.max&&(p=a.max),d<f.min&&(d=f.min),v>f.max&&(v=f.max);var m=a.d2p(p)-a.d2p(h)-(a.d2p(p)+c<=this.plotWidth?0:c),g=f.d2p(d)-f.d2p(v)-(f.d2p(d)+c<=this.plotHeight?0:c);l.fillStyle="rgba(0,0,0,0.05)",l.fillRect(Math.min(a.d2p(h)+c,this.plotWidth),Math.min(f.d2p(v)+c,this.plotHeight),m,g)}},extendXRange:function(e){if(e.options.max===null){var t=e.min,n=e.max,r,i,s,o,u,a={},f={},l=null;for(r=0;r<this.series.length;++r){o=this.series[r],u=o.gantt;if(u.show&&o.xaxis==e){for(i=0;i<o.data.length;i++)u.show&&(y=o.data[i][0]+"",a[y]=Math.max(a[y]||0,o.data[i][1]+o.data[i][2]),l=o);for(i in a)n=Math.max(a[i],n)}}e.lastSerie=l,e.max=n,e.min=t}},extendYRange:function(e){if(e.options.max===null){var t=Number.MIN_VALUE,n=Number.MAX_VALUE,r,i,s,o,u={},a={},f=null;for(r=0;r<this.series.length;++r){s=this.series[r],o=s.gantt;if(o.show&&!s.hide&&s.yaxis==e){var l=Number.MIN_VALUE,c=Number.MAX_VALUE;for(i=0;i<s.data.length;i++)l=Math.max(l,s.data[i][0]),c=Math.min(c,s.data[i][0]);o.centered?(t=Math.max(l+.5,t),n=Math.min(c-.5,n)):(t=Math.max(l+1,t),n=Math.min(c,n)),o.barWidth+l>t&&(t=e.max+o.barWidth)}}e.lastSerie=f,e.max=t,e.min=n,e.tickSize=Flotr.getTickSize(e.options.noTicks,n,t,e.options.tickDecimals)}}}),function(){function e(e){return typeof e=="object"&&e.constructor&&(Image?!0:e.constructor===Image)}Flotr.defaultMarkerFormatter=function(e){return Math.round(e.y*100)/100+""},Flotr.addType("markers",{options:{show:!1,lineWidth:1,color:"#000000",fill:!1,fillColor:"#FFFFFF",fillOpacity:.4,stroke:!1,position:"ct",verticalMargin:0,labelFormatter:Flotr.defaultMarkerFormatter,fontSize:Flotr.defaultOptions.fontSize,stacked:!1,stackingType:"b",horizontal:!1},stack:{positive:[],negative:[],values:[]},draw:function(e){function h(e,t){return o=r.negative[e]||0,s=r.positive[e]||0,t>0?(r.positive[e]=o+t,o+t):(r.negative[e]=s+t,s+t)}var t=e.data,n=e.context,r=e.stacked?e.stack:!1,i=e.stackingType,s,o,u,a,f,l,c;n.save(),n.lineJoin="round",n.lineWidth=e.lineWidth,n.strokeStyle="rgba(0,0,0,0.5)",n.fillStyle=e.fillStyle;for(a=0;a<t.length;++a)f=t[a][0],l=t[a][1],r&&(i=="b"?e.horizontal?l=h(l,f):f=h(f,l):i=="a"&&(u=r.values[f]||0,r.values[f]=u+l,l=u+l)),c=e.labelFormatter({x:f,y:l,index:a,data:t}),this.plot(e.xScale(f),e.yScale(l),c,e);n.restore()},plot:function(t,n,r,i){var s=i.context;if(e(r)&&!r.complete)throw"Marker image not loaded.";this._plot(t,n,r,i)},_plot:function(t,n,r,i){var s=i.context,o=2,u=t,a=n,f;e(r)?f={height:r.height,width:r.width}:f=i.text.canvas(r),f.width=Math.floor(f.width+o*2),f.height=Math.floor(f.height+o*2),i.position.indexOf("c")!=-1?u-=f.width/2+o:i.position.indexOf("l")!=-1&&(u-=f.width),i.position.indexOf("m")!=-1?a-=f.height/2+o:i.position.indexOf("t")!=-1?a-=f.height+i.verticalMargin:a+=i.verticalMargin,u=Math.floor(u)+.5,a=Math.floor(a)+.5,i.fill&&s.fillRect(u,a,f.width,f.height),i.stroke&&s.strokeRect(u,a,f.width,f.height),e(r)?s.drawImage(r,parseInt(u+o,10),parseInt(a+o,10)):Flotr.drawText(s,r,u+o,a+o,{textBaseline:"top",textAlign:"left",size:i.fontSize,color:i.color})}})}(),function(){var e=Flotr._;Flotr.defaultPieLabelFormatter=function(e,t){return(100*t/e).toFixed(2)+"%"},Flotr.addType("pie",{options:{show:!1,lineWidth:1,fill:!0,fillColor:null,fillOpacity:.6,explode:6,sizeRatio:.6,startAngle:Math.PI/4,labelFormatter:Flotr.defaultPieLabelFormatter,pie3D:!1,pie3DviewAngle:Math.PI/2*.8,pie3DspliceThickness:20,epsilon:.1},draw:function(e){var t=e.data,n=e.context,r=e.lineWidth,i=e.shadowSize,s=e.sizeRatio,o=e.height,u=e.width,a=e.explode,f=e.color,l=e.fill,c=e.fillStyle,h=Math.min(u,o)*s/2,p=t[0][1],d=[],v=1,m=Math.PI*2*p/this.total,g=this.startAngle||2*Math.PI*e.startAngle,y=g+m,b=g+m/2,w=e.labelFormatter(this.total,p),E=a+h+4,S=Math.cos(b)*E,x=Math.sin(b)*E,T=S<0?"right":"left",N=x>0?"top":"bottom",C,k,L;n.save(),n.translate(u/2,o/2),n.scale(1,v),k=Math.cos(b)*a,L=Math.sin(b)*a,i>0&&(this.plotSlice(k+i,L+i,h,g,y,n),l&&(n.fillStyle="rgba(0,0,0,0.1)",n.fill())),this.plotSlice(k,L,h,g,y,n),l&&(n.fillStyle=c,n.fill()),n.lineWidth=r,n.strokeStyle=f,n.stroke(),C={size:e.fontSize*1.2,color:e.fontColor,weight:1.5},w&&(e.htmlText||!e.textEnabled?(divStyle="position:absolute;"+N+":"+(o/2+(N==="top"?x:-x))+"px;",divStyle+=T+":"+(u/2+(T==="right"?-S:S))+"px;",d.push('<div style="',divStyle,'" class="flotr-grid-label">',w,"</div>")):(C.textAlign=T,C.textBaseline=N,Flotr.drawText(n,w,S,x,C)));if(e.htmlText||!e.textEnabled){var A=Flotr.DOM.node('<div style="color:'+e.fontColor+'" class="flotr-labels"></div>');Flotr.DOM.insert(A,d.join("")),Flotr.DOM.insert(e.element,A)}n.restore(),this.startAngle=y,this.slices=this.slices||[],this.slices.push({radius:h,x:k,y:L,explode:a,start:g,end:y})},plotSlice:function(e,t,n,r,i,s){s.beginPath(),s.moveTo(e,t),s.arc(e,t,n,r,i,!1),s.lineTo(e,t),s.closePath()},hit:function(e){var t=e.data[0],n=e.args,r=e.index,i=n[0],s=n[1],o=this.slices[r],u=i.relX-e.width/2,a=i.relY-e.height/2,f=Math.sqrt(u*u+a*a),l=Math.atan(a/u),c=Math.PI*2,h=o.explode||e.explode,p=o.start%c,d=o.end%c,v=e.epsilon;u<0?l+=Math.PI:u>0&&a<0&&(l+=c),f<o.radius+h&&f>h&&(l>p&&l<d||p>d&&(l<d||l>p)||p===d&&(o.start===o.end&&Math.abs(l-p)<v||o.start!==o.end&&Math.abs(l-p)>v))&&(s.x=t[0],s.y=t[1],s.sAngle=p,s.eAngle=d,s.index=0,s.seriesIndex=r,s.fraction=t[1]/this.total)},drawHit:function(e){var t=e.context,n=this.slices[e.args.seriesIndex];t.save(),t.translate(e.width/2,e.height/2),this.plotSlice(n.x,n.y,n.radius,n.start,n.end,t),t.stroke(),t.restore()},clearHit:function(e){var t=e.context,n=this.slices[e.args.seriesIndex],r=2*e.lineWidth,i=n.radius+r;t.save(),t.translate(e.width/2,e.height/2),t.clearRect(n.x-i,n.y-i,2*i+r,2*i+r),t.restore()},extendYRange:function(e,t){this.total=(this.total||0)+t[0][1]}})}(),Flotr.addType("points",{options:{show:!1,radius:3,lineWidth:2,fill:!0,fillColor:"#FFFFFF",fillOpacity:1,hitRadius:null},draw:function(e){var t=e.context,n=e.lineWidth,r=e.shadowSize;t.save(),r>0&&(t.lineWidth=r/2,t.strokeStyle="rgba(0,0,0,0.1)",this.plot(e,r/2+t.lineWidth/2),t.strokeStyle="rgba(0,0,0,0.2)",this.plot(e,t.lineWidth/2)),t.lineWidth=e.lineWidth,t.strokeStyle=e.color,e.fill&&(t.fillStyle=e.fillStyle),this.plot(e),t.restore()},plot:function(e,t){var n=e.data,r=e.context,i=e.xScale,s=e.yScale,o,u,a;for(o=n.length-1;o>-1;--o){a=n[o][1];if(a===null)continue;u=i(n[o][0]),a=s(a);if(u<0||u>e.width||a<0||a>e.height)continue;r.beginPath(),t?r.arc(u,a+t,e.radius,0,Math.PI,!1):(r.arc(u,a,e.radius,0,2*Math.PI,!0),e.fill&&r.fill()),r.stroke(),r.closePath()}}}),Flotr.addType("radar",{options:{show:!1,lineWidth:2,fill:!0,fillOpacity:.4,radiusRatio:.9,sensibility:2},draw:function(e){var t=e.context,n=e.shadowSize;t.save(),t.translate(e.width/2,e.height/2),t.lineWidth=e.lineWidth,t.fillStyle="rgba(0,0,0,0.05)",t.strokeStyle="rgba(0,0,0,0.05)",this.plot(e,n/2),t.strokeStyle="rgba(0,0,0,0.1)",this.plot(e,n/4),t.strokeStyle=e.color,t.fillStyle=e.fillStyle,this.plot(e),t.restore()},plot:function(e,t){var n=e.data,r=e.context,i=Math.min(e.height,e.width)*e.radiusRatio/2,s=2*Math.PI/n.length,o=-Math.PI/2,u,a;t=t||0,r.beginPath();for(u=0;u<n.length;++u)a=n[u][1]/this.max,r[u===0?"moveTo":"lineTo"](Math.cos(u*s+o)*i*a+t,Math.sin(u*s+o)*i*a+t);r.closePath(),e.fill&&r.fill(),r.stroke()},getGeometry:function(e,t){var n=Math.min(t.height,t.width)*t.radiusRatio/2,r=2*Math.PI/t.data.length,i=-Math.PI/2,s=e[1]/this.max;return{x:Math.cos(e[0]*r+i)*n*s+t.width/2,y:Math.sin(e[0]*r+i)*n*s+t.height/2}},hit:function(e){var t=e.args,n=t[0],r=t[1],i=n.relX,s=n.relY,o,u,a,f;for(var l=0;l<r.series.length;l++){var c=r.series[l],h=c.data;for(var p=h.length;p--;){u=this.getGeometry(h[p],e),a=u.x-i,f=u.y-s,o=Math.sqrt(a*a+f*f);if(o<e.sensibility*2)return r.x=h[p][0],r.y=h[p][1],r.index=p,r.seriesIndex=l,r}}},drawHit:function(e){var t=2*Math.PI/e.data.length,n=-Math.PI/2,r=Math.min(e.height,e.width)*e.radiusRatio/2,i=e.args.series,s=i.points.hitRadius||i.points.radius||i.mouse.radius,o=e.context;o.translate(e.width/2,e.height/2);var u=e.args.index,a=e.data[u][1]/this.max,f=Math.cos(u*t+n)*r*a,l=Math.sin(u*t+n)*r*a;o.beginPath(),o.arc(f,l,s,0,2*Math.PI,!0),o.closePath(),o.stroke()},clearHit:function(e){var t=2*Math.PI/e.data.length,n=-Math.PI/2,r=Math.min(e.height,e.width)*e.radiusRatio/2,i=e.context,s=e.args.series,o=s.points?s.points.lineWidth:1;offset=(s.points.hitRadius||s.points.radius||s.mouse.radius)+o,i.translate(e.width/2,e.height/2);var u=e.args.index,a=e.data[u][1]/this.max,f=Math.cos(u*t+n)*r*a,l=Math.sin(u*t+n)*r*a;i.clearRect(f-offset,l-offset,offset*2,offset*2)},extendYRange:function(e,t){this.max=Math.max(e.max,this.max||-Number.MAX_VALUE)}}),Flotr.addType("timeline",{options:{show:!1,lineWidth:1,barWidth:.2,fill:!0,fillColor:null,fillOpacity:.4,centered:!0},draw:function(e){var t=e.context;t.save(),t.lineJoin="miter",t.lineWidth=e.lineWidth,t.strokeStyle=e.color,t.fillStyle=e.fillStyle,this.plot(e),t.restore()},plot:function(e){var t=e.data,n=e.context,r=e.xScale,i=e.yScale,s=e.barWidth,o=e.lineWidth,u;Flotr._.each(t,function(e){var t=e[0],u=e[1],a=e[2],f=s,l=Math.ceil(r(t)),c=Math.ceil(r(t+a))-l,h=Math.round(i(u)),p=Math.round(i(u-f))-h,d=l-o/2,v=Math.round(h-p/2)-o/2;n.strokeRect(d,v,c,p),n.fillRect(d,v,c,p)})},extendRange:function(e){var t=e.data,n=e.xaxis,r=e.yaxis,i=e.timeline.barWidth;n.options.min===null&&(n.min=n.datamin-i/2);if(n.options.max===null){var s=n.max;Flotr._.each(t,function(e){s=Math.max(s,e[0]+e[2])},this),n.max=s+i/2}r.options.min===null&&(r.min=r.datamin-i),r.options.min===null&&(r.max=r.datamax+i)}}),function(){var e=Flotr.DOM;Flotr.addPlugin("crosshair",{options:{mode:null,color:"#FF0000",hideCursor:!0},callbacks:{"flotr:mousemove":function(e,t){this.options.crosshair.mode&&(this.crosshair.clearCrosshair(),this.crosshair.drawCrosshair(t))}},drawCrosshair:function(t){var n=this.octx,r=this.options.crosshair,i=this.plotOffset,s=i.left+Math.round(t.relX)+.5,o=i.top+Math.round(t.relY)+.5;if(t.relX<0||t.relY<0||t.relX>this.plotWidth||t.relY>this.plotHeight){this.el.style.cursor=null,e.removeClass(this.el,"flotr-crosshair");return}r.hideCursor&&(this.el.style.cursor="none",e.addClass(this.el,"flotr-crosshair")),n.save(),n.strokeStyle=r.color,n.lineWidth=1,n.beginPath(),r.mode.indexOf("x")!=-1&&(n.moveTo(s,i.top),n.lineTo(s,i.top+this.plotHeight)),r.mode.indexOf("y")!=-1&&(n.moveTo(i.left,o),n.lineTo(i.left+this.plotWidth,o)),n.stroke(),n.restore()},clearCrosshair:function(){var e=this.plotOffset,t=this.lastMousePos,n=this.octx;t&&(n.clearRect(Math.round(t.relX)+e.left,e.top,1,this.plotHeight+1),n.clearRect(e.left,Math.round(t.relY)+e.top,this.plotWidth+1,1))}})}(),function(){function n(e,t,n,r,i,s){var o="image/"+e,u=n.getImageData(0,0,r,i),a=new Image;return n.save(),n.globalCompositeOperation="destination-over",n.fillStyle=s,n.fillRect(0,0,r,i),a.src=t.toDataURL(o),n.restore(),n.clearRect(0,0,r,i),n.putImageData(u,0,0),a}var e=Flotr.DOM,t=Flotr._;Flotr.addPlugin("download",{saveImage:function(r,i,s,o){var u=this.options.grid,a;if(Flotr.isIE&&Flotr.isIE<9)return a="<html><body>"+this.canvas.firstChild.innerHTML+"</body></html>",window.open().document.write(a);if(r!=="jpeg"&&r!=="png")return;a=n(r,this.canvas,this.ctx,this.canvasWidth,this.canvasHeight,u&&u.backgroundColor||"#ffffff");if(!t.isElement(a)||!o)return window.open(a.src);this.download.restoreCanvas(),e.hide(this.canvas),e.hide(this.overlay),e.setStyles({position:"absolute"}),e.insert(this.el,a),this.saveImageElement=a},restoreCanvas:function(){e.show(this.canvas),e.show(this.overlay),this.saveImageElement&&this.el.removeChild(this.saveImageElement),this.saveImageElement=null}})}(),function(){var e=Flotr.EventAdapter,t=Flotr._;Flotr.addPlugin("graphGrid",{callbacks:{"flotr:beforedraw":function(){this.graphGrid.drawGrid()},"flotr:afterdraw":function(){this.graphGrid.drawOutline()}},drawGrid:function(){function v(e){for(p=0;p<e.length;++p){var t=e[p].v/c.max;for(d=0;d<=w;++d)n[d===0?"moveTo":"lineTo"](Math.cos(d*S+x)*b*t,Math.sin(d*S+x)*b*t)}}function m(e,r){t.each(t.pluck(e,"v"),function(e){if(e<=c.min||e>=c.max||(e==c.min||e==c.max)&&i.outlineWidth)return;r(Math.floor(c.d2p(e))+n.lineWidth/2)})}function g(e){n.moveTo(e,0),n.lineTo(e,f)}function y(e){n.moveTo(0,e),n.lineTo(l,e)}var n=this.ctx,r=this.options,i=r.grid,s=i.verticalLines,o=i.horizontalLines,u=i.minorVerticalLines,a=i.minorHorizontalLines,f=this.plotHeight,l=this.plotWidth,c,h,p,d;(s||u||o||a)&&e.fire(this.el,"flotr:beforegrid",[this.axes.x,this.axes.y,r,this]),n.save(),n.lineWidth=1,n.strokeStyle=i.tickColor;if(i.circular){n.translate(this.plotOffset.left+l/2,this.plotOffset.top+f/2);var b=Math.min(f,l)*r.radar.radiusRatio/2,w=this.axes.x.ticks.length,S=2*(Math.PI/w),x=-Math.PI/2;n.beginPath(),c=this.axes.y,o&&v(c.ticks),a&&v(c.minorTicks),s&&t.times(w,function(e){n.moveTo(0,0),n.lineTo(Math.cos(e*S+x)*b,Math.sin(e*S+x)*b)}),n.stroke()}else n.translate(this.plotOffset.left,this.plotOffset.top),i.backgroundColor&&(n.fillStyle=this.processColor(i.backgroundColor,{x1:0,y1:0,x2:l,y2:f}),n.fillRect(0,0,l,f)),n.beginPath(),c=this.axes.x,s&&m(c.ticks,g),u&&m(c.minorTicks,g),c=this.axes.y,o&&m(c.ticks,y),a&&m(c.minorTicks,y),n.stroke();n.restore(),(s||u||o||a)&&e.fire(this.el,"flotr:aftergrid",[this.axes.x,this.axes.y,r,this])},drawOutline:function(){var e=this,t=e.options,n=t.grid,r=n.outline,s=e.ctx,o=n.backgroundImage,u=e.plotOffset,a=u.left,f=u.top,l=e.plotWidth,c=e.plotHeight,h,p,d,v,m,g;if(!n.outlineWidth)return;s.save();if(n.circular){s.translate(a+l/2,f+c/2);var y=Math.min(c,l)*t.radar.radiusRatio/2,b=this.axes.x.ticks.length,w=2*(Math.PI/b),E=-Math.PI/2;s.beginPath(),s.lineWidth=n.outlineWidth,s.strokeStyle=n.color,s.lineJoin="round";for(i=0;i<=b;++i)s[i===0?"moveTo":"lineTo"](Math.cos(i*w+E)*y,Math.sin(i*w+E)*y);s.stroke()}else{s.translate(a,f);var S=n.outlineWidth,x=.5-S+(S+1)%2/2,T="lineTo",N="moveTo";s.lineWidth=S,s.strokeStyle=n.color,s.lineJoin="miter",s.beginPath(),s.moveTo(x,x),l-=S/2%1,c+=S/2,s[r.indexOf("n")!==-1?T:N](l,x),s[r.indexOf("e")!==-1?T:N](l,c),s[r.indexOf("s")!==-1?T:N](x,c),s[r.indexOf("w")!==-1?T:N](x,x),s.stroke(),s.closePath()}s.restore(),o&&(d=o.src||o,v=(parseInt(o.left,10)||0)+u.left,m=(parseInt(o.top,10)||0)+u.top,p=new Image,p.onload=function(){s.save(),o.alpha&&(s.globalAlpha=o.alpha),s.globalCompositeOperation="destination-over",s.drawImage(p,0,0,p.width,p.height,v,m,l,c),s.restore()},p.src=d)}})}(),function(){var e=Flotr.DOM,t=Flotr._,n=Flotr,r="opacity:0.7;background-color:#000;color:#fff;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;";Flotr.addPlugin("hit",{callbacks:{"flotr:mousemove":function(e,t){this.hit.track(t)},"flotr:click":function(e){var n=this.hit.track(e);n&&!t.isUndefined(n.index)&&(e.hit=n)},"flotr:mouseout":function(e){e.relatedTarget!==this.mouseTrack&&this.hit.clearHit()},"flotr:destroy":function(){this.options.mouse.container&&e.remove(this.mouseTrack),this.mouseTrack=null}},track:function(e){if(this.options.mouse.track||t.any(this.series,function(e){return e.mouse&&e.mouse.track}))return this.hit.hit(e)},executeOnType:function(e,r,i){function u(e,u){t.each(t.keys(n.graphTypes),function(t){e[t]&&e[t].show&&!e.hide&&this[t][r]&&(o=this.getOptions(e,t),o.fill=!!e.mouse.fillColor,o.fillStyle=this.processColor(e.mouse.fillColor||"#ffffff",{opacity:e.mouse.fillOpacity}),o.color=e.mouse.lineColor,o.context=this.octx,o.index=u,i&&(o.args=i),this[t][r].call(this[t],o),s=!0)},this)}var s=!1,o;return t.isArray(e)||(e=[e]),t.each(e,u,this),s},drawHit:function(e){var t=this.octx,n=e.series;if(n.mouse.lineColor){t.save(),t.lineWidth=n.points?n.points.lineWidth:1,t.strokeStyle=n.mouse.lineColor,t.fillStyle=this.processColor(n.mouse.fillColor||"#ffffff",{opacity:n.mouse.fillOpacity}),t.translate(this.plotOffset.left,this.plotOffset.top);if(!this.hit.executeOnType(n,"drawHit",e)){var r=e.xaxis,i=e.yaxis;t.beginPath(),t.arc(r.d2p(e.x),i.d2p(e.y),n.points.hitRadius||n.points.radius||n.mouse.radius,0,2*Math.PI,!0),t.fill(),t.stroke(),t.closePath()}t.restore(),this.clip(t)}this.prevHit=e},clearHit:function(){var t=this.prevHit,n=this.octx,r=this.plotOffset;n.save(),n.translate(r.left,r.top);if(t){if(!this.hit.executeOnType(t.series,"clearHit",this.prevHit)){var i=t.series,s=i.points?i.points.lineWidth:1;offset=(i.points.hitRadius||i.points.radius||i.mouse.radius)+s,n.clearRect(t.xaxis.d2p(t.x)-offset,t.yaxis.d2p(t.y)-offset,offset*2,offset*2)}e.hide(this.mouseTrack),this.prevHit=null}n.restore()},hit:function(e){var n=this.options,r=this.prevHit,i,s,o,u,a,f,l,c,h;if(this.series.length===0)return;h={relX:e.relX,relY:e.relY,absX:e.absX,absY:e.absY,series:this.series};if(n.mouse.trackY&&!n.mouse.trackAll&&this.hit.executeOnType(this.series,"hit",[e,h])&&!t.isUndefined(h.seriesIndex))a=this.series[h.seriesIndex],h.series=a,h.mouse=a.mouse,h.xaxis=a.xaxis,h.yaxis=a.yaxis;else{i=this.hit.closest(e);if(i){i=n.mouse.trackY?i.point:i.x,u=i.seriesIndex,a=this.series[u],l=a.xaxis,c=a.yaxis,s=2*a.mouse.sensibility;if(n.mouse.trackAll||i.distanceX<s/l.scale&&(!n.mouse.trackY||i.distanceY<s/c.scale))h.series=a,h.xaxis=a.xaxis,h.yaxis=a.yaxis,h.mouse=a.mouse,h.x=i.x,h.y=i.y,h.dist=i.distance,h.index=i.dataIndex,h.seriesIndex=u}}if(!r||r.index!==h.index||r.seriesIndex!==h.seriesIndex)this.hit.clearHit(),h.series&&h.mouse&&h.mouse.track&&(this.hit.drawMouseTrack(h),this.hit.drawHit(h),Flotr.EventAdapter.fire(this.el,"flotr:hit",[h,this]));return h},closest:function(e){function E(e){e.distance=h,e.distanceX=p,e.distanceY=d,e.seriesIndex=b,e.dataIndex=w,e.x=g,e.y=y,f=!0}var t=this.series,n=this.options,r=e.relX,i=e.relY,s=Number.MAX_VALUE,o=Number.MAX_VALUE,u={},a={},f=!1,l,c,h,p,d,v,m,g,y,b,w;for(b=0;b<t.length;b++){l=t[b],c=l.data,v=l.xaxis.p2d(r),m=l.yaxis.p2d(i);if(l.hide)continue;for(w=c.length;w--;){g=c[w][0],y=c[w][1],c[w].y0&&(y+=c[w].y0);if(g===null||y===null)continue;if(g<l.xaxis.min||g>l.xaxis.max)continue;p=Math.abs(g-v),d=Math.abs(y-m),h=p*p+d*d,h<s&&(s=h,E(u)),p<o&&(o=p,E(a))}}return f?{point:u,x:a}:!1},drawMouseTrack:function(n){var i="",s=n.series,o=n.mouse.position,u=n.mouse.margin,a=n.x,f=n.y,l=r,c=this.mouseTrack,h=this.plotOffset,p=h.left,d=h.right,v=h.bottom,m=h.top,g=n.mouse.trackDecimals,y=this.options,b=y.mouse.container,w=0,E=0,S,x,T;c||(c=e.node('<div class="flotr-mouse-value" style="'+l+'"></div>'),this.mouseTrack=c,e.insert(b||this.el,c));if(!g||g<0)g=0;a&&a.toFixed&&(a=a.toFixed(g)),f&&f.toFixed&&(f=f.toFixed(g)),T=n.mouse.trackFormatter({x:a,y:f,series:n.series,index:n.index,nearest:n,fraction:n.fraction});if(t.isNull(T)||t.isUndefined(T)){e.hide(c);return}c.innerHTML=T,e.show(c);if(!o)return;x=e.size(c),b&&(S=e.position(this.el),w=S.top,E=S.left);if(!n.mouse.relative)i+="top:",o.charAt(0)=="n"?i+=w+u+m:o.charAt(0)=="s"&&(i+=w-u+m+this.plotHeight-x.height),i+="px;bottom:auto;left:",o.charAt(1)=="e"?i+=E-u+p+this.plotWidth-x.width:o.charAt(1)=="w"&&(i+=E+u+p),i+="px;right:auto;";else if(s.pie&&s.pie.show){var N={x:this.plotWidth/2,y:this.plotHeight/2},C=Math.min(this.canvasWidth,this.canvasHeight)*s.pie.sizeRatio/2,k=n.sAngle<n.eAngle?(n.sAngle+n.eAngle)/2:(n.sAngle+n.eAngle+2*Math.PI)/2;i+="bottom:"+(u-m-N.y-Math.sin(k)*C/2+this.canvasHeight)+"px;top:auto;",i+="left:"+(u+p+N.x+Math.cos(k)*C/2)+"px;right:auto;"}else i+="top:",/n/.test(o)?i+=w-u+m+n.yaxis.d2p(n.y)-x.height:i+=w+u+m+n.yaxis.d2p(n.y),i+="px;bottom:auto;left:",/w/.test(o)?i+=E-u+p+n.xaxis.d2p(n.x)-x.width:i+=E+u+p+n.xaxis.d2p(n.x),i+="px;right:auto;";c.style.cssText=l+i,n.mouse.relative&&(/[ew]/.test(o)?/[ns]/.test(o)||(c.style.top=w+m+n.yaxis.d2p(n.y)-e.size(c).height/2+"px"):c.style.left=E+p+n.xaxis.d2p(n.x)-e.size(c).width/2+"px")}})}(),function(){function e(e,t){return e.which?e.which===1:e.button===0||e.button===1}function t(e,t){return Math.min(Math.max(0,e),t.plotWidth-1)}function n(e,t){return Math.min(Math.max(0,e),t.plotHeight)}var r=Flotr.DOM,i=Flotr.EventAdapter,s=Flotr._;Flotr.addPlugin("selection",{options:{pinchOnly:null,mode:null,color:"#B6D9FF",fps:20},callbacks:{"flotr:mouseup":function(e){var t=this.options.selection,n=this.selection,r=this.getEventPosition(e);if(!t||!t.mode)return;n.interval&&clearInterval(n.interval),this.multitouches?n.updateSelection():t.pinchOnly||n.setSelectionPos(n.selection.second,r),n.clearSelection(),n.selecting&&n.selectionIsSane()&&(n.drawSelection(),n.fireSelectEvent(),this.ignoreClick=!0)},"flotr:mousedown":function(t){var n=this.options.selection,r=this.selection,i=this.getEventPosition(t);if(!n||!n.mode)return;if(!n.mode||!e(t)&&s.isUndefined(t.touches))return;n.pinchOnly||r.setSelectionPos(r.selection.first,i),r.interval&&clearInterval(r.interval),this.lastMousePos.pageX=null,r.selecting=!1,r.interval=setInterval(s.bind(r.updateSelection,this),1e3/n.fps)},"flotr:destroy":function(e){clearInterval(this.selection.interval)}},getArea:function(){var e=this.selection.selection,t=this.axes,n=e.first,r=e.second,i,s,o,u;return i=t.x.p2d(e.first.x),s=t.x.p2d(e.second.x),o=t.y.p2d(e.first.y),u=t.y.p2d(e.second.y),{x1:Math.min(i,s),y1:Math.min(o,u),x2:Math.max(i,s),y2:Math.max(o,u),xfirst:i,xsecond:s,yfirst:o,ysecond:u}},selection:{first:{x:-1,y:-1},second:{x:-1,y:-1}},prevSelection:null,interval:null,fireSelectEvent:function(e){var t=this.selection.getArea();e=e||"select",t.selection=this.selection.selection,i.fire(this.el,"flotr:"+e,[t,this])},setSelection:function(e,r){var i=this.options,s=this.axes.x,o=this.axes.y,u=o.scale,a=s.scale,f=i.selection.mode.indexOf("x")!=-1,l=i.selection.mode.indexOf("y")!=-1,c=this.selection.selection;this.selection.clearSelection(),c.first.y=n(f&&!l?0:(o.max-e.y1)*u,this),c.second.y=n(f&&!l?this.plotHeight-1:(o.max-e.y2)*u,this),c.first.x=t(l&&!f?0:(e.x1-s.min)*a,this),c.second.x=t(l&&!f?this.plotWidth:(e.x2-s.min)*a,this),this.selection.drawSelection(),r||this.selection.fireSelectEvent()},setSelectionPos:function(e,r){var i=this.options.selection.mode,s=this.selection.selection;i.indexOf("x")==-1?e.x=e==s.first?0:this.plotWidth:e.x=t(r.relX,this),i.indexOf("y")==-1?e.y=e==s.first?0:this.plotHeight-1:e.y=n(r.relY,this)},drawSelection:function(){this.selection.fireSelectEvent("selecting");var e=this.selection.selection,t=this.octx,n=this.options,r=this.plotOffset,i=this.selection.prevSelection;if(i&&e.first.x==i.first.x&&e.first.y==i.first.y&&e.second.x==i.second.x&&e.second.y==i.second.y)return;t.save(),t.strokeStyle=this.processColor(n.selection.color,{opacity:.8}),t.lineWidth=1,t.lineJoin="miter",t.fillStyle=this.processColor(n.selection.color,{opacity:.4}),this.selection.prevSelection={first:{x:e.first.x,y:e.first.y},second:{x:e.second.x,y:e.second.y}};var s=Math.min(e.first.x,e.second.x),o=Math.min(e.first.y,e.second.y),u=Math.abs(e.second.x-e.first.x),a=Math.abs(e.second.y-e.first.y);t.fillRect(s+r.left+.5,o+r.top+.5,u,a),t.strokeRect(s+r.left+.5,o+r.top+.5,u,a),t.restore()},updateSelection:function(){if(!this.lastMousePos.pageX)return;this.selection.selecting=!0;if(this.multitouches)this.selection.setSelectionPos(this.selection.selection.first,this.getEventPosition(this.multitouches[0])),this.selection.setSelectionPos(this.selection.selection.second,this.getEventPosition(this.multitouches[1]));else{if(this.options.selection.pinchOnly)return;this.selection.setSelectionPos(this.selection.selection.second,this.lastMousePos)}this.selection.clearSelection(),this.selection.selectionIsSane()&&this.selection.drawSelection()},clearSelection:function(){if(!this.selection.prevSelection)return;var e=this.selection.prevSelection,t=1,n=this.plotOffset,r=Math.min(e.first.x,e.second.x),i=Math.min(e.first.y,e.second.y),s=Math.abs(e.second.x-e.first.x),o=Math.abs(e.second.y-e.first.y);this.octx.clearRect(r+n.left-t+.5,i+n.top-t,s+2*t+.5,o+2*t+.5),this.selection.prevSelection=null},selectionIsSane:function(){var e=this.selection.selection;return Math.abs(e.second.x-e.first.x)>=5||Math.abs(e.second.y-e.first.y)>=5}})}(),function(){var e=Flotr.DOM;Flotr.addPlugin("labels",{callbacks:{"flotr:afterdraw":function(){this.labels.draw()}},draw:function(){function b(e,t,r){var i=r?t.minorTicks:t.ticks,s=t.orientation===1,u=t.n===1,l,h;l={color:t.options.color||d.grid.color,angle:Flotr.toRad(t.options.labelsAngle),textBaseline:"middle"};for(c=0;c<i.length&&(r?t.options.showMinorLabels:t.options.showLabels);++c){n=i[c],n.label+="";if(!n.label||!n.label.length)continue;x=Math.cos(c*a+f)*o,y=Math.sin(c*a+f)*o,l.textAlign=s?Math.abs(x)<.1?"center":x<0?"right":"left":"left",Flotr.drawText(v,n.label,s?x:3,s?y:-(t.ticks[c].v/t.max)*(o-d.fontSize),l)}}function w(e,t,r,i){function f(e){return e.options.showLabels&&e.used}function l(e,t,n,r){return e.plotOffset.left+(t?r:n?-d.grid.labelMargin:d.grid.labelMargin+e.plotWidth)}function h(e,t,n,r){return e.plotOffset.top+(t?d.grid.labelMargin:r)+(t&&n?e.plotHeight:0)}var s=t.orientation===1,o=t.n===1,u,a;u={color:t.options.color||d.grid.color,textAlign:r,textBaseline:i,angle:Flotr.toRad(t.options.labelsAngle)},u=Flotr.getBestTextAlign(u.angle,u);for(c=0;c<t.ticks.length&&f(t);++c){n=t.ticks[c];if(!n.label||!n.label.length)continue;a=t.d2p(n.v);if(a<0||a>(s?e.plotWidth:e.plotHeight))continue;Flotr.drawText(v,n.label,l(e,s,o,a),h(e,s,o,a),u),!s&&!o&&(v.save(),v.strokeStyle=u.color,v.beginPath(),v.moveTo(e.plotOffset.left+e.plotWidth-8,e.plotOffset.top+t.d2p(n.v)),v.lineTo(e.plotOffset.left+e.plotWidth,e.plotOffset.top+t.d2p(n.v)),v.stroke(),v.restore())}}function E(e,t){var r=t.orientation===1,i=t.n===1,o="",u,a,f,l=e.plotOffset;!r&&!i&&(v.save(),v.strokeStyle=t.options.color||d.grid.color,v.beginPath());if(t.options.showLabels&&(i?!0:t.used))for(c=0;c<t.ticks.length;++c){n=t.ticks[c];if(!n.label||!n.label.length||(r?l.left:l.top)+t.d2p(n.v)<0||(r?l.left:l.top)+t.d2p(n.v)>(r?e.canvasWidth:e.canvasHeight))continue;f=l.top+(r?(i?1:-1)*(e.plotHeight+d.grid.labelMargin):t.d2p(n.v)-t.maxLabel.height/2),u=r?l.left+t.d2p(n.v)-s/2:0,o="",c===0?o=" first":c===t.ticks.length-1&&(o=" last"),o+=r?" flotr-grid-label-x":" flotr-grid-label-y",h+=['<div style="position:absolute; text-align:'+(r?"center":"right")+"; ","top:"+f+"px; ",(!r&&!i?"right:":"left:")+u+"px; ","width:"+(r?s:(i?l.left:l.right)-d.grid.labelMargin)+"px; ",t.options.color?"color:"+t.options.color+"; ":" ",'" class="flotr-grid-label'+o+'">'+n.label+"</div>"].join(" "),!r&&!i&&(v.moveTo(l.left+e.plotWidth-8,l.top+t.d2p(n.v)),v.lineTo(l.left+e.plotWidth,l.top+t.d2p(n.v)))}}var t,n,r,i,s,o,u,a,f,l,c,h="",p=0,d=this.options,v=this.ctx,m=this.axes,g={size:d.fontSize};for(c=0;c<m.x.ticks.length;++c)m.x.ticks[c].label&&++p;s=this.plotWidth/p,d.grid.circular&&(v.save(),v.translate(this.plotOffset.left+this.plotWidth/2,this.plotOffset.top+this.plotHeight/2),o=this.plotHeight*d.radar.radiusRatio/2+d.fontSize,u=this.axes.x.ticks.length,a=2*(Math.PI/u),f=-Math.PI/2,b(this,m.x,!1),b(this,m.x,!0),b(this,m.y,!1),b(this,m.y,!0),v.restore()),!d.HtmlText&&this.textEnabled?(w(this,m.x,"center","top"),w(this,m.x2,"center","bottom"),w(this,m.y,"right","middle"),w(this,m.y2,"left","middle")):(m.x.options.showLabels||m.x2.options.showLabels||m.y.options.showLabels||m.y2.options.showLabels)&&!d.grid.circular&&(h="",E(this,m.x),E(this,m.x2),E(this,m.y),E(this,m.y2),v.stroke(),v.restore(),l=e.create("div"),e.setStyles(l,{fontSize:"smaller",color:d.grid.color}),l.className="flotr-labels",e.insert(this.el,l),e.insert(l,h))}})}(),function(){var e=Flotr.DOM,t=Flotr._;Flotr.addPlugin("legend",{options:{show:!0,noColumns:1,labelFormatter:function(e){return e},labelBoxBorderColor:"#CCCCCC",labelBoxWidth:14,labelBoxHeight:10,labelBoxMargin:5,container:null,position:"nw",margin:5,backgroundColor:"#F0F0F0",backgroundOpacity:.85},callbacks:{"flotr:afterinit":function(){this.legend.insertLegend()},"flotr:destroy":function(){var t=this.legend.markup;t&&(this.legend.markup=null,e.remove(t))}},insertLegend:function(){if(!this.options.legend.show)return;var n=this.series,r=this.plotOffset,i=this.options,s=i.legend,o=[],u=!1,a=this.ctx,f=t.filter(n,function(e){return e.label&&!e.hide}).length,l=s.position,c=s.margin,h=s.backgroundOpacity,p,d,v;if(f){var m=s.labelBoxWidth,g=s.labelBoxHeight,y=s.labelBoxMargin,b=r.left+c,w=r.top+c,E=0,S={size:i.fontSize*1.1,color:i.grid.color};for(p=n.length-1;p>-1;--p){if(!n[p].label||n[p].hide)continue;d=s.labelFormatter(n[p].label),E=Math.max(E,this._text.measureText(d,S).width)}var x=Math.round(m+y*3+E),T=Math.round(f*(y+g)+y);!h&&h!==0&&(h=.1);if(!i.HtmlText&&this.textEnabled&&!s.container){l.charAt(0)=="s"&&(w=r.top+this.plotHeight-(c+T)),l.charAt(0)=="c"&&(w=r.top+this.plotHeight/2-(c+T/2)),l.charAt(1)=="e"&&(b=r.left+this.plotWidth-(c+x)),v=this.processColor(s.backgroundColor,{opacity:h}),a.fillStyle=v,a.fillRect(b,w,x,T),a.strokeStyle=s.labelBoxBorderColor,a.strokeRect(Flotr.toPixel(b),Flotr.toPixel(w),x,T);var N=b+y,C=w+y;for(p=0;p<n.length;p++){if(!n[p].label||n[p].hide)continue;d=s.labelFormatter(n[p].label),a.fillStyle=n[p].color,a.fillRect(N,C,m-1,g-1),a.strokeStyle=s.labelBoxBorderColor,a.lineWidth=1,a.strokeRect(Math.ceil(N)-1.5,Math.ceil(C)-1.5,m+2,g+2),Flotr.drawText(a,d,N+m+y,C+g,S),C+=g+y}}else{for(p=0;p<n.length;++p){if(!n[p].label||n[p].hide)continue;p%s.noColumns===0&&(o.push(u?"</tr><tr>":"<tr>"),u=!0);var k=n[p],L=s.labelBoxWidth,A=s.labelBoxHeight;d=s.labelFormatter(k.label),v="background-color:"+(k.bars&&k.bars.show&&k.bars.fillColor&&k.bars.fill?k.bars.fillColor:k.color)+";",o.push('<td class="flotr-legend-color-box">','<div style="border:1px solid ',s.labelBoxBorderColor,';padding:1px">','<div style="width:',L-1,"px;height:",A-1,"px;border:1px solid ",n[p].color,'">','<div style="width:',L,"px;height:",A,"px;",v,'"></div>',"</div>","</div>","</td>",'<td class="flotr-legend-label">',d,"</td>")}u&&o.push("</tr>");if(o.length>0){var O='<table style="font-size:smaller;color:'+i.grid.color+'">'+o.join("")+"</table>";if(s.container)O=e.node(O),this.legend.markup=O,e.insert(s.container,O);else{var M={position:"absolute",zIndex:"2",border:"1px solid "+s.labelBoxBorderColor};l.charAt(0)=="n"?(M.top=c+r.top+"px",M.bottom="auto"):l.charAt(0)=="c"?(M.top=c+(this.plotHeight-T)/2+"px",M.bottom="auto"):l.charAt(0)=="s"&&(M.bottom=c+r.bottom+"px",M.top="auto"),l.charAt(1)=="e"?(M.right=c+r.right+"px",M.left="auto"):l.charAt(1)=="w"&&(M.left=c+r.left+"px",M.right="auto");var P=e.create("div"),H;P.className="flotr-legend",e.setStyles(P,M),e.insert(P,O),e.insert(this.el,P);if(!h)return;var B=s.backgroundColor||i.grid.backgroundColor||"#ffffff";t.extend(M,e.size(P),{backgroundColor:B,zIndex:"",border:""}),M.width+="px",M.height+="px",P=e.create("div"),P.className="flotr-legend-bg",e.setStyles(P,M),e.opacity(P,h),e.insert(P," "),e.insert(this.el,P)}}}}}})}(),function(){function e(e){if(this.options.spreadsheet.tickFormatter)return this.options.spreadsheet.tickFormatter(e);var t=n.find(this.axes.x.ticks,function(t){return t.v==e});return t?t.label:e}var t=Flotr.DOM,n=Flotr._;Flotr.addPlugin("spreadsheet",{options:{show:!1,tabGraphLabel:"Graph",tabDataLabel:"Data",toolbarDownload:"Download CSV",toolbarSelectAll:"Select all",csvFileSeparator:",",decimalSeparator:".",tickFormatter:null,initialTab:"graph"},callbacks:{"flotr:afterconstruct":function(){if(!this.options.spreadsheet.show)return;var e=this.spreadsheet,n=t.node('<div class="flotr-tabs-group" style="position:absolute;left:0px;width:'+this.canvasWidth+'px"></div>'),r=t.node('<div style="float:left" class="flotr-tab selected">'+this.options.spreadsheet.tabGraphLabel+"</div>"),i=t.node('<div style="float:left" class="flotr-tab">'+this.options.spreadsheet.tabDataLabel+"</div>"),s;e.tabsContainer=n,e.tabs={graph:r,data:i},t.insert(n,r),t.insert(n,i),t.insert(this.el,n),s=t.size(i).height+2,this.plotOffset.bottom+=s,t.setStyles(n,{top:this.canvasHeight-s+"px"}),this.observe(r,"click",function(){e.showTab("graph")}).observe(i,"click",function(){e.showTab("data")}),this.options.spreadsheet.initialTab!=="graph"&&e.showTab(this.options.spreadsheet.initialTab)}},loadDataGrid:function(){if(this.seriesData)return this.seriesData;var e=this.series,t={};return n.each(e,function(e,r){n.each(e.data,function(e){var n=e[0],s=e[1],o=t[n];if(o)o[r+1]=s;else{var u=[];u[0]=n,u[r+1]=s,t[n]=u}})}),this.seriesData=n.sortBy(t,function(e,t){return parseInt(t,10)}),this.seriesData},constructDataGrid:function(){if(this.spreadsheet.datagrid)return this.spreadsheet.datagrid;var r=this.series,i=this.spreadsheet.loadDataGrid(),s=["<colgroup><col />"],o,u,a,f=['<table class="flotr-datagrid"><tr class="first-row">'];f.push("<th>&nbsp;</th>"),n.each(r,function(e,t){f.push('<th scope="col">'+(e.label||String.fromCharCode(65+t))+"</th>"),s.push("<col />")}),f.push("</tr>"),n.each(i,function(t){f.push("<tr>"),n.times(r.length+1,function(r){var i="td",s=t[r],o=n.isUndefined(s)?"":Math.round(s*1e5)/1e5;if(r===0){i="th";var u=e.call(this,o);u&&(o=u)}f.push("<"+i+(i=="th"?' scope="row"':"")+">"+o+"</"+i+">")},this),f.push("</tr>")},this),s.push("</colgroup>"),a=t.node(f.join("")),o=t.node('<button type="button" class="flotr-datagrid-toolbar-button">'+this.options.spreadsheet.toolbarDownload+"</button>"),u=t.node('<button type="button" class="flotr-datagrid-toolbar-button">'+this.options.spreadsheet.toolbarSelectAll+"</button>"),this.observe(o,"click",n.bind(this.spreadsheet.downloadCSV,this)).observe(u,"click",n.bind(this.spreadsheet.selectAllData,this));var l=t.node('<div class="flotr-datagrid-toolbar"></div>');t.insert(l,o),t.insert(l,u);var c=this.canvasHeight-t.size(this.spreadsheet.tabsContainer).height-2,h=t.node('<div class="flotr-datagrid-container" style="position:absolute;left:0px;top:0px;width:'+this.canvasWidth+"px;height:"+c+'px;overflow:auto;z-index:10"></div>');return t.insert(h,l),t.insert(h,a),t.insert(this.el,h),this.spreadsheet.datagrid=a,this.spreadsheet.container=h,a},showTab:function(e){if(this.spreadsheet.activeTab===e)return;switch(e){case"graph":t.hide(this.spreadsheet.container),t.removeClass(this.spreadsheet.tabs.data,"selected"),t.addClass(this.spreadsheet.tabs.graph,"selected");break;case"data":this.spreadsheet.datagrid||this.spreadsheet.constructDataGrid(),t.show(this.spreadsheet.container),t.addClass(this.spreadsheet.tabs.data,"selected"),t.removeClass(this.spreadsheet.tabs.graph,"selected");break;default:throw"Illegal tab name: "+e}this.spreadsheet.activeTab=e},selectAllData:function(){if(this.spreadsheet.tabs){var e,t,n,r,i=this.spreadsheet.constructDataGrid();return this.spreadsheet.showTab("data"),setTimeout(function(){(n=i.ownerDocument)&&(r=n.defaultView)&&r.getSelection&&n.createRange&&(e=window.getSelection())&&e.removeAllRanges?(t=n.createRange(),t.selectNode(i),e.removeAllRanges(),e.addRange(t)):document.body&&document.body.createTextRange&&(t=document.body.createTextRange())&&(t.moveToElementText(i),t.select())},0),!0}return!1},downloadCSV:function(){var t="",r=this.series,i=this.options,s=this.spreadsheet.loadDataGrid(),o=encodeURIComponent(i.spreadsheet.csvFileSeparator);if(i.spreadsheet.decimalSeparator===i.spreadsheet.csvFileSeparator)throw"The decimal separator is the same as the column separator ("+i.spreadsheet.decimalSeparator+")";n.each(r,function(e,n){t+=o+'"'+(e.label||String.fromCharCode(65+n)).replace(/\"/g,'\\"')+'"'}),t+="%0D%0A",t+=n.reduce(s,function(t,n){var r=e.call(this,n[0])||"";r='"'+(r+"").replace(/\"/g,'\\"')+'"';var s=n.slice(1).join(o);return i.spreadsheet.decimalSeparator!=="."&&(s=s.replace(/\./g,i.spreadsheet.decimalSeparator)),t+r+o+s+"%0D%0A"},"",this),Flotr.isIE&&Flotr.isIE<9?(t=t.replace(new RegExp(o,"g"),decodeURIComponent(o)).replace(/%0A/g,"\n").replace(/%0D/g,"\r"),window.open().document.write(t)):window.open("data:text/csv,"+t)}})}(),function(){var e=Flotr.DOM;Flotr.addPlugin("titles",{callbacks:{"flotr:afterdraw":function(){this.titles.drawTitles()}},drawTitles:function(){var t,n=this.options,r=n.grid.labelMargin,i=this.ctx,s=this.axes;if(!n.HtmlText&&this.textEnabled){var o={size:n.fontSize,color:n.grid.color,textAlign:"center"};n.subtitle&&Flotr.drawText(i,n.subtitle,this.plotOffset.left+this.plotWidth/2,this.titleHeight+this.subtitleHeight-2,o),o.weight=1.5,o.size*=1.5,n.title&&Flotr.drawText(i,n.title,this.plotOffset.left+this.plotWidth/2,this.titleHeight-2,o),o.weight=1.8,o.size*=.8,s.x.options.title&&s.x.used&&(o.textAlign=s.x.options.titleAlign||"center",o.textBaseline="top",o.angle=Flotr.toRad(s.x.options.titleAngle),o=Flotr.getBestTextAlign(o.angle,o),Flotr.drawText(i,s.x.options.title,this.plotOffset.left+this.plotWidth/2,this.plotOffset.top+s.x.maxLabel.height+this.plotHeight+2*r,o)),s.x2.options.title&&s.x2.used&&(o.textAlign=s.x2.options.titleAlign||"center",o.textBaseline="bottom",o.angle=Flotr.toRad(s.x2.options.titleAngle),o=Flotr.getBestTextAlign(o.angle,o),Flotr.drawText(i,s.x2.options.title,this.plotOffset.left+this.plotWidth/2,this.plotOffset.top-s.x2.maxLabel.height-2*r,o)),s.y.options.title&&s.y.used&&(o.textAlign=s.y.options.titleAlign||"right",o.textBaseline="middle",o.angle=Flotr.toRad(s.y.options.titleAngle),o=Flotr.getBestTextAlign(o.angle,o),Flotr.drawText(i,s.y.options.title,this.plotOffset.left-s.y.maxLabel.width-2*r,this.plotOffset.top+this.plotHeight/2,o)),s.y2.options.title&&s.y2.used&&(o.textAlign=s.y2.options.titleAlign||"left",o.textBaseline="middle",o.angle=Flotr.toRad(s.y2.options.titleAngle),o=Flotr.getBestTextAlign(o.angle,o),Flotr.drawText(i,s.y2.options.title,this.plotOffset.left+this.plotWidth+s.y2.maxLabel.width+2*r,this.plotOffset.top+this.plotHeight/2,o))}else{t=[],n.title&&t.push('<div style="position:absolute;top:0;left:',this.plotOffset.left,"px;font-size:1em;font-weight:bold;text-align:center;width:",this.plotWidth,'px;" class="flotr-title">',n.title,"</div>"),n.subtitle&&t.push('<div style="position:absolute;top:',this.titleHeight,"px;left:",this.plotOffset.left,"px;font-size:smaller;text-align:center;width:",this.plotWidth,'px;" class="flotr-subtitle">',n.subtitle,"</div>"),t.push("</div>"),t.push('<div class="flotr-axis-title" style="font-weight:bold;">'),s.x.options.title&&s.x.used&&t.push('<div style="position:absolute;top:',this.plotOffset.top+this.plotHeight+n.grid.labelMargin+s.x.titleSize.height,"px;left:",this.plotOffset.left,"px;width:",this.plotWidth,"px;text-align:",s.x.options.titleAlign,';" class="flotr-axis-title flotr-axis-title-x1">',s.x.options.title,"</div>"),s.x2.options.title&&s.x2.used&&t.push('<div style="position:absolute;top:0;left:',this.plotOffset.left,"px;width:",this.plotWidth,"px;text-align:",s.x2.options.titleAlign,';" class="flotr-axis-title flotr-axis-title-x2">',s.x2.options.title,"</div>"),s.y.options.title&&s.y.used&&t.push('<div style="position:absolute;top:',this.plotOffset.top+this.plotHeight/2-s.y.titleSize.height/2,"px;left:0;text-align:",s.y.options.titleAlign,';" class="flotr-axis-title flotr-axis-title-y1">',s.y.options.title,"</div>"),s.y2.options.title&&s.y2.used&&t.push('<div style="position:absolute;top:',this.plotOffset.top+this.plotHeight/2-s.y.titleSize.height/2,"px;right:0;text-align:",s.y2.options.titleAlign,';" class="flotr-axis-title flotr-axis-title-y2">',s.y2.options.title,"</div>"),t=t.join("");var u=e.create("div");e.setStyles({color:n.grid.color}),u.className="flotr-titles",e.insert(this.el,u),e.insert(u,t)}}})}();
diff --git a/library/sip_statistics.php b/library/sip_statistics.php
index 752414f..f798451 100644
--- a/library/sip_statistics.php
+++ b/library/sip_statistics.php
@@ -1,684 +1,904 @@
<?
/*
Copyright (c) 2008 AG Projects
http://ag-projects.com
Author Adrian Georgescu
*/
class NetworkStatistics {
// obtain statistics from SIP Thor network
var $statistics = array();
var $status = array();
var $sip_summary = array();
var $sip_proxies = array();
var $node_statistics = array();
var $domain_statistics = array();
var $allowedRoles = array('sip_proxy',
'media_relay',
'provisioning_server',
'xcap_server',
'thor_dnsmanager',
'thor_database',
'voicemail_server'
);
var $allowedSummary = array('online_accounts',
'total_accounts'
);
function NetworkStatistics($engineId,$allowedDomains=array()) {
if (!strlen($engineId)) return false;
$this->allowedDomains = $allowedDomains;
$this->soapEngineId = $engineId;
require("/etc/cdrtool/ngnpro_engines.inc");
require_once("ngnpro_soap_library.php");
$this->SOAPlogin = array(
"username" => $soapEngines[$this->soapEngineId]['username'],
"password" => $soapEngines[$this->soapEngineId]['password'],
"admin" => true
);
$this->SOAPurl=$soapEngines[$this->soapEngineId]['url'];
$this->SoapAuth = array('auth', $this->SOAPlogin , 'urn:AGProjects:NGNPro', 0, '');
$this->soapclient = new WebService_NGNPro_NetworkPort($this->SOAPurl);
$this->soapclient->setOpt('curl', CURLOPT_TIMEOUT, 5);
$this->soapclient->setOpt('curl', CURLOPT_SSL_VERIFYPEER, 0);
$this->soapclient->setOpt('curl', CURLOPT_SSL_VERIFYHOST, 0);
if (is_array($soapEngines[$this->soapEngineId]['hostnames'])) {
$this->hostnames=$soapEngines[$this->soapEngineId]['hostnames'];
} else {
$this->hostnames=array();
}
}
function getStatistics() {
$this->soapclient->addHeader($this->SoapAuth);
$result = $this->soapclient->getStatistics();
if ((new PEAR)->isError($result)) {
$error_msg = $result->getMessage();
$error_fault = $result->getFault();
$error_code = $result->getCode();
$log=sprintf("Error from %s: %s: %s",$this->SOAPurl,$error_fault->detail->exception->errorcode,$error_fault->detail->exception->errorstring);
syslog(LOG_NOTICE,$log);
return false;
}
$this->statistics=json_decode($result,true);;
foreach (array_keys($this->statistics) as $_ip) {
if ($_ip == 'summary') {
foreach (array_keys($this->statistics[$_ip]) as $_role) {
if ($_role == 'sip_proxy') {
foreach (array_keys($this->statistics[$_ip][$_role]) as $_section) {
foreach (array_keys($this->statistics[$_ip][$_role][$_section]) as $_domain) {
if (count($this->allowedDomains) && !in_array($_domain,$this->allowedDomains)) continue;
if (count($this->allowedDomains) && !in_array($_section,$this->allowedSummary)) continue;
$this->sip_summary[$_section]=$this->sip_summary[$_section]+$this->statistics[$_ip][$_role][$_section][$_domain];
}
}
}
}
continue;
}
foreach (array_keys($this->statistics[$_ip]) as $_role) {
if ($_role == 'sip_proxy') {
$this->node_statistics[$_ip]['sip_proxy']=true;
foreach (array_keys($this->statistics[$_ip][$_role]) as $_section) {
foreach (array_keys($this->statistics[$_ip][$_role][$_section]) as $_domain) {
if (count($this->allowedDomains) && !in_array($_domain,$this->allowedDomains)) continue;
$this->domain_statistics[$_domain][$_section] = $this->domain_statistics[$_domain][$_section] + $this->statistics[$_ip][$_role][$_section][$_domain];
$this->domain_statistics['total'][$_section] = $this->domain_statistics['total'][$_section] + $this->statistics[$_ip][$_role][$_section][$_domain];
$this->node_statistics[$_ip][$_section] = $this->node_statistics[$_ip][$_section] + $this->statistics[$_ip][$_role][$_section][$_domain];
}
}
}
}
}
}
function getStatus() {
$this->soapclient->addHeader($this->SoapAuth);
$result = $this->soapclient->getStatus();
if ((new PEAR)->isError($result)) {
$error_msg = $result->getMessage();
$error_fault = $result->getFault();
$error_code = $result->getCode();
$log=sprintf("Error from %s: %s: %s",$this->SOAPurl,$error_fault->detail->exception->errorcode,$error_fault->detail->exception->errorstring);
syslog(LOG_NOTICE,$log);
return false;
}
$this->status=json_decode($result,true);
foreach (array_keys($this->status) as $_id) {
foreach ($this->status[$_id]['roles'] as $_role) {
if ($_role=='sip_proxy') $this->sip_proxies[$this->status[$_id]['ip']]++;
if ($_role=='thor_dnsmanager') $this->dns_managers[$this->status[$_id]['ip']]++;
if ($_role=='thor_manager') $this->thor_managers[$this->status[$_id]['ip']]++;
if ($_role=='conference_server') $this->conference_servers[$this->status[$_id]['ip']]++;
if ($_role=='voicemail_server') $this->voicemail_servers[$this->status[$_id]['ip']]++;
$ip=$this->status[$_id]['ip'];
$this->roles[$_role][$ip]=array('ip' => $ip,
'version' => $this->status[$_id]['version']
);
foreach(array_keys($this->status[$_id]) as $_attr) {
if ($_attr == 'ip' || $_attr == 'version' || $_attr == 'roles') continue;
$this->roles[$_role][$ip]['attributes'][$_attr]=$this->status[$_id][$_attr];
}
}
}
}
function showStatus() {
$this->getStatus();
print "<div class=row><table class=\"span12 table table-condensed table-striped\">";
print "<thead><tr>";
if (count($this->allowedDomains)) {
print "<th>Role</th><th>Address</th><th>Version</th></tr>";
} else {
print "<th>Role</th><th>Address</th><th>Version</th><th>Attributes</th></tr></thead>";
}
foreach (array_keys($this->roles) as $_role) {
if (count($this->allowedDomains)) {
if (!in_array($_role,$this->allowedRoles)) continue;
}
foreach ($this->roles[$_role] as $_entity) {
if (!$print_role[$_role]) {
$_role_print=preg_replace("/_/"," ",$_role);
} else {
$_role_print='';
}
if (count($this->allowedDomains)) {
printf ("<tr><td><b>%s</b></td><td class=span4>%s</td><td>%s</td></tr>",
ucfirst($_role_print),$this->ip2host($_entity['ip']),$_entity['version']);
} else {
$a_print='';
if ( is_array($_entity['attributes'])) {
foreach (array_keys($_entity['attributes']) as $_a1) {
if ($_a1 == 'dburi') {
if (preg_match("/^(mysql:\/\/\w*):\w*(@.*)$/",$_entity['attributes'][$_a1],$m)) {
$val=$m[1].':xxx'.$m[2];
} else {
$val=$_entity['attributes'][$_a1];
}
} else {
$val=$_entity['attributes'][$_a1];
}
$a_print .= sprintf ("%s=%s ",$_a1,$val);
}
}
printf ("<tr><td class=span3><b>%s</b></td><td class=span2>%s</td><td class=span2>%s</td><td>%s</td></tr>",
ucfirst($_role_print),$this->ip2host($_entity['ip']),$_entity['version'],$a_print);
}
$print_role[$_role]++;
}
}
print "</table>";
}
function showStatistics() {
$this->getStatistics();
print "<div class=row><table class='span12 table table-condensed table-striped'>";
print "<thead><tr>";
foreach (array_keys($this->sip_summary) as $_section) {
$_section_print=preg_replace("/_/"," ",$_section);
printf ("<th>%s</th>",ucfirst($_section_print));
}
print "</tr></thead><tr>";
foreach (array_keys($this->sip_summary) as $_section) {
printf ("<td style='text-align:center'>%s</td>",$this->sip_summary[$_section]);
}
print "</tr>";
print "</table></div>";
}
function ip2host($ip) {
if ($this->hostnames[$ip]) {
return $this->hostnames[$ip];
} else {
return $ip;
}
}
}
class SipThorNetworkImage {
// plot graphical SIP Thor network status
var $imgsize = 630;
var $nodes = array();
var $node_statistics = array();
var $display_options = array();
var $accounts_item = 'online_accounts';
function SipThorNetworkImage($engineId,$allowedDomains=array(),$display_options=array()) {
if (!strlen($engineId)) return false;
if (is_array($display_options)) {
$this->display_options=$display_options;
}
if ($this->display_options['accounts_item']) {
$this->accounts_item=$this->display_options['accounts_item'];
}
$this->soapEngineId=$engineId;
$NetworkStatistics=new NetworkStatistics($engineId,$allowedDomains);
$NetworkStatistics->getStatus();
$NetworkStatistics->getStatistics();
$this->sip_proxies = $NetworkStatistics->sip_proxies;
$this->conference_servers = $NetworkStatistics->conference_servers;
$this->voicemail_servers = $NetworkStatistics->voicemail_servers;
$this->dns_managers = $NetworkStatistics->dns_managers;
$this->thor_mangers = $NetworkStatistics->thor_managers;
$this->node_statistics = $NetworkStatistics->node_statistics;
$this->hostnames = $NetworkStatistics->hostnames;
if (!$this->display_options['hide_sessions']) {
require_once("media_sessions.php");
$MediaSessions = new MediaSessionsNGNPro($engineId);
$MediaSessions->getSummary();
foreach ($MediaSessions->summary as $_relay) {
$this->node_statistics[$_relay['ip']]['sessions']=$_relay['session_count'];
}
}
}
function returnImageData(){
if ($this->display_options['hide_accounts']) {
foreach($this->node_statistics as $key => $value){
if ($value['sip_proxy']) {
$this->node_statistics[$key] = array();
$this->node_statistics[$key]['sip_proxy'] = true;
}
}
}
return $this;
}
function buildImage() {
$img = imagecreatetruecolor($this->imgsize, $this->imgsize);
$white = imagecolorallocate($img, 255, 255, 255);
$black = imagecolorallocate($img, 0, 0, 0);
imagefill($img, 0, 0, $white);
$c=count($this->sip_proxies);
$cx=$this->imgsize/2;
$cy=$cx;
$radius=0.7*$cx;
// Sip Thor node image
$sip_thor_node_img = @imagecreatefrompng('SipThorNode.png');
list($nw, $nh) = getimagesize('SipThorNode.png');
// Internet cloud Image
$cloud_img = @imagecreatefrompng('InternetCloud.png');
list($cw, $ch) = getimagesize('InternetCloud.png');
// Sip Thor title rectangle image
$sip_thor_background_img = @imagecreatefrompng('SipThorNetworkBackground.png');
list($tw, $th) = getimagesize('SipThorNetworkBackground.png');
if (!$this->display_options['hide_frame']) {
imagecopy ($img,$sip_thor_background_img, $this->imgsize/2-$tw/2, $this->imgsize/2-$th/2, 0, 0, $tw, $th);
}
imagecopy ($img,$cloud_img, $this->imgsize/2-$cw/2, $this->imgsize/2-$ch/2, 0, 0, $cw, $ch);
$dash=false;
$dashsize=2;
for ($angle=0; $angle<=(180+$dashsize); $angle+=$dashsize) {
$x = ($radius * cos(deg2rad($angle)));
$y = ($radius * sin(deg2rad($angle)));
if ($dash) {
imageline($img, $cx+$px, $cy+$py, $cx+$x, $cy+$y, $black);
imageline($img, $cx-$px, $cx-$py, $cx-$x, $cy-$y, $black);
}
$dash=!$dash;
$px=$x;
$py=$y;
if ($dash) {
imageline($img, $cx+$px, $cy+$py, $cx+$x, $cy+$y, $black);
imageline($img, $cx-$px, $cx-$py, $cx-$x, $cy-$y, $black);
}
}
if (count($this->dns_managers)) {
$h1=0;
$t=count($this->dns_managers);
foreach (array_keys($this->dns_managers) as $_ip) {
imagecopy ($img,$sip_thor_node_img, $this->imgsize-120-$h1, 0, 0, 0, $nw-20, $nh-20);
$text=sprintf("DNS%s",$t--);
imagestring ($img, 3, $this->imgsize-65-$h1, 80, $text, $black);
$v1=$v1+10;
$h1=$h1+50;
}
$v1=100;
foreach (array_keys($this->dns_managers) as $_ip) {
imagestring ($img, 3, $this->imgsize-125, $v1, $_ip, $black);
$v1=$v1+10;
}
}
if (count($this->node_statistics)) {
$dashsize=360/count($this->node_statistics);
$j=0;
$node_names=array_keys($this->node_statistics);
for ($angle=0; $angle<360; $angle+=$dashsize) {
$x = ($radius * cos(deg2rad($angle)));
$y = ($radius * sin(deg2rad($angle)));
if ($this->hostnames[$node_names[$j]]) {
$text = $this->hostnames[$node_names[$j]];
//$text = $node_names[$j];
} else {
$text = $node_names[$j];
}
$px=$x;
$py=$y;
if (strlen($this->node_statistics[$node_names[$j]]['online_accounts']) && strlen($this->node_statistics[$node_names[$j]]['sessions'])) {
if (!$this->display_options['hide_accounts']) {
$extra_text1=intval($this->node_statistics[$node_names[$j]][$this->accounts_item]). ' accounts';
}
if (!$this->display_options['hide_sessions']) {
$extra_text2=intval($this->node_statistics[$node_names[$j]]['sessions']). ' sessions';
}
} else if (strlen($this->node_statistics[$node_names[$j]]['online_accounts'])) {
if (!$this->display_options['hide_accounts']) {
$extra_text1=intval($this->node_statistics[$node_names[$j]][$this->accounts_item]). ' accounts';
} else {
$extra_text1=$node_names[$j];
}
$extra_text2="";
} else if (strlen($this->node_statistics[$node_names[$j]]['sessions'])) {
if (!$this->display_options['hide_sessions']) {
$extra_text1=intval($this->node_statistics[$node_names[$j]]['sessions']). ' sessions';
}
$extra_text2="";
}
if (($angle >= 120 && $angle < 240)) {
imagestring ($img, 3, $cx+$px-70, $cy+$py-72, $text, $black);
imagestring ($img, 3, $cx+$px-70, $cy+$py-62, $extra_text1, $black);
imagestring ($img, 3, $cx+$px-70, $cy+$py-52, $extra_text2, $black);
} else {
imagestring ($img, 3, $cx+$px-110, $cy+$py-30, $text, $black);
imagestring ($img, 3, $cx+$px-110, $cy+$py-20, $extra_text1, $black);
imagestring ($img, 3, $cx+$px-110, $cy+$py-10, $extra_text2, $black);
}
imagecopy ($img,$sip_thor_node_img, $cx+$px-$nw/2+7, $cy+$py-$nh/2+5, 0, 0, $nw-20, $nh-20);
$j++;
}
}
return $img;
}
}
class SIPstatistics {
// build graphical statistics with sip registrar and media relay usage
var $domains = array('total'=>'total');
function SIPstatistics () {
global $CDRTool;
$this->path=$CDRTool['Path'];
$this->harvest_file = "/tmp/CDRTool-sip-statistics.txt";
$this->harvest_script = $this->path."/scripts/harvestStatistics.php";
$this->mrtgcfg_dir = $this->path."/status/usage";
$this->mrtgcfg_file = $this->path."/status/usage/sip_statistics.mrtg";
$this->mrtg_data_script = $this->path."/scripts/generateMrtgData.php";
$this->mrtg_config_script = $this->path."/scripts/generateMrtgConfig.php";
$this->getDomains();
}
function getDomains () {
global $CDRTool;
if (!is_array($CDRTool['statistics']['domains'])) return;
foreach ($CDRTool['statistics']['domains'] as $_domain) {
$this->domains[$_domain]=$_domain;
}
}
function generateMrtgConfigFile () {
if (!$handle = fopen($this->mrtgcfg_file, 'w+')) {
echo "Error opening {$this->mrtgcfg_file}.\n";
return 0;
}
// printing cfg header
fwrite($handle,"
### Global Config Options
WorkDir: {$this->mrtgcfg_dir}
IconDir: {$this->mrtgcfg_dir}/images
Refresh: 300
#WriteExpires: Yes
");
while(list($key,$value) = each($this->domains)) {
fwrite($handle,"\n\n
## {$key}
Target[{$key}_users]: `{$this->mrtg_data_script} {$key} users`
Options[{$key}_users]: growright, gauge, nobanner
BodyTag[{$key}_users]: <BODY LEFTMARGIN=\"1\" TOPMARGIN=\"1\">
#PNGTitle[{$key}_users]: <center>Online Users for {$key}</center>
MaxBytes[{$key}_users]: 5000000
Title[{$key}_users]: Online Users for {$key}
ShortLegend[{$key}_users]: U
XSize[{$key}_users]: 300
YSize[{$key}_users]: 75
Ylegend[{$key}_users]: Users
Legend1[{$key}_users]: Online Users
LegendI[{$key}_users]: Online Users
LegendO[{$key}_users]:
PageTop[{$key}_users]: <H1> Online Users for {$key} </H1>
Target[{$key}_sessions]: `{$this->mrtg_data_script} {$key} sessions`
Options[{$key}_sessions]: growright, nobanner, gauge
BodyTag[{$key}_sessions]: <BODY LEFTMARGIN=\"1\" TOPMARGIN=\"1\">
MaxBytes[{$key}_sessions]: 50000
Title[{$key}_sessions]: Sessions Statistics for {$key}
ShortLegend[{$key}_sessions]: Ses
XSize[{$key}_sessions]: 300
YSize[{$key}_sessions]: 75
Ylegend[{$key}_sessions]: Sessions
Legend1[{$key}_sessions]: Active Sessions
LegendI[{$key}_sessions]: Active Sessions
LegendO[{$key}_sessions]:
PageTop[{$key}_sessions]: <H1> Active Sessions for {$key} </H1>
Target[{$key}_traffic]: `{$this->mrtg_data_script} {$key} traffic`
Options[{$key}_traffic]: gauge, growright, bits, nobanner
BodyTag[{$key}_traffic]: <BODY LEFTMARGIN=\"1\" TOPMARGIN=\"1\">
#PNGTitle[{$key}_traffic]: {$key} traffic
MaxBytes[{$key}_traffic]: 1250000000
Title[{$key}_traffic]: IP traffic for {$key}
XSize[{$key}_traffic]: 300
YSize[{$key}_traffic]: 75
Legend1[{$key}_traffic]: Caller Traffic in Bits per Second
Legend2[{$key}_traffic]: Called Traffic in Bits per Second
LegendI[{$key}_traffic]: caller
LegendO[{$key}_traffic]: called
PageTop[{$key}_traffic]: <H1> IP Traffic for {$key} </H1>
");
}
fclose($handle);
}
function generateMrtgData($domain,$dataType) {
$value1=0;
$value2=0;
$domain= str_replace(".","\.",$domain);
$lines=explode("\n",file_get_contents($this->harvest_file));
foreach ($lines as $line) {
if (preg_match("/^$domain\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/",$line,$m)) {
if ($dataType == 'sessions') {
$value1 = $m[2];
$value2 = $m[2];
} else if ($dataType == 'traffic') {
$value1 = $m[3];
$value2 = $m[4];
} else if ($dataType == 'users') {
$value1 = $m[1];
$value2 = $m[1];
}
}
}
printf ("%d\n%d\n0\n0\n\n",$value1,$value2);
}
function getOnlineAccountsFromMySQL($class) {
$domains=array();
$online_devices=0;
$online_accounts=0;
if (!class_exists($class)) return array();
$db = new $class();
$query="select count(*) as c, domain from location group by domain";
dprint($query);
if (!$db->query($query)) {
$log=sprintf ("Database error for query %s: %s (%s)",$query,$db->Error,$db->Errno);
print $log;
syslog(LOG_NOTICE, $log);
return array();
}
if (!$db->num_rows()) return array();
while ($db->next_record()) {
$domains[$db->f('domain')]['online_devices'] = intval($db->f('c'));
$online_devices = $online_devices + intval($db->f('c'));
}
$query="select count(distinct(concat(username,domain))) as c, domain from location group by domain";
dprint($query);
if (!$db->query($query)) {
$log=sprintf ("Database error for query %s: %s (%s)",$query,$db->Error,$db->Errno);
print $log;
syslog(LOG_NOTICE, $log);
return array();
}
if (!$db->num_rows()) return array();
while ($db->next_record()) {
$domains[$db->f('domain')]['online_accounts'] = intval($db->f('c'));
$online_accounts=$online_accounts+intval($db->f('c'));
}
$domains['total']['online_devices']=$online_devices;
$domains['total']['online_accounts']=$online_accounts;
return $domains;
}
function writeHarvestFile($body) {
if (!strlen($body)) return 0;
if (!$handle = fopen($this->harvest_file, 'w+')) {
$log=sprintf ("Error opening mrtg harvest file %s\n",$this->harvest_file);
print $log;
syslog(LOG_NOTICE, $log);
return false;
}
fwrite($handle,$body);
fclose($handle);
}
function harvestStatistics() {
global $DATASOURCES;
$datasources=array_keys($DATASOURCES);
$totals=array();
foreach ($datasources as $datasource) {
if (!$DATASOURCES[$datasource]['skipStatistics']) {
if ($DATASOURCES[$datasource]['mediaSessions']) {
// MediaProxy 2 via NGNPro
require_once("media_sessions.php");
$MediaSessions = new MediaSessionsNGNPro($DATASOURCES[$datasource]['mediaSessions']);
$MediaSessions->getSessions();
$totals=array_merge_recursive($totals,$MediaSessions->domain_statistics);
} else if ($DATASOURCES[$datasource]['mediaDispatcher']) {
// MediaProxy 2 via dispatcher tcp socket
require_once("media_sessions.php");
$MediaSessions = new MediaSessions($DATASOURCES[$datasource]['mediaDispatcher']);
$MediaSessions->getSessions();
$totals=array_merge_recursive($totals,$MediaSessions->domain_statistics);
} else if ($DATASOURCES[$datasource]['mediaServers']){
// MediaProxy 1 via relay tcp socket
$MediaSessions = new MediaSessions1($DATASOURCES[$datasource]['mediaServers'],$allowedDomains);
$MediaSessions->getSessions();
$totals=array_merge_recursive($totals,$MediaSessions->domain_statistics);
}
if ($DATASOURCES[$datasource]['networkStatus']) {
// OpenSIPS via NGNPro
$NetworkStatistics = new NetworkStatistics($DATASOURCES[$datasource]['networkStatus']);
$NetworkStatistics->getStatistics();
$totals=array_merge_recursive($totals,$NetworkStatistics->domain_statistics);
} else if ($DATASOURCES[$datasource]['db_registrar']) {
// OpenSIPS via MySQL query
$db_registrar_domains=$this->getOnlineAccountsFromMySQL($DATASOURCES[$datasource]['db_registrar']);
$totals=array_merge_recursive($totals,$db_registrar_domains);
}
}
}
$body="domains\t\t\tonline_accounts\tsessions\tcaller\tcallee\n\n";
foreach (array_keys($totals) as $_domain) {
if (!$totals[$_domain]['online_accounts'] && !$totals[$_domain]['sessions']) continue;
$body.=sprintf("%s\t\t%d\t\t%d\t\t%s\t%s\n",
$_domain,
$totals[$_domain]['online_accounts'],
$totals[$_domain]['sessions'],
intval($totals[$_domain]['caller']),
intval($totals[$_domain]['callee'])
);
}
$this->writeHarvestFile($body);
}
function buildStatistics() {
system($this->mrtg_config_script);
system($this->harvest_script);
system("env LANG=C mrtg $this->mrtgcfg_file");
}
}
+/**
+ * MRTGGraphs class sets up the dashboard and creates Entity objects.
+ */
+class MRTGGraphs {
+
+ /**
+ * MRTG entities
+ *
+ * @var array
+ */
+ public $entities = array();
+
+ /**
+ * Server hostname;
+ *
+ * @var type
+ */
+ public $hostname;
+
+ /**
+ * Domains;
+ *
+ * @var type
+ */
+ public $domains = array();
+
+ /**
+ * Graphs types;
+ *
+ * @var type
+ */
+ public $graph_types = array('users','sessions','traffic');
+
+ /**
+ * Construct.
+ */
+ public function __construct() {
+ //require("sip_statistics.php");
+
+ $title='Platform usage';
+
+ if (is_readable("/etc/cdrtool/local/header.phtml")) {
+ include("/etc/cdrtool/local/header.phtml");
+ } else {
+ include("header.phtml");
+ }
+
+ $layout = new pageLayoutLocal();
+ $layout->showTopMenu($title);
+
+ global $CDRTool;
+
+ $SIPstatistics = new SIPstatistics ();
+
+ if (strlen($CDRTool['filter']['domain'])) {
+ $allowedDomains=explode(' ',$CDRTool['filter']['domain']);
+ }
+
+ if (is_array($allowedDomains)) {
+ $domains=array_intersect($allowedDomains,array_keys($SIPstatistics->domains));
+ } else {
+ $domains=array_keys($SIPstatistics->domains);
+ }
+ $this->domains[]= $domains;
+
+ // read entities in directory for domain list
+
+ foreach ($domains as $entity){
+ $entity = sprintf("status/usage/%s",$entity);
+ foreach ($this->graph_types as $type) {
+ $final_entity = sprintf("%s_%s.log", $entity, $type);
+ $this->entities[] = new MRTGEntity(preg_replace("/.log$/", "", $final_entity));
+ }
+ }
+
+ // determine hostname
+ exec("hostname -f", $hostname);
+ $this->hostname = $hostname[0];
+
+ }
+}
+
+
+/**
+ * Entity class represents each MRTG entity found in the directory.
+ */
+class MRTGEntity {
+
+ /**
+ * Entity name.
+ *
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Entity title.
+ *
+ * @var string
+ */
+ public $title;
+
+ /**
+ * Entity page link.
+ *
+ * @var string
+ */
+ public $link;
+
+ /**
+ * Entity log file.
+ *
+ * @var string
+ */
+ public $log;
+
+ /**
+ * Construct.
+ *
+ * @throws Exception
+ */
+ public function __construct($name) {
+
+ // check entity exists
+ if (!is_file("{$name}.html")) throw new Exception("Could not find MRTG files for entity {$name}");
+
+ // add name
+ $this->name = $name;
+
+ // create nicer-looking title
+ $np = explode('_', $name);
+ $this->title = preg_replace("/status\/usage\//","",$np[0]);
+ array_shift($np);
+
+ if (in_array('users',$np) && strstr($this->title,'total')){
+ $this->title = ucfirst($this->title)." SIP Accounts online";
+ } else if (in_array('users',$np)) {
+ $this->title = "Online SIP accounts on $this->title";
+ } else if (in_array('traffic',$np) && strstr($this->title,'total')){
+ $this->title = ucfirst($this->title)." relayed RTP traffic";
+ } else if (in_array('traffic',$np)) {
+ $this->title = "Relayed RTP traffic for $this->title";
+ } else if (in_array('sessions',$np) && strstr($this->title,'total')){
+ $this->title = ucfirst($this->title)." active RTP media sessions";
+ } else if (in_array('sessions',$np)) {
+ $this->title = "Active RTP media sessions for $this->title";
+ } else {
+ $this->title .= " (".implode(" ",$np).")";
+ }
+
+ // add HTML and log files
+ $this->link = $name.'.html';
+ $this->log = $name.'.log';
+
+ }
+
+ /**
+ * Retrieve and process the entity's log file.
+ *
+ * @param boolean $max Set to true to retrieve maximum in/out rather than average.
+ *
+ * @return string JSON encoded data
+ */
+ public function retrieveLog($max=false) {
+ // arrays for parsed data
+ $in = $out = $stamps = array();
+ global $start_date,$stop_date;
+
+ foreach (file("{$this->name}.log") as $line) {
+
+ // ignore the summary line
+ if (!isset($header)) {
+ $header = $line;
+ continue;
+ }
+
+ $parts = explode(' ', rtrim($line));
+ //if ($parts[1] == 0 && $parts[2] == 0 && $parts[3] == 0 && $parts[4] == 0) continue;
+ if ($parts[0] < $start_date->getTimestamp()) continue;
+ if ($parts[0] > $stop_date->getTimestamp()) continue;
+
+ array_push($stamps, $parts[0]);
+ if (strstr($this->name,'traffic')){
+ if ($max) {
+ $in[] = array($parts[0],round(($parts[3]*8)/1000));
+ $out[] = array($parts[0]*1000,round(($parts[4]*8)/1000));
+ } else {
+ $in[] = array(date('Y-m-d H:i:s',$parts[0]), round(($parts[1]*8)));
+ $out[] = array(date('Y-m-d H:i:s',$parts[0]), round(($parts[2]*8)));
+ }
+ } else {
+ if ($max) {
+ $in[] = array(date('Y-m-d H:i:s',$parts[0]),round($parts[3]));
+ $out[] = array(date('Y-m-d H:i:s',$parts[0]),round($parts[4]));
+ } else {
+ $in[] = array(date('Y-m-d H:i:s',$parts[0]),round($parts[1]));
+ $out[] = array(date('Y-m-d H:i:s',$parts[0]),round($parts[2]));
+ }
+ }
+ }
+
+ // determine earliest and latest timestamps
+ $latest = array_shift($stamps);
+ $earliest = array_pop($stamps);
+
+ $interval = 300;
+
+ // encode and return response
+ return json_encode(array(
+ 'earliest' => $earliest,
+ 'latest' => $latest,
+ 'start' => $start_date,
+ 'stop' => $stop_date,
+ 'interval' => $interval,
+ 'intervalMin' => round($interval/60, 0),
+ 'inData' => array_reverse($in),
+ 'outData' => array_reverse($out)
+ ));
+ }
+}
?>
diff --git a/sip_usage.phtml b/sip_usage.phtml
index 7dd3275..d75690f 100644
--- a/sip_usage.phtml
+++ b/sip_usage.phtml
@@ -1,111 +1,271 @@
-<?
+<?php
require("/etc/cdrtool/global.inc");
page_open(
- array("sess" => "CDRTool_Session",
- "auth" => "CDRTool_Auth",
- "perm" => "CDRTool_Perm"));
+ array("sess" => "CDRTool_Session",
+ "auth" => "CDRTool_Auth",
+ "perm" => "CDRTool_Perm"
+ ));
$loginname=$auth->auth["uname"];
$perm->check("statistics");
+$start_date = isset($_REQUEST['start_date']) ? new DateTime($_REQUEST['start_date']) : new DateTime('-1 days');
+$stop_date = isset($_REQUEST['stop_date']) ? new DateTime($_REQUEST['stop_date']) : new DateTime('now');
+
+if ($start_date == $stop_date){
+ $start_date = $start_date->setTime(00, 00, 00);
+ $stop_date = $stop_date->setTime(00, 00, 00);
+ $stop_date->add(new DateInterval('P1D'));
+}
+
+global $CDRTool;
require("sip_statistics.php");
-$title = "Platform status/Usage";
-if (is_readable("/etc/cdrtool/local/header.phtml")) {
- include("/etc/cdrtool/local/header.phtml");
-} else {
- include("header.phtml");
+// trap AJAX requests
+if (isset($_POST['action'])) {
+ switch ($_POST['action']) {
+ case 'log':
+ $layout = new pageLayoutLocal();
+ try {
+ $entity = new MRTGEntity($_POST['entity']);
+ $json = $entity->retrieveLog();
+ } catch (Exception $e) {
+ $json = json_encode(array('error' => "entity not found"));
+ }
+ break;
+ }
+
+ // return data and terminate
+ print $json;
+ exit;
}
-$layout = new pageLayoutLocal();
-$layout->showTopMenu($title);
+// setup
+$mrtggraphs = new MRTGGraphs();
+?>
-$SIPstatistics=new SIPstatistics();
-//global $CDRTool;
+<style type="text/css">
-if (strlen($CDRTool['filter']['domain'])) {
- $allowedDomains=explode(' ',$CDRTool['filter']['domain']);
-}
+ DIV.entity H4 {
+ font-weight: bolder;
+ margin-left: 10px;
+ font-size: 12px;
+ }
- //$layout= $this->newPageLayout();
- //showTopMenu($title);
-?>
+ .graph {
+ height: 125px;
+ margin: 8px auto;
+ }
-<h2>Platform usage</h2>
+ .graphs {
+ margin-left: auto;
+ margin-right: auto;
+ }
-<TABLE BORDER=0 CELLPADDING=0 CELLSPACING=10>
+ .flotr-mouse-value {
+ background-color:rgb(249, 249, 249) !important;
+ color: #333333 !important;
+ opacity: 0.9 !important;
+ border: solid 1px #000000 !important;
+ z-index: 200;
+ }
-<? if ($perm->have_perm('admin')) { ?>
+ .grid {
+ padding: 10px 0;
+ }
-<TR>
-<TD><DIV align='center'><B> Total SIP accounts</B></DIV>
-<DIV><A HREF='status/usage/total_users.html' target='Usage'><IMG BORDER=0 ALT='total_users Traffic Graph' SRC='status/usage/total_users-day.png'></A>
-<SMALL><!--#flastmod file='total_users.html' --></SMALL></DIV>
-</TD><TD><DIV align='center'><B> Total RTP media sessions</B></DIV>
-<DIV><A HREF='status/usage/total_sessions.html' target='Usage'><IMG><IMG BORDER=0 ALT='total_sessions Traffic Graph' SRC='status/usage/total_sessions-day.png'></A>
-<SMALL><!--#flastmod file='total_sessions.html' --></SMALL></DIV>
-</TD><TD><DIV align='center'><B> Total relayed RTP traffic </B></DIV>
-<DIV><A HREF='status/usage/total_traffic.html' target='Usage'><IMG><IMG BORDER=0 ALT='total_traffic Traffic Graph' SRC='status/usage/total_traffic-day.png'></A>
-<SMALL><!--#flastmod file='total_traffic.html' --></SMALL></DIV>
-</TD></TR>
-<TR>
+ hr + .grid {
+ padding:0;
+ padding-bottom: 10px;
+ }
+ .grid:nth-child(even) {
+ background-color:#f9f9f9;
+ }
+ .flotr-axis-title-y1 {
+ -ms-transform: rotate(-90deg); /* IE 9 */
+ -webkit-transform: rotate(-90deg); /* Chrome, Safari, Opera */
+ transform: rotate(-90deg);
+ }
+ .graphs table {
+ margin: 0 auto; /* or margin: 0 auto 0 auto */
+ }
+</style>
-<?
-}
+<?php print "<script type=\"text/javascript\">var start_date_set=\"".$start_date->format('Y-m-d H:i:s')."\";var stop_date_set=\"".$stop_date->format('Y-m-d H:i:s')."\";</script>";?>
+<script type="text/javascript">
-if (is_array($allowedDomains)) {
- $domains=array_intersect($allowedDomains,array_keys($SIPstatistics->domains));
-} else {
- $domains=array_keys($SIPstatistics->domains);
-}
+ $(document).ready(function() {
+ $('#reportrange').detach().appendTo('.page-header h1');
+ // initial update
+ updateGraphs();
+ });
-foreach ($domains as $key) {
- if ($key == 'total') continue;
- printf ("
- <TR>
- <TD><DIV align='center'><B> SIP accounts on %s </B></DIV>
- <DIV><A HREF='status/usage/%s_users.html' target='Usage'><IMG><IMG BORDER=0 ALT='%s_users Traffic Graph' SRC='status/usage/%s_users-day.png'></A>
- <SMALL><!--#flastmod file='%s_users.html' --></SMALL></DIV>
- </TD><TD><DIV align='center'><B> RTP media sessions for %s </B></DIV>
- <DIV><A HREF='status/usage/%s_sessions.html' target='Usage'><IMG><IMG BORDER=0 ALT='%s_sessions Traffic Graph' SRC='status/usage/%s_sessions-day.png'></A>
- <SMALL><!--#flastmod file='%s_sessions.html' --></SMALL></DIV>
- </TD><TD><DIV align='center'><B> Relayed RTP traffic for %s </B></DIV>
- <DIV><A HREF='status/usage/%s_traffic.html' target='Usage'><IMG><IMG BORDER=0 ALT='%s_traffic Traffic Graph' SRC='status/usage/%s_traffic-day.png'></A>
- <SMALL><!--#flastmod file='%s_traffic.html' --></SMALL></DIV>
- </TD></TR>
- <TR>
- ",
- $key,
- $key,
- $key,
- $key,
- $key,
- $key,
- $key,
- $key,
- $key,
- $key,
- $key,
- $key,
- $key,
- $key,
- $key,
- $key,
- $key
- );
-}
+ function addGraph(entity, obj) {
+ $.ajax({
-?>
+ dataType: 'json',
+ type: 'POST',
+ data: { action: 'log', entity: entity },
-</TABLE>
-</BODY>
-</HTML>
-<?
+ success: function(log) {
-page_close();
-?>
+ better_data = [];
+ better_data1 = [];
+
+ var suffix = '';
+ var extra_options = {};
+ for (var j = 0; j < log.inData.length; j++) {
+ var t = log.inData[j][0].split(/[- :]/);
+ log.inData[j][0] = Date.UTC(t[0], t[1]-1, t[2], t[3], t[4], t[5]);
+ better_data1[j]=[log.inData[j][0],log.inData[j][1]];
+ }
+
+
+ var flotr_data = []
+ var ticks1 = function(y){
+ return y;
+ };
+ var title;
+ var trackY = false;
+ var suffix = ''
+
+ flotr_data = [
+ {idx: 0,data: better_data1},
+ ]
+
+ if (entity.indexOf("traffic") != -1) {
+ title = 'bits/s';
+ suffix = 'bits/s';
+ ticks1 = function(y){
+ return bytes(y, true);
+ };
+
+ better_data2 = [];
+ for (var j = 0; j < log.outData.length; j++) {
+ var t = log.outData[j][0].split(/[- :]/);
+ log.outData[j][0] = Date.UTC(t[0], t[1]-1, t[2], t[3], t[4], t[5]);
+ better_data2[j]=[log.outData[j][0],log.outData[j][1]];
+ }
+ flotr_data[0]['label'] = 'Caller';
+ flotr_data[0]['suffix'] = suffix;
+
+ flotr_data[1] = {idx: 1,label:'Called',data: better_data2,suffix: suffix, ticks: function(y){
+ return bytes(y, true);
+ }}
+
+ trackY = true;
+ }
+ else if (entity.indexOf("users") != -1) {
+ title = "Users";
+ suffix = ' users';
+
+ flotr_data[0]['label'] = 'Users';
+ flotr_data[0]['suffix'] = suffix;
+ flotr_data[0]['lines'] = { fill : true };
+ }
+ else if (entity.indexOf("sessions") != -1) {
+ title = 'sessions';
+ suffix = ' sessions';
+
+ flotr_data[0]['label'] = 'Sessions';
+ flotr_data[0]['suffix'] = suffix;
+ flotr_data[0]['lines'] = { fill : true }
+ }
+
+ extra_options = {
+ ytitle: title,
+ title: '',
+ suffix: suffix,
+ ticks1: ticks1,
+ trackY: trackY
+ };
+ basicTimeGraph(document.getElementById(entity.replace(/\/|[0-9]|\./g,'')), document.getElementById('LegendA'+entity.replace(/\/|[0-9]|\./g,'')),flotr_data,extra_options);
+ },
+ error: function() {
+ $('#error').html('<div class="alert alert-error">Unable to load '+$(this).attr('entity')+'.log from the server.</div>');
+ }
+ });
+ }
+
+
+ /**
+ * Refresh all entities.
+ *
+ * @param specificGraph - pass one graph just to process that one
+ *
+ * @return void
+ */
+ function updateGraphs(specificGraph) {
+
+ // if no specific graph passed then update all
+ if (specificGraph) {
+ var graphs = specificGraph;
+ } else {
+ var graphs = $("DIV.graph");
+ }
+
+ // update graphs
+ graphs.each(function() {
+ var graph = $(this);
+ entity = $(this).attr('entity');
+ addGraph(entity, $(this));
+ });
+ }
+</script>
+
+ <div id='error'></div>
+ <div id="reportrange" class="btn pull-right">
+ <i class="icon-calendar"></i>
+ <span><?php echo $start_date->format('F j, Y H:i:s')." - ".$stop_date->format('F j, Y H:i:s')?></span>
+ <b class=\"caret\"></b>
+ </div>
+ <div style='clear:both'></div>
+ <br />
+ <div class='row-fluid'>
+
+ <?php rsort($mrtggraphs->entities) ; foreach ($mrtggraphs->entities as $entity) :
+ if (strstr($entity->name,'total')) { ?>
+ <div class="entity span4">
+ <h4><?php print $entity->title; ?><small> (<a href="<?php print $entity->link; ?>">MRTG</a>)</small></h4>
+ <div class="graphs">
+ <div class='graph gg' entity='<?php print $entity->name; ?>' id='<?php print preg_replace("/\/|[0-9]|\./","",$entity->name); ?>'></div>
+ <div id='LegendA<?php print preg_replace("/\/|[0-9]|\./","",$entity->name); ?>'></div>
+ </div>
+
+ </div>
+
+ <?php } endforeach; ?>
+
+ </div><hr />
+
+ <div class='row-fluid grid'>
+
+ <?php $counter = 0; rsort($mrtggraphs->entities) ; foreach ($mrtggraphs->entities as $entity) :
+ if (!strstr($entity->name,'total')) {
+
+ if ($counter == 3){
+ $counter = 0;
+ print "</div><div class='row-fluid grid'>";
+ }
+ $counter++; ?>
+ <div class="entity span4">
+ <div id="chart"></div>
+ <h4><?php print $entity->title; ?><small> (<a href="<?php print $entity->link; ?>">MRTG</a>)</small></h4>
+ <div class="graphs">
+ <div class='graph' entity='<?php print $entity->name; ?>' id='<?php print preg_replace("/\/|[0-9]|\./","",$entity->name); ?>'></div>
+ <div id='LegendA<?php print preg_replace("/\/|[0-9]|\./","",$entity->name); ?>'></div>
+ </div>
+
+ </div>
+
+ <?php } endforeach; ?>
+
+ </div>
+</body>
+</html>
+<?php page_close(); ?>

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 4:50 AM (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3408804
Default Alt Text
(494 KB)

Event Timeline