· 6 years ago · Apr 16, 2019, 05:58 PM
1<?xml version="1.0" encoding="utf-8"?>
2<html xmlns="http://www.w3.org/1999/xhtml">
3 <head>
4 <meta http-equiv="content-type" content="application/xhtml+xml; charset=utf-8" />
5 <title>Repository statistics for 'image-shutter'</title>
6 <script type="application/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
7 <script type="application/javascript">
8(function($){$.extend({tablesorter:new
9function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",cssChildRow:"expand-child",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,sortLocaleCompare:true,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'/\.|\,/g',onRenderHeader:null,selectorHeaders:'thead th',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}if(table.tBodies.length==0)return;var rows=table.tBodies[0].rows;if(rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i<l;i++){var p=false;if($.metadata&&($($headers[i]).metadata()&&$($headers[i]).metadata().sorter)){p=getParserById($($headers[i]).metadata().sorter);}else if((table.config.headers[i]&&table.config.headers[i].sorter)){p=getParserById(table.config.headers[i].sorter);}if(!p){p=detectParserForColumn(table,rows,-1,i);}if(table.config.debug){parsersDebug+="column:"+i+" parser:"+p.id+"\n";}list.push(p);}}if(table.config.debug){log(parsersDebug);}return list;};function detectParserForColumn(table,rows,rowIndex,cellIndex){var l=parsers.length,node=false,nodeValue=false,keepLooking=true;while(nodeValue==''&&keepLooking){rowIndex++;if(rows[rowIndex]){node=getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex);nodeValue=trimAndGetNodeText(table.config,node);if(table.config.debug){log('Checking if value was empty on row:'+rowIndex);}}else{keepLooking=false;}}for(var i=1;i<l;i++){if(parsers[i].is(nodeValue,table,node)){return parsers[i];}}return parsers[0];}function getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex){return rows[rowIndex].cells[cellIndex];}function trimAndGetNodeText(config,node){return $.trim(getElementText(config,node));}function getParserById(name){var l=parsers.length;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==name.toLowerCase()){return parsers[i];}}return false;}function buildCache(table){if(table.config.debug){var cacheTime=new Date();}var totalRows=(table.tBodies[0]&&table.tBodies[0].rows.length)||0,totalCells=(table.tBodies[0].rows[0]&&table.tBodies[0].rows[0].cells.length)||0,parsers=table.config.parsers,cache={row:[],normalized:[]};for(var i=0;i<totalRows;++i){var c=$(table.tBodies[0].rows[i]),cols=[];if(c.hasClass(table.config.cssChildRow)){cache.row[cache.row.length-1]=cache.row[cache.row.length-1].add(c);continue;}cache.row.push(c);for(var j=0;j<totalCells;++j){cols.push(parsers[j].format(getElementText(table.config,c[0].cells[j]),table,c[0].cells[j]));}cols.push(cache.normalized.length);cache.normalized.push(cols);cols=null;};if(table.config.debug){benchmark("Building cache for "+totalRows+" rows:",cacheTime);}return cache;};function getElementText(config,node){var text="";if(!node)return"";if(!config.supportsTextContent)config.supportsTextContent=node.textContent||false;if(config.textExtraction=="simple"){if(config.supportsTextContent){text=node.textContent;}else{if(node.childNodes[0]&&node.childNodes[0].hasChildNodes()){text=node.childNodes[0].innerHTML;}else{text=node.innerHTML;}}}else{if(typeof(config.textExtraction)=="function"){text=config.textExtraction(node);}else{text=$(node).text();}}return text;}function appendToTable(table,cache){if(table.config.debug){var appendTime=new Date()}var c=cache,r=c.row,n=c.normalized,totalRows=n.length,checkCell=(n[0].length-1),tableBody=$(table.tBodies[0]),rows=[];for(var i=0;i<totalRows;i++){var pos=n[i][checkCell];rows.push(r[pos]);if(!table.config.appender){var l=r[pos].length;for(var j=0;j<l;j++){tableBody[0].appendChild(r[pos][j]);}}}if(table.config.appender){table.config.appender(table,rows);}rows=null;if(table.config.debug){benchmark("Rebuilt table:",appendTime);}applyWidget(table);setTimeout(function(){$(table).trigger("sortEnd");},0);};function buildHeaders(table){if(table.config.debug){var time=new Date();}var meta=($.metadata)?true:false;var header_index=computeTableHeaderCellIndexes(table);$tableHeaders=$(table.config.selectorHeaders,table).each(function(index){this.column=header_index[this.parentNode.rowIndex+"-"+this.cellIndex];this.order=formatSortingOrder(table.config.sortInitialOrder);this.count=this.order;if(checkHeaderMetadata(this)||checkHeaderOptions(table,index))this.sortDisabled=true;if(checkHeaderOptionsSortingLocked(table,index))this.order=this.lockedOrder=checkHeaderOptionsSortingLocked(table,index);if(!this.sortDisabled){var $th=$(this).addClass(table.config.cssHeader);if(table.config.onRenderHeader)table.config.onRenderHeader.apply($th);}table.config.headerList[index]=this;});if(table.config.debug){benchmark("Built headers:",time);log($tableHeaders);}return $tableHeaders;};function computeTableHeaderCellIndexes(t){var matrix=[];var lookup={};var thead=t.getElementsByTagName('THEAD')[0];var trs=thead.getElementsByTagName('TR');for(var i=0;i<trs.length;i++){var cells=trs[i].cells;for(var j=0;j<cells.length;j++){var c=cells[j];var rowIndex=c.parentNode.rowIndex;var cellId=rowIndex+"-"+c.cellIndex;var rowSpan=c.rowSpan||1;var colSpan=c.colSpan||1
10var firstAvailCol;if(typeof(matrix[rowIndex])=="undefined"){matrix[rowIndex]=[];}for(var k=0;k<matrix[rowIndex].length+1;k++){if(typeof(matrix[rowIndex][k])=="undefined"){firstAvailCol=k;break;}}lookup[cellId]=firstAvailCol;for(var k=rowIndex;k<rowIndex+rowSpan;k++){if(typeof(matrix[k])=="undefined"){matrix[k]=[];}var matrixrow=matrix[k];for(var l=firstAvailCol;l<firstAvailCol+colSpan;l++){matrixrow[l]="x";}}}}return lookup;}function checkCellColSpan(table,rows,row){var arr=[],r=table.tHead.rows,c=r[row].cells;for(var i=0;i<c.length;i++){var cell=c[i];if(cell.colSpan>1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function checkHeaderOptionsSortingLocked(table,i){if((table.config.headers[i])&&(table.config.headers[i].lockedOrder))return table.config.headers[i].lockedOrder;return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i<l;i++){getWidgetById(c[i]).format(table);}}function getWidgetById(name){var l=widgets.length;for(var i=0;i<l;i++){if(widgets[i].id.toLowerCase()==name.toLowerCase()){return widgets[i];}}};function formatSortingOrder(v){if(typeof(v)!="Number"){return(v.toLowerCase()=="desc")?1:0;}else{return(v==1)?1:0;}}function isValueInArray(v,a){var l=a.length;for(var i=0;i<l;i++){if(a[i][0]==v){return true;}}return false;}function setHeadersCss(table,$headers,list,css){$headers.removeClass(css[0]).removeClass(css[1]);var h=[];$headers.each(function(offset){if(!this.sortDisabled){h[this.column]=$(this);}});var l=list.length;for(var i=0;i<l;i++){h[list[i][0]].addClass(css[list[i][1]]);}}function fixColumnWidth(table,$headers){var c=table.config;if(c.widthFixed){var colgroup=$('<colgroup>');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('<col>').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;i<l;i++){var s=sortList[i],o=c.headerList[s[0]];o.count=s[1];o.count++;}}function multisort(table,sortList,cache){if(table.config.debug){var sortTime=new Date();}var dynamicExp="var sortWrapper = function(a,b) {",l=sortList.length;for(var i=0;i<l;i++){var c=sortList[i][0];var order=sortList[i][1];var s=(table.config.parsers[c].type=="text")?((order==0)?makeSortFunction("text","asc",c):makeSortFunction("text","desc",c)):((order==0)?makeSortFunction("numeric","asc",c):makeSortFunction("numeric","desc",c));var e="e"+i;dynamicExp+="var "+e+" = "+s;dynamicExp+="if("+e+") { return "+e+"; } ";dynamicExp+="else { ";}var orgOrderCol=cache.normalized[0].length-1;dynamicExp+="return a["+orgOrderCol+"]-b["+orgOrderCol+"];";for(var i=0;i<l;i++){dynamicExp+="}; ";}dynamicExp+="return 0; ";dynamicExp+="}; ";if(table.config.debug){benchmark("Evaling expression:"+dynamicExp,new Date());}eval(dynamicExp);cache.normalized.sort(sortWrapper);if(table.config.debug){benchmark("Sorting on "+sortList.toString()+" and dir "+order+" time:",sortTime);}return cache;};function makeSortFunction(type,direction,index){var a="a["+index+"]",b="b["+index+"]";if(type=='text'&&direction=='asc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+a+" < "+b+") ? -1 : 1 )));";}else if(type=='text'&&direction=='desc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+b+" < "+a+") ? -1 : 1 )));";}else if(type=='numeric'&&direction=='asc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+a+" - "+b+"));";}else if(type=='numeric'&&direction=='desc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+b+" - "+a+"));";}};function makeSortText(i){return"((a["+i+"] < b["+i+"]) ? -1 : ((a["+i+"] > b["+i+"]) ? 1 : 0));";};function makeSortTextDesc(i){return"((b["+i+"] < a["+i+"]) ? -1 : ((b["+i+"] > a["+i+"]) ? 1 : 0));";};function makeSortNumeric(i){return"a["+i+"]-b["+i+"];";};function makeSortNumericDesc(i){return"b["+i+"]-a["+i+"];";};function sortText(a,b){if(table.config.sortLocaleCompare)return a.localeCompare(b);return((a<b)?-1:((a>b)?1:0));};function sortTextDesc(a,b){if(table.config.sortLocaleCompare)return b.localeCompare(a);return((b<a)?-1:((b>a)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$.data(this,"tablesorter",config);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){$this.trigger("sortStart");var $cell=$(this);var i=this.column;this.order=this.count++%2;if(this.lockedOrder)this.order=this.lockedOrder;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j<a.length;j++){if(a[j][0]!=i){config.sortList.push(a[j]);}}}config.sortList.push([i,this.order]);}else{if(isValueInArray(i,config.sortList)){for(var j=0;j<config.sortList.length;j++){var s=config.sortList[j],o=config.headerList[s[0]];if(s[0]==i){o.count=s[1];o.count++;s[1]=o.count%2;}}}else{config.sortList.push([i,this.order]);}};setTimeout(function(){setHeadersCss($this[0],$headers,config.sortList,sortCSS);appendToTable($this[0],multisort($this[0],config.sortList,cache));},1);return false;}}).mousedown(function(){if(config.cancelSelection){this.onselectstart=function(){return false};return false;}});$this.bind("update",function(){var me=this;setTimeout(function(){me.config.parsers=buildParserCache(me,$headers);cache=buildCache(me);},1);}).bind("updateCell",function(e,cell){var config=this.config;var pos=[(cell.parentNode.rowIndex-1),cell.cellIndex];cache.normalized[pos[0]][pos[1]]=config.parsers[pos[1]].format(getElementText(config,cell),cell);}).bind("sorton",function(e,list){$(this).trigger("sortStart");config.sortList=list;var sortList=config.sortList;updateHeaderSortCount(this,sortList);setHeadersCss(this,$headers,sortList,sortCSS);appendToTable(this,multisort(this,sortList,cache));}).bind("appendCache",function(){appendToTable(this,cache);}).bind("applyWidgetId",function(e,id){getWidgetById(id).format(this);}).bind("applyWidgets",function(){applyWidget(this);});if($.metadata&&($(this).metadata()&&$(this).metadata().sortlist)){config.sortList=$(this).metadata().sortlist;}if(config.sortList.length>0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==parser.id.toLowerCase()){a=false;}}if(a){parsers.push(parser);};};this.addWidget=function(widget){widgets.push(widget);};this.formatFloat=function(s){var i=parseFloat(s);return(isNaN(i))?0:i;};this.formatInt=function(s){var i=parseInt(s);return(isNaN(i))?0:i;};this.isDigit=function(s,config){return/^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g,'')));};this.clearTableBody=function(table){if($.browser.msie){function empty(){while(this.firstChild)this.removeChild(this.firstChild);}empty.apply(table.tBodies[0]);}else{table.tBodies[0].innerHTML="";}};}});$.fn.extend({tablesorter:$.tablesorter.construct});var ts=$.tablesorter;ts.addParser({id:"text",is:function(s){return true;},format:function(s){return $.trim(s.toLocaleLowerCase());},type:"text"});ts.addParser({id:"digit",is:function(s,table){var c=table.config;return $.tablesorter.isDigit(s,c);},format:function(s){return $.tablesorter.formatFloat(s);},type:"numeric"});ts.addParser({id:"currency",is:function(s){return/^[$?.]/.test(s);},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/[$]/g),""));},type:"numeric"});ts.addParser({id:"ipAddress",is:function(s){return/^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s);},format:function(s){var a=s.split("."),r="",l=a.length;for(var i=0;i<l;i++){var item=a[i];if(item.length==2){r+="0"+item;}else{r+=item;}}return $.tablesorter.formatFloat(r);},type:"numeric"});ts.addParser({id:"url",is:function(s){return/^(https?|ftp|file):\/\/$/.test(s);},format:function(s){return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//),''));},type:"text"});ts.addParser({id:"isoDate",is:function(s){return/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s);},format:function(s){return $.tablesorter.formatFloat((s!="")?new Date(s.replace(new RegExp(/-/g),"/")).getTime():"0");},type:"numeric"});ts.addParser({id:"percent",is:function(s){return/\%$/.test($.trim(s));},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g),""));},type:"numeric"});ts.addParser({id:"usLongDate",is:function(s){return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/));},format:function(s){return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"shortDate",is:function(s){return/\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s);},format:function(s,table){var c=table.config;s=s.replace(/\-/g,"/");if(c.dateFormat=="us"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$1/$2");}else if(c.dateFormat=="uk"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$2/$1");}else if(c.dateFormat=="dd/mm/yy"||c.dateFormat=="dd-mm-yy"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/,"$1/$2/$3");}return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"time",is:function(s){return/^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s);},format:function(s){return $.tablesorter.formatFloat(new Date("2000/01/01 "+s).getTime());},type:"numeric"});ts.addParser({id:"metadata",is:function(s){return false;},format:function(s,table,cell){var c=table.config,p=(!c.parserMetadataName)?'sortValue':c.parserMetadataName;return $(cell).metadata()[p];},type:"numeric"});ts.addWidget({id:"zebra",format:function(table){if(table.config.debug){var time=new Date();}var $tr,row=-1,odd;$("tr:visible",table.tBodies[0]).each(function(i){$tr=$(this);if(!$tr.hasClass(table.config.cssChildRow))row++;odd=(row%2==0);$tr.removeClass(table.config.widgetZebra.css[odd?0:1]).addClass(table.config.widgetZebra.css[odd?1:0])});if(table.config.debug){$.tablesorter.benchmark("Applying Zebra widget",time);}}});})(jQuery);
11</script>
12 <script type="application/javascript">/* Javascript plotting library for jQuery, version 0.8.3.
13
14Copyright (c) 2007-2014 IOLA and Ole Laursen.
15Licensed under the MIT license.
16
17*/
18
19// first an inline dependency, jquery.colorhelpers.js, we inline it here
20// for convenience
21
22/* Plugin for jQuery for working with colors.
23 *
24 * Version 1.1.
25 *
26 * Inspiration from jQuery color animation plugin by John Resig.
27 *
28 * Released under the MIT license by Ole Laursen, October 2009.
29 *
30 * Examples:
31 *
32 * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
33 * var c = $.color.extract($("#mydiv"), 'background-color');
34 * console.log(c.r, c.g, c.b, c.a);
35 * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
36 *
37 * Note that .scale() and .add() return the same modified object
38 * instead of making a new one.
39 *
40 * V. 1.1: Fix error handling so e.g. parsing an empty string does
41 * produce a color rather than just crashing.
42 */
43(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i<c.length;++i)o[c.charAt(i)]+=d;return o.normalize()};o.scale=function(c,f){for(var i=0;i<c.length;++i)o[c.charAt(i)]*=f;return o.normalize()};o.toString=function(){if(o.a>=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return value<min?min:value>max?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/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(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/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(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={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]}})(jQuery);
44
45// the actual Flot code
46(function($) {
47
48 // Cache the prototype hasOwnProperty for faster access
49
50 var hasOwnProperty = Object.prototype.hasOwnProperty;
51
52 // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM
53 // operation produces the same effect as detach, i.e. removing the element
54 // without touching its jQuery data.
55
56 // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+.
57
58 if (!$.fn.detach) {
59 $.fn.detach = function() {
60 return this.each(function() {
61 if (this.parentNode) {
62 this.parentNode.removeChild( this );
63 }
64 });
65 };
66 }
67
68 ///////////////////////////////////////////////////////////////////////////
69 // The Canvas object is a wrapper around an HTML5 <canvas> tag.
70 //
71 // @constructor
72 // @param {string} cls List of classes to apply to the canvas.
73 // @param {element} container Element onto which to append the canvas.
74 //
75 // Requiring a container is a little iffy, but unfortunately canvas
76 // operations don't work unless the canvas is attached to the DOM.
77
78 function Canvas(cls, container) {
79
80 var element = container.children("." + cls)[0];
81
82 if (element == null) {
83
84 element = document.createElement("canvas");
85 element.className = cls;
86
87 $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 })
88 .appendTo(container);
89
90 // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas
91
92 if (!element.getContext) {
93 if (window.G_vmlCanvasManager) {
94 element = window.G_vmlCanvasManager.initElement(element);
95 } else {
96 throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode.");
97 }
98 }
99 }
100
101 this.element = element;
102
103 var context = this.context = element.getContext("2d");
104
105 // Determine the screen's ratio of physical to device-independent
106 // pixels. This is the ratio between the canvas width that the browser
107 // advertises and the number of pixels actually present in that space.
108
109 // The iPhone 4, for example, has a device-independent width of 320px,
110 // but its screen is actually 640px wide. It therefore has a pixel
111 // ratio of 2, while most normal devices have a ratio of 1.
112
113 var devicePixelRatio = window.devicePixelRatio || 1,
114 backingStoreRatio =
115 context.webkitBackingStorePixelRatio ||
116 context.mozBackingStorePixelRatio ||
117 context.msBackingStorePixelRatio ||
118 context.oBackingStorePixelRatio ||
119 context.backingStorePixelRatio || 1;
120
121 this.pixelRatio = devicePixelRatio / backingStoreRatio;
122
123 // Size the canvas to match the internal dimensions of its container
124
125 this.resize(container.width(), container.height());
126
127 // Collection of HTML div layers for text overlaid onto the canvas
128
129 this.textContainer = null;
130 this.text = {};
131
132 // Cache of text fragments and metrics, so we can avoid expensively
133 // re-calculating them when the plot is re-rendered in a loop.
134
135 this._textCache = {};
136 }
137
138 // Resizes the canvas to the given dimensions.
139 //
140 // @param {number} width New width of the canvas, in pixels.
141 // @param {number} width New height of the canvas, in pixels.
142
143 Canvas.prototype.resize = function(width, height) {
144
145 if (width <= 0 || height <= 0) {
146 throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height);
147 }
148
149 var element = this.element,
150 context = this.context,
151 pixelRatio = this.pixelRatio;
152
153 // Resize the canvas, increasing its density based on the display's
154 // pixel ratio; basically giving it more pixels without increasing the
155 // size of its element, to take advantage of the fact that retina
156 // displays have that many more pixels in the same advertised space.
157
158 // Resizing should reset the state (excanvas seems to be buggy though)
159
160 if (this.width != width) {
161 element.width = width * pixelRatio;
162 element.style.width = width + "px";
163 this.width = width;
164 }
165
166 if (this.height != height) {
167 element.height = height * pixelRatio;
168 element.style.height = height + "px";
169 this.height = height;
170 }
171
172 // Save the context, so we can reset in case we get replotted. The
173 // restore ensure that we're really back at the initial state, and
174 // should be safe even if we haven't saved the initial state yet.
175
176 context.restore();
177 context.save();
178
179 // Scale the coordinate space to match the display density; so even though we
180 // may have twice as many pixels, we still want lines and other drawing to
181 // appear at the same size; the extra pixels will just make them crisper.
182
183 context.scale(pixelRatio, pixelRatio);
184 };
185
186 // Clears the entire canvas area, not including any overlaid HTML text
187
188 Canvas.prototype.clear = function() {
189 this.context.clearRect(0, 0, this.width, this.height);
190 };
191
192 // Finishes rendering the canvas, including managing the text overlay.
193
194 Canvas.prototype.render = function() {
195
196 var cache = this._textCache;
197
198 // For each text layer, add elements marked as active that haven't
199 // already been rendered, and remove those that are no longer active.
200
201 for (var layerKey in cache) {
202 if (hasOwnProperty.call(cache, layerKey)) {
203
204 var layer = this.getTextLayer(layerKey),
205 layerCache = cache[layerKey];
206
207 layer.hide();
208
209 for (var styleKey in layerCache) {
210 if (hasOwnProperty.call(layerCache, styleKey)) {
211 var styleCache = layerCache[styleKey];
212 for (var key in styleCache) {
213 if (hasOwnProperty.call(styleCache, key)) {
214
215 var positions = styleCache[key].positions;
216
217 for (var i = 0, position; position = positions[i]; i++) {
218 if (position.active) {
219 if (!position.rendered) {
220 layer.append(position.element);
221 position.rendered = true;
222 }
223 } else {
224 positions.splice(i--, 1);
225 if (position.rendered) {
226 position.element.detach();
227 }
228 }
229 }
230
231 if (positions.length == 0) {
232 delete styleCache[key];
233 }
234 }
235 }
236 }
237 }
238
239 layer.show();
240 }
241 }
242 };
243
244 // Creates (if necessary) and returns the text overlay container.
245 //
246 // @param {string} classes String of space-separated CSS classes used to
247 // uniquely identify the text layer.
248 // @return {object} The jQuery-wrapped text-layer div.
249
250 Canvas.prototype.getTextLayer = function(classes) {
251
252 var layer = this.text[classes];
253
254 // Create the text layer if it doesn't exist
255
256 if (layer == null) {
257
258 // Create the text layer container, if it doesn't exist
259
260 if (this.textContainer == null) {
261 this.textContainer = $("<div class='flot-text'></div>")
262 .css({
263 position: "absolute",
264 top: 0,
265 left: 0,
266 bottom: 0,
267 right: 0,
268 'font-size': "smaller",
269 color: "#545454"
270 })
271 .insertAfter(this.element);
272 }
273
274 layer = this.text[classes] = $("<div></div>")
275 .addClass(classes)
276 .css({
277 position: "absolute",
278 top: 0,
279 left: 0,
280 bottom: 0,
281 right: 0
282 })
283 .appendTo(this.textContainer);
284 }
285
286 return layer;
287 };
288
289 // Creates (if necessary) and returns a text info object.
290 //
291 // The object looks like this:
292 //
293 // {
294 // width: Width of the text's wrapper div.
295 // height: Height of the text's wrapper div.
296 // element: The jQuery-wrapped HTML div containing the text.
297 // positions: Array of positions at which this text is drawn.
298 // }
299 //
300 // The positions array contains objects that look like this:
301 //
302 // {
303 // active: Flag indicating whether the text should be visible.
304 // rendered: Flag indicating whether the text is currently visible.
305 // element: The jQuery-wrapped HTML div containing the text.
306 // x: X coordinate at which to draw the text.
307 // y: Y coordinate at which to draw the text.
308 // }
309 //
310 // Each position after the first receives a clone of the original element.
311 //
312 // The idea is that that the width, height, and general 'identity' of the
313 // text is constant no matter where it is placed; the placements are a
314 // secondary property.
315 //
316 // Canvas maintains a cache of recently-used text info objects; getTextInfo
317 // either returns the cached element or creates a new entry.
318 //
319 // @param {string} layer A string of space-separated CSS classes uniquely
320 // identifying the layer containing this text.
321 // @param {string} text Text string to retrieve info for.
322 // @param {(string|object)=} font Either a string of space-separated CSS
323 // classes or a font-spec object, defining the text's font and style.
324 // @param {number=} angle Angle at which to rotate the text, in degrees.
325 // Angle is currently unused, it will be implemented in the future.
326 // @param {number=} width Maximum width of the text before it wraps.
327 // @return {object} a text info object.
328
329 Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) {
330
331 var textStyle, layerCache, styleCache, info;
332
333 // Cast the value to a string, in case we were given a number or such
334
335 text = "" + text;
336
337 // If the font is a font-spec object, generate a CSS font definition
338
339 if (typeof font === "object") {
340 textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family;
341 } else {
342 textStyle = font;
343 }
344
345 // Retrieve (or create) the cache for the text's layer and styles
346
347 layerCache = this._textCache[layer];
348
349 if (layerCache == null) {
350 layerCache = this._textCache[layer] = {};
351 }
352
353 styleCache = layerCache[textStyle];
354
355 if (styleCache == null) {
356 styleCache = layerCache[textStyle] = {};
357 }
358
359 info = styleCache[text];
360
361 // If we can't find a matching element in our cache, create a new one
362
363 if (info == null) {
364
365 var element = $("<div></div>").html(text)
366 .css({
367 position: "absolute",
368 'max-width': width,
369 top: -9999
370 })
371 .appendTo(this.getTextLayer(layer));
372
373 if (typeof font === "object") {
374 element.css({
375 font: textStyle,
376 color: font.color
377 });
378 } else if (typeof font === "string") {
379 element.addClass(font);
380 }
381
382 info = styleCache[text] = {
383 width: element.outerWidth(true),
384 height: element.outerHeight(true),
385 element: element,
386 positions: []
387 };
388
389 element.detach();
390 }
391
392 return info;
393 };
394
395 // Adds a text string to the canvas text overlay.
396 //
397 // The text isn't drawn immediately; it is marked as rendering, which will
398 // result in its addition to the canvas on the next render pass.
399 //
400 // @param {string} layer A string of space-separated CSS classes uniquely
401 // identifying the layer containing this text.
402 // @param {number} x X coordinate at which to draw the text.
403 // @param {number} y Y coordinate at which to draw the text.
404 // @param {string} text Text string to draw.
405 // @param {(string|object)=} font Either a string of space-separated CSS
406 // classes or a font-spec object, defining the text's font and style.
407 // @param {number=} angle Angle at which to rotate the text, in degrees.
408 // Angle is currently unused, it will be implemented in the future.
409 // @param {number=} width Maximum width of the text before it wraps.
410 // @param {string=} halign Horizontal alignment of the text; either "left",
411 // "center" or "right".
412 // @param {string=} valign Vertical alignment of the text; either "top",
413 // "middle" or "bottom".
414
415 Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) {
416
417 var info = this.getTextInfo(layer, text, font, angle, width),
418 positions = info.positions;
419
420 // Tweak the div's position to match the text's alignment
421
422 if (halign == "center") {
423 x -= info.width / 2;
424 } else if (halign == "right") {
425 x -= info.width;
426 }
427
428 if (valign == "middle") {
429 y -= info.height / 2;
430 } else if (valign == "bottom") {
431 y -= info.height;
432 }
433
434 // Determine whether this text already exists at this position.
435 // If so, mark it for inclusion in the next render pass.
436
437 for (var i = 0, position; position = positions[i]; i++) {
438 if (position.x == x && position.y == y) {
439 position.active = true;
440 return;
441 }
442 }
443
444 // If the text doesn't exist at this position, create a new entry
445
446 // For the very first position we'll re-use the original element,
447 // while for subsequent ones we'll clone it.
448
449 position = {
450 active: true,
451 rendered: false,
452 element: positions.length ? info.element.clone() : info.element,
453 x: x,
454 y: y
455 };
456
457 positions.push(position);
458
459 // Move the element to its final position within the container
460
461 position.element.css({
462 top: Math.round(y),
463 left: Math.round(x),
464 'text-align': halign // In case the text wraps
465 });
466 };
467
468 // Removes one or more text strings from the canvas text overlay.
469 //
470 // If no parameters are given, all text within the layer is removed.
471 //
472 // Note that the text is not immediately removed; it is simply marked as
473 // inactive, which will result in its removal on the next render pass.
474 // This avoids the performance penalty for 'clear and redraw' behavior,
475 // where we potentially get rid of all text on a layer, but will likely
476 // add back most or all of it later, as when redrawing axes, for example.
477 //
478 // @param {string} layer A string of space-separated CSS classes uniquely
479 // identifying the layer containing this text.
480 // @param {number=} x X coordinate of the text.
481 // @param {number=} y Y coordinate of the text.
482 // @param {string=} text Text string to remove.
483 // @param {(string|object)=} font Either a string of space-separated CSS
484 // classes or a font-spec object, defining the text's font and style.
485 // @param {number=} angle Angle at which the text is rotated, in degrees.
486 // Angle is currently unused, it will be implemented in the future.
487
488 Canvas.prototype.removeText = function(layer, x, y, text, font, angle) {
489 if (text == null) {
490 var layerCache = this._textCache[layer];
491 if (layerCache != null) {
492 for (var styleKey in layerCache) {
493 if (hasOwnProperty.call(layerCache, styleKey)) {
494 var styleCache = layerCache[styleKey];
495 for (var key in styleCache) {
496 if (hasOwnProperty.call(styleCache, key)) {
497 var positions = styleCache[key].positions;
498 for (var i = 0, position; position = positions[i]; i++) {
499 position.active = false;
500 }
501 }
502 }
503 }
504 }
505 }
506 } else {
507 var positions = this.getTextInfo(layer, text, font, angle).positions;
508 for (var i = 0, position; position = positions[i]; i++) {
509 if (position.x == x && position.y == y) {
510 position.active = false;
511 }
512 }
513 }
514 };
515
516 ///////////////////////////////////////////////////////////////////////////
517 // The top-level container for the entire plot.
518
519 function Plot(placeholder, data_, options_, plugins) {
520 // data is on the form:
521 // [ series1, series2 ... ]
522 // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
523 // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
524
525 var series = [],
526 options = {
527 // the color theme used for graphs
528 colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
529 legend: {
530 show: true,
531 noColumns: 1, // number of colums in legend table
532 labelFormatter: null, // fn: string -> string
533 labelBoxBorderColor: "#ccc", // border color for the little label boxes
534 container: null, // container (as jQuery object) to put legend in, null means default on top of graph
535 position: "ne", // position of default legend container within plot
536 margin: 5, // distance from grid edge to default legend container within plot
537 backgroundColor: null, // null means auto-detect
538 backgroundOpacity: 0.85, // set to 0 to avoid background
539 sorted: null // default to no legend sorting
540 },
541 xaxis: {
542 show: null, // null = auto-detect, true = always, false = never
543 position: "bottom", // or "top"
544 mode: null, // null or "time"
545 font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" }
546 color: null, // base color, labels, ticks
547 tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
548 transform: null, // null or f: number -> number to transform axis
549 inverseTransform: null, // if transform is set, this should be the inverse function
550 min: null, // min. value to show, null means set automatically
551 max: null, // max. value to show, null means set automatically
552 autoscaleMargin: null, // margin in % to add if auto-setting min/max
553 ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
554 tickFormatter: null, // fn: number -> string
555 labelWidth: null, // size of tick labels in pixels
556 labelHeight: null,
557 reserveSpace: null, // whether to reserve space even if axis isn't shown
558 tickLength: null, // size in pixels of ticks, or "full" for whole line
559 alignTicksWithAxis: null, // axis number or null for no sync
560 tickDecimals: null, // no. of decimals, null means auto
561 tickSize: null, // number or [number, "unit"]
562 minTickSize: null // number or [number, "unit"]
563 },
564 yaxis: {
565 autoscaleMargin: 0.02,
566 position: "left" // or "right"
567 },
568 xaxes: [],
569 yaxes: [],
570 series: {
571 points: {
572 show: false,
573 radius: 3,
574 lineWidth: 2, // in pixels
575 fill: true,
576 fillColor: "#ffffff",
577 symbol: "circle" // or callback
578 },
579 lines: {
580 // we don't put in show: false so we can see
581 // whether lines were actively disabled
582 lineWidth: 2, // in pixels
583 fill: false,
584 fillColor: null,
585 steps: false
586 // Omit 'zero', so we can later default its value to
587 // match that of the 'fill' option.
588 },
589 bars: {
590 show: false,
591 lineWidth: 2, // in pixels
592 barWidth: 1, // in units of the x axis
593 fill: true,
594 fillColor: null,
595 align: "left", // "left", "right", or "center"
596 horizontal: false,
597 zero: true
598 },
599 shadowSize: 3,
600 highlightColor: null
601 },
602 grid: {
603 show: true,
604 aboveData: false,
605 color: "#545454", // primary color used for outline and labels
606 backgroundColor: null, // null for transparent, else color
607 borderColor: null, // set if different from the grid color
608 tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
609 margin: 0, // distance from the canvas edge to the grid
610 labelMargin: 5, // in pixels
611 axisMargin: 8, // in pixels
612 borderWidth: 2, // in pixels
613 minBorderMargin: null, // in pixels, null means taken from points radius
614 markings: null, // array of ranges or fn: axes -> array of ranges
615 markingsColor: "#f4f4f4",
616 markingsLineWidth: 2,
617 // interactive stuff
618 clickable: false,
619 hoverable: false,
620 autoHighlight: true, // highlight in case mouse is near
621 mouseActiveRadius: 10 // how far the mouse can be away to activate an item
622 },
623 interaction: {
624 redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow
625 },
626 hooks: {}
627 },
628 surface = null, // the canvas for the plot itself
629 overlay = null, // canvas for interactive stuff on top of plot
630 eventHolder = null, // jQuery object that events should be bound to
631 ctx = null, octx = null,
632 xaxes = [], yaxes = [],
633 plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
634 plotWidth = 0, plotHeight = 0,
635 hooks = {
636 processOptions: [],
637 processRawData: [],
638 processDatapoints: [],
639 processOffset: [],
640 drawBackground: [],
641 drawSeries: [],
642 draw: [],
643 bindEvents: [],
644 drawOverlay: [],
645 shutdown: []
646 },
647 plot = this;
648
649 // public functions
650 plot.setData = setData;
651 plot.setupGrid = setupGrid;
652 plot.draw = draw;
653 plot.getPlaceholder = function() { return placeholder; };
654 plot.getCanvas = function() { return surface.element; };
655 plot.getPlotOffset = function() { return plotOffset; };
656 plot.width = function () { return plotWidth; };
657 plot.height = function () { return plotHeight; };
658 plot.offset = function () {
659 var o = eventHolder.offset();
660 o.left += plotOffset.left;
661 o.top += plotOffset.top;
662 return o;
663 };
664 plot.getData = function () { return series; };
665 plot.getAxes = function () {
666 var res = {}, i;
667 $.each(xaxes.concat(yaxes), function (_, axis) {
668 if (axis)
669 res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
670 });
671 return res;
672 };
673 plot.getXAxes = function () { return xaxes; };
674 plot.getYAxes = function () { return yaxes; };
675 plot.c2p = canvasToAxisCoords;
676 plot.p2c = axisToCanvasCoords;
677 plot.getOptions = function () { return options; };
678 plot.highlight = highlight;
679 plot.unhighlight = unhighlight;
680 plot.triggerRedrawOverlay = triggerRedrawOverlay;
681 plot.pointOffset = function(point) {
682 return {
683 left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10),
684 top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10)
685 };
686 };
687 plot.shutdown = shutdown;
688 plot.destroy = function () {
689 shutdown();
690 placeholder.removeData("plot").empty();
691
692 series = [];
693 options = null;
694 surface = null;
695 overlay = null;
696 eventHolder = null;
697 ctx = null;
698 octx = null;
699 xaxes = [];
700 yaxes = [];
701 hooks = null;
702 highlights = [];
703 plot = null;
704 };
705 plot.resize = function () {
706 var width = placeholder.width(),
707 height = placeholder.height();
708 surface.resize(width, height);
709 overlay.resize(width, height);
710 };
711
712 // public attributes
713 plot.hooks = hooks;
714
715 // initialize
716 initPlugins(plot);
717 parseOptions(options_);
718 setupCanvases();
719 setData(data_);
720 setupGrid();
721 draw();
722 bindEvents();
723
724
725 function executeHooks(hook, args) {
726 args = [plot].concat(args);
727 for (var i = 0; i < hook.length; ++i)
728 hook[i].apply(this, args);
729 }
730
731 function initPlugins() {
732
733 // References to key classes, allowing plugins to modify them
734
735 var classes = {
736 Canvas: Canvas
737 };
738
739 for (var i = 0; i < plugins.length; ++i) {
740 var p = plugins[i];
741 p.init(plot, classes);
742 if (p.options)
743 $.extend(true, options, p.options);
744 }
745 }
746
747 function parseOptions(opts) {
748
749 $.extend(true, options, opts);
750
751 // $.extend merges arrays, rather than replacing them. When less
752 // colors are provided than the size of the default palette, we
753 // end up with those colors plus the remaining defaults, which is
754 // not expected behavior; avoid it by replacing them here.
755
756 if (opts && opts.colors) {
757 options.colors = opts.colors;
758 }
759
760 if (options.xaxis.color == null)
761 options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString();
762 if (options.yaxis.color == null)
763 options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString();
764
765 if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility
766 options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color;
767 if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility
768 options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color;
769
770 if (options.grid.borderColor == null)
771 options.grid.borderColor = options.grid.color;
772 if (options.grid.tickColor == null)
773 options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
774
775 // Fill in defaults for axis options, including any unspecified
776 // font-spec fields, if a font-spec was provided.
777
778 // If no x/y axis options were provided, create one of each anyway,
779 // since the rest of the code assumes that they exist.
780
781 var i, axisOptions, axisCount,
782 fontSize = placeholder.css("font-size"),
783 fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13,
784 fontDefaults = {
785 style: placeholder.css("font-style"),
786 size: Math.round(0.8 * fontSizeDefault),
787 variant: placeholder.css("font-variant"),
788 weight: placeholder.css("font-weight"),
789 family: placeholder.css("font-family")
790 };
791
792 axisCount = options.xaxes.length || 1;
793 for (i = 0; i < axisCount; ++i) {
794
795 axisOptions = options.xaxes[i];
796 if (axisOptions && !axisOptions.tickColor) {
797 axisOptions.tickColor = axisOptions.color;
798 }
799
800 axisOptions = $.extend(true, {}, options.xaxis, axisOptions);
801 options.xaxes[i] = axisOptions;
802
803 if (axisOptions.font) {
804 axisOptions.font = $.extend({}, fontDefaults, axisOptions.font);
805 if (!axisOptions.font.color) {
806 axisOptions.font.color = axisOptions.color;
807 }
808 if (!axisOptions.font.lineHeight) {
809 axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15);
810 }
811 }
812 }
813
814 axisCount = options.yaxes.length || 1;
815 for (i = 0; i < axisCount; ++i) {
816
817 axisOptions = options.yaxes[i];
818 if (axisOptions && !axisOptions.tickColor) {
819 axisOptions.tickColor = axisOptions.color;
820 }
821
822 axisOptions = $.extend(true, {}, options.yaxis, axisOptions);
823 options.yaxes[i] = axisOptions;
824
825 if (axisOptions.font) {
826 axisOptions.font = $.extend({}, fontDefaults, axisOptions.font);
827 if (!axisOptions.font.color) {
828 axisOptions.font.color = axisOptions.color;
829 }
830 if (!axisOptions.font.lineHeight) {
831 axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15);
832 }
833 }
834 }
835
836 // backwards compatibility, to be removed in future
837 if (options.xaxis.noTicks && options.xaxis.ticks == null)
838 options.xaxis.ticks = options.xaxis.noTicks;
839 if (options.yaxis.noTicks && options.yaxis.ticks == null)
840 options.yaxis.ticks = options.yaxis.noTicks;
841 if (options.x2axis) {
842 options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
843 options.xaxes[1].position = "top";
844 // Override the inherit to allow the axis to auto-scale
845 if (options.x2axis.min == null) {
846 options.xaxes[1].min = null;
847 }
848 if (options.x2axis.max == null) {
849 options.xaxes[1].max = null;
850 }
851 }
852 if (options.y2axis) {
853 options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
854 options.yaxes[1].position = "right";
855 // Override the inherit to allow the axis to auto-scale
856 if (options.y2axis.min == null) {
857 options.yaxes[1].min = null;
858 }
859 if (options.y2axis.max == null) {
860 options.yaxes[1].max = null;
861 }
862 }
863 if (options.grid.coloredAreas)
864 options.grid.markings = options.grid.coloredAreas;
865 if (options.grid.coloredAreasColor)
866 options.grid.markingsColor = options.grid.coloredAreasColor;
867 if (options.lines)
868 $.extend(true, options.series.lines, options.lines);
869 if (options.points)
870 $.extend(true, options.series.points, options.points);
871 if (options.bars)
872 $.extend(true, options.series.bars, options.bars);
873 if (options.shadowSize != null)
874 options.series.shadowSize = options.shadowSize;
875 if (options.highlightColor != null)
876 options.series.highlightColor = options.highlightColor;
877
878 // save options on axes for future reference
879 for (i = 0; i < options.xaxes.length; ++i)
880 getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
881 for (i = 0; i < options.yaxes.length; ++i)
882 getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];
883
884 // add hooks from options
885 for (var n in hooks)
886 if (options.hooks[n] && options.hooks[n].length)
887 hooks[n] = hooks[n].concat(options.hooks[n]);
888
889 executeHooks(hooks.processOptions, [options]);
890 }
891
892 function setData(d) {
893 series = parseData(d);
894 fillInSeriesOptions();
895 processData();
896 }
897
898 function parseData(d) {
899 var res = [];
900 for (var i = 0; i < d.length; ++i) {
901 var s = $.extend(true, {}, options.series);
902
903 if (d[i].data != null) {
904 s.data = d[i].data; // move the data instead of deep-copy
905 delete d[i].data;
906
907 $.extend(true, s, d[i]);
908
909 d[i].data = s.data;
910 }
911 else
912 s.data = d[i];
913 res.push(s);
914 }
915
916 return res;
917 }
918
919 function axisNumber(obj, coord) {
920 var a = obj[coord + "axis"];
921 if (typeof a == "object") // if we got a real axis, extract number
922 a = a.n;
923 if (typeof a != "number")
924 a = 1; // default to first axis
925 return a;
926 }
927
928 function allAxes() {
929 // return flat array without annoying null entries
930 return $.grep(xaxes.concat(yaxes), function (a) { return a; });
931 }
932
933 function canvasToAxisCoords(pos) {
934 // return an object with x/y corresponding to all used axes
935 var res = {}, i, axis;
936 for (i = 0; i < xaxes.length; ++i) {
937 axis = xaxes[i];
938 if (axis && axis.used)
939 res["x" + axis.n] = axis.c2p(pos.left);
940 }
941
942 for (i = 0; i < yaxes.length; ++i) {
943 axis = yaxes[i];
944 if (axis && axis.used)
945 res["y" + axis.n] = axis.c2p(pos.top);
946 }
947
948 if (res.x1 !== undefined)
949 res.x = res.x1;
950 if (res.y1 !== undefined)
951 res.y = res.y1;
952
953 return res;
954 }
955
956 function axisToCanvasCoords(pos) {
957 // get canvas coords from the first pair of x/y found in pos
958 var res = {}, i, axis, key;
959
960 for (i = 0; i < xaxes.length; ++i) {
961 axis = xaxes[i];
962 if (axis && axis.used) {
963 key = "x" + axis.n;
964 if (pos[key] == null && axis.n == 1)
965 key = "x";
966
967 if (pos[key] != null) {
968 res.left = axis.p2c(pos[key]);
969 break;
970 }
971 }
972 }
973
974 for (i = 0; i < yaxes.length; ++i) {
975 axis = yaxes[i];
976 if (axis && axis.used) {
977 key = "y" + axis.n;
978 if (pos[key] == null && axis.n == 1)
979 key = "y";
980
981 if (pos[key] != null) {
982 res.top = axis.p2c(pos[key]);
983 break;
984 }
985 }
986 }
987
988 return res;
989 }
990
991 function getOrCreateAxis(axes, number) {
992 if (!axes[number - 1])
993 axes[number - 1] = {
994 n: number, // save the number for future reference
995 direction: axes == xaxes ? "x" : "y",
996 options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)
997 };
998
999 return axes[number - 1];
1000 }
1001
1002 function fillInSeriesOptions() {
1003
1004 var neededColors = series.length, maxIndex = -1, i;
1005
1006 // Subtract the number of series that already have fixed colors or
1007 // color indexes from the number that we still need to generate.
1008
1009 for (i = 0; i < series.length; ++i) {
1010 var sc = series[i].color;
1011 if (sc != null) {
1012 neededColors--;
1013 if (typeof sc == "number" && sc > maxIndex) {
1014 maxIndex = sc;
1015 }
1016 }
1017 }
1018
1019 // If any of the series have fixed color indexes, then we need to
1020 // generate at least as many colors as the highest index.
1021
1022 if (neededColors <= maxIndex) {
1023 neededColors = maxIndex + 1;
1024 }
1025
1026 // Generate all the colors, using first the option colors and then
1027 // variations on those colors once they're exhausted.
1028
1029 var c, colors = [], colorPool = options.colors,
1030 colorPoolSize = colorPool.length, variation = 0;
1031
1032 for (i = 0; i < neededColors; i++) {
1033
1034 c = $.color.parse(colorPool[i % colorPoolSize] || "#666");
1035
1036 // Each time we exhaust the colors in the pool we adjust
1037 // a scaling factor used to produce more variations on
1038 // those colors. The factor alternates negative/positive
1039 // to produce lighter/darker colors.
1040
1041 // Reset the variation after every few cycles, or else
1042 // it will end up producing only white or black colors.
1043
1044 if (i % colorPoolSize == 0 && i) {
1045 if (variation >= 0) {
1046 if (variation < 0.5) {
1047 variation = -variation - 0.2;
1048 } else variation = 0;
1049 } else variation = -variation;
1050 }
1051
1052 colors[i] = c.scale('rgb', 1 + variation);
1053 }
1054
1055 // Finalize the series options, filling in their colors
1056
1057 var colori = 0, s;
1058 for (i = 0; i < series.length; ++i) {
1059 s = series[i];
1060
1061 // assign colors
1062 if (s.color == null) {
1063 s.color = colors[colori].toString();
1064 ++colori;
1065 }
1066 else if (typeof s.color == "number")
1067 s.color = colors[s.color].toString();
1068
1069 // turn on lines automatically in case nothing is set
1070 if (s.lines.show == null) {
1071 var v, show = true;
1072 for (v in s)
1073 if (s[v] && s[v].show) {
1074 show = false;
1075 break;
1076 }
1077 if (show)
1078 s.lines.show = true;
1079 }
1080
1081 // If nothing was provided for lines.zero, default it to match
1082 // lines.fill, since areas by default should extend to zero.
1083
1084 if (s.lines.zero == null) {
1085 s.lines.zero = !!s.lines.fill;
1086 }
1087
1088 // setup axes
1089 s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));
1090 s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));
1091 }
1092 }
1093
1094 function processData() {
1095 var topSentry = Number.POSITIVE_INFINITY,
1096 bottomSentry = Number.NEGATIVE_INFINITY,
1097 fakeInfinity = Number.MAX_VALUE,
1098 i, j, k, m, length,
1099 s, points, ps, x, y, axis, val, f, p,
1100 data, format;
1101
1102 function updateAxis(axis, min, max) {
1103 if (min < axis.datamin && min != -fakeInfinity)
1104 axis.datamin = min;
1105 if (max > axis.datamax && max != fakeInfinity)
1106 axis.datamax = max;
1107 }
1108
1109 $.each(allAxes(), function (_, axis) {
1110 // init axis
1111 axis.datamin = topSentry;
1112 axis.datamax = bottomSentry;
1113 axis.used = false;
1114 });
1115
1116 for (i = 0; i < series.length; ++i) {
1117 s = series[i];
1118 s.datapoints = { points: [] };
1119
1120 executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
1121 }
1122
1123 // first pass: clean and copy data
1124 for (i = 0; i < series.length; ++i) {
1125 s = series[i];
1126
1127 data = s.data;
1128 format = s.datapoints.format;
1129
1130 if (!format) {
1131 format = [];
1132 // find out how to copy
1133 format.push({ x: true, number: true, required: true });
1134 format.push({ y: true, number: true, required: true });
1135
1136 if (s.bars.show || (s.lines.show && s.lines.fill)) {
1137 var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero));
1138 format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale });
1139 if (s.bars.horizontal) {
1140 delete format[format.length - 1].y;
1141 format[format.length - 1].x = true;
1142 }
1143 }
1144
1145 s.datapoints.format = format;
1146 }
1147
1148 if (s.datapoints.pointsize != null)
1149 continue; // already filled in
1150
1151 s.datapoints.pointsize = format.length;
1152
1153 ps = s.datapoints.pointsize;
1154 points = s.datapoints.points;
1155
1156 var insertSteps = s.lines.show && s.lines.steps;
1157 s.xaxis.used = s.yaxis.used = true;
1158
1159 for (j = k = 0; j < data.length; ++j, k += ps) {
1160 p = data[j];
1161
1162 var nullify = p == null;
1163 if (!nullify) {
1164 for (m = 0; m < ps; ++m) {
1165 val = p[m];
1166 f = format[m];
1167
1168 if (f) {
1169 if (f.number && val != null) {
1170 val = +val; // convert to number
1171 if (isNaN(val))
1172 val = null;
1173 else if (val == Infinity)
1174 val = fakeInfinity;
1175 else if (val == -Infinity)
1176 val = -fakeInfinity;
1177 }
1178
1179 if (val == null) {
1180 if (f.required)
1181 nullify = true;
1182
1183 if (f.defaultValue != null)
1184 val = f.defaultValue;
1185 }
1186 }
1187
1188 points[k + m] = val;
1189 }
1190 }
1191
1192 if (nullify) {
1193 for (m = 0; m < ps; ++m) {
1194 val = points[k + m];
1195 if (val != null) {
1196 f = format[m];
1197 // extract min/max info
1198 if (f.autoscale !== false) {
1199 if (f.x) {
1200 updateAxis(s.xaxis, val, val);
1201 }
1202 if (f.y) {
1203 updateAxis(s.yaxis, val, val);
1204 }
1205 }
1206 }
1207 points[k + m] = null;
1208 }
1209 }
1210 else {
1211 // a little bit of line specific stuff that
1212 // perhaps shouldn't be here, but lacking
1213 // better means...
1214 if (insertSteps && k > 0
1215 && points[k - ps] != null
1216 && points[k - ps] != points[k]
1217 && points[k - ps + 1] != points[k + 1]) {
1218 // copy the point to make room for a middle point
1219 for (m = 0; m < ps; ++m)
1220 points[k + ps + m] = points[k + m];
1221
1222 // middle point has same y
1223 points[k + 1] = points[k - ps + 1];
1224
1225 // we've added a point, better reflect that
1226 k += ps;
1227 }
1228 }
1229 }
1230 }
1231
1232 // give the hooks a chance to run
1233 for (i = 0; i < series.length; ++i) {
1234 s = series[i];
1235
1236 executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
1237 }
1238
1239 // second pass: find datamax/datamin for auto-scaling
1240 for (i = 0; i < series.length; ++i) {
1241 s = series[i];
1242 points = s.datapoints.points;
1243 ps = s.datapoints.pointsize;
1244 format = s.datapoints.format;
1245
1246 var xmin = topSentry, ymin = topSentry,
1247 xmax = bottomSentry, ymax = bottomSentry;
1248
1249 for (j = 0; j < points.length; j += ps) {
1250 if (points[j] == null)
1251 continue;
1252
1253 for (m = 0; m < ps; ++m) {
1254 val = points[j + m];
1255 f = format[m];
1256 if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity)
1257 continue;
1258
1259 if (f.x) {
1260 if (val < xmin)
1261 xmin = val;
1262 if (val > xmax)
1263 xmax = val;
1264 }
1265 if (f.y) {
1266 if (val < ymin)
1267 ymin = val;
1268 if (val > ymax)
1269 ymax = val;
1270 }
1271 }
1272 }
1273
1274 if (s.bars.show) {
1275 // make sure we got room for the bar on the dancing floor
1276 var delta;
1277
1278 switch (s.bars.align) {
1279 case "left":
1280 delta = 0;
1281 break;
1282 case "right":
1283 delta = -s.bars.barWidth;
1284 break;
1285 default:
1286 delta = -s.bars.barWidth / 2;
1287 }
1288
1289 if (s.bars.horizontal) {
1290 ymin += delta;
1291 ymax += delta + s.bars.barWidth;
1292 }
1293 else {
1294 xmin += delta;
1295 xmax += delta + s.bars.barWidth;
1296 }
1297 }
1298
1299 updateAxis(s.xaxis, xmin, xmax);
1300 updateAxis(s.yaxis, ymin, ymax);
1301 }
1302
1303 $.each(allAxes(), function (_, axis) {
1304 if (axis.datamin == topSentry)
1305 axis.datamin = null;
1306 if (axis.datamax == bottomSentry)
1307 axis.datamax = null;
1308 });
1309 }
1310
1311 function setupCanvases() {
1312
1313 // Make sure the placeholder is clear of everything except canvases
1314 // from a previous plot in this container that we'll try to re-use.
1315
1316 placeholder.css("padding", 0) // padding messes up the positioning
1317 .children().filter(function(){
1318 return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base');
1319 }).remove();
1320
1321 if (placeholder.css("position") == 'static')
1322 placeholder.css("position", "relative"); // for positioning labels and overlay
1323
1324 surface = new Canvas("flot-base", placeholder);
1325 overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features
1326
1327 ctx = surface.context;
1328 octx = overlay.context;
1329
1330 // define which element we're listening for events on
1331 eventHolder = $(overlay.element).unbind();
1332
1333 // If we're re-using a plot object, shut down the old one
1334
1335 var existing = placeholder.data("plot");
1336
1337 if (existing) {
1338 existing.shutdown();
1339 overlay.clear();
1340 }
1341
1342 // save in case we get replotted
1343 placeholder.data("plot", plot);
1344 }
1345
1346 function bindEvents() {
1347 // bind events
1348 if (options.grid.hoverable) {
1349 eventHolder.mousemove(onMouseMove);
1350
1351 // Use bind, rather than .mouseleave, because we officially
1352 // still support jQuery 1.2.6, which doesn't define a shortcut
1353 // for mouseenter or mouseleave. This was a bug/oversight that
1354 // was fixed somewhere around 1.3.x. We can return to using
1355 // .mouseleave when we drop support for 1.2.6.
1356
1357 eventHolder.bind("mouseleave", onMouseLeave);
1358 }
1359
1360 if (options.grid.clickable)
1361 eventHolder.click(onClick);
1362
1363 executeHooks(hooks.bindEvents, [eventHolder]);
1364 }
1365
1366 function shutdown() {
1367 if (redrawTimeout)
1368 clearTimeout(redrawTimeout);
1369
1370 eventHolder.unbind("mousemove", onMouseMove);
1371 eventHolder.unbind("mouseleave", onMouseLeave);
1372 eventHolder.unbind("click", onClick);
1373
1374 executeHooks(hooks.shutdown, [eventHolder]);
1375 }
1376
1377 function setTransformationHelpers(axis) {
1378 // set helper functions on the axis, assumes plot area
1379 // has been computed already
1380
1381 function identity(x) { return x; }
1382
1383 var s, m, t = axis.options.transform || identity,
1384 it = axis.options.inverseTransform;
1385
1386 // precompute how much the axis is scaling a point
1387 // in canvas space
1388 if (axis.direction == "x") {
1389 s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
1390 m = Math.min(t(axis.max), t(axis.min));
1391 }
1392 else {
1393 s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
1394 s = -s;
1395 m = Math.max(t(axis.max), t(axis.min));
1396 }
1397
1398 // data point to canvas coordinate
1399 if (t == identity) // slight optimization
1400 axis.p2c = function (p) { return (p - m) * s; };
1401 else
1402 axis.p2c = function (p) { return (t(p) - m) * s; };
1403 // canvas coordinate to data point
1404 if (!it)
1405 axis.c2p = function (c) { return m + c / s; };
1406 else
1407 axis.c2p = function (c) { return it(m + c / s); };
1408 }
1409
1410 function measureTickLabels(axis) {
1411
1412 var opts = axis.options,
1413 ticks = axis.ticks || [],
1414 labelWidth = opts.labelWidth || 0,
1415 labelHeight = opts.labelHeight || 0,
1416 maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null),
1417 legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis",
1418 layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles,
1419 font = opts.font || "flot-tick-label tickLabel";
1420
1421 for (var i = 0; i < ticks.length; ++i) {
1422
1423 var t = ticks[i];
1424
1425 if (!t.label)
1426 continue;
1427
1428 var info = surface.getTextInfo(layer, t.label, font, null, maxWidth);
1429
1430 labelWidth = Math.max(labelWidth, info.width);
1431 labelHeight = Math.max(labelHeight, info.height);
1432 }
1433
1434 axis.labelWidth = opts.labelWidth || labelWidth;
1435 axis.labelHeight = opts.labelHeight || labelHeight;
1436 }
1437
1438 function allocateAxisBoxFirstPhase(axis) {
1439 // find the bounding box of the axis by looking at label
1440 // widths/heights and ticks, make room by diminishing the
1441 // plotOffset; this first phase only looks at one
1442 // dimension per axis, the other dimension depends on the
1443 // other axes so will have to wait
1444
1445 var lw = axis.labelWidth,
1446 lh = axis.labelHeight,
1447 pos = axis.options.position,
1448 isXAxis = axis.direction === "x",
1449 tickLength = axis.options.tickLength,
1450 axisMargin = options.grid.axisMargin,
1451 padding = options.grid.labelMargin,
1452 innermost = true,
1453 outermost = true,
1454 first = true,
1455 found = false;
1456
1457 // Determine the axis's position in its direction and on its side
1458
1459 $.each(isXAxis ? xaxes : yaxes, function(i, a) {
1460 if (a && (a.show || a.reserveSpace)) {
1461 if (a === axis) {
1462 found = true;
1463 } else if (a.options.position === pos) {
1464 if (found) {
1465 outermost = false;
1466 } else {
1467 innermost = false;
1468 }
1469 }
1470 if (!found) {
1471 first = false;
1472 }
1473 }
1474 });
1475
1476 // The outermost axis on each side has no margin
1477
1478 if (outermost) {
1479 axisMargin = 0;
1480 }
1481
1482 // The ticks for the first axis in each direction stretch across
1483
1484 if (tickLength == null) {
1485 tickLength = first ? "full" : 5;
1486 }
1487
1488 if (!isNaN(+tickLength))
1489 padding += +tickLength;
1490
1491 if (isXAxis) {
1492 lh += padding;
1493
1494 if (pos == "bottom") {
1495 plotOffset.bottom += lh + axisMargin;
1496 axis.box = { top: surface.height - plotOffset.bottom, height: lh };
1497 }
1498 else {
1499 axis.box = { top: plotOffset.top + axisMargin, height: lh };
1500 plotOffset.top += lh + axisMargin;
1501 }
1502 }
1503 else {
1504 lw += padding;
1505
1506 if (pos == "left") {
1507 axis.box = { left: plotOffset.left + axisMargin, width: lw };
1508 plotOffset.left += lw + axisMargin;
1509 }
1510 else {
1511 plotOffset.right += lw + axisMargin;
1512 axis.box = { left: surface.width - plotOffset.right, width: lw };
1513 }
1514 }
1515
1516 // save for future reference
1517 axis.position = pos;
1518 axis.tickLength = tickLength;
1519 axis.box.padding = padding;
1520 axis.innermost = innermost;
1521 }
1522
1523 function allocateAxisBoxSecondPhase(axis) {
1524 // now that all axis boxes have been placed in one
1525 // dimension, we can set the remaining dimension coordinates
1526 if (axis.direction == "x") {
1527 axis.box.left = plotOffset.left - axis.labelWidth / 2;
1528 axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth;
1529 }
1530 else {
1531 axis.box.top = plotOffset.top - axis.labelHeight / 2;
1532 axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight;
1533 }
1534 }
1535
1536 function adjustLayoutForThingsStickingOut() {
1537 // possibly adjust plot offset to ensure everything stays
1538 // inside the canvas and isn't clipped off
1539
1540 var minMargin = options.grid.minBorderMargin,
1541 axis, i;
1542
1543 // check stuff from the plot (FIXME: this should just read
1544 // a value from the series, otherwise it's impossible to
1545 // customize)
1546 if (minMargin == null) {
1547 minMargin = 0;
1548 for (i = 0; i < series.length; ++i)
1549 minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2));
1550 }
1551
1552 var margins = {
1553 left: minMargin,
1554 right: minMargin,
1555 top: minMargin,
1556 bottom: minMargin
1557 };
1558
1559 // check axis labels, note we don't check the actual
1560 // labels but instead use the overall width/height to not
1561 // jump as much around with replots
1562 $.each(allAxes(), function (_, axis) {
1563 if (axis.reserveSpace && axis.ticks && axis.ticks.length) {
1564 if (axis.direction === "x") {
1565 margins.left = Math.max(margins.left, axis.labelWidth / 2);
1566 margins.right = Math.max(margins.right, axis.labelWidth / 2);
1567 } else {
1568 margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2);
1569 margins.top = Math.max(margins.top, axis.labelHeight / 2);
1570 }
1571 }
1572 });
1573
1574 plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left));
1575 plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right));
1576 plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top));
1577 plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom));
1578 }
1579
1580 function setupGrid() {
1581 var i, axes = allAxes(), showGrid = options.grid.show;
1582
1583 // Initialize the plot's offset from the edge of the canvas
1584
1585 for (var a in plotOffset) {
1586 var margin = options.grid.margin || 0;
1587 plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0;
1588 }
1589
1590 executeHooks(hooks.processOffset, [plotOffset]);
1591
1592 // If the grid is visible, add its border width to the offset
1593
1594 for (var a in plotOffset) {
1595 if(typeof(options.grid.borderWidth) == "object") {
1596 plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0;
1597 }
1598 else {
1599 plotOffset[a] += showGrid ? options.grid.borderWidth : 0;
1600 }
1601 }
1602
1603 $.each(axes, function (_, axis) {
1604 var axisOpts = axis.options;
1605 axis.show = axisOpts.show == null ? axis.used : axisOpts.show;
1606 axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace;
1607 setRange(axis);
1608 });
1609
1610 if (showGrid) {
1611
1612 var allocatedAxes = $.grep(axes, function (axis) {
1613 return axis.show || axis.reserveSpace;
1614 });
1615
1616 $.each(allocatedAxes, function (_, axis) {
1617 // make the ticks
1618 setupTickGeneration(axis);
1619 setTicks(axis);
1620 snapRangeToTicks(axis, axis.ticks);
1621 // find labelWidth/Height for axis
1622 measureTickLabels(axis);
1623 });
1624
1625 // with all dimensions calculated, we can compute the
1626 // axis bounding boxes, start from the outside
1627 // (reverse order)
1628 for (i = allocatedAxes.length - 1; i >= 0; --i)
1629 allocateAxisBoxFirstPhase(allocatedAxes[i]);
1630
1631 // make sure we've got enough space for things that
1632 // might stick out
1633 adjustLayoutForThingsStickingOut();
1634
1635 $.each(allocatedAxes, function (_, axis) {
1636 allocateAxisBoxSecondPhase(axis);
1637 });
1638 }
1639
1640 plotWidth = surface.width - plotOffset.left - plotOffset.right;
1641 plotHeight = surface.height - plotOffset.bottom - plotOffset.top;
1642
1643 // now we got the proper plot dimensions, we can compute the scaling
1644 $.each(axes, function (_, axis) {
1645 setTransformationHelpers(axis);
1646 });
1647
1648 if (showGrid) {
1649 drawAxisLabels();
1650 }
1651
1652 insertLegend();
1653 }
1654
1655 function setRange(axis) {
1656 var opts = axis.options,
1657 min = +(opts.min != null ? opts.min : axis.datamin),
1658 max = +(opts.max != null ? opts.max : axis.datamax),
1659 delta = max - min;
1660
1661 if (delta == 0.0) {
1662 // degenerate case
1663 var widen = max == 0 ? 1 : 0.01;
1664
1665 if (opts.min == null)
1666 min -= widen;
1667 // always widen max if we couldn't widen min to ensure we
1668 // don't fall into min == max which doesn't work
1669 if (opts.max == null || opts.min != null)
1670 max += widen;
1671 }
1672 else {
1673 // consider autoscaling
1674 var margin = opts.autoscaleMargin;
1675 if (margin != null) {
1676 if (opts.min == null) {
1677 min -= delta * margin;
1678 // make sure we don't go below zero if all values
1679 // are positive
1680 if (min < 0 && axis.datamin != null && axis.datamin >= 0)
1681 min = 0;
1682 }
1683 if (opts.max == null) {
1684 max += delta * margin;
1685 if (max > 0 && axis.datamax != null && axis.datamax <= 0)
1686 max = 0;
1687 }
1688 }
1689 }
1690 axis.min = min;
1691 axis.max = max;
1692 }
1693
1694 function setupTickGeneration(axis) {
1695 var opts = axis.options;
1696
1697 // estimate number of ticks
1698 var noTicks;
1699 if (typeof opts.ticks == "number" && opts.ticks > 0)
1700 noTicks = opts.ticks;
1701 else
1702 // heuristic based on the model a*sqrt(x) fitted to
1703 // some data points that seemed reasonable
1704 noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height);
1705
1706 var delta = (axis.max - axis.min) / noTicks,
1707 dec = -Math.floor(Math.log(delta) / Math.LN10),
1708 maxDec = opts.tickDecimals;
1709
1710 if (maxDec != null && dec > maxDec) {
1711 dec = maxDec;
1712 }
1713
1714 var magn = Math.pow(10, -dec),
1715 norm = delta / magn, // norm is between 1.0 and 10.0
1716 size;
1717
1718 if (norm < 1.5) {
1719 size = 1;
1720 } else if (norm < 3) {
1721 size = 2;
1722 // special case for 2.5, requires an extra decimal
1723 if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
1724 size = 2.5;
1725 ++dec;
1726 }
1727 } else if (norm < 7.5) {
1728 size = 5;
1729 } else {
1730 size = 10;
1731 }
1732
1733 size *= magn;
1734
1735 if (opts.minTickSize != null && size < opts.minTickSize) {
1736 size = opts.minTickSize;
1737 }
1738
1739 axis.delta = delta;
1740 axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
1741 axis.tickSize = opts.tickSize || size;
1742
1743 // Time mode was moved to a plug-in in 0.8, and since so many people use it
1744 // we'll add an especially friendly reminder to make sure they included it.
1745
1746 if (opts.mode == "time" && !axis.tickGenerator) {
1747 throw new Error("Time mode requires the flot.time plugin.");
1748 }
1749
1750 // Flot supports base-10 axes; any other mode else is handled by a plug-in,
1751 // like flot.time.js.
1752
1753 if (!axis.tickGenerator) {
1754
1755 axis.tickGenerator = function (axis) {
1756
1757 var ticks = [],
1758 start = floorInBase(axis.min, axis.tickSize),
1759 i = 0,
1760 v = Number.NaN,
1761 prev;
1762
1763 do {
1764 prev = v;
1765 v = start + i * axis.tickSize;
1766 ticks.push(v);
1767 ++i;
1768 } while (v < axis.max && v != prev);
1769 return ticks;
1770 };
1771
1772 axis.tickFormatter = function (value, axis) {
1773
1774 var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1;
1775 var formatted = "" + Math.round(value * factor) / factor;
1776
1777 // If tickDecimals was specified, ensure that we have exactly that
1778 // much precision; otherwise default to the value's own precision.
1779
1780 if (axis.tickDecimals != null) {
1781 var decimal = formatted.indexOf(".");
1782 var precision = decimal == -1 ? 0 : formatted.length - decimal - 1;
1783 if (precision < axis.tickDecimals) {
1784 return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision);
1785 }
1786 }
1787
1788 return formatted;
1789 };
1790 }
1791
1792 if ($.isFunction(opts.tickFormatter))
1793 axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };
1794
1795 if (opts.alignTicksWithAxis != null) {
1796 var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
1797 if (otherAxis && otherAxis.used && otherAxis != axis) {
1798 // consider snapping min/max to outermost nice ticks
1799 var niceTicks = axis.tickGenerator(axis);
1800 if (niceTicks.length > 0) {
1801 if (opts.min == null)
1802 axis.min = Math.min(axis.min, niceTicks[0]);
1803 if (opts.max == null && niceTicks.length > 1)
1804 axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
1805 }
1806
1807 axis.tickGenerator = function (axis) {
1808 // copy ticks, scaled to this axis
1809 var ticks = [], v, i;
1810 for (i = 0; i < otherAxis.ticks.length; ++i) {
1811 v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
1812 v = axis.min + v * (axis.max - axis.min);
1813 ticks.push(v);
1814 }
1815 return ticks;
1816 };
1817
1818 // we might need an extra decimal since forced
1819 // ticks don't necessarily fit naturally
1820 if (!axis.mode && opts.tickDecimals == null) {
1821 var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1),
1822 ts = axis.tickGenerator(axis);
1823
1824 // only proceed if the tick interval rounded
1825 // with an extra decimal doesn't give us a
1826 // zero at end
1827 if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))
1828 axis.tickDecimals = extraDec;
1829 }
1830 }
1831 }
1832 }
1833
1834 function setTicks(axis) {
1835 var oticks = axis.options.ticks, ticks = [];
1836 if (oticks == null || (typeof oticks == "number" && oticks > 0))
1837 ticks = axis.tickGenerator(axis);
1838 else if (oticks) {
1839 if ($.isFunction(oticks))
1840 // generate the ticks
1841 ticks = oticks(axis);
1842 else
1843 ticks = oticks;
1844 }
1845
1846 // clean up/labelify the supplied ticks, copy them over
1847 var i, v;
1848 axis.ticks = [];
1849 for (i = 0; i < ticks.length; ++i) {
1850 var label = null;
1851 var t = ticks[i];
1852 if (typeof t == "object") {
1853 v = +t[0];
1854 if (t.length > 1)
1855 label = t[1];
1856 }
1857 else
1858 v = +t;
1859 if (label == null)
1860 label = axis.tickFormatter(v, axis);
1861 if (!isNaN(v))
1862 axis.ticks.push({ v: v, label: label });
1863 }
1864 }
1865
1866 function snapRangeToTicks(axis, ticks) {
1867 if (axis.options.autoscaleMargin && ticks.length > 0) {
1868 // snap to ticks
1869 if (axis.options.min == null)
1870 axis.min = Math.min(axis.min, ticks[0].v);
1871 if (axis.options.max == null && ticks.length > 1)
1872 axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
1873 }
1874 }
1875
1876 function draw() {
1877
1878 surface.clear();
1879
1880 executeHooks(hooks.drawBackground, [ctx]);
1881
1882 var grid = options.grid;
1883
1884 // draw background, if any
1885 if (grid.show && grid.backgroundColor)
1886 drawBackground();
1887
1888 if (grid.show && !grid.aboveData) {
1889 drawGrid();
1890 }
1891
1892 for (var i = 0; i < series.length; ++i) {
1893 executeHooks(hooks.drawSeries, [ctx, series[i]]);
1894 drawSeries(series[i]);
1895 }
1896
1897 executeHooks(hooks.draw, [ctx]);
1898
1899 if (grid.show && grid.aboveData) {
1900 drawGrid();
1901 }
1902
1903 surface.render();
1904
1905 // A draw implies that either the axes or data have changed, so we
1906 // should probably update the overlay highlights as well.
1907
1908 triggerRedrawOverlay();
1909 }
1910
1911 function extractRange(ranges, coord) {
1912 var axis, from, to, key, axes = allAxes();
1913
1914 for (var i = 0; i < axes.length; ++i) {
1915 axis = axes[i];
1916 if (axis.direction == coord) {
1917 key = coord + axis.n + "axis";
1918 if (!ranges[key] && axis.n == 1)
1919 key = coord + "axis"; // support x1axis as xaxis
1920 if (ranges[key]) {
1921 from = ranges[key].from;
1922 to = ranges[key].to;
1923 break;
1924 }
1925 }
1926 }
1927
1928 // backwards-compat stuff - to be removed in future
1929 if (!ranges[key]) {
1930 axis = coord == "x" ? xaxes[0] : yaxes[0];
1931 from = ranges[coord + "1"];
1932 to = ranges[coord + "2"];
1933 }
1934
1935 // auto-reverse as an added bonus
1936 if (from != null && to != null && from > to) {
1937 var tmp = from;
1938 from = to;
1939 to = tmp;
1940 }
1941
1942 return { from: from, to: to, axis: axis };
1943 }
1944
1945 function drawBackground() {
1946 ctx.save();
1947 ctx.translate(plotOffset.left, plotOffset.top);
1948
1949 ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
1950 ctx.fillRect(0, 0, plotWidth, plotHeight);
1951 ctx.restore();
1952 }
1953
1954 function drawGrid() {
1955 var i, axes, bw, bc;
1956
1957 ctx.save();
1958 ctx.translate(plotOffset.left, plotOffset.top);
1959
1960 // draw markings
1961 var markings = options.grid.markings;
1962 if (markings) {
1963 if ($.isFunction(markings)) {
1964 axes = plot.getAxes();
1965 // xmin etc. is backwards compatibility, to be
1966 // removed in the future
1967 axes.xmin = axes.xaxis.min;
1968 axes.xmax = axes.xaxis.max;
1969 axes.ymin = axes.yaxis.min;
1970 axes.ymax = axes.yaxis.max;
1971
1972 markings = markings(axes);
1973 }
1974
1975 for (i = 0; i < markings.length; ++i) {
1976 var m = markings[i],
1977 xrange = extractRange(m, "x"),
1978 yrange = extractRange(m, "y");
1979
1980 // fill in missing
1981 if (xrange.from == null)
1982 xrange.from = xrange.axis.min;
1983 if (xrange.to == null)
1984 xrange.to = xrange.axis.max;
1985 if (yrange.from == null)
1986 yrange.from = yrange.axis.min;
1987 if (yrange.to == null)
1988 yrange.to = yrange.axis.max;
1989
1990 // clip
1991 if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
1992 yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
1993 continue;
1994
1995 xrange.from = Math.max(xrange.from, xrange.axis.min);
1996 xrange.to = Math.min(xrange.to, xrange.axis.max);
1997 yrange.from = Math.max(yrange.from, yrange.axis.min);
1998 yrange.to = Math.min(yrange.to, yrange.axis.max);
1999
2000 var xequal = xrange.from === xrange.to,
2001 yequal = yrange.from === yrange.to;
2002
2003 if (xequal && yequal) {
2004 continue;
2005 }
2006
2007 // then draw
2008 xrange.from = Math.floor(xrange.axis.p2c(xrange.from));
2009 xrange.to = Math.floor(xrange.axis.p2c(xrange.to));
2010 yrange.from = Math.floor(yrange.axis.p2c(yrange.from));
2011 yrange.to = Math.floor(yrange.axis.p2c(yrange.to));
2012
2013 if (xequal || yequal) {
2014 var lineWidth = m.lineWidth || options.grid.markingsLineWidth,
2015 subPixel = lineWidth % 2 ? 0.5 : 0;
2016 ctx.beginPath();
2017 ctx.strokeStyle = m.color || options.grid.markingsColor;
2018 ctx.lineWidth = lineWidth;
2019 if (xequal) {
2020 ctx.moveTo(xrange.to + subPixel, yrange.from);
2021 ctx.lineTo(xrange.to + subPixel, yrange.to);
2022 } else {
2023 ctx.moveTo(xrange.from, yrange.to + subPixel);
2024 ctx.lineTo(xrange.to, yrange.to + subPixel);
2025 }
2026 ctx.stroke();
2027 } else {
2028 ctx.fillStyle = m.color || options.grid.markingsColor;
2029 ctx.fillRect(xrange.from, yrange.to,
2030 xrange.to - xrange.from,
2031 yrange.from - yrange.to);
2032 }
2033 }
2034 }
2035
2036 // draw the ticks
2037 axes = allAxes();
2038 bw = options.grid.borderWidth;
2039
2040 for (var j = 0; j < axes.length; ++j) {
2041 var axis = axes[j], box = axis.box,
2042 t = axis.tickLength, x, y, xoff, yoff;
2043 if (!axis.show || axis.ticks.length == 0)
2044 continue;
2045
2046 ctx.lineWidth = 1;
2047
2048 // find the edges
2049 if (axis.direction == "x") {
2050 x = 0;
2051 if (t == "full")
2052 y = (axis.position == "top" ? 0 : plotHeight);
2053 else
2054 y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);
2055 }
2056 else {
2057 y = 0;
2058 if (t == "full")
2059 x = (axis.position == "left" ? 0 : plotWidth);
2060 else
2061 x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);
2062 }
2063
2064 // draw tick bar
2065 if (!axis.innermost) {
2066 ctx.strokeStyle = axis.options.color;
2067 ctx.beginPath();
2068 xoff = yoff = 0;
2069 if (axis.direction == "x")
2070 xoff = plotWidth + 1;
2071 else
2072 yoff = plotHeight + 1;
2073
2074 if (ctx.lineWidth == 1) {
2075 if (axis.direction == "x") {
2076 y = Math.floor(y) + 0.5;
2077 } else {
2078 x = Math.floor(x) + 0.5;
2079 }
2080 }
2081
2082 ctx.moveTo(x, y);
2083 ctx.lineTo(x + xoff, y + yoff);
2084 ctx.stroke();
2085 }
2086
2087 // draw ticks
2088
2089 ctx.strokeStyle = axis.options.tickColor;
2090
2091 ctx.beginPath();
2092 for (i = 0; i < axis.ticks.length; ++i) {
2093 var v = axis.ticks[i].v;
2094
2095 xoff = yoff = 0;
2096
2097 if (isNaN(v) || v < axis.min || v > axis.max
2098 // skip those lying on the axes if we got a border
2099 || (t == "full"
2100 && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0)
2101 && (v == axis.min || v == axis.max)))
2102 continue;
2103
2104 if (axis.direction == "x") {
2105 x = axis.p2c(v);
2106 yoff = t == "full" ? -plotHeight : t;
2107
2108 if (axis.position == "top")
2109 yoff = -yoff;
2110 }
2111 else {
2112 y = axis.p2c(v);
2113 xoff = t == "full" ? -plotWidth : t;
2114
2115 if (axis.position == "left")
2116 xoff = -xoff;
2117 }
2118
2119 if (ctx.lineWidth == 1) {
2120 if (axis.direction == "x")
2121 x = Math.floor(x) + 0.5;
2122 else
2123 y = Math.floor(y) + 0.5;
2124 }
2125
2126 ctx.moveTo(x, y);
2127 ctx.lineTo(x + xoff, y + yoff);
2128 }
2129
2130 ctx.stroke();
2131 }
2132
2133
2134 // draw border
2135 if (bw) {
2136 // If either borderWidth or borderColor is an object, then draw the border
2137 // line by line instead of as one rectangle
2138 bc = options.grid.borderColor;
2139 if(typeof bw == "object" || typeof bc == "object") {
2140 if (typeof bw !== "object") {
2141 bw = {top: bw, right: bw, bottom: bw, left: bw};
2142 }
2143 if (typeof bc !== "object") {
2144 bc = {top: bc, right: bc, bottom: bc, left: bc};
2145 }
2146
2147 if (bw.top > 0) {
2148 ctx.strokeStyle = bc.top;
2149 ctx.lineWidth = bw.top;
2150 ctx.beginPath();
2151 ctx.moveTo(0 - bw.left, 0 - bw.top/2);
2152 ctx.lineTo(plotWidth, 0 - bw.top/2);
2153 ctx.stroke();
2154 }
2155
2156 if (bw.right > 0) {
2157 ctx.strokeStyle = bc.right;
2158 ctx.lineWidth = bw.right;
2159 ctx.beginPath();
2160 ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top);
2161 ctx.lineTo(plotWidth + bw.right / 2, plotHeight);
2162 ctx.stroke();
2163 }
2164
2165 if (bw.bottom > 0) {
2166 ctx.strokeStyle = bc.bottom;
2167 ctx.lineWidth = bw.bottom;
2168 ctx.beginPath();
2169 ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2);
2170 ctx.lineTo(0, plotHeight + bw.bottom / 2);
2171 ctx.stroke();
2172 }
2173
2174 if (bw.left > 0) {
2175 ctx.strokeStyle = bc.left;
2176 ctx.lineWidth = bw.left;
2177 ctx.beginPath();
2178 ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom);
2179 ctx.lineTo(0- bw.left/2, 0);
2180 ctx.stroke();
2181 }
2182 }
2183 else {
2184 ctx.lineWidth = bw;
2185 ctx.strokeStyle = options.grid.borderColor;
2186 ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
2187 }
2188 }
2189
2190 ctx.restore();
2191 }
2192
2193 function drawAxisLabels() {
2194
2195 $.each(allAxes(), function (_, axis) {
2196 var box = axis.box,
2197 legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis",
2198 layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles,
2199 font = axis.options.font || "flot-tick-label tickLabel",
2200 tick, x, y, halign, valign;
2201
2202 // Remove text before checking for axis.show and ticks.length;
2203 // otherwise plugins, like flot-tickrotor, that draw their own
2204 // tick labels will end up with both theirs and the defaults.
2205
2206 surface.removeText(layer);
2207
2208 if (!axis.show || axis.ticks.length == 0)
2209 return;
2210
2211 for (var i = 0; i < axis.ticks.length; ++i) {
2212
2213 tick = axis.ticks[i];
2214 if (!tick.label || tick.v < axis.min || tick.v > axis.max)
2215 continue;
2216
2217 if (axis.direction == "x") {
2218 halign = "center";
2219 x = plotOffset.left + axis.p2c(tick.v);
2220 if (axis.position == "bottom") {
2221 y = box.top + box.padding;
2222 } else {
2223 y = box.top + box.height - box.padding;
2224 valign = "bottom";
2225 }
2226 } else {
2227 valign = "middle";
2228 y = plotOffset.top + axis.p2c(tick.v);
2229 if (axis.position == "left") {
2230 x = box.left + box.width - box.padding;
2231 halign = "right";
2232 } else {
2233 x = box.left + box.padding;
2234 }
2235 }
2236
2237 surface.addText(layer, x, y, tick.label, font, null, null, halign, valign);
2238 }
2239 });
2240 }
2241
2242 function drawSeries(series) {
2243 if (series.lines.show)
2244 drawSeriesLines(series);
2245 if (series.bars.show)
2246 drawSeriesBars(series);
2247 if (series.points.show)
2248 drawSeriesPoints(series);
2249 }
2250
2251 function drawSeriesLines(series) {
2252 function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
2253 var points = datapoints.points,
2254 ps = datapoints.pointsize,
2255 prevx = null, prevy = null;
2256
2257 ctx.beginPath();
2258 for (var i = ps; i < points.length; i += ps) {
2259 var x1 = points[i - ps], y1 = points[i - ps + 1],
2260 x2 = points[i], y2 = points[i + 1];
2261
2262 if (x1 == null || x2 == null)
2263 continue;
2264
2265 // clip with ymin
2266 if (y1 <= y2 && y1 < axisy.min) {
2267 if (y2 < axisy.min)
2268 continue; // line segment is outside
2269 // compute new intersection point
2270 x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
2271 y1 = axisy.min;
2272 }
2273 else if (y2 <= y1 && y2 < axisy.min) {
2274 if (y1 < axisy.min)
2275 continue;
2276 x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
2277 y2 = axisy.min;
2278 }
2279
2280 // clip with ymax
2281 if (y1 >= y2 && y1 > axisy.max) {
2282 if (y2 > axisy.max)
2283 continue;
2284 x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
2285 y1 = axisy.max;
2286 }
2287 else if (y2 >= y1 && y2 > axisy.max) {
2288 if (y1 > axisy.max)
2289 continue;
2290 x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
2291 y2 = axisy.max;
2292 }
2293
2294 // clip with xmin
2295 if (x1 <= x2 && x1 < axisx.min) {
2296 if (x2 < axisx.min)
2297 continue;
2298 y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
2299 x1 = axisx.min;
2300 }
2301 else if (x2 <= x1 && x2 < axisx.min) {
2302 if (x1 < axisx.min)
2303 continue;
2304 y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
2305 x2 = axisx.min;
2306 }
2307
2308 // clip with xmax
2309 if (x1 >= x2 && x1 > axisx.max) {
2310 if (x2 > axisx.max)
2311 continue;
2312 y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
2313 x1 = axisx.max;
2314 }
2315 else if (x2 >= x1 && x2 > axisx.max) {
2316 if (x1 > axisx.max)
2317 continue;
2318 y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
2319 x2 = axisx.max;
2320 }
2321
2322 if (x1 != prevx || y1 != prevy)
2323 ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
2324
2325 prevx = x2;
2326 prevy = y2;
2327 ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
2328 }
2329 ctx.stroke();
2330 }
2331
2332 function plotLineArea(datapoints, axisx, axisy) {
2333 var points = datapoints.points,
2334 ps = datapoints.pointsize,
2335 bottom = Math.min(Math.max(0, axisy.min), axisy.max),
2336 i = 0, top, areaOpen = false,
2337 ypos = 1, segmentStart = 0, segmentEnd = 0;
2338
2339 // we process each segment in two turns, first forward
2340 // direction to sketch out top, then once we hit the
2341 // end we go backwards to sketch the bottom
2342 while (true) {
2343 if (ps > 0 && i > points.length + ps)
2344 break;
2345
2346 i += ps; // ps is negative if going backwards
2347
2348 var x1 = points[i - ps],
2349 y1 = points[i - ps + ypos],
2350 x2 = points[i], y2 = points[i + ypos];
2351
2352 if (areaOpen) {
2353 if (ps > 0 && x1 != null && x2 == null) {
2354 // at turning point
2355 segmentEnd = i;
2356 ps = -ps;
2357 ypos = 2;
2358 continue;
2359 }
2360
2361 if (ps < 0 && i == segmentStart + ps) {
2362 // done with the reverse sweep
2363 ctx.fill();
2364 areaOpen = false;
2365 ps = -ps;
2366 ypos = 1;
2367 i = segmentStart = segmentEnd + ps;
2368 continue;
2369 }
2370 }
2371
2372 if (x1 == null || x2 == null)
2373 continue;
2374
2375 // clip x values
2376
2377 // clip with xmin
2378 if (x1 <= x2 && x1 < axisx.min) {
2379 if (x2 < axisx.min)
2380 continue;
2381 y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
2382 x1 = axisx.min;
2383 }
2384 else if (x2 <= x1 && x2 < axisx.min) {
2385 if (x1 < axisx.min)
2386 continue;
2387 y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
2388 x2 = axisx.min;
2389 }
2390
2391 // clip with xmax
2392 if (x1 >= x2 && x1 > axisx.max) {
2393 if (x2 > axisx.max)
2394 continue;
2395 y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
2396 x1 = axisx.max;
2397 }
2398 else if (x2 >= x1 && x2 > axisx.max) {
2399 if (x1 > axisx.max)
2400 continue;
2401 y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
2402 x2 = axisx.max;
2403 }
2404
2405 if (!areaOpen) {
2406 // open area
2407 ctx.beginPath();
2408 ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
2409 areaOpen = true;
2410 }
2411
2412 // now first check the case where both is outside
2413 if (y1 >= axisy.max && y2 >= axisy.max) {
2414 ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
2415 ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
2416 continue;
2417 }
2418 else if (y1 <= axisy.min && y2 <= axisy.min) {
2419 ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
2420 ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
2421 continue;
2422 }
2423
2424 // else it's a bit more complicated, there might
2425 // be a flat maxed out rectangle first, then a
2426 // triangular cutout or reverse; to find these
2427 // keep track of the current x values
2428 var x1old = x1, x2old = x2;
2429
2430 // clip the y values, without shortcutting, we
2431 // go through all cases in turn
2432
2433 // clip with ymin
2434 if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
2435 x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
2436 y1 = axisy.min;
2437 }
2438 else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
2439 x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
2440 y2 = axisy.min;
2441 }
2442
2443 // clip with ymax
2444 if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
2445 x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
2446 y1 = axisy.max;
2447 }
2448 else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
2449 x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
2450 y2 = axisy.max;
2451 }
2452
2453 // if the x value was changed we got a rectangle
2454 // to fill
2455 if (x1 != x1old) {
2456 ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
2457 // it goes to (x1, y1), but we fill that below
2458 }
2459
2460 // fill triangular section, this sometimes result
2461 // in redundant points if (x1, y1) hasn't changed
2462 // from previous line to, but we just ignore that
2463 ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
2464 ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
2465
2466 // fill the other rectangle if it's there
2467 if (x2 != x2old) {
2468 ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
2469 ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
2470 }
2471 }
2472 }
2473
2474 ctx.save();
2475 ctx.translate(plotOffset.left, plotOffset.top);
2476 ctx.lineJoin = "round";
2477
2478 var lw = series.lines.lineWidth,
2479 sw = series.shadowSize;
2480 // FIXME: consider another form of shadow when filling is turned on
2481 if (lw > 0 && sw > 0) {
2482 // draw shadow as a thick and thin line with transparency
2483 ctx.lineWidth = sw;
2484 ctx.strokeStyle = "rgba(0,0,0,0.1)";
2485 // position shadow at angle from the mid of line
2486 var angle = Math.PI/18;
2487 plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
2488 ctx.lineWidth = sw/2;
2489 plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
2490 }
2491
2492 ctx.lineWidth = lw;
2493 ctx.strokeStyle = series.color;
2494 var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
2495 if (fillStyle) {
2496 ctx.fillStyle = fillStyle;
2497 plotLineArea(series.datapoints, series.xaxis, series.yaxis);
2498 }
2499
2500 if (lw > 0)
2501 plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
2502 ctx.restore();
2503 }
2504
2505 function drawSeriesPoints(series) {
2506 function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
2507 var points = datapoints.points, ps = datapoints.pointsize;
2508
2509 for (var i = 0; i < points.length; i += ps) {
2510 var x = points[i], y = points[i + 1];
2511 if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
2512 continue;
2513
2514 ctx.beginPath();
2515 x = axisx.p2c(x);
2516 y = axisy.p2c(y) + offset;
2517 if (symbol == "circle")
2518 ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
2519 else
2520 symbol(ctx, x, y, radius, shadow);
2521 ctx.closePath();
2522
2523 if (fillStyle) {
2524 ctx.fillStyle = fillStyle;
2525 ctx.fill();
2526 }
2527 ctx.stroke();
2528 }
2529 }
2530
2531 ctx.save();
2532 ctx.translate(plotOffset.left, plotOffset.top);
2533
2534 var lw = series.points.lineWidth,
2535 sw = series.shadowSize,
2536 radius = series.points.radius,
2537 symbol = series.points.symbol;
2538
2539 // If the user sets the line width to 0, we change it to a very
2540 // small value. A line width of 0 seems to force the default of 1.
2541 // Doing the conditional here allows the shadow setting to still be
2542 // optional even with a lineWidth of 0.
2543
2544 if( lw == 0 )
2545 lw = 0.0001;
2546
2547 if (lw > 0 && sw > 0) {
2548 // draw shadow in two steps
2549 var w = sw / 2;
2550 ctx.lineWidth = w;
2551 ctx.strokeStyle = "rgba(0,0,0,0.1)";
2552 plotPoints(series.datapoints, radius, null, w + w/2, true,
2553 series.xaxis, series.yaxis, symbol);
2554
2555 ctx.strokeStyle = "rgba(0,0,0,0.2)";
2556 plotPoints(series.datapoints, radius, null, w/2, true,
2557 series.xaxis, series.yaxis, symbol);
2558 }
2559
2560 ctx.lineWidth = lw;
2561 ctx.strokeStyle = series.color;
2562 plotPoints(series.datapoints, radius,
2563 getFillStyle(series.points, series.color), 0, false,
2564 series.xaxis, series.yaxis, symbol);
2565 ctx.restore();
2566 }
2567
2568 function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
2569 var left, right, bottom, top,
2570 drawLeft, drawRight, drawTop, drawBottom,
2571 tmp;
2572
2573 // in horizontal mode, we start the bar from the left
2574 // instead of from the bottom so it appears to be
2575 // horizontal rather than vertical
2576 if (horizontal) {
2577 drawBottom = drawRight = drawTop = true;
2578 drawLeft = false;
2579 left = b;
2580 right = x;
2581 top = y + barLeft;
2582 bottom = y + barRight;
2583
2584 // account for negative bars
2585 if (right < left) {
2586 tmp = right;
2587 right = left;
2588 left = tmp;
2589 drawLeft = true;
2590 drawRight = false;
2591 }
2592 }
2593 else {
2594 drawLeft = drawRight = drawTop = true;
2595 drawBottom = false;
2596 left = x + barLeft;
2597 right = x + barRight;
2598 bottom = b;
2599 top = y;
2600
2601 // account for negative bars
2602 if (top < bottom) {
2603 tmp = top;
2604 top = bottom;
2605 bottom = tmp;
2606 drawBottom = true;
2607 drawTop = false;
2608 }
2609 }
2610
2611 // clip
2612 if (right < axisx.min || left > axisx.max ||
2613 top < axisy.min || bottom > axisy.max)
2614 return;
2615
2616 if (left < axisx.min) {
2617 left = axisx.min;
2618 drawLeft = false;
2619 }
2620
2621 if (right > axisx.max) {
2622 right = axisx.max;
2623 drawRight = false;
2624 }
2625
2626 if (bottom < axisy.min) {
2627 bottom = axisy.min;
2628 drawBottom = false;
2629 }
2630
2631 if (top > axisy.max) {
2632 top = axisy.max;
2633 drawTop = false;
2634 }
2635
2636 left = axisx.p2c(left);
2637 bottom = axisy.p2c(bottom);
2638 right = axisx.p2c(right);
2639 top = axisy.p2c(top);
2640
2641 // fill the bar
2642 if (fillStyleCallback) {
2643 c.fillStyle = fillStyleCallback(bottom, top);
2644 c.fillRect(left, top, right - left, bottom - top)
2645 }
2646
2647 // draw outline
2648 if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
2649 c.beginPath();
2650
2651 // FIXME: inline moveTo is buggy with excanvas
2652 c.moveTo(left, bottom);
2653 if (drawLeft)
2654 c.lineTo(left, top);
2655 else
2656 c.moveTo(left, top);
2657 if (drawTop)
2658 c.lineTo(right, top);
2659 else
2660 c.moveTo(right, top);
2661 if (drawRight)
2662 c.lineTo(right, bottom);
2663 else
2664 c.moveTo(right, bottom);
2665 if (drawBottom)
2666 c.lineTo(left, bottom);
2667 else
2668 c.moveTo(left, bottom);
2669 c.stroke();
2670 }
2671 }
2672
2673 function drawSeriesBars(series) {
2674 function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) {
2675 var points = datapoints.points, ps = datapoints.pointsize;
2676
2677 for (var i = 0; i < points.length; i += ps) {
2678 if (points[i] == null)
2679 continue;
2680 drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
2681 }
2682 }
2683
2684 ctx.save();
2685 ctx.translate(plotOffset.left, plotOffset.top);
2686
2687 // FIXME: figure out a way to add shadows (for instance along the right edge)
2688 ctx.lineWidth = series.bars.lineWidth;
2689 ctx.strokeStyle = series.color;
2690
2691 var barLeft;
2692
2693 switch (series.bars.align) {
2694 case "left":
2695 barLeft = 0;
2696 break;
2697 case "right":
2698 barLeft = -series.bars.barWidth;
2699 break;
2700 default:
2701 barLeft = -series.bars.barWidth / 2;
2702 }
2703
2704 var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
2705 plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis);
2706 ctx.restore();
2707 }
2708
2709 function getFillStyle(filloptions, seriesColor, bottom, top) {
2710 var fill = filloptions.fill;
2711 if (!fill)
2712 return null;
2713
2714 if (filloptions.fillColor)
2715 return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
2716
2717 var c = $.color.parse(seriesColor);
2718 c.a = typeof fill == "number" ? fill : 0.4;
2719 c.normalize();
2720 return c.toString();
2721 }
2722
2723 function insertLegend() {
2724
2725 if (options.legend.container != null) {
2726 $(options.legend.container).html("");
2727 } else {
2728 placeholder.find(".legend").remove();
2729 }
2730
2731 if (!options.legend.show) {
2732 return;
2733 }
2734
2735 var fragments = [], entries = [], rowStarted = false,
2736 lf = options.legend.labelFormatter, s, label;
2737
2738 // Build a list of legend entries, with each having a label and a color
2739
2740 for (var i = 0; i < series.length; ++i) {
2741 s = series[i];
2742 if (s.label) {
2743 label = lf ? lf(s.label, s) : s.label;
2744 if (label) {
2745 entries.push({
2746 label: label,
2747 color: s.color
2748 });
2749 }
2750 }
2751 }
2752
2753 // Sort the legend using either the default or a custom comparator
2754
2755 if (options.legend.sorted) {
2756 if ($.isFunction(options.legend.sorted)) {
2757 entries.sort(options.legend.sorted);
2758 } else if (options.legend.sorted == "reverse") {
2759 entries.reverse();
2760 } else {
2761 var ascending = options.legend.sorted != "descending";
2762 entries.sort(function(a, b) {
2763 return a.label == b.label ? 0 : (
2764 (a.label < b.label) != ascending ? 1 : -1 // Logical XOR
2765 );
2766 });
2767 }
2768 }
2769
2770 // Generate markup for the list of entries, in their final order
2771
2772 for (var i = 0; i < entries.length; ++i) {
2773
2774 var entry = entries[i];
2775
2776 if (i % options.legend.noColumns == 0) {
2777 if (rowStarted)
2778 fragments.push('</tr>');
2779 fragments.push('<tr>');
2780 rowStarted = true;
2781 }
2782
2783 fragments.push(
2784 '<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + entry.color + ';overflow:hidden"></div></div></td>' +
2785 '<td class="legendLabel">' + entry.label + '</td>'
2786 );
2787 }
2788
2789 if (rowStarted)
2790 fragments.push('</tr>');
2791
2792 if (fragments.length == 0)
2793 return;
2794
2795 var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
2796 if (options.legend.container != null)
2797 $(options.legend.container).html(table);
2798 else {
2799 var pos = "",
2800 p = options.legend.position,
2801 m = options.legend.margin;
2802 if (m[0] == null)
2803 m = [m, m];
2804 if (p.charAt(0) == "n")
2805 pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
2806 else if (p.charAt(0) == "s")
2807 pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
2808 if (p.charAt(1) == "e")
2809 pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
2810 else if (p.charAt(1) == "w")
2811 pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
2812 var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
2813 if (options.legend.backgroundOpacity != 0.0) {
2814 // put in the transparent background
2815 // separately to avoid blended labels and
2816 // label boxes
2817 var c = options.legend.backgroundColor;
2818 if (c == null) {
2819 c = options.grid.backgroundColor;
2820 if (c && typeof c == "string")
2821 c = $.color.parse(c);
2822 else
2823 c = $.color.extract(legend, 'background-color');
2824 c.a = 1;
2825 c = c.toString();
2826 }
2827 var div = legend.children();
2828 $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
2829 }
2830 }
2831 }
2832
2833
2834 // interactive features
2835
2836 var highlights = [],
2837 redrawTimeout = null;
2838
2839 // returns the data item the mouse is over, or null if none is found
2840 function findNearbyItem(mouseX, mouseY, seriesFilter) {
2841 var maxDistance = options.grid.mouseActiveRadius,
2842 smallestDistance = maxDistance * maxDistance + 1,
2843 item = null, foundPoint = false, i, j, ps;
2844
2845 for (i = series.length - 1; i >= 0; --i) {
2846 if (!seriesFilter(series[i]))
2847 continue;
2848
2849 var s = series[i],
2850 axisx = s.xaxis,
2851 axisy = s.yaxis,
2852 points = s.datapoints.points,
2853 mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
2854 my = axisy.c2p(mouseY),
2855 maxx = maxDistance / axisx.scale,
2856 maxy = maxDistance / axisy.scale;
2857
2858 ps = s.datapoints.pointsize;
2859 // with inverse transforms, we can't use the maxx/maxy
2860 // optimization, sadly
2861 if (axisx.options.inverseTransform)
2862 maxx = Number.MAX_VALUE;
2863 if (axisy.options.inverseTransform)
2864 maxy = Number.MAX_VALUE;
2865
2866 if (s.lines.show || s.points.show) {
2867 for (j = 0; j < points.length; j += ps) {
2868 var x = points[j], y = points[j + 1];
2869 if (x == null)
2870 continue;
2871
2872 // For points and lines, the cursor must be within a
2873 // certain distance to the data point
2874 if (x - mx > maxx || x - mx < -maxx ||
2875 y - my > maxy || y - my < -maxy)
2876 continue;
2877
2878 // We have to calculate distances in pixels, not in
2879 // data units, because the scales of the axes may be different
2880 var dx = Math.abs(axisx.p2c(x) - mouseX),
2881 dy = Math.abs(axisy.p2c(y) - mouseY),
2882 dist = dx * dx + dy * dy; // we save the sqrt
2883
2884 // use <= to ensure last point takes precedence
2885 // (last generally means on top of)
2886 if (dist < smallestDistance) {
2887 smallestDistance = dist;
2888 item = [i, j / ps];
2889 }
2890 }
2891 }
2892
2893 if (s.bars.show && !item) { // no other point can be nearby
2894
2895 var barLeft, barRight;
2896
2897 switch (s.bars.align) {
2898 case "left":
2899 barLeft = 0;
2900 break;
2901 case "right":
2902 barLeft = -s.bars.barWidth;
2903 break;
2904 default:
2905 barLeft = -s.bars.barWidth / 2;
2906 }
2907
2908 barRight = barLeft + s.bars.barWidth;
2909
2910 for (j = 0; j < points.length; j += ps) {
2911 var x = points[j], y = points[j + 1], b = points[j + 2];
2912 if (x == null)
2913 continue;
2914
2915 // for a bar graph, the cursor must be inside the bar
2916 if (series[i].bars.horizontal ?
2917 (mx <= Math.max(b, x) && mx >= Math.min(b, x) &&
2918 my >= y + barLeft && my <= y + barRight) :
2919 (mx >= x + barLeft && mx <= x + barRight &&
2920 my >= Math.min(b, y) && my <= Math.max(b, y)))
2921 item = [i, j / ps];
2922 }
2923 }
2924 }
2925
2926 if (item) {
2927 i = item[0];
2928 j = item[1];
2929 ps = series[i].datapoints.pointsize;
2930
2931 return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
2932 dataIndex: j,
2933 series: series[i],
2934 seriesIndex: i };
2935 }
2936
2937 return null;
2938 }
2939
2940 function onMouseMove(e) {
2941 if (options.grid.hoverable)
2942 triggerClickHoverEvent("plothover", e,
2943 function (s) { return s["hoverable"] != false; });
2944 }
2945
2946 function onMouseLeave(e) {
2947 if (options.grid.hoverable)
2948 triggerClickHoverEvent("plothover", e,
2949 function (s) { return false; });
2950 }
2951
2952 function onClick(e) {
2953 triggerClickHoverEvent("plotclick", e,
2954 function (s) { return s["clickable"] != false; });
2955 }
2956
2957 // trigger click or hover event (they send the same parameters
2958 // so we share their code)
2959 function triggerClickHoverEvent(eventname, event, seriesFilter) {
2960 var offset = eventHolder.offset(),
2961 canvasX = event.pageX - offset.left - plotOffset.left,
2962 canvasY = event.pageY - offset.top - plotOffset.top,
2963 pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
2964
2965 pos.pageX = event.pageX;
2966 pos.pageY = event.pageY;
2967
2968 var item = findNearbyItem(canvasX, canvasY, seriesFilter);
2969
2970 if (item) {
2971 // fill in mouse pos for any listeners out there
2972 item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10);
2973 item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10);
2974 }
2975
2976 if (options.grid.autoHighlight) {
2977 // clear auto-highlights
2978 for (var i = 0; i < highlights.length; ++i) {
2979 var h = highlights[i];
2980 if (h.auto == eventname &&
2981 !(item && h.series == item.series &&
2982 h.point[0] == item.datapoint[0] &&
2983 h.point[1] == item.datapoint[1]))
2984 unhighlight(h.series, h.point);
2985 }
2986
2987 if (item)
2988 highlight(item.series, item.datapoint, eventname);
2989 }
2990
2991 placeholder.trigger(eventname, [ pos, item ]);
2992 }
2993
2994 function triggerRedrawOverlay() {
2995 var t = options.interaction.redrawOverlayInterval;
2996 if (t == -1) { // skip event queue
2997 drawOverlay();
2998 return;
2999 }
3000
3001 if (!redrawTimeout)
3002 redrawTimeout = setTimeout(drawOverlay, t);
3003 }
3004
3005 function drawOverlay() {
3006 redrawTimeout = null;
3007
3008 // draw highlights
3009 octx.save();
3010 overlay.clear();
3011 octx.translate(plotOffset.left, plotOffset.top);
3012
3013 var i, hi;
3014 for (i = 0; i < highlights.length; ++i) {
3015 hi = highlights[i];
3016
3017 if (hi.series.bars.show)
3018 drawBarHighlight(hi.series, hi.point);
3019 else
3020 drawPointHighlight(hi.series, hi.point);
3021 }
3022 octx.restore();
3023
3024 executeHooks(hooks.drawOverlay, [octx]);
3025 }
3026
3027 function highlight(s, point, auto) {
3028 if (typeof s == "number")
3029 s = series[s];
3030
3031 if (typeof point == "number") {
3032 var ps = s.datapoints.pointsize;
3033 point = s.datapoints.points.slice(ps * point, ps * (point + 1));
3034 }
3035
3036 var i = indexOfHighlight(s, point);
3037 if (i == -1) {
3038 highlights.push({ series: s, point: point, auto: auto });
3039
3040 triggerRedrawOverlay();
3041 }
3042 else if (!auto)
3043 highlights[i].auto = false;
3044 }
3045
3046 function unhighlight(s, point) {
3047 if (s == null && point == null) {
3048 highlights = [];
3049 triggerRedrawOverlay();
3050 return;
3051 }
3052
3053 if (typeof s == "number")
3054 s = series[s];
3055
3056 if (typeof point == "number") {
3057 var ps = s.datapoints.pointsize;
3058 point = s.datapoints.points.slice(ps * point, ps * (point + 1));
3059 }
3060
3061 var i = indexOfHighlight(s, point);
3062 if (i != -1) {
3063 highlights.splice(i, 1);
3064
3065 triggerRedrawOverlay();
3066 }
3067 }
3068
3069 function indexOfHighlight(s, p) {
3070 for (var i = 0; i < highlights.length; ++i) {
3071 var h = highlights[i];
3072 if (h.series == s && h.point[0] == p[0]
3073 && h.point[1] == p[1])
3074 return i;
3075 }
3076 return -1;
3077 }
3078
3079 function drawPointHighlight(series, point) {
3080 var x = point[0], y = point[1],
3081 axisx = series.xaxis, axisy = series.yaxis,
3082 highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString();
3083
3084 if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
3085 return;
3086
3087 var pointRadius = series.points.radius + series.points.lineWidth / 2;
3088 octx.lineWidth = pointRadius;
3089 octx.strokeStyle = highlightColor;
3090 var radius = 1.5 * pointRadius;
3091 x = axisx.p2c(x);
3092 y = axisy.p2c(y);
3093
3094 octx.beginPath();
3095 if (series.points.symbol == "circle")
3096 octx.arc(x, y, radius, 0, 2 * Math.PI, false);
3097 else
3098 series.points.symbol(octx, x, y, radius, false);
3099 octx.closePath();
3100 octx.stroke();
3101 }
3102
3103 function drawBarHighlight(series, point) {
3104 var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(),
3105 fillStyle = highlightColor,
3106 barLeft;
3107
3108 switch (series.bars.align) {
3109 case "left":
3110 barLeft = 0;
3111 break;
3112 case "right":
3113 barLeft = -series.bars.barWidth;
3114 break;
3115 default:
3116 barLeft = -series.bars.barWidth / 2;
3117 }
3118
3119 octx.lineWidth = series.bars.lineWidth;
3120 octx.strokeStyle = highlightColor;
3121
3122 drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
3123 function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
3124 }
3125
3126 function getColorOrGradient(spec, bottom, top, defaultColor) {
3127 if (typeof spec == "string")
3128 return spec;
3129 else {
3130 // assume this is a gradient spec; IE currently only
3131 // supports a simple vertical gradient properly, so that's
3132 // what we support too
3133 var gradient = ctx.createLinearGradient(0, top, 0, bottom);
3134
3135 for (var i = 0, l = spec.colors.length; i < l; ++i) {
3136 var c = spec.colors[i];
3137 if (typeof c != "string") {
3138 var co = $.color.parse(defaultColor);
3139 if (c.brightness != null)
3140 co = co.scale('rgb', c.brightness);
3141 if (c.opacity != null)
3142 co.a *= c.opacity;
3143 c = co.toString();
3144 }
3145 gradient.addColorStop(i / (l - 1), c);
3146 }
3147
3148 return gradient;
3149 }
3150 }
3151 }
3152
3153 // Add the plot function to the top level of the jQuery object
3154
3155 $.plot = function(placeholder, data, options) {
3156 //var t0 = new Date();
3157 var plot = new Plot($(placeholder), data, options, $.plot.plugins);
3158 //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
3159 return plot;
3160 };
3161
3162 $.plot.version = "0.8.3";
3163
3164 $.plot.plugins = [];
3165
3166 // Also add the plot function as a chainable property
3167
3168 $.fn.plot = function(data, options) {
3169 return this.each(function() {
3170 $.plot(this, data, options);
3171 });
3172 };
3173
3174 // round to nearby lower multiple of base
3175 function floorInBase(n, base) {
3176 return base * Math.floor(n / base);
3177 }
3178
3179})(jQuery);
3180</script>
3181 <script type="application/javascript">/* Flot plugin for rendering pie charts.
3182
3183Copyright (c) 2007-2014 IOLA and Ole Laursen.
3184Licensed under the MIT license.
3185
3186The plugin assumes that each series has a single data value, and that each
3187value is a positive integer or zero. Negative numbers don't make sense for a
3188pie chart, and have unpredictable results. The values do NOT need to be
3189passed in as percentages; the plugin will calculate the total and per-slice
3190percentages internally.
3191
3192* Created by Brian Medendorp
3193
3194* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars
3195
3196The plugin supports these options:
3197
3198 series: {
3199 pie: {
3200 show: true/false
3201 radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto'
3202 innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect
3203 startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result
3204 tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show)
3205 offset: {
3206 top: integer value to move the pie up or down
3207 left: integer value to move the pie left or right, or 'auto'
3208 },
3209 stroke: {
3210 color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF')
3211 width: integer pixel width of the stroke
3212 },
3213 label: {
3214 show: true/false, or 'auto'
3215 formatter: a user-defined function that modifies the text/style of the label text
3216 radius: 0-1 for percentage of fullsize, or a specified pixel length
3217 background: {
3218 color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000')
3219 opacity: 0-1
3220 },
3221 threshold: 0-1 for the percentage value at which to hide labels (if they're too small)
3222 },
3223 combine: {
3224 threshold: 0-1 for the percentage value at which to combine slices (if they're too small)
3225 color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined
3226 label: any text value of what the combined slice should be labeled
3227 }
3228 highlight: {
3229 opacity: 0-1
3230 }
3231 }
3232 }
3233
3234More detail and specific examples can be found in the included HTML file.
3235
3236*/
3237
3238(function($) {
3239
3240 // Maximum redraw attempts when fitting labels within the plot
3241
3242 var REDRAW_ATTEMPTS = 10;
3243
3244 // Factor by which to shrink the pie when fitting labels within the plot
3245
3246 var REDRAW_SHRINK = 0.95;
3247
3248 function init(plot) {
3249
3250 var canvas = null,
3251 target = null,
3252 options = null,
3253 maxRadius = null,
3254 centerLeft = null,
3255 centerTop = null,
3256 processed = false,
3257 ctx = null;
3258
3259 // interactive variables
3260
3261 var highlights = [];
3262
3263 // add hook to determine if pie plugin in enabled, and then perform necessary operations
3264
3265 plot.hooks.processOptions.push(function(plot, options) {
3266 if (options.series.pie.show) {
3267
3268 options.grid.show = false;
3269
3270 // set labels.show
3271
3272 if (options.series.pie.label.show == "auto") {
3273 if (options.legend.show) {
3274 options.series.pie.label.show = false;
3275 } else {
3276 options.series.pie.label.show = true;
3277 }
3278 }
3279
3280 // set radius
3281
3282 if (options.series.pie.radius == "auto") {
3283 if (options.series.pie.label.show) {
3284 options.series.pie.radius = 3/4;
3285 } else {
3286 options.series.pie.radius = 1;
3287 }
3288 }
3289
3290 // ensure sane tilt
3291
3292 if (options.series.pie.tilt > 1) {
3293 options.series.pie.tilt = 1;
3294 } else if (options.series.pie.tilt < 0) {
3295 options.series.pie.tilt = 0;
3296 }
3297 }
3298 });
3299
3300 plot.hooks.bindEvents.push(function(plot, eventHolder) {
3301 var options = plot.getOptions();
3302 if (options.series.pie.show) {
3303 if (options.grid.hoverable) {
3304 eventHolder.unbind("mousemove").mousemove(onMouseMove);
3305 }
3306 if (options.grid.clickable) {
3307 eventHolder.unbind("click").click(onClick);
3308 }
3309 }
3310 });
3311
3312 plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) {
3313 var options = plot.getOptions();
3314 if (options.series.pie.show) {
3315 processDatapoints(plot, series, data, datapoints);
3316 }
3317 });
3318
3319 plot.hooks.drawOverlay.push(function(plot, octx) {
3320 var options = plot.getOptions();
3321 if (options.series.pie.show) {
3322 drawOverlay(plot, octx);
3323 }
3324 });
3325
3326 plot.hooks.draw.push(function(plot, newCtx) {
3327 var options = plot.getOptions();
3328 if (options.series.pie.show) {
3329 draw(plot, newCtx);
3330 }
3331 });
3332
3333 function processDatapoints(plot, series, datapoints) {
3334 if (!processed) {
3335 processed = true;
3336 canvas = plot.getCanvas();
3337 target = $(canvas).parent();
3338 options = plot.getOptions();
3339 plot.setData(combine(plot.getData()));
3340 }
3341 }
3342
3343 function combine(data) {
3344
3345 var total = 0,
3346 combined = 0,
3347 numCombined = 0,
3348 color = options.series.pie.combine.color,
3349 newdata = [];
3350
3351 // Fix up the raw data from Flot, ensuring the data is numeric
3352
3353 for (var i = 0; i < data.length; ++i) {
3354
3355 var value = data[i].data;
3356
3357 // If the data is an array, we'll assume that it's a standard
3358 // Flot x-y pair, and are concerned only with the second value.
3359
3360 // Note how we use the original array, rather than creating a
3361 // new one; this is more efficient and preserves any extra data
3362 // that the user may have stored in higher indexes.
3363
3364 if ($.isArray(value) && value.length == 1) {
3365 value = value[0];
3366 }
3367
3368 if ($.isArray(value)) {
3369 // Equivalent to $.isNumeric() but compatible with jQuery < 1.7
3370 if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) {
3371 value[1] = +value[1];
3372 } else {
3373 value[1] = 0;
3374 }
3375 } else if (!isNaN(parseFloat(value)) && isFinite(value)) {
3376 value = [1, +value];
3377 } else {
3378 value = [1, 0];
3379 }
3380
3381 data[i].data = [value];
3382 }
3383
3384 // Sum up all the slices, so we can calculate percentages for each
3385
3386 for (var i = 0; i < data.length; ++i) {
3387 total += data[i].data[0][1];
3388 }
3389
3390 // Count the number of slices with percentages below the combine
3391 // threshold; if it turns out to be just one, we won't combine.
3392
3393 for (var i = 0; i < data.length; ++i) {
3394 var value = data[i].data[0][1];
3395 if (value / total <= options.series.pie.combine.threshold) {
3396 combined += value;
3397 numCombined++;
3398 if (!color) {
3399 color = data[i].color;
3400 }
3401 }
3402 }
3403
3404 for (var i = 0; i < data.length; ++i) {
3405 var value = data[i].data[0][1];
3406 if (numCombined < 2 || value / total > options.series.pie.combine.threshold) {
3407 newdata.push(
3408 $.extend(data[i], { /* extend to allow keeping all other original data values
3409 and using them e.g. in labelFormatter. */
3410 data: [[1, value]],
3411 color: data[i].color,
3412 label: data[i].label,
3413 angle: value * Math.PI * 2 / total,
3414 percent: value / (total / 100)
3415 })
3416 );
3417 }
3418 }
3419
3420 if (numCombined > 1) {
3421 newdata.push({
3422 data: [[1, combined]],
3423 color: color,
3424 label: options.series.pie.combine.label,
3425 angle: combined * Math.PI * 2 / total,
3426 percent: combined / (total / 100)
3427 });
3428 }
3429
3430 return newdata;
3431 }
3432
3433 function draw(plot, newCtx) {
3434
3435 if (!target) {
3436 return; // if no series were passed
3437 }
3438
3439 var canvasWidth = plot.getPlaceholder().width(),
3440 canvasHeight = plot.getPlaceholder().height(),
3441 legendWidth = target.children().filter(".legend").children().width() || 0;
3442
3443 ctx = newCtx;
3444
3445 // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE!
3446
3447 // When combining smaller slices into an 'other' slice, we need to
3448 // add a new series. Since Flot gives plugins no way to modify the
3449 // list of series, the pie plugin uses a hack where the first call
3450 // to processDatapoints results in a call to setData with the new
3451 // list of series, then subsequent processDatapoints do nothing.
3452
3453 // The plugin-global 'processed' flag is used to control this hack;
3454 // it starts out false, and is set to true after the first call to
3455 // processDatapoints.
3456
3457 // Unfortunately this turns future setData calls into no-ops; they
3458 // call processDatapoints, the flag is true, and nothing happens.
3459
3460 // To fix this we'll set the flag back to false here in draw, when
3461 // all series have been processed, so the next sequence of calls to
3462 // processDatapoints once again starts out with a slice-combine.
3463 // This is really a hack; in 0.9 we need to give plugins a proper
3464 // way to modify series before any processing begins.
3465
3466 processed = false;
3467
3468 // calculate maximum radius and center point
3469
3470 maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2;
3471 centerTop = canvasHeight / 2 + options.series.pie.offset.top;
3472 centerLeft = canvasWidth / 2;
3473
3474 if (options.series.pie.offset.left == "auto") {
3475 if (options.legend.position.match("w")) {
3476 centerLeft += legendWidth / 2;
3477 } else {
3478 centerLeft -= legendWidth / 2;
3479 }
3480 if (centerLeft < maxRadius) {
3481 centerLeft = maxRadius;
3482 } else if (centerLeft > canvasWidth - maxRadius) {
3483 centerLeft = canvasWidth - maxRadius;
3484 }
3485 } else {
3486 centerLeft += options.series.pie.offset.left;
3487 }
3488
3489 var slices = plot.getData(),
3490 attempts = 0;
3491
3492 // Keep shrinking the pie's radius until drawPie returns true,
3493 // indicating that all the labels fit, or we try too many times.
3494
3495 do {
3496 if (attempts > 0) {
3497 maxRadius *= REDRAW_SHRINK;
3498 }
3499 attempts += 1;
3500 clear();
3501 if (options.series.pie.tilt <= 0.8) {
3502 drawShadow();
3503 }
3504 } while (!drawPie() && attempts < REDRAW_ATTEMPTS)
3505
3506 if (attempts >= REDRAW_ATTEMPTS) {
3507 clear();
3508 target.prepend("<div class='error'>Could not draw pie with labels contained inside canvas</div>");
3509 }
3510
3511 if (plot.setSeries && plot.insertLegend) {
3512 plot.setSeries(slices);
3513 plot.insertLegend();
3514 }
3515
3516 // we're actually done at this point, just defining internal functions at this point
3517
3518 function clear() {
3519 ctx.clearRect(0, 0, canvasWidth, canvasHeight);
3520 target.children().filter(".pieLabel, .pieLabelBackground").remove();
3521 }
3522
3523 function drawShadow() {
3524
3525 var shadowLeft = options.series.pie.shadow.left;
3526 var shadowTop = options.series.pie.shadow.top;
3527 var edge = 10;
3528 var alpha = options.series.pie.shadow.alpha;
3529 var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
3530
3531 if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) {
3532 return; // shadow would be outside canvas, so don't draw it
3533 }
3534
3535 ctx.save();
3536 ctx.translate(shadowLeft,shadowTop);
3537 ctx.globalAlpha = alpha;
3538 ctx.fillStyle = "#000";
3539
3540 // center and rotate to starting position
3541
3542 ctx.translate(centerLeft,centerTop);
3543 ctx.scale(1, options.series.pie.tilt);
3544
3545 //radius -= edge;
3546
3547 for (var i = 1; i <= edge; i++) {
3548 ctx.beginPath();
3549 ctx.arc(0, 0, radius, 0, Math.PI * 2, false);
3550 ctx.fill();
3551 radius -= i;
3552 }
3553
3554 ctx.restore();
3555 }
3556
3557 function drawPie() {
3558
3559 var startAngle = Math.PI * options.series.pie.startAngle;
3560 var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
3561
3562 // center and rotate to starting position
3563
3564 ctx.save();
3565 ctx.translate(centerLeft,centerTop);
3566 ctx.scale(1, options.series.pie.tilt);
3567 //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera
3568
3569 // draw slices
3570
3571 ctx.save();
3572 var currentAngle = startAngle;
3573 for (var i = 0; i < slices.length; ++i) {
3574 slices[i].startAngle = currentAngle;
3575 drawSlice(slices[i].angle, slices[i].color, true);
3576 }
3577 ctx.restore();
3578
3579 // draw slice outlines
3580
3581 if (options.series.pie.stroke.width > 0) {
3582 ctx.save();
3583 ctx.lineWidth = options.series.pie.stroke.width;
3584 currentAngle = startAngle;
3585 for (var i = 0; i < slices.length; ++i) {
3586 drawSlice(slices[i].angle, options.series.pie.stroke.color, false);
3587 }
3588 ctx.restore();
3589 }
3590
3591 // draw donut hole
3592
3593 drawDonutHole(ctx);
3594
3595 ctx.restore();
3596
3597 // Draw the labels, returning true if they fit within the plot
3598
3599 if (options.series.pie.label.show) {
3600 return drawLabels();
3601 } else return true;
3602
3603 function drawSlice(angle, color, fill) {
3604
3605 if (angle <= 0 || isNaN(angle)) {
3606 return;
3607 }
3608
3609 if (fill) {
3610 ctx.fillStyle = color;
3611 } else {
3612 ctx.strokeStyle = color;
3613 ctx.lineJoin = "round";
3614 }
3615
3616 ctx.beginPath();
3617 if (Math.abs(angle - Math.PI * 2) > 0.000000001) {
3618 ctx.moveTo(0, 0); // Center of the pie
3619 }
3620
3621 //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera
3622 ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false);
3623 ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false);
3624 ctx.closePath();
3625 //ctx.rotate(angle); // This doesn't work properly in Opera
3626 currentAngle += angle;
3627
3628 if (fill) {
3629 ctx.fill();
3630 } else {
3631 ctx.stroke();
3632 }
3633 }
3634
3635 function drawLabels() {
3636
3637 var currentAngle = startAngle;
3638 var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius;
3639
3640 for (var i = 0; i < slices.length; ++i) {
3641 if (slices[i].percent >= options.series.pie.label.threshold * 100) {
3642 if (!drawLabel(slices[i], currentAngle, i)) {
3643 return false;
3644 }
3645 }
3646 currentAngle += slices[i].angle;
3647 }
3648
3649 return true;
3650
3651 function drawLabel(slice, startAngle, index) {
3652
3653 if (slice.data[0][1] == 0) {
3654 return true;
3655 }
3656
3657 // format label text
3658
3659 var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter;
3660
3661 if (lf) {
3662 text = lf(slice.label, slice);
3663 } else {
3664 text = slice.label;
3665 }
3666
3667 if (plf) {
3668 text = plf(text, slice);
3669 }
3670
3671 var halfAngle = ((startAngle + slice.angle) + startAngle) / 2;
3672 var x = centerLeft + Math.round(Math.cos(halfAngle) * radius);
3673 var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt;
3674
3675 var html = "<span class='pieLabel' id='pieLabel" + index + "' style='position:absolute;top:" + y + "px;left:" + x + "px;'>" + text + "</span>";
3676 target.append(html);
3677
3678 var label = target.children("#pieLabel" + index);
3679 var labelTop = (y - label.height() / 2);
3680 var labelLeft = (x - label.width() / 2);
3681
3682 label.css("top", labelTop);
3683 label.css("left", labelLeft);
3684
3685 // check to make sure that the label is not outside the canvas
3686
3687 if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) {
3688 return false;
3689 }
3690
3691 if (options.series.pie.label.background.opacity != 0) {
3692
3693 // put in the transparent background separately to avoid blended labels and label boxes
3694
3695 var c = options.series.pie.label.background.color;
3696
3697 if (c == null) {
3698 c = slice.color;
3699 }
3700
3701 var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;";
3702 $("<div class='pieLabelBackground' style='position:absolute;width:" + label.width() + "px;height:" + label.height() + "px;" + pos + "background-color:" + c + ";'></div>")
3703 .css("opacity", options.series.pie.label.background.opacity)
3704 .insertBefore(label);
3705 }
3706
3707 return true;
3708 } // end individual label function
3709 } // end drawLabels function
3710 } // end drawPie function
3711 } // end draw function
3712
3713 // Placed here because it needs to be accessed from multiple locations
3714
3715 function drawDonutHole(layer) {
3716 if (options.series.pie.innerRadius > 0) {
3717
3718 // subtract the center
3719
3720 layer.save();
3721 var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius;
3722 layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color
3723 layer.beginPath();
3724 layer.fillStyle = options.series.pie.stroke.color;
3725 layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
3726 layer.fill();
3727 layer.closePath();
3728 layer.restore();
3729
3730 // add inner stroke
3731
3732 layer.save();
3733 layer.beginPath();
3734 layer.strokeStyle = options.series.pie.stroke.color;
3735 layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
3736 layer.stroke();
3737 layer.closePath();
3738 layer.restore();
3739
3740 // TODO: add extra shadow inside hole (with a mask) if the pie is tilted.
3741 }
3742 }
3743
3744 //-- Additional Interactive related functions --
3745
3746 function isPointInPoly(poly, pt) {
3747 for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
3748 ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1]))
3749 && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0])
3750 && (c = !c);
3751 return c;
3752 }
3753
3754 function findNearbySlice(mouseX, mouseY) {
3755
3756 var slices = plot.getData(),
3757 options = plot.getOptions(),
3758 radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius,
3759 x, y;
3760
3761 for (var i = 0; i < slices.length; ++i) {
3762
3763 var s = slices[i];
3764
3765 if (s.pie.show) {
3766
3767 ctx.save();
3768 ctx.beginPath();
3769 ctx.moveTo(0, 0); // Center of the pie
3770 //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here.
3771 ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false);
3772 ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false);
3773 ctx.closePath();
3774 x = mouseX - centerLeft;
3775 y = mouseY - centerTop;
3776
3777 if (ctx.isPointInPath) {
3778 if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) {
3779 ctx.restore();
3780 return {
3781 datapoint: [s.percent, s.data],
3782 dataIndex: 0,
3783 series: s,
3784 seriesIndex: i
3785 };
3786 }
3787 } else {
3788
3789 // excanvas for IE doesn;t support isPointInPath, this is a workaround.
3790
3791 var p1X = radius * Math.cos(s.startAngle),
3792 p1Y = radius * Math.sin(s.startAngle),
3793 p2X = radius * Math.cos(s.startAngle + s.angle / 4),
3794 p2Y = radius * Math.sin(s.startAngle + s.angle / 4),
3795 p3X = radius * Math.cos(s.startAngle + s.angle / 2),
3796 p3Y = radius * Math.sin(s.startAngle + s.angle / 2),
3797 p4X = radius * Math.cos(s.startAngle + s.angle / 1.5),
3798 p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5),
3799 p5X = radius * Math.cos(s.startAngle + s.angle),
3800 p5Y = radius * Math.sin(s.startAngle + s.angle),
3801 arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]],
3802 arrPoint = [x, y];
3803
3804 // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt?
3805
3806 if (isPointInPoly(arrPoly, arrPoint)) {
3807 ctx.restore();
3808 return {
3809 datapoint: [s.percent, s.data],
3810 dataIndex: 0,
3811 series: s,
3812 seriesIndex: i
3813 };
3814 }
3815 }
3816
3817 ctx.restore();
3818 }
3819 }
3820
3821 return null;
3822 }
3823
3824 function onMouseMove(e) {
3825 triggerClickHoverEvent("plothover", e);
3826 }
3827
3828 function onClick(e) {
3829 triggerClickHoverEvent("plotclick", e);
3830 }
3831
3832 // trigger click or hover event (they send the same parameters so we share their code)
3833
3834 function triggerClickHoverEvent(eventname, e) {
3835
3836 var offset = plot.offset();
3837 var canvasX = parseInt(e.pageX - offset.left);
3838 var canvasY = parseInt(e.pageY - offset.top);
3839 var item = findNearbySlice(canvasX, canvasY);
3840
3841 if (options.grid.autoHighlight) {
3842
3843 // clear auto-highlights
3844
3845 for (var i = 0; i < highlights.length; ++i) {
3846 var h = highlights[i];
3847 if (h.auto == eventname && !(item && h.series == item.series)) {
3848 unhighlight(h.series);
3849 }
3850 }
3851 }
3852
3853 // highlight the slice
3854
3855 if (item) {
3856 highlight(item.series, eventname);
3857 }
3858
3859 // trigger any hover bind events
3860
3861 var pos = { pageX: e.pageX, pageY: e.pageY };
3862 target.trigger(eventname, [pos, item]);
3863 }
3864
3865 function highlight(s, auto) {
3866 //if (typeof s == "number") {
3867 // s = series[s];
3868 //}
3869
3870 var i = indexOfHighlight(s);
3871
3872 if (i == -1) {
3873 highlights.push({ series: s, auto: auto });
3874 plot.triggerRedrawOverlay();
3875 } else if (!auto) {
3876 highlights[i].auto = false;
3877 }
3878 }
3879
3880 function unhighlight(s) {
3881 if (s == null) {
3882 highlights = [];
3883 plot.triggerRedrawOverlay();
3884 }
3885
3886 //if (typeof s == "number") {
3887 // s = series[s];
3888 //}
3889
3890 var i = indexOfHighlight(s);
3891
3892 if (i != -1) {
3893 highlights.splice(i, 1);
3894 plot.triggerRedrawOverlay();
3895 }
3896 }
3897
3898 function indexOfHighlight(s) {
3899 for (var i = 0; i < highlights.length; ++i) {
3900 var h = highlights[i];
3901 if (h.series == s)
3902 return i;
3903 }
3904 return -1;
3905 }
3906
3907 function drawOverlay(plot, octx) {
3908
3909 var options = plot.getOptions();
3910
3911 var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
3912
3913 octx.save();
3914 octx.translate(centerLeft, centerTop);
3915 octx.scale(1, options.series.pie.tilt);
3916
3917 for (var i = 0; i < highlights.length; ++i) {
3918 drawHighlight(highlights[i].series);
3919 }
3920
3921 drawDonutHole(octx);
3922
3923 octx.restore();
3924
3925 function drawHighlight(series) {
3926
3927 if (series.angle <= 0 || isNaN(series.angle)) {
3928 return;
3929 }
3930
3931 //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString();
3932 octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor
3933 octx.beginPath();
3934 if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) {
3935 octx.moveTo(0, 0); // Center of the pie
3936 }
3937 octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false);
3938 octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false);
3939 octx.closePath();
3940 octx.fill();
3941 }
3942 }
3943 } // end init (plugin body)
3944
3945 // define pie specific options and their default values
3946
3947 var options = {
3948 series: {
3949 pie: {
3950 show: false,
3951 radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value)
3952 innerRadius: 0, /* for donut */
3953 startAngle: 3/2,
3954 tilt: 1,
3955 shadow: {
3956 left: 5, // shadow left offset
3957 top: 15, // shadow top offset
3958 alpha: 0.02 // shadow alpha
3959 },
3960 offset: {
3961 top: 0,
3962 left: "auto"
3963 },
3964 stroke: {
3965 color: "#fff",
3966 width: 1
3967 },
3968 label: {
3969 show: "auto",
3970 formatter: function(label, slice) {
3971 return "<div style='font-size:x-small;text-align:center;padding:2px;color:" + slice.color + ";'>" + label + "<br/>" + Math.round(slice.percent) + "%</div>";
3972 }, // formatter function
3973 radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value)
3974 background: {
3975 color: null,
3976 opacity: 0
3977 },
3978 threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow)
3979 },
3980 combine: {
3981 threshold: -1, // percentage at which to combine little slices into one larger slice
3982 color: null, // color to give the new slice (auto-generated if null)
3983 label: "Other" // label to give the new slice
3984 },
3985 highlight: {
3986 //color: "#fff", // will add this functionality once parseColor is available
3987 opacity: 0.5
3988 }
3989 }
3990 }
3991 };
3992
3993 $.plot.plugins.push({
3994 init: init,
3995 options: options,
3996 name: "pie",
3997 version: "1.1"
3998 });
3999
4000})(jQuery);
4001</script>
4002 <script type="application/javascript">/* Flot plugin for automatically redrawing plots as the placeholder resizes.
4003
4004Copyright (c) 2007-2014 IOLA and Ole Laursen.
4005Licensed under the MIT license.
4006
4007It works by listening for changes on the placeholder div (through the jQuery
4008resize event plugin) - if the size changes, it will redraw the plot.
4009
4010There are no options. If you need to disable the plugin for some plots, you
4011can just fix the size of their placeholders.
4012
4013*/
4014
4015/* Inline dependency:
4016 * jQuery resize event - v1.1 - 3/14/2010
4017 * http://benalman.com/projects/jquery-resize-plugin/
4018 *
4019 * Copyright (c) 2010 "Cowboy" Ben Alman
4020 * Dual licensed under the MIT and GPL licenses.
4021 * http://benalman.com/about/license/
4022 */
4023(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this);
4024
4025(function ($) {
4026 var options = { }; // no options
4027
4028 function init(plot) {
4029 function onResize() {
4030 var placeholder = plot.getPlaceholder();
4031
4032 // somebody might have hidden us and we can't plot
4033 // when we don't have the dimensions
4034 if (placeholder.width() == 0 || placeholder.height() == 0)
4035 return;
4036
4037 plot.resize();
4038 plot.setupGrid();
4039 plot.draw();
4040 }
4041
4042 function bindEvents(plot, eventHolder) {
4043 plot.getPlaceholder().resize(onResize);
4044 }
4045
4046 function shutdown(plot, eventHolder) {
4047 plot.getPlaceholder().unbind("resize", onResize);
4048 }
4049
4050 plot.hooks.bindEvents.push(bindEvents);
4051 plot.hooks.shutdown.push(shutdown);
4052 }
4053
4054 $.plot.plugins.push({
4055 init: init,
4056 options: options,
4057 name: 'resize',
4058 version: '1.0'
4059 });
4060})(jQuery);
4061</script>
4062 <script type="application/javascript">
4063 $(document).ready(function() {
4064 var row = 0;
4065 var MINOR_AUTHOR_PERCENTAGE = 1.00;
4066 var isReversed = false;
4067
4068 var colorRows = function() {
4069 $(this).removeClass("odd");
4070
4071 if (row++ % 2 == 1) {
4072 $(this).addClass("odd");
4073 }
4074
4075 if(this == $(this).parent().find("tr:visible").get(-1)) {
4076 row = 0;
4077 }
4078 }
4079
4080 // Fix header and set it to the right width.
4081 var remainingHeaderWidth = ($("div.logo").width() - 4) - ($("div.logo img").innerWidth() + 48)
4082 $("div.logo p").css("width", remainingHeaderWidth);
4083
4084 var filterResponsibilities = function() {
4085 $("table#blame tbody tr td:last-child").filter(function() {
4086 return parseFloat(this.innerHTML) < MINOR_AUTHOR_PERCENTAGE;
4087 }).parent().find("td:first-child").each(function() {
4088 $("div#responsibilities div h3:contains(\"" + $(this).text() + "\")").parent().hide();
4089 });
4090 }
4091
4092 var filterTimeLine = function() {
4093 $("div#timeline table.git tbody tr").filter(function() {
4094 return $(this).find("td:has(div)").length == 0;
4095 }).hide();
4096 }
4097
4098 $("table#changes tbody tr td:last-child").filter(function() {
4099 return parseFloat(this.innerHTML) < MINOR_AUTHOR_PERCENTAGE;
4100 }).parent().hide();
4101
4102 $("table#blame tbody tr td:last-child").filter(function() {
4103 return parseFloat(this.innerHTML) < MINOR_AUTHOR_PERCENTAGE;
4104 }).parent().hide();
4105
4106 $("table.git tbody tr:visible").each(colorRows);
4107
4108 $("table#changes, table#blame").tablesorter({
4109 sortList: [[0,0]],
4110 headers: {
4111 0: { sorter: "text" }
4112 }
4113 }).bind("sortEnd", function() {
4114 $(this).find("tbody tr:visible").each(colorRows);
4115 });
4116
4117 $("table#changes thead tr th, table#blame thead tr th").click(function() {
4118 $(this).parent().find("th strong").remove();
4119 var parentIndex = $(this).index();
4120
4121 if (this.isReversed) {
4122 $(this).append("<strong> ∧</strong>");
4123 } else {
4124 $(this).append("<strong> ∨</strong>");
4125 }
4126 this.isReversed = !this.isReversed;
4127 });
4128
4129 $("table#changes thead tr th:first-child, table#blame thead tr th:first-child").each(function() {
4130 this.isReversed = true;
4131 $(this).append("<strong> ∨</strong>");
4132 });
4133
4134 $("table.git tfoot tr td:first-child").filter(function() {
4135 this.hiddenCount = $(this).parent().parent().parent().find("tbody tr:hidden").length;
4136 return this.hiddenCount > 0;
4137 }).each(function() {
4138 $(this).addClass("hoverable");
4139 this.innerHTML = "Show minor authors (" + this.hiddenCount + ") ∨";
4140 }).click(function() {
4141 this.clicked = !this.clicked;
4142
4143 if (this.clicked) {
4144 this.innerHTML = "Hide minor authors (" + this.hiddenCount + ") ∧";
4145 $(this).parent().parent().parent().find("tbody tr").show().each(colorRows);
4146 } else {
4147 this.innerHTML = "Show minor authors (" + this.hiddenCount + ") ∨";
4148 $(this).parent().parent().parent().find("tbody tr td:last-child").filter(function() {
4149 return parseFloat(this.innerHTML) < MINOR_AUTHOR_PERCENTAGE;
4150 }).parent().hide();
4151 $("table.git tbody tr:visible").each(colorRows);
4152 }
4153 });
4154
4155 filterResponsibilities();
4156 var hiddenResponsibilitiesCount = $("div#responsibilities div h3:hidden").length;
4157 if (hiddenResponsibilitiesCount > 0) {
4158 $("div#responsibilities div h3:visible").each(colorRows);
4159 $("div#responsibilities").prepend("<div class=\"button\">Show minor authors (" + hiddenResponsibilitiesCount + ") ∨</div>");
4160
4161 $("div#responsibilities div.button").click(function() {
4162 this.clicked = !this.clicked;
4163 if (this.clicked) {
4164 this.innerHTML = "Hide minor authors (" + hiddenResponsibilitiesCount + ") ∧";
4165 $("div#responsibilities div").show();
4166 } else {
4167 this.innerHTML = "Show minor authors (" + hiddenResponsibilitiesCount + ") ∨";
4168 filterResponsibilities();
4169 }
4170 });
4171 }
4172
4173
4174 filterTimeLine();
4175 var hiddenTimelineCount = $("div#timeline table.git tbody tr:hidden").length;
4176 if (hiddenTimelineCount > 0) {
4177 $("div#timeline table.git tbody tr:visible").each(colorRows);
4178 $("div#timeline").prepend("<div class=\"button\">Show rows with minor work (" + hiddenTimelineCount + ") ∨</div>");
4179
4180 $("div#timeline div.button").click(function() {
4181 this.clicked = !this.clicked;
4182 if (this.clicked) {
4183 this.innerHTML = "Hide rows with minor work (" + hiddenTimelineCount + ") ∧";
4184 $("div#timeline table.git tbody tr").show().each(colorRows);
4185 } else {
4186 this.innerHTML = "Show rows with minor work (" + hiddenTimelineCount + ") ∨";
4187 filterTimeLine();
4188 $("div#timeline table.git tbody tr:visible").each(colorRows);
4189 }
4190 });
4191 }
4192
4193 $("#blame_chart, #changes_chart").bind("plothover", function(event, pos, obj) {
4194 if (obj) {
4195 var selection = "table tbody tr td:contains(\"" + obj.series.label + "\")";
4196 var element = $(this).parent().find(selection);
4197
4198 if (element) {
4199 if (this.hoveredElement && this.hoveredElement.html() != element.parent().html()) {
4200 this.hoveredElement.removeClass("piehover");
4201 }
4202
4203 element.parent().addClass("piehover");
4204 this.hoveredElement = element.parent();
4205 }
4206 } else if (this.hoveredElement) {
4207 this.hoveredElement.removeClass("piehover");
4208 }
4209 });
4210
4211 // Make sure the two pie charts use the same colors.
4212
4213 var author_colors = {};
4214 $.each(changes_plot.getData(), function(i, v) {
4215 author_colors[v["label"]] = v["color"];
4216 });
4217
4218 $.each(blame_plot.getData(), function(i, v) {
4219 if (author_colors[v["label"]] != undefined) {
4220 v["color"] = author_colors[v["label"]];
4221 }
4222 });
4223
4224 blame_plot.setupGrid();
4225 blame_plot.draw();
4226
4227 // Color in metrics levels.
4228
4229 $("div#metrics div div").each(function() {
4230 var rgb = $(this).css("background-color").match(/\d+/g);
4231 rgb[0] = parseInt(rgb[0]);
4232 rgb[1] = parseInt(rgb[1]);
4233 rgb[2] = parseInt(rgb[2]);
4234
4235 if ($(this).hasClass("minimal")) {
4236 rgb[0] -= 10;
4237 rgb[1] += 10;
4238 rgb[2] -= 10;
4239 } else if ($(this).hasClass("minor")) {
4240 rgb[1] += 10;
4241 } else if ($(this).hasClass("medium")) {
4242 rgb[0] += 10;
4243 rgb[1] += 10;
4244 } else if ($(this).hasClass("bad")) {
4245 rgb[0] += 10;
4246 rgb[1] -= 10;
4247 rgb[2] -= 10;
4248 } else if ($(this).hasClass("severe")) {
4249 rgb[0] += 20;
4250 rgb[1] -= 20;
4251 rgb[2] -= 20;
4252 }
4253
4254 $(this).css("background-color", "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")");
4255 });
4256 });
4257 </script>
4258 <style type="text/css">
4259 body {
4260 background: -webkit-linear-gradient(left, #8f8a9a, #dad2d7, #8f8a9a);
4261 background: -moz-linear-gradient(left, #8f8a9a, #dad2d7, #8f8a9a);
4262 }
4263 html, body {
4264 margin: 0;
4265 font-family: "Arial";
4266 }
4267 body > div {
4268 margin: 0 auto;
4269 width: 58em;
4270 }
4271 div.box {
4272 border: 4px solid #ddd;
4273 background-color: #eee;
4274 margin: 0.75em;
4275 padding: 5px;
4276 font-size: small;
4277 border-radius: 15px;
4278 -moz-border-radius: 15px;
4279 box-shadow: 1px 1px 3px #666;
4280 -moz-box-shadow: 1px 1px 3px #666;
4281 }
4282 div.logo p {
4283 width: 60em;
4284 display:inline-block;
4285 vertical-align:middle;
4286 }
4287 div.logo img {
4288 vertical-align:middle;
4289 padding: 2px 10px 2px 2px;
4290 }
4291 body > div {
4292 display: block-inline;
4293 }
4294 body > div > div > div {
4295 position: relative;
4296 width: 100%;
4297 min-height: 140px;
4298 }
4299 table.git {
4300 font-size: small;
4301 width: 65%;
4302 padding-right: 5px;
4303 }
4304 table.full {
4305 width: 100%;
4306 }
4307 table.git th, table.git tfoot tr td {
4308 padding: 0.3em;
4309 background-color: #ddcece;
4310 border-radius: 8px 8px 0px 0px;
4311 -moz-border-radius: 8px 8px 0px 0px;
4312 }
4313 table#changes thead tr th, table#blame thead tr th, table.git tfoot tr td {
4314 border: 1px solid #eee;
4315 text-align: center;
4316 }
4317 table.git tfoot tr td {
4318 border-radius: 0px 0px 8px 8px;
4319 -moz-border-radius: 0px 0px 8px 8px;
4320 text-align: center;
4321 }
4322 table.git td, table.git th, table#timeline td, table#timeline th {
4323 padding: 0.35em;
4324 height: 2em;
4325 }
4326 table.git td div.insert {
4327 background-color: #7a7;
4328 }
4329 table.git td div.remove {
4330 background-color: #c66;
4331 }
4332 table.git td div.insert, table.git td div.remove {
4333 height: 100%;
4334 float: left;
4335 }
4336 table.git tr.odd {
4337 background-color: #dbdbdb;
4338 }
4339 table.git tr.piehover {
4340 background-color: #dddece;
4341 }
4342 div.chart {
4343 position: absolute;
4344 top: 5px;
4345 bottom: 5px;
4346 right: 0px;
4347 width: 35%;
4348 min-height: 100px;
4349 max-height: 210px;
4350 font-size: x-small;
4351 }
4352 p.error {
4353 color: #700;
4354 }
4355 table#changes thead tr th:hover, table#blame thead tr th:hover,
4356 table#changes tfoot tr td.hoverable:hover, table#blame tfoot tr td.hoverable:hover,
4357 div.button:hover, div#responsibilities div.button:hover {
4358 background-color: #eddede;
4359 border: 1px solid #bbb;
4360 cursor: hand;
4361 }
4362 div#responsibilities div, div#responsibilities div div, div#metrics div, div#metrics div div {
4363 min-height: 0px;
4364 padding: 0.5em 0.2em;
4365 width: auto;
4366 }
4367 div#metrics div {
4368 background-color: #eee;
4369 }
4370 div#responsibilities div.odd, div#metrics div.odd {
4371 background-color: #dbdbdb;
4372 }
4373 div#responsibilities p {
4374 margin-bottom: 0px;
4375 }
4376 td img, h3 img {
4377 border-radius: 3px 3px 3px 3px;
4378 -moz-border-radius: 3px 3px 3px 3px;
4379 vertical-align: middle;
4380 margin-right: 0.4em;
4381 opacity: 0.85;
4382 }
4383 td img {
4384 width: 20px;
4385 height: 20px;
4386 }
4387 h3 img {
4388 width: 32px;
4389 height: 32px;
4390 }
4391 h3, h4 {
4392 border-radius: 8px 8px 8px 8px;
4393 -moz-border-radius: 8px 8px 8px 8px;
4394 background-color: #ddcece;
4395 margin-bottom: 0.2em;
4396 margin-top: 0.6em;
4397 }
4398 h4 {
4399 margin-top: 0.2em;
4400 padding: 0.5em;
4401 }
4402 div.button, div#responsibilities div.button {
4403 border-radius: 8px 8px 8px 8px;
4404 -moz-border-radius: 8px 8px 8px 8px;
4405 border: 1px solid #eee;
4406 float: right;
4407 width: auto;
4408 padding: 0.5em;
4409 background-color: #ddcece;
4410 min-height: 0;
4411 }
4412 </style>
4413 </head>
4414 <body>
4415 <div><div class="box logo">
4416 <a href="https://github.com/ejwa/gitinspector"><img src="data:image/png;base64,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jLWufKctGoWnIw4AcGOjWN/y1tdsx6GxKtsJ7HPao0+f886GAQBQtmmvw4RQvUhb7HjaWFiBVcalOXiYHvK3AWhLOm5ICXL8QpQ9tVQOjkDQst3kjljKgheKswFgxfJHApMnVFxNgF8wAMqfusPPawTJ4Lcx9PdmlFLG72y/fs8UtE6wPpJ5rHMWn6z9yyKsZNk/gDMeMJhiEvVN0fR/wouoJzEUz7x8dUdqxVlH3rspmvJ3xoqHOIVkUgC+esbU4b2wmn7vso5tt332juTE8AP+aHqnz7Srh6oE2Qs7D09dHikMB4ihlaGYUkwoCCRoBhzLhhxayA/GqfK9hSMoMEDu1IjDb9/4StNyeNnEeK7Mond3vTGetu7ZNfLHjhQAHPPgX5dt/dZn25Jh9yfJgDgOJG2wNgCG1iVqnB2XA4asMOKOC8+OjHw2rcHsfjNeG5KO4xzQGOC4kZ0sRHAc7Wqt9e40/fILyzc8AyCBMqdAx4ti5NA718yf35exjaRjKrcEgXABSGB520DL0qaOGEbODWP395dMi/X0TG0diAciSbPk32f5pPBBW6ztV03t/fD6Z2K8hxj/Hwcf/wvPBJZ+9MswgwAAADt0RVh0QXV0aG9yAEFkYW0gV2FsZGVuYmVyZywgYmFzZWQgb24gdGhlIEdpdCBMb2dvIGJ5IEphc29uIExvbmdbAluwAAAAJWlUWHRDb3B5cmlnaHQAAAAAAMKpIDIwMTUgQWRhbSBXYWxkZW5iZXJn6lGO2AAAAsJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0n77u/JyBpZD0nVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkJz8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0nYWRvYmU6bnM6bWV0YS8nIHg6eG1wdGs9J0ltYWdlOjpFeGlmVG9vbCA5LjI3Jz4KPHJkZjpSREYgeG1sbnM6cmRmPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjJz4KCiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0nJwogIHhtbG5zOmNjPSdodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMnPgogIDxjYzpsaWNlbnNlPkNyZWF0aXZlIENvbW1vbnMgQXR0cmlidXRpb24gMy4wIFVucG9ydGVkIExpY2Vuc2UuIFRoaXMgbGljZW5zZSBsZXRzIG90aGVycyBkaXN0cmlidXRlLCByZW1peCwgdHdlYWssIGFuZCBidWlsZCB1cG9uIHlvdXIgd29yaywgZXZlbiBjb21tZXJjaWFsbHksIGFzIGxvbmcgYXMgdGhleSBjcmVkaXQgeW91IGZvciB0aGUgb3JpZ2luYWwgY3JlYXRpb24uIFRoaXMgaXMgdGhlIG1vc3QgYWNjb21tb2RhdGluZyBvZiB0aGUgQ0MgbGljZW5zZXMgb2ZmZXJlZC4gUmVjb21tZW5kZWQgZm9yIG1heGltdW0gZGlzc2VtaW5hdGlvbiBhbmQgdXNlIG9mIGxpY2Vuc2VkIG1hdGVyaWFscy48L2NjOmxpY2Vuc2U+CiA8L3JkZjpEZXNjcmlwdGlvbj4KPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0ncic/PtC9Q+UAAAAASUVORK5CYII=" /></a>
4417 <p>Statistical information for the repository 'image-shutter' was gathered on 2019/04/16.<br>The output has been generated by <a href="https://github.com/ejwa/gitinspector">gitinspector</a> 0.5.0dev. The statistical analysis tool for git repositories.</p>
4418 </div></div>
4419
4420<div><div class="box"><p>The following historical commit information, by author, was found.</p><div><table id="changes" class="git"><thead><tr> <th>Author</th> <th>Commits</th> <th>Insertions</th> <th>Deletions</th> <th>% of changes</th></tr></thead><tbody><tr ><td><img src="https://www.gravatar.com/avatar/edf40ca4e849fe30d132544d0f716374?default=identicon&size=20"/>Florian Laborde</td><td>13</td><td>526</td><td>58</td><td>61.28</td></tr><tr class="odd"><td><img src="https://www.gravatar.com/avatar/d0021ffd8e95a1fa329db2139323a807?default=identicon&size=20"/>afinck</td><td>2</td><td>140</td><td>21</td><td>16.89</td></tr><tr ><td><img src="https://www.gravatar.com/avatar/61775f15bd39d3835fed0e42e5de68de?default=identicon&size=20"/>antoinefinck</td><td>7</td><td>155</td><td>53</td><td>21.83</td></tr><tfoot><tr> <td colspan="5"> </td> </tr></tfoot></tbody></table><div class="chart" id="changes_chart"></div></div><script type="text/javascript"> changes_plot = $.plot($("#changes_chart"), [{label: "Florian Laborde", data: 61.28}, {label: "afinck", data: 16.89}, {label: "antoinefinck", data: 21.83}], { series: { pie: { innerRadius: 0.4, show: true, combine: { threshold: 0.01, label: "Minor Authors" } } }, grid: { hoverable: true } });</script></div></div>
4421<div><div class="box"><p>Below are the number of rows from each author that have survived and are still intact in the current revision.</p><div><table id="blame" class="git"><thead><tr> <th>Author</th> <th>Rows</th> <th>Stability</th> <th>Age</th> <th>% in comments</th> </tr></thead><tbody><tr ><td><img src="https://www.gravatar.com/avatar/edf40ca4e849fe30d132544d0f716374?default=identicon&size=20"/>Florian Laborde</td><td>449</td><td>85.4</td><td>0.5</td><td>18.49</td><td style="display: none">65.17</td></tr><tr class="odd"><td><img src="https://www.gravatar.com/avatar/d0021ffd8e95a1fa329db2139323a807?default=identicon&size=20"/>afinck</td><td>107</td><td>76.4</td><td>0.0</td><td>15.89</td><td style="display: none">15.53</td></tr><tr ><td><img src="https://www.gravatar.com/avatar/61775f15bd39d3835fed0e42e5de68de?default=identicon&size=20"/>antoinefinck</td><td>133</td><td>85.8</td><td>0.3</td><td>4.51</td><td style="display: none">19.30</td></tr><tfoot><tr> <td colspan="5"> </td> </tr></tfoot></tbody></table><div class="chart" id="blame_chart"></div></div><script type="text/javascript"> blame_plot = $.plot($("#blame_chart"), [{label: "Florian Laborde", data: 65.17}, {label: "afinck", data: 15.53}, {label: "antoinefinck", data: 19.30}], { series: { pie: { innerRadius: 0.4, show: true, combine: { threshold: 0.01, label: "Minor Authors" } } }, grid: { hoverable: true } });</script></div></div>
4422 </body>
4423</html>