diff --git a/classes/HighChart.php b/classes/HighChart.php index c4b3e88..1026a75 100644 --- a/classes/HighChart.php +++ b/classes/HighChart.php @@ -16,15 +16,21 @@ abstract class HighChart { // Chart title protected $_title = ''; + protected $_subtitle = ''; // Default chart size. protected $_height = '300'; - protected $_width = '575'; + protected $_width = '575'; + protected $_xmetric = ''; protected $_ymetric = ''; + protected $_tooltip = ''; // Colors to use for series private $_series_colours = array('AAACCC','E0E0E0','CCC888','EEEBBB','666CCC','888888'); + // Specific data types valid for chart (eg: stacked, bar, pie) + protected $_data_type = array(); + // Render the chart data in a json format abstract public function json(); @@ -37,10 +43,16 @@ abstract class HighChart { break; case 'height': $this->_height = array_shift($args); break; + case 'subtitle': $this->_subtitle = array_shift($args); + break; case 'title': $this->_title = array_shift($args); break; + case 'tooltip': $this->_tooltip = array_shift($args); + break; case 'width': $this->_width = array_shift($args); break; + case 'xmetric': $this->_xmetric = array_shift($args); + break; case 'ymetric': $this->_ymetric = array_shift($args); break; default: @@ -74,22 +86,52 @@ abstract class HighChart { return new $c(); } - public function series($type,$axis) { - if (! in_array($type,$this->_valid_axis)) - throw new Kohana_Exception('Type not allowed for Chart :type',array(':type'=>$type)); + /** + * Common required items for all graphs + * @todo: Call this in all sub-classes for consitency + */ + final protected function render_init() { + Script::add(array( + 'type'=>'file', + 'data'=>'media/js/highcharts.js', + )); + + Script::add(array( + 'type'=>'file', + 'data'=>'media/js/modules/exporting.js', + )); + } + + /** + * Common required items for all graphs + * @todo: Call this in all sub-classes for consitency + */ + final protected function render_post() { + return sprintf('
',$this->_divname); + } + + /** + * Add values to a series + * + * @param: string Series Type + * @param: string Series Name (used for the legend) + */ + public function series($type,$name) { + if ($this->_data_type AND ! in_array($type,$this->_data_type)) + throw new Kohana_Exception('Type not allowed for Chart (:type)',array(':type'=>$type)); $x = 'HighChart_Type_'.ucfirst($type); if (! class_exists($x)) throw new Kohana_Exception('Class does not exist :class',array(':class'=>$x)); - if (! isset($this->_series[$axis])) - $this->_series[$axis] = array(); + if (! isset($this->_series[$name])) + $this->_series[$name] = array(); - $c = count($this->_series[$axis]); + $c = count($this->_series[$name]); - $this->_series[$axis][$c] = new $x; + $this->_series[$name][$c] = new $x; - return $this->_series[$axis][$c]; + return $this->_series[$name][$c]; } /** @@ -100,7 +142,7 @@ abstract class HighChart { return NULL; static $colours = NULL; - + if (is_null($colours)) $colors = count($this->_series_colours); diff --git a/classes/HighChart/Bubble.php b/classes/HighChart/Bubble.php new file mode 100644 index 0000000..3ddb95d --- /dev/null +++ b/classes/HighChart/Bubble.php @@ -0,0 +1,91 @@ +_series,'order()',0); + + foreach ($this->_series as $axis => $datapoints) { + if ($output) + $output .= ','; + + $result['data'] = array(); + + foreach ($datapoints as $o) + array_push($result['data'],$o->as_array()); + + $result['name'] = $axis; + + $output .= json_encode($result); + } + + return '['.$output.']'; + } + + public function render() { + parent::render_init(); + + Script::add(array( + 'type'=>'file', + 'data'=>'media/js/highcharts-more.js', + )); + + Script::add(array( + 'type'=>'stdin', + 'data'=>" +$(document).ready(function() { + $('#".$this->_divname."').highcharts({ + chart: { + type: 'bubble', + height: ".$this->_height.", + width: ".$this->_width.", + }, + title: { + text: '".$this->_title."', + }, + subtitle: { + text: '".$this->_subtitle."', + }, + xAxis: { + title: { + text: '".$this->_xmetric."' + }, + }, + yAxis: { + title: { + text: '".$this->_ymetric."' + }, + },".($this->_tooltip ? " + tooltip: { + pointFormat: '".$this->_tooltip."', + }," : '')." + credits: { + enabled: false + }, + series: ".$this->json().", + }); +});", + )); + + return parent::render_post(); + } +} +?> diff --git a/classes/HighChart/Type.php b/classes/HighChart/Type.php index 54756eb..af2f101 100644 --- a/classes/HighChart/Type.php +++ b/classes/HighChart/Type.php @@ -10,7 +10,9 @@ * @license http://dev.leenooks.net/license.html */ abstract class HighChart_Type { - // The series data in the format X=Y, eg: "12-11"=>16057,... + // What axis are required when we receive data. + protected $_required_axis = array(); + // The series data in the format axis = value protected $_sdata = array(); protected $_sindex = NULL; protected $_soptions = array(); @@ -20,10 +22,15 @@ abstract class HighChart_Type { public function __call($name,$args) { switch ($name) { case 'data': + $args = array_shift($args); + + if (array_diff(array_keys($args),$this->_required_axis)) + throw new Kohana_Exception('Invalid data arguments for chart, expecting [:args]',array(':args'=>implode('|',$this->_required_axis))); + if (! $args) return $this->_sdata; - $this->_sdata = array_shift($args); + $this->_sdata = $args; break; case 'index': diff --git a/classes/HighChart/Type/Bubble.php b/classes/HighChart/Type/Bubble.php new file mode 100644 index 0000000..3520fe3 --- /dev/null +++ b/classes/HighChart/Type/Bubble.php @@ -0,0 +1,27 @@ +_sdata as $k=>$v) + if (in_array($k,array('x','y','z'))) + $result[$k] = $v; + + $result['name'] = $this->_stitle; + + return $result; + } +} diff --git a/media/js/adapters/standalone-framework.js b/media/js/adapters/standalone-framework.js index b36c4a4..fac4061 100644 --- a/media/js/adapters/standalone-framework.js +++ b/media/js/adapters/standalone-framework.js @@ -1,17 +1,18 @@ /* - Highcharts JS v3.0.7 (2013-10-24) + Highcharts JS v4.1.5 (2015-04-13) Standalone Highcharts Framework License: MIT License */ -var HighchartsAdapter=function(){function o(c){function a(a,b,d){a.removeEventListener(b,d,!1)}function d(a,b,d){d=a.HCProxiedMethods[d.toString()];a.detachEvent("on"+b,d)}function b(b,c){var f=b.HCEvents,i,g,k,j;if(b.removeEventListener)i=a;else if(b.attachEvent)i=d;else return;c?(g={},g[c]=!0):g=f;for(j in g)if(f[j])for(k=f[j].length;k--;)i(b,j,f[j][k])}c.HCExtended||Highcharts.extend(c,{HCExtended:!0,HCEvents:{},bind:function(b,a){var d=this,c=this.HCEvents,g;if(d.addEventListener)d.addEventListener(b, -a,!1);else if(d.attachEvent){g=function(b){a.call(d,b)};if(!d.HCProxiedMethods)d.HCProxiedMethods={};d.HCProxiedMethods[a.toString()]=g;d.attachEvent("on"+b,g)}c[b]===r&&(c[b]=[]);c[b].push(a)},unbind:function(c,h){var f,i;c?(f=this.HCEvents[c]||[],h?(i=HighchartsAdapter.inArray(h,f),i>-1&&(f.splice(i,1),this.HCEvents[c]=f),this.removeEventListener?a(this,c,h):this.attachEvent&&d(this,c,h)):(b(this,c),this.HCEvents[c]=[])):(b(this),this.HCEvents={})},trigger:function(b,a){var d=this.HCEvents[b]|| -[],c=d.length,g,k,j;k=function(){a.defaultPrevented=!0};for(g=0;gr(2*b.len,200)&&ka(19,!0),a=f?(b.getNonLinearTimeTicks||Eb)(Cb(b.tickInterval,
-d.units),b.min,b.max,d.startOfWeek,b.ordinalPositions,b.closestPointRange,!0):e?b.getLogTickPositions(b.tickInterval,b.min,b.max):b.getLinearTickPositions(b.tickInterval,b.min,b.max),q&&a.splice(1,a.length-2),b.tickPositions=a;if(!h)e=a[0],f=a[a.length-1],h=b.minPointOffset||0,d.startOnTick?b.min=e:b.min-h>e&&a.shift(),d.endOnTick?b.max=f:b.max+h
',pointFormat:"x: {point.x}
y: {point.y}
",followPointer:!0},stickyTracking:!1});la=ha(Q,{type:"scatter",sorted:!1,requireSorting:!1,noSharedTooltip:!0,trackerGroups:["markerGroup"],takeOrdinalPosition:!1,drawTracker:F.prototype.drawTracker,setTooltipPoints:oa});X.scatter=la;Z.pie=x(Y,{borderColor:"#FFFFFF",borderWidth:1,center:[null,null],clip:!1,
-colorByPoint:!0,dataLabels:{distance:30,enabled:!0,formatter:function(){return this.point.name}},ignoreHiddenPoint:!0,legendType:"point",marker:null,size:null,showInLegend:!1,slicedOffset:10,states:{hover:{brightness:0.1,shadow:!1}},stickyTracking:!1,tooltip:{followPointer:!0}});Y={type:"pie",isCartesian:!1,pointClass:ha(Pa,{init:function(){Pa.prototype.init.apply(this,arguments);var a=this,b;if(a.y<0)a.y=null;s(a,{visible:a.visible!==!1,name:o(a.name,"Slice")});b=function(b){a.slice(b.type==="select")};
-K(a,"select",b);K(a,"unselect",b);return a},setVisible:function(a){var b=this,c=b.series,d=c.chart,e;b.visible=b.options.visible=a=a===v?!b.visible:a;c.options.data[pa(b,c.data)]=b.options;e=a?"show":"hide";n(["graphic","dataLabel","connector","shadowGroup"],function(a){if(b[a])b[a][e]()});b.legendItem&&d.legend.colorizeItem(b,a);if(!c.isDirty&&c.options.ignoreHiddenPoint)c.isDirty=!0,d.redraw()},slice:function(a,b,c){var d=this.series;La(c,d.chart);o(b,!0);this.sliced=this.options.sliced=a=u(a)?
-a:!this.sliced;d.options.data[pa(this,d.data)]=this.options;a=a?this.slicedTranslation:{translateX:0,translateY:0};this.graphic.animate(a);this.shadowGroup&&this.shadowGroup.animate(a)}}),requireSorting:!1,noSharedTooltip:!0,trackerGroups:["group","dataLabelsGroup"],pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth",fill:"color"},getColor:oa,animate:function(a){var b=this,c=b.points,d=b.startAngleRad;if(!a)n(c,function(a){var c=a.graphic,a=a.shapeArgs;c&&(c.attr({r:b.center[3]/
-2,start:d,end:d}),c.animate({r:a.r,start:a.start,end:a.end},b.options.animation))}),b.animate=null},setData:function(a,b){Q.prototype.setData.call(this,a,!1);this.processData();this.generatePoints();o(b,!0)&&this.chart.redraw()},generatePoints:function(){var a,b=0,c,d,e,f=this.options.ignoreHiddenPoint;Q.prototype.generatePoints.call(this);c=this.points;d=c.length;for(a=0;a
=-90&&a<=90)j=n(O(i.h/$(ga*a))),b=j+O(a/360),b
=d&&e<=c&&!v&&g!==""&&(g=a.split(i),k(g,function(b,a){a>=h&&a<=n&&(f[a-h]||(f[a-h]=[]),f[a-h][s]=b)}),s+=1)}),this.dataFound())},parseTable:function(){var b=this.options,a= +b.table,e=this.columns,f=b.startRow||0,d=b.endRow||Number.MAX_VALUE,c=b.startColumn||0,h=b.endColumn||Number.MAX_VALUE;a&&(typeof a==="string"&&(a=document.getElementById(a)),k(a.getElementsByTagName("tr"),function(b,a){a>=f&&a<=d&&k(b.children,function(b,d){if((b.tagName==="TD"||b.tagName==="TH")&&d>=c&&d<=h)e[d-c]||(e[d-c]=[]),e[d-c][a-f]=b.innerHTML})}),this.dataFound())},parseGoogleSpreadsheet:function(){var b=this,a=this.options,e=a.googleSpreadsheetKey,f=this.columns,d=a.startRow||0,c=a.endRow|| +Number.MAX_VALUE,h=a.startColumn||0,n=a.endColumn||Number.MAX_VALUE,i,g;e&&jQuery.ajax({dataType:"json",url:"https://spreadsheets.google.com/feeds/cells/"+e+"/"+(a.googleSpreadsheetWorksheet||"od6")+"/public/values?alt=json-in-script&callback=?",error:a.error,success:function(a){var a=a.feed.entry,e,k=a.length,m=0,j=0,l;for(l=0;l=h&&l<=n)f[l-h]=[],f[l-h].length=Math.min(j,c-d);for(l=0;l =h&&g<=n&&i>=d&&i<=c)f[g-h][i-d]=e.content.$t;b.dataFound()}})},trim:function(b,a){typeof b==="string"&&(b=b.replace(/^\s+|\s+$/g,""),a&&/^[0-9\s]+$/.test(b)&&(b=b.replace(/\s/g,"")),this.decimalRegex&&(b=b.replace(this.decimalRegex,"$1.$2")));return b},parseTypes:function(){for(var b=this.columns,a=b.length;a--;)this.parseColumn(b[a],a)},parseColumn:function(b,a){var e=this.rawColumns,f=this.columns,d=b.length,c,h,g,i,k=this.firstRowAsNames,j=r(a,this.valueCount.xColumns)!== +-1,o=[],q=this.chartOptions,m,p=(this.options.columnTypes||[])[a],q=j&&(q&&q.xAxis&&u(q.xAxis)[0].type==="category"||p==="string");for(e[a]||(e[a]=[]);d--;)if(c=o[d]||b[d],g=this.trim(c),i=this.trim(c,!0),h=parseFloat(i),e[a][d]===void 0&&(e[a][d]=g),q||d===0&&k)b[d]=g;else if(+i===h)b[d]=h,h>31536E6&&p!=="float"?b.isDatetime=!0:b.isNumeric=!0,b[d+1]!==void 0&&(m=h>b[d+1]);else if(h=this.parseDate(c),j&&typeof h==="number"&&!isNaN(h)&&p!=="float"){if(o[d]=c,b[d]=h,b.isDatetime=!0,b[d+1]!==void 0){c= +h>b[d+1];if(c!==m&&m!==void 0)this.alternativeFormat?(this.dateFormat=this.alternativeFormat,d=b.length,this.alternativeFormat=this.dateFormats[this.dateFormat].alternative):b.unsorted=!0;m=c}}else if(b[d]=g===""?null:g,d!==0&&(b.isDatetime||b.isNumeric))b.mixed=!0;j&&b.mixed&&(f[a]=e[a]);if(j&&m&&this.options.sort)for(a=0;a 0;){i=new j;i.addColumnReader(0,"x");c=r(0,d);c!==-1&&d.splice(c,1);for(c= +0;c 0&&g[0].readers.length>0&&(i=b[g[0].readers[0].columnIndex],i!==void 0&&(i.isDatetime?a="datetime":i.isNumeric||(a="category")));if(a==="category")for(c=0;c = +2&&(d=this.getReferencedColumnIndexes(),d.length>=2))d.shift(),d.sort(),this.name=b[d.shift()].name;return f};j.prototype.addColumnReader=function(b,a){this.readers.push({columnIndex:b,configName:a});if(!(a==="x"||a==="y"||a===void 0))this.pointIsArray=!1};j.prototype.getReferencedColumnIndexes=function(){var b,a=[],e;for(b=0;b = startRow && rowNo <= endRow && !isComment && !isBlank) { + items = line.split(itemDelimiter); + each(items, function (item, colNo) { + if (colNo >= startColumn && colNo <= endColumn) { + if (!columns[colNo - startColumn]) { + columns[colNo - startColumn] = []; + } + + columns[colNo - startColumn][activeRowNo] = item; + } + }); + activeRowNo += 1; + } + }); + + this.dataFound(); + } + }, + + /** + * Parse a HTML table + */ + parseTable: function () { + var options = this.options, + table = options.table, + columns = this.columns, + startRow = options.startRow || 0, + endRow = options.endRow || Number.MAX_VALUE, + startColumn = options.startColumn || 0, + endColumn = options.endColumn || Number.MAX_VALUE; + + if (table) { + + if (typeof table === 'string') { + table = document.getElementById(table); + } + + each(table.getElementsByTagName('tr'), function (tr, rowNo) { + if (rowNo >= startRow && rowNo <= endRow) { + each(tr.children, function (item, colNo) { + if ((item.tagName === 'TD' || item.tagName === 'TH') && colNo >= startColumn && colNo <= endColumn) { + if (!columns[colNo - startColumn]) { + columns[colNo - startColumn] = []; + } + + columns[colNo - startColumn][rowNo - startRow] = item.innerHTML; + } + }); + } + }); + + this.dataFound(); // continue + } + }, + + /** + */ + parseGoogleSpreadsheet: function () { + var self = this, + options = this.options, + googleSpreadsheetKey = options.googleSpreadsheetKey, + columns = this.columns, + startRow = options.startRow || 0, + endRow = options.endRow || Number.MAX_VALUE, + startColumn = options.startColumn || 0, + endColumn = options.endColumn || Number.MAX_VALUE, + gr, // google row + gc; // google column + + if (googleSpreadsheetKey) { + jQuery.ajax({ + dataType: 'json', + url: 'https://spreadsheets.google.com/feeds/cells/' + + googleSpreadsheetKey + '/' + (options.googleSpreadsheetWorksheet || 'od6') + + '/public/values?alt=json-in-script&callback=?', + error: options.error, + success: function (json) { + // Prepare the data from the spreadsheat + var cells = json.feed.entry, + cell, + cellCount = cells.length, + colCount = 0, + rowCount = 0, + i; + + // First, find the total number of columns and rows that + // are actually filled with data + for (i = 0; i < cellCount; i++) { + cell = cells[i]; + colCount = Math.max(colCount, cell.gs$cell.col); + rowCount = Math.max(rowCount, cell.gs$cell.row); + } + + // Set up arrays containing the column data + for (i = 0; i < colCount; i++) { + if (i >= startColumn && i <= endColumn) { + // Create new columns with the length of either end-start or rowCount + columns[i - startColumn] = []; + + // Setting the length to avoid jslint warning + columns[i - startColumn].length = Math.min(rowCount, endRow - startRow); + } + } + + // Loop over the cells and assign the value to the right + // place in the column arrays + for (i = 0; i < cellCount; i++) { + cell = cells[i]; + gr = cell.gs$cell.row - 1; // rows start at 1 + gc = cell.gs$cell.col - 1; // columns start at 1 + + // If both row and col falls inside start and end + // set the transposed cell value in the newly created columns + if (gc >= startColumn && gc <= endColumn && + gr >= startRow && gr <= endRow) { + columns[gc - startColumn][gr - startRow] = cell.content.$t; + } + } + self.dataFound(); + } + }); + } + }, + + /** + * Trim a string from whitespace + */ + trim: function (str, inside) { + if (typeof str === 'string') { + str = str.replace(/^\s+|\s+$/g, ''); + + // Clear white space insdie the string, like thousands separators + if (inside && /^[0-9\s]+$/.test(str)) { + str = str.replace(/\s/g, ''); + } + + if (this.decimalRegex) { + str = str.replace(this.decimalRegex, '$1.$2'); + } + } + return str; + }, + + /** + * Parse numeric cells in to number types and date types in to true dates. + */ + parseTypes: function () { + var columns = this.columns, + col = columns.length; + + while (col--) { + this.parseColumn(columns[col], col); + } + + }, + + /** + * Parse a single column. Set properties like .isDatetime and .isNumeric. + */ + parseColumn: function (column, col) { + var rawColumns = this.rawColumns, + columns = this.columns, + row = column.length, + val, + floatVal, + trimVal, + trimInsideVal, + firstRowAsNames = this.firstRowAsNames, + isXColumn = inArray(col, this.valueCount.xColumns) !== -1, + dateVal, + backup = [], + diff, + chartOptions = this.chartOptions, + descending, + columnTypes = this.options.columnTypes || [], + columnType = columnTypes[col], + forceCategory = isXColumn && ((chartOptions && chartOptions.xAxis && splat(chartOptions.xAxis)[0].type === 'category') || columnType === 'string'); + + if (!rawColumns[col]) { + rawColumns[col] = []; + } + while (row--) { + val = backup[row] || column[row]; + + trimVal = this.trim(val); + trimInsideVal = this.trim(val, true); + floatVal = parseFloat(trimInsideVal); + + // Set it the first time + if (rawColumns[col][row] === undefined) { + rawColumns[col][row] = trimVal; + } + + // Disable number or date parsing by setting the X axis type to category + if (forceCategory || (row === 0 && firstRowAsNames)) { + column[row] = trimVal; + + } else if (+trimInsideVal === floatVal) { // is numeric + + column[row] = floatVal; + + // If the number is greater than milliseconds in a year, assume datetime + if (floatVal > 365 * 24 * 3600 * 1000 && columnType !== 'float') { + column.isDatetime = true; + } else { + column.isNumeric = true; + } + + if (column[row + 1] !== undefined) { + descending = floatVal > column[row + 1]; + } + + // String, continue to determine if it is a date string or really a string + } else { + dateVal = this.parseDate(val); + // Only allow parsing of dates if this column is an x-column + if (isXColumn && typeof dateVal === 'number' && !isNaN(dateVal) && columnType !== 'float') { // is date + backup[row] = val; + column[row] = dateVal; + column.isDatetime = true; + + // Check if the dates are uniformly descending or ascending. If they + // are not, chances are that they are a different time format, so check + // for alternative. + if (column[row + 1] !== undefined) { + diff = dateVal > column[row + 1]; + if (diff !== descending && descending !== undefined) { + if (this.alternativeFormat) { + this.dateFormat = this.alternativeFormat; + row = column.length; + this.alternativeFormat = this.dateFormats[this.dateFormat].alternative; + } else { + column.unsorted = true; + } + } + descending = diff; + } + + } else { // string + column[row] = trimVal === '' ? null : trimVal; + if (row !== 0 && (column.isDatetime || column.isNumeric)) { + column.mixed = true; + } + } + } + } + + // If strings are intermixed with numbers or dates in a parsed column, it is an indication + // that parsing went wrong or the data was not intended to display as numbers or dates and + // parsing is too aggressive. Fall back to categories. Demonstrated in the + // highcharts/demo/column-drilldown sample. + if (isXColumn && column.mixed) { + columns[col] = rawColumns[col]; + } + + // If the 0 column is date or number and descending, reverse all columns. + if (isXColumn && descending && this.options.sort) { + for (col = 0; col < columns.length; col++) { + columns[col].reverse(); + if (firstRowAsNames) { + columns[col].unshift(columns[col].pop()); + } + } + } + }, + + /** + * A collection of available date formats, extendable from the outside to support + * custom date formats. + */ + dateFormats: { + 'YYYY-mm-dd': { + regex: /^([0-9]{4})[\-\/\.]([0-9]{2})[\-\/\.]([0-9]{2})$/, + parser: function (match) { + return Date.UTC(+match[1], match[2] - 1, +match[3]); + } + }, + 'dd/mm/YYYY': { + regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/, + parser: function (match) { + return Date.UTC(+match[3], match[2] - 1, +match[1]); + }, + alternative: 'mm/dd/YYYY' // different format with the same regex + }, + 'mm/dd/YYYY': { + regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/, + parser: function (match) { + return Date.UTC(+match[3], match[1] - 1, +match[2]); + } + }, + 'dd/mm/YY': { + regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/, + parser: function (match) { + return Date.UTC(+match[3] + 2000, match[2] - 1, +match[1]); + }, + alternative: 'mm/dd/YY' // different format with the same regex + }, + 'mm/dd/YY': { + regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/, + parser: function (match) { + return Date.UTC(+match[3] + 2000, match[1] - 1, +match[2]); + } + } + }, + + /** + * Parse a date and return it as a number. Overridable through options.parseDate. + */ + parseDate: function (val) { + var parseDate = this.options.parseDate, + ret, + key, + format, + dateFormat = this.options.dateFormat || this.dateFormat, + match; + + if (parseDate) { + ret = parseDate(val); + + } else if (typeof val === 'string') { + // Auto-detect the date format the first time + if (!dateFormat) { + for (key in this.dateFormats) { + format = this.dateFormats[key]; + match = val.match(format.regex); + if (match) { + this.dateFormat = dateFormat = key; + this.alternativeFormat = format.alternative; + ret = format.parser(match); + break; + } + } + // Next time, use the one previously found + } else { + format = this.dateFormats[dateFormat]; + match = val.match(format.regex); + if (match) { + ret = format.parser(match); + } + } + // Fall back to Date.parse + if (!match) { + match = Date.parse(val); + // External tools like Date.js and MooTools extend Date object and + // returns a date. + if (typeof match === 'object' && match !== null && match.getTime) { + ret = match.getTime() - match.getTimezoneOffset() * 60000; + + // Timestamp + } else if (typeof match === 'number' && !isNaN(match)) { + ret = match - (new Date(match)).getTimezoneOffset() * 60000; + } + } + } + return ret; + }, + + /** + * Reorganize rows into columns + */ + rowsToColumns: function (rows) { + var row, + rowsLength, + col, + colsLength, + columns; + + if (rows) { + columns = []; + rowsLength = rows.length; + for (row = 0; row < rowsLength; row++) { + colsLength = rows[row].length; + for (col = 0; col < colsLength; col++) { + if (!columns[col]) { + columns[col] = []; + } + columns[col][row] = rows[row][col]; + } + } + } + return columns; + }, + + /** + * A hook for working directly on the parsed columns + */ + parsed: function () { + if (this.options.parsed) { + return this.options.parsed.call(this, this.columns); + } + }, + + getFreeIndexes: function (numberOfColumns, seriesBuilders) { + var s, + i, + freeIndexes = [], + freeIndexValues = [], + referencedIndexes; + + // Add all columns as free + for (i = 0; i < numberOfColumns; i = i + 1) { + freeIndexes.push(true); + } + + // Loop all defined builders and remove their referenced columns + for (s = 0; s < seriesBuilders.length; s = s + 1) { + referencedIndexes = seriesBuilders[s].getReferencedColumnIndexes(); + + for (i = 0; i < referencedIndexes.length; i = i + 1) { + freeIndexes[referencedIndexes[i]] = false; + } + } + + // Collect the values for the free indexes + for (i = 0; i < freeIndexes.length; i = i + 1) { + if (freeIndexes[i]) { + freeIndexValues.push(i); + } + } + + return freeIndexValues; + }, + + /** + * If a complete callback function is provided in the options, interpret the + * columns into a Highcharts options object. + */ + complete: function () { + + var columns = this.columns, + xColumns = [], + type, + options = this.options, + series, + data, + i, + j, + r, + seriesIndex, + chartOptions, + allSeriesBuilders = [], + builder, + freeIndexes, + typeCol, + index; + + xColumns.length = columns.length; + if (options.complete || options.afterComplete) { + + // Get the names and shift the top row + for (i = 0; i < columns.length; i++) { + if (this.firstRowAsNames) { + columns[i].name = columns[i].shift(); + } + } + + // Use the next columns for series + series = []; + freeIndexes = this.getFreeIndexes(columns.length, this.valueCount.seriesBuilders); + + // Populate defined series + for (seriesIndex = 0; seriesIndex < this.valueCount.seriesBuilders.length; seriesIndex++) { + builder = this.valueCount.seriesBuilders[seriesIndex]; + + // If the builder can be populated with remaining columns, then add it to allBuilders + if (builder.populateColumns(freeIndexes)) { + allSeriesBuilders.push(builder); + } + } + + // Populate dynamic series + while (freeIndexes.length > 0) { + builder = new SeriesBuilder(); + builder.addColumnReader(0, 'x'); + + // Mark index as used (not free) + index = inArray(0, freeIndexes); + if (index !== -1) { + freeIndexes.splice(index, 1); + } + + for (i = 0; i < this.valueCount.global; i++) { + // Create and add a column reader for the next free column index + builder.addColumnReader(undefined, this.valueCount.globalPointArrayMap[i]); + } + + // If the builder can be populated with remaining columns, then add it to allBuilders + if (builder.populateColumns(freeIndexes)) { + allSeriesBuilders.push(builder); + } + } + + // Get the data-type from the first series x column + if (allSeriesBuilders.length > 0 && allSeriesBuilders[0].readers.length > 0) { + typeCol = columns[allSeriesBuilders[0].readers[0].columnIndex]; + if (typeCol !== undefined) { + if (typeCol.isDatetime) { + type = 'datetime'; + } else if (!typeCol.isNumeric) { + type = 'category'; + } + } + } + // Axis type is category, then the "x" column should be called "name" + if (type === 'category') { + for (seriesIndex = 0; seriesIndex < allSeriesBuilders.length; seriesIndex++) { + builder = allSeriesBuilders[seriesIndex]; + for (r = 0; r < builder.readers.length; r++) { + if (builder.readers[r].configName === 'x') { + builder.readers[r].configName = 'name'; + } + } + } + } + + // Read data for all builders + for (seriesIndex = 0; seriesIndex < allSeriesBuilders.length; seriesIndex++) { + builder = allSeriesBuilders[seriesIndex]; + + // Iterate down the cells of each column and add data to the series + data = []; + for (j = 0; j < columns[0].length; j++) { // TODO: which column's length should we use here + data[j] = builder.read(columns, j); + } + + // Add the series + series[seriesIndex] = { + data: data + }; + if (builder.name) { + series[seriesIndex].name = builder.name; + } + if (type === 'category') { + series[seriesIndex].turboThreshold = 0; + } + } + + + + // Do the callback + chartOptions = { + series: series + }; + if (type) { + chartOptions.xAxis = { + type: type + }; + } + + if (options.complete) { + options.complete(chartOptions); + } + + // The afterComplete hook is used internally to avoid conflict with the externally + // available complete option. + if (options.afterComplete) { + options.afterComplete(chartOptions); + } + } + } + }); + + // Register the Data prototype and data function on Highcharts + Highcharts.Data = Data; + Highcharts.data = function (options, chartOptions) { + return new Data(options, chartOptions); + }; + + // Extend Chart.init so that the Chart constructor accepts a new configuration + // option group, data. + Highcharts.wrap(Highcharts.Chart.prototype, 'init', function (proceed, userOptions, callback) { + var chart = this; + + if (userOptions && userOptions.data) { + Highcharts.data(Highcharts.extend(userOptions.data, { + + afterComplete: function (dataOptions) { + var i, series; + + // Merge series configs + if (userOptions.hasOwnProperty('series')) { + if (typeof userOptions.series === 'object') { + i = Math.max(userOptions.series.length, dataOptions.series.length); + while (i--) { + series = userOptions.series[i] || {}; + userOptions.series[i] = Highcharts.merge(series, dataOptions.series[i]); + } + } else { // Allow merging in dataOptions.series (#2856) + delete userOptions.series; + } + } + + // Do the merge + userOptions = Highcharts.merge(dataOptions, userOptions); + + proceed.call(chart, userOptions, callback); + } + }), userOptions); + } else { + proceed.call(chart, userOptions, callback); + } + }); + + /** + * Creates a new SeriesBuilder. A SeriesBuilder consists of a number + * of ColumnReaders that reads columns and give them a name. + * Ex: A series builder can be constructed to read column 3 as 'x' and + * column 7 and 8 as 'y1' and 'y2'. + * The output would then be points/rows of the form {x: 11, y1: 22, y2: 33} + * + * The name of the builder is taken from the second column. In the above + * example it would be the column with index 7. + * @constructor + */ + SeriesBuilder = function () { + this.readers = []; + this.pointIsArray = true; + }; + + /** + * Populates readers with column indexes. A reader can be added without + * a specific index and for those readers the index is taken sequentially + * from the free columns (this is handled by the ColumnCursor instance). + * @returns {boolean} + */ + SeriesBuilder.prototype.populateColumns = function (freeIndexes) { + var builder = this, + enoughColumns = true; + + // Loop each reader and give it an index if its missing. + // The freeIndexes.shift() will return undefined if there + // are no more columns. + each(builder.readers, function (reader) { + if (reader.columnIndex === undefined) { + reader.columnIndex = freeIndexes.shift(); + } + }); + + // Now, all readers should have columns mapped. If not + // then return false to signal that this series should + // not be added. + each(builder.readers, function (reader) { + if (reader.columnIndex === undefined) { + enoughColumns = false; + } + }); + + return enoughColumns; + }; + + /** + * Reads a row from the dataset and returns a point or array depending + * on the names of the readers. + * @param columns + * @param rowIndex + * @returns {Array | Object} + */ + SeriesBuilder.prototype.read = function (columns, rowIndex) { + var builder = this, + pointIsArray = builder.pointIsArray, + point = pointIsArray ? [] : {}, + columnIndexes; + + // Loop each reader and ask it to read its value. + // Then, build an array or point based on the readers names. + each(builder.readers, function (reader) { + var value = columns[reader.columnIndex][rowIndex]; + if (pointIsArray) { + point.push(value); + } else { + point[reader.configName] = value; + } + }); + + // The name comes from the first column (excluding the x column) + if (this.name === undefined && builder.readers.length >= 2) { + columnIndexes = builder.getReferencedColumnIndexes(); + if (columnIndexes.length >= 2) { + // remove the first one (x col) + columnIndexes.shift(); + + // Sort the remaining + columnIndexes.sort(); + + // Now use the lowest index as name column + this.name = columns[columnIndexes.shift()].name; + } + } + + return point; + }; + + /** + * Creates and adds ColumnReader from the given columnIndex and configName. + * ColumnIndex can be undefined and in that case the reader will be given + * an index when columns are populated. + * @param columnIndex {Number | undefined} + * @param configName + */ + SeriesBuilder.prototype.addColumnReader = function (columnIndex, configName) { + this.readers.push({ + columnIndex: columnIndex, + configName: configName + }); + + if (!(configName === 'x' || configName === 'y' || configName === undefined)) { + this.pointIsArray = false; + } + }; + + /** + * Returns an array of column indexes that the builder will use when + * reading data. + * @returns {Array} + */ + SeriesBuilder.prototype.getReferencedColumnIndexes = function () { + var i, + referencedColumnIndexes = [], + columnReader; + + for (i = 0; i < this.readers.length; i = i + 1) { + columnReader = this.readers[i]; + if (columnReader.columnIndex !== undefined) { + referencedColumnIndexes.push(columnReader.columnIndex); + } + } + + return referencedColumnIndexes; + }; + + /** + * Returns true if the builder has a reader for the given configName. + * @param configName + * @returns {boolean} + */ + SeriesBuilder.prototype.hasReader = function (configName) { + var i, columnReader; + for (i = 0; i < this.readers.length; i = i + 1) { + columnReader = this.readers[i]; + if (columnReader.configName === configName) { + return true; + } + } + // Else return undefined + }; + + + +}(Highcharts)); diff --git a/media/js/modules/drilldown.js b/media/js/modules/drilldown.js index ce91be5..7a7c52f 100644 --- a/media/js/modules/drilldown.js +++ b/media/js/modules/drilldown.js @@ -1,11 +1,17 @@ -(function(e){function q(b,a,c){return"rgba("+[Math.round(b[0]+(a[0]-b[0])*c),Math.round(b[1]+(a[1]-b[1])*c),Math.round(b[2]+(a[2]-b[2])*c),b[3]+(a[3]-b[3])*c].join(",")+")"}var m=function(){},j=e.getOptions(),g=e.each,n=e.extend,o=e.wrap,h=e.Chart,i=e.seriesTypes,k=i.pie,l=i.column,r=HighchartsAdapter.fireEvent,t=HighchartsAdapter.inArray;n(j.lang,{drillUpText:"◁ Back to {series.name}"});j.drilldown={activeAxisLabelStyle:{cursor:"pointer",color:"#039",fontWeight:"bold",textDecoration:"underline"}, -activeDataLabelStyle:{cursor:"pointer",color:"#039",fontWeight:"bold",textDecoration:"underline"},animation:{duration:500},drillUpButton:{position:{align:"right",x:-10,y:10}}};e.SVGRenderer.prototype.Element.prototype.fadeIn=function(){this.attr({opacity:0.1,visibility:"visible"}).animate({opacity:1},{duration:250})};h.prototype.drilldownLevels=[];h.prototype.addSeriesAsDrilldown=function(b,a){var c=b.series,d=c.xAxis,f=c.yAxis,e;e=b.color||c.color;var g,a=n({color:e},a);g=t(b,c.points);this.drilldownLevels.push({seriesOptions:c.userOptions, -shapeArgs:b.shapeArgs,bBox:b.graphic.getBBox(),color:e,newSeries:a,pointOptions:c.options.data[g],pointIndex:g,oldExtremes:{xMin:d&&d.userMin,xMax:d&&d.userMax,yMin:f&&f.userMin,yMax:f&&f.userMax}});e=this.addSeries(a,!1);if(d)d.oldPos=d.pos,d.userMin=d.userMax=null,f.userMin=f.userMax=null;if(c.type===e.type)e.animate=e.animateDrilldown||m,e.options.animation=!0;c.remove(!1);this.redraw();this.showDrillUpButton()};h.prototype.getDrilldownBackText=function(){return this.options.lang.drillUpText.replace("{series.name}", -this.drilldownLevels[this.drilldownLevels.length-1].seriesOptions.name)};h.prototype.showDrillUpButton=function(){var b=this,a=this.getDrilldownBackText(),c=b.options.drilldown.drillUpButton;this.drillUpButton?this.drillUpButton.attr({text:a}).align():this.drillUpButton=this.renderer.button(a,null,null,function(){b.drillUp()}).attr(n({align:c.position.align,zIndex:9},c.theme)).add().align(c.position,!1,c.relativeTo||"plotBox")};h.prototype.drillUp=function(){var b=this.drilldownLevels.pop(),a=this.series[0], -c=b.oldExtremes,d=this.addSeries(b.seriesOptions,!1);r(this,"drillup",{seriesOptions:b.seriesOptions});if(d.type===a.type)d.drilldownLevel=b,d.animate=d.animateDrillupTo||m,d.options.animation=!0,a.animateDrillupFrom&&a.animateDrillupFrom(b);a.remove(!1);d.xAxis&&(d.xAxis.setExtremes(c.xMin,c.xMax,!1),d.yAxis.setExtremes(c.yMin,c.yMax,!1));this.redraw();this.drilldownLevels.length===0?this.drillUpButton=this.drillUpButton.destroy():this.drillUpButton.attr({text:this.getDrilldownBackText()}).align()}; -k.prototype.animateDrilldown=function(b){var a=this.chart.drilldownLevels[this.chart.drilldownLevels.length-1],c=this.chart.options.drilldown.animation,d=a.shapeArgs,f=d.start,s=(d.end-f)/this.points.length,h=e.Color(a.color).rgba;b||g(this.points,function(a,b){var g=e.Color(a.color).rgba;a.graphic.attr(e.merge(d,{start:f+b*s,end:f+(b+1)*s})).animate(a.shapeArgs,e.merge(c,{step:function(a,d){d.prop==="start"&&this.attr({fill:q(h,g,d.pos)})}}))})};k.prototype.animateDrillupTo=l.prototype.animateDrillupTo= -function(b){if(!b){var a=this,c=a.drilldownLevel;g(this.points,function(a){a.graphic.hide();a.dataLabel&&a.dataLabel.hide();a.connector&&a.connector.hide()});setTimeout(function(){g(a.points,function(a,b){var e=b===c.pointIndex?"show":"fadeIn";a.graphic[e]();if(a.dataLabel)a.dataLabel[e]();if(a.connector)a.connector[e]()})},Math.max(this.chart.options.drilldown.animation.duration-50,0));this.animate=m}};l.prototype.animateDrilldown=function(b){var a=this.chart.drilldownLevels[this.chart.drilldownLevels.length- -1].shapeArgs,c=this.chart.options.drilldown.animation;b||(a.x+=this.xAxis.oldPos-this.xAxis.pos,g(this.points,function(b){b.graphic.attr(a).animate(b.shapeArgs,c)}))};l.prototype.animateDrillupFrom=k.prototype.animateDrillupFrom=function(b){var a=this.chart.options.drilldown.animation,c=this.group;delete this.group;g(this.points,function(d){var f=d.graphic,g=e.Color(d.color).rgba;delete d.graphic;f.animate(b.shapeArgs,e.merge(a,{step:function(a,c){c.prop==="start"&&this.attr({fill:q(g,e.Color(b.color).rgba, -c.pos)})},complete:function(){f.destroy();c&&(c=c.destroy())}}))})};e.Point.prototype.doDrilldown=function(){for(var b=this.series.chart,a=b.options.drilldown,c=a.series.length,d;c--&&!d;)a.series[c].id===this.drilldown&&(d=a.series[c]);r(b,"drilldown",{point:this,seriesOptions:d});d&&b.addSeriesAsDrilldown(this,d)};o(e.Point.prototype,"init",function(b,a,c,d){var f=b.call(this,a,c,d),b=a.chart,a=(a=a.xAxis&&a.xAxis.ticks[d])&&a.label;if(f.drilldown){if(e.addEvent(f,"click",function(){f.doDrilldown()}), -a){if(!a._basicStyle)a._basicStyle=a.element.getAttribute("style");a.addClass("highcharts-drilldown-axis-label").css(b.options.drilldown.activeAxisLabelStyle).on("click",function(){f.doDrilldown&&f.doDrilldown()})}}else a&&a._basicStyle&&a.element.setAttribute("style",a._basicStyle);return f});o(e.Series.prototype,"drawDataLabels",function(b){var a=this.chart.options.drilldown.activeDataLabelStyle;b.call(this);g(this.points,function(b){if(b.drilldown&&b.dataLabel)b.dataLabel.attr({"class":"highcharts-drilldown-data-label"}).css(a).on("click", -function(){b.doDrilldown()})})});l.prototype.supportsDrilldown=!0;k.prototype.supportsDrilldown=!0;var p,j=function(b){b.call(this);g(this.points,function(a){a.drilldown&&a.graphic&&a.graphic.attr({"class":"highcharts-drilldown-point"}).css({cursor:"pointer"})})};for(p in i)i[p].prototype.supportsDrilldown&&o(i[p].prototype,"drawTracker",j)})(Highcharts); +(function(f){function A(a,b,c){var d;!b.rgba.length||!a.rgba.length?a=b.raw||"none":(a=a.rgba,b=b.rgba,d=b[3]!==1||a[3]!==1,a=(d?"rgba(":"rgb(")+Math.round(b[0]+(a[0]-b[0])*(1-c))+","+Math.round(b[1]+(a[1]-b[1])*(1-c))+","+Math.round(b[2]+(a[2]-b[2])*(1-c))+(d?","+(b[3]+(a[3]-b[3])*(1-c)):"")+")");return a}var t=function(){},q=f.getOptions(),h=f.each,l=f.extend,B=f.format,u=f.pick,r=f.wrap,m=f.Chart,p=f.seriesTypes,v=p.pie,n=p.column,w=f.Tick,x=HighchartsAdapter.fireEvent,y=HighchartsAdapter.inArray, +z=1;h(["fill","stroke"],function(a){HighchartsAdapter.addAnimSetter(a,function(b){b.elem.attr(a,A(f.Color(b.start),f.Color(b.end),b.pos))})});l(q.lang,{drillUpText:"◁ Back to {series.name}"});q.drilldown={activeAxisLabelStyle:{cursor:"pointer",color:"#0d233a",fontWeight:"bold",textDecoration:"underline"},activeDataLabelStyle:{cursor:"pointer",color:"#0d233a",fontWeight:"bold",textDecoration:"underline"},animation:{duration:500},drillUpButton:{position:{align:"right",x:-10,y:10}}};f.SVGRenderer.prototype.Element.prototype.fadeIn= +function(a){this.attr({opacity:0.1,visibility:"inherit"}).animate({opacity:u(this.newOpacity,1)},a||{duration:250})};m.prototype.addSeriesAsDrilldown=function(a,b){this.addSingleSeriesAsDrilldown(a,b);this.applyDrilldown()};m.prototype.addSingleSeriesAsDrilldown=function(a,b){var c=a.series,d=c.xAxis,g=c.yAxis,e;e=a.color||c.color;var i,f=[],j=[],k,o;if(!this.drilldownLevels)this.drilldownLevels=[];k=c.options._levelNumber||0;(o=this.drilldownLevels[this.drilldownLevels.length-1])&&o.levelNumber!== +k&&(o=void 0);b=l({color:e,_ddSeriesId:z++},b);i=y(a,c.points);h(c.chart.series,function(a){if(a.xAxis===d&&!a.isDrilling)a.options._ddSeriesId=a.options._ddSeriesId||z++,a.options._colorIndex=a.userOptions._colorIndex,a.options._levelNumber=a.options._levelNumber||k,o?(f=o.levelSeries,j=o.levelSeriesOptions):(f.push(a),j.push(a.options))});e={levelNumber:k,seriesOptions:c.options,levelSeriesOptions:j,levelSeries:f,shapeArgs:a.shapeArgs,bBox:a.graphic?a.graphic.getBBox():{},color:e,lowerSeriesOptions:b, +pointOptions:c.options.data[i],pointIndex:i,oldExtremes:{xMin:d&&d.userMin,xMax:d&&d.userMax,yMin:g&&g.userMin,yMax:g&&g.userMax}};this.drilldownLevels.push(e);e=e.lowerSeries=this.addSeries(b,!1);e.options._levelNumber=k+1;if(d)d.oldPos=d.pos,d.userMin=d.userMax=null,g.userMin=g.userMax=null;if(c.type===e.type)e.animate=e.animateDrilldown||t,e.options.animation=!0};m.prototype.applyDrilldown=function(){var a=this.drilldownLevels,b;if(a&&a.length>0)b=a[a.length-1].levelNumber,h(this.drilldownLevels, +function(a){a.levelNumber===b&&h(a.levelSeries,function(a){a.options&&a.options._levelNumber===b&&a.remove(!1)})});this.redraw();this.showDrillUpButton()};m.prototype.getDrilldownBackText=function(){var a=this.drilldownLevels;if(a&&a.length>0)return a=a[a.length-1],a.series=a.seriesOptions,B(this.options.lang.drillUpText,a)};m.prototype.showDrillUpButton=function(){var a=this,b=this.getDrilldownBackText(),c=a.options.drilldown.drillUpButton,d,g;this.drillUpButton?this.drillUpButton.attr({text:b}).align(): +(g=(d=c.theme)&&d.states,this.drillUpButton=this.renderer.button(b,null,null,function(){a.drillUp()},d,g&&g.hover,g&&g.select).attr({align:c.position.align,zIndex:9}).add().align(c.position,!1,c.relativeTo||"plotBox"))};m.prototype.drillUp=function(){for(var a=this,b=a.drilldownLevels,c=b[b.length-1].levelNumber,d=b.length,g=a.series,e,i,f,j,k=function(b){var c;h(g,function(a){a.options._ddSeriesId===b._ddSeriesId&&(c=a)});c=c||a.addSeries(b,!1);if(c.type===f.type&&c.animateDrillupTo)c.animate=c.animateDrillupTo; +b===i.seriesOptions&&(j=c)};d--;)if(i=b[d],i.levelNumber===c){b.pop();f=i.lowerSeries;if(!f.chart)for(e=g.length;e--;)if(g[e].options.id===i.lowerSeriesOptions.id&&g[e].options._levelNumber===c+1){f=g[e];break}f.xData=[];h(i.levelSeriesOptions,k);x(a,"drillup",{seriesOptions:i.seriesOptions});if(j.type===f.type)j.drilldownLevel=i,j.options.animation=a.options.drilldown.animation,f.animateDrillupFrom&&f.chart&&f.animateDrillupFrom(i);j.options._levelNumber=c;f.remove(!1);if(j.xAxis)e=i.oldExtremes, +j.xAxis.setExtremes(e.xMin,e.xMax,!1),j.yAxis.setExtremes(e.yMin,e.yMax,!1)}this.redraw();this.drilldownLevels.length===0?this.drillUpButton=this.drillUpButton.destroy():this.drillUpButton.attr({text:this.getDrilldownBackText()}).align();this.ddDupes.length=[]};n.prototype.supportsDrilldown=!0;n.prototype.animateDrillupTo=function(a){if(!a){var b=this,c=b.drilldownLevel;h(this.points,function(a){a.graphic&&a.graphic.hide();a.dataLabel&&a.dataLabel.hide();a.connector&&a.connector.hide()});setTimeout(function(){b.points&& +h(b.points,function(a,b){var e=b===(c&&c.pointIndex)?"show":"fadeIn",f=e==="show"?!0:void 0;if(a.graphic)a.graphic[e](f);if(a.dataLabel)a.dataLabel[e](f);if(a.connector)a.connector[e](f)})},Math.max(this.chart.options.drilldown.animation.duration-50,0));this.animate=t}};n.prototype.animateDrilldown=function(a){var b=this,c=this.chart.drilldownLevels,d,g=this.chart.options.drilldown.animation,e=this.xAxis;if(!a)h(c,function(a){if(b.options._ddSeriesId===a.lowerSeriesOptions._ddSeriesId)d=a.shapeArgs, +d.fill=a.color}),d.x+=u(e.oldPos,e.pos)-e.pos,h(this.points,function(a){a.graphic&&a.graphic.attr(d).animate(l(a.shapeArgs,{fill:a.color}),g);a.dataLabel&&a.dataLabel.fadeIn(g)}),this.animate=null};n.prototype.animateDrillupFrom=function(a){var b=this.chart.options.drilldown.animation,c=this.group,d=this;h(d.trackerGroups,function(a){if(d[a])d[a].on("mouseover")});delete this.group;h(this.points,function(d){var e=d.graphic,i=function(){e.destroy();c&&(c=c.destroy())};e&&(delete d.graphic,b?e.animate(l(a.shapeArgs, +{fill:a.color}),f.merge(b,{complete:i})):(e.attr(a.shapeArgs),i()))})};v&&l(v.prototype,{supportsDrilldown:!0,animateDrillupTo:n.prototype.animateDrillupTo,animateDrillupFrom:n.prototype.animateDrillupFrom,animateDrilldown:function(a){var b=this.chart.drilldownLevels[this.chart.drilldownLevels.length-1],c=this.chart.options.drilldown.animation,d=b.shapeArgs,g=d.start,e=(d.end-g)/this.points.length;if(!a)h(this.points,function(a,h){a.graphic.attr(f.merge(d,{start:g+h*e,end:g+(h+1)*e,fill:b.color}))[c? +"animate":"attr"](l(a.shapeArgs,{fill:a.color}),c)}),this.animate=null}});f.Point.prototype.doDrilldown=function(a,b){var c=this.series.chart,d=c.options.drilldown,f=(d.series||[]).length,e;if(!c.ddDupes)c.ddDupes=[];for(;f--&&!e;)d.series[f].id===this.drilldown&&y(this.drilldown,c.ddDupes)===-1&&(e=d.series[f],c.ddDupes.push(this.drilldown));x(c,"drilldown",{point:this,seriesOptions:e,category:b,points:b!==void 0&&this.series.xAxis.ddPoints[b].slice(0)});e&&(a?c.addSingleSeriesAsDrilldown(this,e): +c.addSeriesAsDrilldown(this,e))};f.Axis.prototype.drilldownCategory=function(a){var b,c,d=this.ddPoints[a];for(b in d)(c=d[b])&&c.series&&c.series.visible&&c.doDrilldown&&c.doDrilldown(!0,a);this.chart.applyDrilldown()};f.Axis.prototype.getDDPoints=function(a,b){var c=this.ddPoints;if(!c)this.ddPoints=c={};c[a]||(c[a]=[]);if(c[a].levelNumber!==b)c[a].length=0;return c[a]};w.prototype.drillable=function(){var a=this.pos,b=this.label,c=this.axis,d=c.ddPoints&&c.ddPoints[a];if(b&&d&&d.length){if(!b.basicStyles)b.basicStyles= +f.merge(b.styles);b.addClass("highcharts-drilldown-axis-label").css(c.chart.options.drilldown.activeAxisLabelStyle).on("click",function(){c.drilldownCategory(a)})}else if(b&&b.basicStyles)b.styles={},b.css(b.basicStyles),b.on("click",null)};r(w.prototype,"addLabel",function(a){a.call(this);this.drillable()});r(f.Point.prototype,"init",function(a,b,c,d){var g=a.call(this,b,c,d),a=(c=b.xAxis)&&c.ticks[d],d=c&&c.getDDPoints(d,b.options._levelNumber);if(g.drilldown&&(f.addEvent(g,"click",function(){g.doDrilldown()}), +d))d.push(g),d.levelNumber=b.options._levelNumber;a&&a.drillable();return g});r(f.Series.prototype,"drawDataLabels",function(a){var b=this.chart.options.drilldown.activeDataLabelStyle;a.call(this);h(this.points,function(a){a.drilldown&&a.dataLabel&&a.dataLabel.attr({"class":"highcharts-drilldown-data-label"}).css(b)})});var s,q=function(a){a.call(this);h(this.points,function(a){a.drilldown&&a.graphic&&a.graphic.attr({"class":"highcharts-drilldown-point"}).css({cursor:"pointer"})})};for(s in p)p[s].prototype.supportsDrilldown&& +r(p[s].prototype,"drawTracker",q)})(Highcharts); diff --git a/media/js/modules/drilldown.src.js b/media/js/modules/drilldown.src.js new file mode 100644 index 0000000..850d9ad --- /dev/null +++ b/media/js/modules/drilldown.src.js @@ -0,0 +1,709 @@ +/** + * Highcharts Drilldown plugin + * + * Author: Torstein Honsi + * License: MIT License + * + * Demo: http://jsfiddle.net/highcharts/Vf3yT/ + */ + +/*global Highcharts,HighchartsAdapter*/ +(function (H) { + + "use strict"; + + var noop = function () {}, + defaultOptions = H.getOptions(), + each = H.each, + extend = H.extend, + format = H.format, + pick = H.pick, + wrap = H.wrap, + Chart = H.Chart, + seriesTypes = H.seriesTypes, + PieSeries = seriesTypes.pie, + ColumnSeries = seriesTypes.column, + Tick = H.Tick, + fireEvent = HighchartsAdapter.fireEvent, + inArray = HighchartsAdapter.inArray, + ddSeriesId = 1; + + // Utilities + /* + * Return an intermediate color between two colors, according to pos where 0 + * is the from color and 1 is the to color. This method is copied from ColorAxis.js + * and should always be kept updated, until we get AMD support. + */ + function tweenColors(from, to, pos) { + // Check for has alpha, because rgba colors perform worse due to lack of + // support in WebKit. + var hasAlpha, + ret; + + // Unsupported color, return to-color (#3920) + if (!to.rgba.length || !from.rgba.length) { + ret = to.raw || 'none'; + + // Interpolate + } else { + from = from.rgba; + to = to.rgba; + hasAlpha = (to[3] !== 1 || from[3] !== 1); + ret = (hasAlpha ? 'rgba(' : 'rgb(') + + Math.round(to[0] + (from[0] - to[0]) * (1 - pos)) + ',' + + Math.round(to[1] + (from[1] - to[1]) * (1 - pos)) + ',' + + Math.round(to[2] + (from[2] - to[2]) * (1 - pos)) + + (hasAlpha ? (',' + (to[3] + (from[3] - to[3]) * (1 - pos))) : '') + ')'; + } + return ret; + } + /** + * Handle animation of the color attributes directly + */ + each(['fill', 'stroke'], function (prop) { + HighchartsAdapter.addAnimSetter(prop, function (fx) { + fx.elem.attr(prop, tweenColors(H.Color(fx.start), H.Color(fx.end), fx.pos)); + }); + }); + + // Add language + extend(defaultOptions.lang, { + drillUpText: '◁ Back to {series.name}' + }); + defaultOptions.drilldown = { + activeAxisLabelStyle: { + cursor: 'pointer', + color: '#0d233a', + fontWeight: 'bold', + textDecoration: 'underline' + }, + activeDataLabelStyle: { + cursor: 'pointer', + color: '#0d233a', + fontWeight: 'bold', + textDecoration: 'underline' + }, + animation: { + duration: 500 + }, + drillUpButton: { + position: { + align: 'right', + x: -10, + y: 10 + } + // relativeTo: 'plotBox' + // theme + } + }; + + /** + * A general fadeIn method + */ + H.SVGRenderer.prototype.Element.prototype.fadeIn = function (animation) { + this + .attr({ + opacity: 0.1, + visibility: 'inherit' + }) + .animate({ + opacity: pick(this.newOpacity, 1) // newOpacity used in maps + }, animation || { + duration: 250 + }); + }; + + Chart.prototype.addSeriesAsDrilldown = function (point, ddOptions) { + this.addSingleSeriesAsDrilldown(point, ddOptions); + this.applyDrilldown(); + }; + Chart.prototype.addSingleSeriesAsDrilldown = function (point, ddOptions) { + var oldSeries = point.series, + xAxis = oldSeries.xAxis, + yAxis = oldSeries.yAxis, + newSeries, + color = point.color || oldSeries.color, + pointIndex, + levelSeries = [], + levelSeriesOptions = [], + level, + levelNumber, + last; + + if (!this.drilldownLevels) { + this.drilldownLevels = []; + } + + levelNumber = oldSeries.options._levelNumber || 0; + + // See if we can reuse the registered series from last run + last = this.drilldownLevels[this.drilldownLevels.length - 1]; + if (last && last.levelNumber !== levelNumber) { + last = undefined; + } + + + ddOptions = extend({ + color: color, + _ddSeriesId: ddSeriesId++ + }, ddOptions); + pointIndex = inArray(point, oldSeries.points); + + // Record options for all current series + each(oldSeries.chart.series, function (series) { + if (series.xAxis === xAxis && !series.isDrilling) { + series.options._ddSeriesId = series.options._ddSeriesId || ddSeriesId++; + series.options._colorIndex = series.userOptions._colorIndex; + series.options._levelNumber = series.options._levelNumber || levelNumber; // #3182 + + if (last) { + levelSeries = last.levelSeries; + levelSeriesOptions = last.levelSeriesOptions; + } else { + levelSeries.push(series); + levelSeriesOptions.push(series.options); + } + } + }); + + // Add a record of properties for each drilldown level + level = { + levelNumber: levelNumber, + seriesOptions: oldSeries.options, + levelSeriesOptions: levelSeriesOptions, + levelSeries: levelSeries, + shapeArgs: point.shapeArgs, + bBox: point.graphic ? point.graphic.getBBox() : {}, // no graphic in line series with markers disabled + color: color, + lowerSeriesOptions: ddOptions, + pointOptions: oldSeries.options.data[pointIndex], + pointIndex: pointIndex, + oldExtremes: { + xMin: xAxis && xAxis.userMin, + xMax: xAxis && xAxis.userMax, + yMin: yAxis && yAxis.userMin, + yMax: yAxis && yAxis.userMax + } + }; + + // Push it to the lookup array + this.drilldownLevels.push(level); + + newSeries = level.lowerSeries = this.addSeries(ddOptions, false); + newSeries.options._levelNumber = levelNumber + 1; + if (xAxis) { + xAxis.oldPos = xAxis.pos; + xAxis.userMin = xAxis.userMax = null; + yAxis.userMin = yAxis.userMax = null; + } + + // Run fancy cross-animation on supported and equal types + if (oldSeries.type === newSeries.type) { + newSeries.animate = newSeries.animateDrilldown || noop; + newSeries.options.animation = true; + } + }; + + Chart.prototype.applyDrilldown = function () { + var drilldownLevels = this.drilldownLevels, + levelToRemove; + + if (drilldownLevels && drilldownLevels.length > 0) { // #3352, async loading + levelToRemove = drilldownLevels[drilldownLevels.length - 1].levelNumber; + each(this.drilldownLevels, function (level) { + if (level.levelNumber === levelToRemove) { + each(level.levelSeries, function (series) { + if (series.options && series.options._levelNumber === levelToRemove) { // Not removed, not added as part of a multi-series drilldown + series.remove(false); + } + }); + } + }); + } + + this.redraw(); + this.showDrillUpButton(); + }; + + Chart.prototype.getDrilldownBackText = function () { + var drilldownLevels = this.drilldownLevels, + lastLevel; + if (drilldownLevels && drilldownLevels.length > 0) { // #3352, async loading + lastLevel = drilldownLevels[drilldownLevels.length - 1]; + lastLevel.series = lastLevel.seriesOptions; + return format(this.options.lang.drillUpText, lastLevel); + } + + }; + + Chart.prototype.showDrillUpButton = function () { + var chart = this, + backText = this.getDrilldownBackText(), + buttonOptions = chart.options.drilldown.drillUpButton, + attr, + states; + + + if (!this.drillUpButton) { + attr = buttonOptions.theme; + states = attr && attr.states; + + this.drillUpButton = this.renderer.button( + backText, + null, + null, + function () { + chart.drillUp(); + }, + attr, + states && states.hover, + states && states.select + ) + .attr({ + align: buttonOptions.position.align, + zIndex: 9 + }) + .add() + .align(buttonOptions.position, false, buttonOptions.relativeTo || 'plotBox'); + } else { + this.drillUpButton.attr({ + text: backText + }) + .align(); + } + }; + + Chart.prototype.drillUp = function () { + var chart = this, + drilldownLevels = chart.drilldownLevels, + levelNumber = drilldownLevels[drilldownLevels.length - 1].levelNumber, + i = drilldownLevels.length, + chartSeries = chart.series, + seriesI, + level, + oldSeries, + newSeries, + oldExtremes, + addSeries = function (seriesOptions) { + var addedSeries; + each(chartSeries, function (series) { + if (series.options._ddSeriesId === seriesOptions._ddSeriesId) { + addedSeries = series; + } + }); + + addedSeries = addedSeries || chart.addSeries(seriesOptions, false); + if (addedSeries.type === oldSeries.type && addedSeries.animateDrillupTo) { + addedSeries.animate = addedSeries.animateDrillupTo; + } + if (seriesOptions === level.seriesOptions) { + newSeries = addedSeries; + } + }; + + while (i--) { + + level = drilldownLevels[i]; + if (level.levelNumber === levelNumber) { + drilldownLevels.pop(); + + // Get the lower series by reference or id + oldSeries = level.lowerSeries; + if (!oldSeries.chart) { // #2786 + seriesI = chartSeries.length; // #2919 + while (seriesI--) { + if (chartSeries[seriesI].options.id === level.lowerSeriesOptions.id && + chartSeries[seriesI].options._levelNumber === levelNumber + 1) { // #3867 + oldSeries = chartSeries[seriesI]; + break; + } + } + } + oldSeries.xData = []; // Overcome problems with minRange (#2898) + + each(level.levelSeriesOptions, addSeries); + + fireEvent(chart, 'drillup', { seriesOptions: level.seriesOptions }); + + if (newSeries.type === oldSeries.type) { + newSeries.drilldownLevel = level; + newSeries.options.animation = chart.options.drilldown.animation; + + if (oldSeries.animateDrillupFrom && oldSeries.chart) { // #2919 + oldSeries.animateDrillupFrom(level); + } + } + newSeries.options._levelNumber = levelNumber; + + oldSeries.remove(false); + + // Reset the zoom level of the upper series + if (newSeries.xAxis) { + oldExtremes = level.oldExtremes; + newSeries.xAxis.setExtremes(oldExtremes.xMin, oldExtremes.xMax, false); + newSeries.yAxis.setExtremes(oldExtremes.yMin, oldExtremes.yMax, false); + } + } + } + + this.redraw(); + + if (this.drilldownLevels.length === 0) { + this.drillUpButton = this.drillUpButton.destroy(); + } else { + this.drillUpButton.attr({ + text: this.getDrilldownBackText() + }) + .align(); + } + + this.ddDupes.length = []; // #3315 + }; + + + ColumnSeries.prototype.supportsDrilldown = true; + + /** + * When drilling up, keep the upper series invisible until the lower series has + * moved into place + */ + ColumnSeries.prototype.animateDrillupTo = function (init) { + if (!init) { + var newSeries = this, + level = newSeries.drilldownLevel; + + each(this.points, function (point) { + if (point.graphic) { // #3407 + point.graphic.hide(); + } + if (point.dataLabel) { + point.dataLabel.hide(); + } + if (point.connector) { + point.connector.hide(); + } + }); + + + // Do dummy animation on first point to get to complete + setTimeout(function () { + if (newSeries.points) { // May be destroyed in the meantime, #3389 + each(newSeries.points, function (point, i) { + // Fade in other points + var verb = i === (level && level.pointIndex) ? 'show' : 'fadeIn', + inherit = verb === 'show' ? true : undefined; + if (point.graphic) { // #3407 + point.graphic[verb](inherit); + } + if (point.dataLabel) { + point.dataLabel[verb](inherit); + } + if (point.connector) { + point.connector[verb](inherit); + } + }); + } + }, Math.max(this.chart.options.drilldown.animation.duration - 50, 0)); + + // Reset + this.animate = noop; + } + + }; + + ColumnSeries.prototype.animateDrilldown = function (init) { + var series = this, + drilldownLevels = this.chart.drilldownLevels, + animateFrom, + animationOptions = this.chart.options.drilldown.animation, + xAxis = this.xAxis; + + if (!init) { + each(drilldownLevels, function (level) { + if (series.options._ddSeriesId === level.lowerSeriesOptions._ddSeriesId) { + animateFrom = level.shapeArgs; + animateFrom.fill = level.color; + } + }); + + animateFrom.x += (pick(xAxis.oldPos, xAxis.pos) - xAxis.pos); + + each(this.points, function (point) { + if (point.graphic) { + point.graphic + .attr(animateFrom) + .animate( + extend(point.shapeArgs, { fill: point.color }), + animationOptions + ); + } + if (point.dataLabel) { + point.dataLabel.fadeIn(animationOptions); + } + }); + this.animate = null; + } + + }; + + /** + * When drilling up, pull out the individual point graphics from the lower series + * and animate them into the origin point in the upper series. + */ + ColumnSeries.prototype.animateDrillupFrom = function (level) { + var animationOptions = this.chart.options.drilldown.animation, + group = this.group, + series = this; + + // Cancel mouse events on the series group (#2787) + each(series.trackerGroups, function (key) { + if (series[key]) { // we don't always have dataLabelsGroup + series[key].on('mouseover'); + } + }); + + + delete this.group; + each(this.points, function (point) { + var graphic = point.graphic, + complete = function () { + graphic.destroy(); + if (group) { + group = group.destroy(); + } + }; + + if (graphic) { + + delete point.graphic; + + if (animationOptions) { + graphic.animate( + extend(level.shapeArgs, { fill: level.color }), + H.merge(animationOptions, { complete: complete }) + ); + } else { + graphic.attr(level.shapeArgs); + complete(); + } + } + }); + }; + + if (PieSeries) { + extend(PieSeries.prototype, { + supportsDrilldown: true, + animateDrillupTo: ColumnSeries.prototype.animateDrillupTo, + animateDrillupFrom: ColumnSeries.prototype.animateDrillupFrom, + + animateDrilldown: function (init) { + var level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1], + animationOptions = this.chart.options.drilldown.animation, + animateFrom = level.shapeArgs, + start = animateFrom.start, + angle = animateFrom.end - start, + startAngle = angle / this.points.length; + + if (!init) { + each(this.points, function (point, i) { + point.graphic + .attr(H.merge(animateFrom, { + start: start + i * startAngle, + end: start + (i + 1) * startAngle, + fill: level.color + }))[animationOptions ? 'animate' : 'attr']( + extend(point.shapeArgs, { fill: point.color }), + animationOptions + ); + }); + this.animate = null; + } + } + }); + } + + H.Point.prototype.doDrilldown = function (_holdRedraw, category) { + var series = this.series, + chart = series.chart, + drilldown = chart.options.drilldown, + i = (drilldown.series || []).length, + seriesOptions; + + if (!chart.ddDupes) { + chart.ddDupes = []; + } + + while (i-- && !seriesOptions) { + if (drilldown.series[i].id === this.drilldown && inArray(this.drilldown, chart.ddDupes) === -1) { + seriesOptions = drilldown.series[i]; + chart.ddDupes.push(this.drilldown); + } + } + + // Fire the event. If seriesOptions is undefined, the implementer can check for + // seriesOptions, and call addSeriesAsDrilldown async if necessary. + fireEvent(chart, 'drilldown', { + point: this, + seriesOptions: seriesOptions, + category: category, + points: category !== undefined && this.series.xAxis.ddPoints[category].slice(0) + }); + + if (seriesOptions) { + if (_holdRedraw) { + chart.addSingleSeriesAsDrilldown(this, seriesOptions); + } else { + chart.addSeriesAsDrilldown(this, seriesOptions); + } + } + }; + + /** + * Drill down to a given category. This is the same as clicking on an axis label. + */ + H.Axis.prototype.drilldownCategory = function (x) { + var key, + point, + ddPointsX = this.ddPoints[x]; + for (key in ddPointsX) { + point = ddPointsX[key]; + if (point && point.series && point.series.visible && point.doDrilldown) { // #3197 + point.doDrilldown(true, x); + } + } + this.chart.applyDrilldown(); + }; + + /** + * Create and return a collection of points associated with the X position. Reset it for each level. + */ + H.Axis.prototype.getDDPoints = function (x, levelNumber) { + var ddPoints = this.ddPoints; + if (!ddPoints) { + this.ddPoints = ddPoints = {}; + } + if (!ddPoints[x]) { + ddPoints[x] = []; + } + if (ddPoints[x].levelNumber !== levelNumber) { + ddPoints[x].length = 0; // reset + } + return ddPoints[x]; + }; + + + /** + * Make a tick label drillable, or remove drilling on update + */ + Tick.prototype.drillable = function () { + var pos = this.pos, + label = this.label, + axis = this.axis, + ddPointsX = axis.ddPoints && axis.ddPoints[pos]; + + if (label && ddPointsX && ddPointsX.length) { + if (!label.basicStyles) { + label.basicStyles = H.merge(label.styles); + } + label + .addClass('highcharts-drilldown-axis-label') + .css(axis.chart.options.drilldown.activeAxisLabelStyle) + .on('click', function () { + axis.drilldownCategory(pos); + }); + + } else if (label && label.basicStyles) { + label.styles = {}; // reset for full overwrite of styles + label.css(label.basicStyles); + label.on('click', null); // #3806 + } + }; + + /** + * Always keep the drillability updated (#3951) + */ + wrap(Tick.prototype, 'addLabel', function (proceed) { + proceed.call(this); + this.drillable(); + }); + + + /** + * On initialization of each point, identify its label and make it clickable. Also, provide a + * list of points associated to that label. + */ + wrap(H.Point.prototype, 'init', function (proceed, series, options, x) { + var point = proceed.call(this, series, options, x), + xAxis = series.xAxis, + tick = xAxis && xAxis.ticks[x], + ddPointsX = xAxis && xAxis.getDDPoints(x, series.options._levelNumber); + + if (point.drilldown) { + + // Add the click event to the point + H.addEvent(point, 'click', function () { + point.doDrilldown(); + }); + /*wrap(point, 'importEvents', function (proceed) { // wrapping importEvents makes point.click event work + if (!this.hasImportedEvents) { + proceed.call(this); + H.addEvent(this, 'click', function () { + this.doDrilldown(); + }); + } + });*/ + + + // Register drilldown points on this X value + if (ddPointsX) { + ddPointsX.push(point); + ddPointsX.levelNumber = series.options._levelNumber; + } + + } + + // Add or remove click handler and style on the tick label + if (tick) { + tick.drillable(); + } + + return point; + }); + + wrap(H.Series.prototype, 'drawDataLabels', function (proceed) { + var css = this.chart.options.drilldown.activeDataLabelStyle; + + proceed.call(this); + + each(this.points, function (point) { + if (point.drilldown && point.dataLabel) { + point.dataLabel + .attr({ + 'class': 'highcharts-drilldown-data-label' + }) + .css(css); + } + }); + }); + + // Mark the trackers with a pointer + var type, + drawTrackerWrapper = function (proceed) { + proceed.call(this); + each(this.points, function (point) { + if (point.drilldown && point.graphic) { + point.graphic + .attr({ + 'class': 'highcharts-drilldown-point' + }) + .css({ cursor: 'pointer' }); + } + }); + }; + for (type in seriesTypes) { + if (seriesTypes[type].prototype.supportsDrilldown) { + wrap(seriesTypes[type].prototype, 'drawTracker', drawTrackerWrapper); + } + } + +}(Highcharts)); diff --git a/media/js/modules/exporting.js b/media/js/modules/exporting.js index 2371f55..99e4827 100644 --- a/media/js/modules/exporting.js +++ b/media/js/modules/exporting.js @@ -1,22 +1,23 @@ /* - Highcharts JS v3.0.7 (2013-10-24) + Highcharts JS v4.1.5 (2015-04-13) Exporting module - (c) 2010-2013 Torstein Hønsi + (c) 2010-2014 Torstein Honsi License: www.highcharts.com/license */ -(function(f){var A=f.Chart,t=f.addEvent,C=f.removeEvent,k=f.createElement,n=f.discardElement,u=f.css,o=f.merge,r=f.each,p=f.extend,D=Math.max,j=document,B=window,E=f.isTouchDevice,F=f.Renderer.prototype.symbols,x=f.getOptions(),y;p(x.lang,{printChart:"Print chart",downloadPNG:"Download PNG image",downloadJPEG:"Download JPEG image",downloadPDF:"Download PDF document",downloadSVG:"Download SVG vector image",contextButtonTitle:"Chart context menu"});x.navigation={menuStyle:{border:"1px solid #A0A0A0", -background:"#FFFFFF",padding:"5px 0"},menuItemStyle:{padding:"0 10px",background:"none",color:"#303030",fontSize:E?"14px":"11px"},menuItemHoverStyle:{background:"#4572A5",color:"#FFFFFF"},buttonOptions:{symbolFill:"#E0E0E0",symbolSize:14,symbolStroke:"#666",symbolStrokeWidth:3,symbolX:12.5,symbolY:10.5,align:"right",buttonSpacing:3,height:22,theme:{fill:"white",stroke:"none"},verticalAlign:"top",width:24}};x.exporting={type:"image/png",url:"http://export.highcharts.com/",buttons:{contextButton:{menuClassName:"highcharts-contextmenu", -symbol:"menu",_titleKey:"contextButtonTitle",menuItems:[{textKey:"printChart",onclick:function(){this.print()}},{separator:!0},{textKey:"downloadPNG",onclick:function(){this.exportChart()}},{textKey:"downloadJPEG",onclick:function(){this.exportChart({type:"image/jpeg"})}},{textKey:"downloadPDF",onclick:function(){this.exportChart({type:"application/pdf"})}},{textKey:"downloadSVG",onclick:function(){this.exportChart({type:"image/svg+xml"})}}]}}};f.post=function(c,a){var d,b;b=k("form",{method:"post", -action:c,enctype:"multipart/form-data"},{display:"none"},j.body);for(d in a)k("input",{type:"hidden",name:d,value:a[d]},null,b);b.submit();n(b)};p(A.prototype,{getSVG:function(c){var a=this,d,b,z,h,g=o(a.options,c);if(!j.createElementNS)j.createElementNS=function(a,b){return j.createElement(b)};c=k("div",null,{position:"absolute",top:"-9999em",width:a.chartWidth+"px",height:a.chartHeight+"px"},j.body);b=a.renderTo.style.width;h=a.renderTo.style.height;b=g.exporting.sourceWidth||g.chart.width||/px$/.test(b)&& -parseInt(b,10)||600;h=g.exporting.sourceHeight||g.chart.height||/px$/.test(h)&&parseInt(h,10)||400;p(g.chart,{animation:!1,renderTo:c,forExport:!0,width:b,height:h});g.exporting.enabled=!1;g.series=[];r(a.series,function(a){z=o(a.options,{animation:!1,showCheckbox:!1,visible:a.visible});z.isInternal||g.series.push(z)});d=new f.Chart(g,a.callback);r(["xAxis","yAxis"],function(b){r(a[b],function(a,c){var g=d[b][c],f=a.getExtremes(),h=f.userMin,f=f.userMax;g&&(h!==void 0||f!==void 0)&&g.setExtremes(h, -f,!0,!1)})});b=d.container.innerHTML;g=null;d.destroy();n(c);b=b.replace(/zIndex="[^"]+"/g,"").replace(/isShadow="[^"]+"/g,"").replace(/symbolName="[^"]+"/g,"").replace(/jQuery[0-9]+="[^"]+"/g,"").replace(/url\([^#]+#/g,"url(#").replace(/").replace(/ /g," ").replace(//g,"").replace(//g,'xlink:href="$1"/>').replace(/id=([^" >]+)/g,'id="$1"').replace(/class=([^" >]+)/g,'class="$1"').replace(/ transform /g," ").replace(/:(path|rect)/g,"$1").replace(/style="([^"]+)"/g,function(a){return a.toLowerCase()});return b=b.replace(/(url\(#highcharts-[0-9]+)"/g,"$1").replace(/"/g,"'")},exportChart:function(c,a){var c=c||{},d=this.options.exporting,d=this.getSVG(o({chart:{borderRadius:0}},d.chartOptions,a,{exporting:{sourceWidth:c.sourceWidth|| -d.sourceWidth,sourceHeight:c.sourceHeight||d.sourceHeight}})),c=o(this.options.exporting,c);f.post(c.url,{filename:c.filename||"chart",type:c.type,width:c.width||0,scale:c.scale||2,svg:d})},print:function(){var c=this,a=c.container,d=[],b=a.parentNode,f=j.body,h=f.childNodes;if(!c.isPrinting)c.isPrinting=!0,r(h,function(a,b){if(a.nodeType===1)d[b]=a.style.display,a.style.display="none"}),f.appendChild(a),B.focus(),B.print(),setTimeout(function(){b.appendChild(a);r(h,function(a,b){if(a.nodeType=== -1)a.style.display=d[b]});c.isPrinting=!1},1E3)},contextMenu:function(c,a,d,b,f,h,g){var e=this,j=e.options.navigation,q=j.menuItemStyle,l=e.chartWidth,m=e.chartHeight,o="cache-"+c,i=e[o],s=D(f,h),v,w,n;if(!i)e[o]=i=k("div",{className:c},{position:"absolute",zIndex:1E3,padding:s+"px"},e.container),v=k("div",null,p({MozBoxShadow:"3px 3px 10px #888",WebkitBoxShadow:"3px 3px 10px #888",boxShadow:"3px 3px 10px #888"},j.menuStyle),i),w=function(){u(i,{display:"none"});g&&g.setState(0);e.openMenu=!1},t(i, -"mouseleave",function(){n=setTimeout(w,500)}),t(i,"mouseenter",function(){clearTimeout(n)}),t(document,"mouseup",function(a){e.pointer.inClass(a.target,c)||w()}),r(a,function(a){if(a){var b=a.separator?k("hr",null,null,v):k("div",{onmouseover:function(){u(this,j.menuItemHoverStyle)},onmouseout:function(){u(this,q)},onclick:function(){w();a.onclick.apply(e,arguments)},innerHTML:a.text||e.options.lang[a.textKey]},p({cursor:"pointer"},q),v);e.exportDivElements.push(b)}}),e.exportDivElements.push(v,i), -e.exportMenuWidth=i.offsetWidth,e.exportMenuHeight=i.offsetHeight;a={display:"block"};d+e.exportMenuWidth>l?a.right=l-d-f-s+"px":a.left=d-s+"px";b+h+e.exportMenuHeight>m&&g.alignOptions.verticalAlign!=="top"?a.bottom=m-b-s+"px":a.top=b+h-s+"px";u(i,a);e.openMenu=!0},addButton:function(c){var a=this,d=a.renderer,b=o(a.options.navigation.buttonOptions,c),j=b.onclick,h=b.menuItems,g,e,k={stroke:b.symbolStroke,fill:b.symbolFill},q=b.symbolSize||12;if(!a.btnCount)a.btnCount=0;if(!a.exportDivElements)a.exportDivElements= -[],a.exportSVGElements=[];if(b.enabled!==!1){var l=b.theme,m=l.states,n=m&&m.hover,m=m&&m.select,i;delete l.states;j?i=function(){j.apply(a,arguments)}:h&&(i=function(){a.contextMenu(e.menuClassName,h,e.translateX,e.translateY,e.width,e.height,e);e.setState(2)});b.text&&b.symbol?l.paddingLeft=f.pick(l.paddingLeft,25):b.text||p(l,{width:b.width,height:b.height,padding:0});e=d.button(b.text,0,0,i,l,n,m).attr({title:a.options.lang[b._titleKey],"stroke-linecap":"round"});e.menuClassName=c.menuClassName|| -"highcharts-menu-"+a.btnCount++;b.symbol&&(g=d.symbol(b.symbol,b.symbolX-q/2,b.symbolY-q/2,q,q).attr(p(k,{"stroke-width":b.symbolStrokeWidth||1,zIndex:1})).add(e));e.add().align(p(b,{width:e.width,x:f.pick(b.x,y)}),!0,"spacingBox");y+=(e.width+b.buttonSpacing)*(b.align==="right"?-1:1);a.exportSVGElements.push(e,g)}},destroyExport:function(c){var c=c.target,a,d;for(a=0;a .*?$/,"").replace(/(fill|stroke)="rgba\(([ 0-9]+,[ 0-9]+,[ 0-9]+),([ 0-9\.]+)\)"/g,'$1="rgb($2)" $1-opacity="$3"').replace(/ /g," ").replace(//g,"").replace(//g,'xlink:href="$1"/>').replace(/ id=([^" >]+)/g,'id="$1"').replace(/class=([^" >]+)/g,'class="$1"').replace(/ transform /g," ").replace(/:(path|rect)/g, +"$1").replace(/style="([^"]+)"/g,function(a){return a.toLowerCase()})},getSVG:function(b){var a=this,e,c,g,y,h,d=l(a.options,b);if(!k.createElementNS)k.createElementNS=function(a,b){return k.createElement(b)};c=j("div",null,{position:"absolute",top:"-9999em",width:a.chartWidth+"px",height:a.chartHeight+"px"},k.body);g=a.renderTo.style.width;h=a.renderTo.style.height;g=d.exporting.sourceWidth||d.chart.width||/px$/.test(g)&&parseInt(g,10)||600;h=d.exporting.sourceHeight||d.chart.height||/px$/.test(h)&& +parseInt(h,10)||400;q(d.chart,{animation:!1,renderTo:c,forExport:!0,width:g,height:h});d.exporting.enabled=!1;delete d.data;d.series=[];m(a.series,function(a){y=l(a.options,{animation:!1,enableMouseTracking:!1,showCheckbox:!1,visible:a.visible});y.isInternal||d.series.push(y)});b&&m(["xAxis","yAxis"],function(a){m(E(b[a]),function(b,c){d[a][c]=l(d[a][c],b)})});e=new f.Chart(d,a.callback);m(["xAxis","yAxis"],function(b){m(a[b],function(a,d){var c=e[b][d],g=a.getExtremes(),h=g.userMin,g=g.userMax;c&& +(h!==void 0||g!==void 0)&&c.setExtremes(h,g,!0,!1)})});g=e.container.innerHTML;d=null;e.destroy();p(c);g=this.sanitizeSVG(g);return g=g.replace(/(url\(#highcharts-[0-9]+)"/g,"$1").replace(/"/g,"'")},getSVGForExport:function(b,a){var e=this.options.exporting;return this.getSVG(l({chart:{borderRadius:0}},e.chartOptions,a,{exporting:{sourceWidth:b&&b.sourceWidth||e.sourceWidth,sourceHeight:b&&b.sourceHeight||e.sourceHeight}}))},exportChart:function(b,a){var e=this.getSVGForExport(b,a),b=l(this.options.exporting, +b);f.post(b.url,{filename:b.filename||"chart",type:b.type,width:b.width||0,scale:b.scale||2,svg:e},b.formAttributes)},print:function(){var b=this,a=b.container,e=[],c=a.parentNode,g=k.body,f=g.childNodes;if(!b.isPrinting)b.isPrinting=!0,B(b,"beforePrint"),m(f,function(a,b){if(a.nodeType===1)e[b]=a.style.display,a.style.display="none"}),g.appendChild(a),C.focus(),C.print(),setTimeout(function(){c.appendChild(a);m(f,function(a,b){if(a.nodeType===1)a.style.display=e[b]});b.isPrinting=!1;B(b,"afterPrint")}, +1E3)},contextMenu:function(b,a,e,c,g,f,h){var d=this,l=d.options.navigation,D=l.menuItemStyle,n=d.chartWidth,o=d.chartHeight,k="cache-"+b,i=d[k],t=F(g,f),v,w,p,r=function(a){d.pointer.inClass(a.target,b)||w()};if(!i)d[k]=i=j("div",{className:b},{position:"absolute",zIndex:1E3,padding:t+"px"},d.container),v=j("div",null,q({MozBoxShadow:"3px 3px 10px #888",WebkitBoxShadow:"3px 3px 10px #888",boxShadow:"3px 3px 10px #888"},l.menuStyle),i),w=function(){u(i,{display:"none"});h&&h.setState(0);d.openMenu= +!1},s(i,"mouseleave",function(){p=setTimeout(w,500)}),s(i,"mouseenter",function(){clearTimeout(p)}),s(document,"mouseup",r),s(d,"destroy",function(){A(document,"mouseup",r)}),m(a,function(a){if(a){var b=a.separator?j("hr",null,null,v):j("div",{onmouseover:function(){u(this,l.menuItemHoverStyle)},onmouseout:function(){u(this,D)},onclick:function(){w();a.onclick&&a.onclick.apply(d,arguments)},innerHTML:a.text||d.options.lang[a.textKey]},q({cursor:"pointer"},D),v);d.exportDivElements.push(b)}}),d.exportDivElements.push(v, +i),d.exportMenuWidth=i.offsetWidth,d.exportMenuHeight=i.offsetHeight;a={display:"block"};e+d.exportMenuWidth>n?a.right=n-e-g-t+"px":a.left=e-t+"px";c+f+d.exportMenuHeight>o&&h.alignOptions.verticalAlign!=="top"?a.bottom=o-c-t+"px":a.top=c+f-t+"px";u(i,a);d.openMenu=!0},addButton:function(b){var a=this,e=a.renderer,c=l(a.options.navigation.buttonOptions,b),g=c.onclick,k=c.menuItems,h,d,m={stroke:c.symbolStroke,fill:c.symbolFill},j=c.symbolSize||12;if(!a.btnCount)a.btnCount=0;if(!a.exportDivElements)a.exportDivElements= +[],a.exportSVGElements=[];if(c.enabled!==!1){var n=c.theme,o=n.states,p=o&&o.hover,o=o&&o.select,i;delete n.states;g?i=function(){g.apply(a,arguments)}:k&&(i=function(){a.contextMenu(d.menuClassName,k,d.translateX,d.translateY,d.width,d.height,d);d.setState(2)});c.text&&c.symbol?n.paddingLeft=f.pick(n.paddingLeft,25):c.text||q(n,{width:c.width,height:c.height,padding:0});d=e.button(c.text,0,0,i,n,p,o).attr({title:a.options.lang[c._titleKey],"stroke-linecap":"round"});d.menuClassName=b.menuClassName|| +"highcharts-menu-"+a.btnCount++;c.symbol&&(h=e.symbol(c.symbol,c.symbolX-j/2,c.symbolY-j/2,j,j).attr(q(m,{"stroke-width":c.symbolStrokeWidth||1,zIndex:1})).add(d));d.add().align(q(c,{width:d.width,x:f.pick(c.x,x)}),!0,"spacingBox");x+=(d.width+c.buttonSpacing)*(c.align==="right"?-1:1);a.exportSVGElements.push(d,h)}},destroyExport:function(b){var b=b.target,a,e;for(a=0;a /g, '>'); + + doc.body.innerHTML = ' ' + svg + ''; + } + } // */ + ] + } + } +}; + +// Add the Highcharts.post utility +Highcharts.post = function (url, data, formAttributes) { + var name, + form; + + // create the form + form = createElement('form', merge({ + method: 'post', + action: url, + enctype: 'multipart/form-data' + }, formAttributes), { + display: NONE + }, doc.body); + + // add the data + for (name in data) { + createElement('input', { + type: HIDDEN, + name: name, + value: data[name] + }, null, form); + } + + // submit + form.submit(); + + // clean up + discardElement(form); +}; + +extend(Chart.prototype, { + + /** + * A collection of regex fixes on the produces SVG to account for expando properties, + * browser bugs, VML problems and other. Returns a cleaned SVG. + */ + sanitizeSVG: function (svg) { + return svg + .replace(/zIndex="[^"]+"/g, '') + .replace(/isShadow="[^"]+"/g, '') + .replace(/symbolName="[^"]+"/g, '') + .replace(/jQuery[0-9]+="[^"]+"/g, '') + .replace(/url\([^#]+#/g, 'url(#') + .replace(/') + // Batik doesn't support rgba fills and strokes (#3095) + .replace(/(fill|stroke)="rgba\(([ 0-9]+,[ 0-9]+,[ 0-9]+),([ 0-9\.]+)\)"/g, '$1="rgb($2)" $1-opacity="$3"') + /* This fails in IE < 8 + .replace(/([0-9]+)\.([0-9]+)/g, function(s1, s2, s3) { // round off to save weight + return s2 +'.'+ s3[0]; + })*/ + + // Replace HTML entities, issue #347 + .replace(/ /g, '\u00A0') // no-break space + .replace(//g, '\u00AD') // soft hyphen + + // IE specific + .replace(//g, 'xlink:href="$1"/>') + .replace(/ id=([^" >]+)/g, 'id="$1"') // #4003 + .replace(/class=([^" >]+)/g, 'class="$1"') + .replace(/ transform /g, ' ') + .replace(/:(path|rect)/g, '$1') + .replace(/style="([^"]+)"/g, function (s) { + return s.toLowerCase(); + }); + }, + + /** + * Return an SVG representation of the chart + * + * @param additionalOptions {Object} Additional chart options for the generated SVG representation + */ + getSVG: function (additionalOptions) { + var chart = this, + chartCopy, + sandbox, + svg, + seriesOptions, + sourceWidth, + sourceHeight, + cssWidth, + cssHeight, + options = merge(chart.options, additionalOptions); // copy the options and add extra options + + // IE compatibility hack for generating SVG content that it doesn't really understand + if (!doc.createElementNS) { + /*jslint unparam: true*//* allow unused parameter ns in function below */ + doc.createElementNS = function (ns, tagName) { + return doc.createElement(tagName); + }; + /*jslint unparam: false*/ + } + + // create a sandbox where a new chart will be generated + sandbox = createElement(DIV, null, { + position: ABSOLUTE, + top: '-9999em', + width: chart.chartWidth + PX, + height: chart.chartHeight + PX + }, doc.body); + + // get the source size + cssWidth = chart.renderTo.style.width; + cssHeight = chart.renderTo.style.height; + sourceWidth = options.exporting.sourceWidth || + options.chart.width || + (/px$/.test(cssWidth) && parseInt(cssWidth, 10)) || + 600; + sourceHeight = options.exporting.sourceHeight || + options.chart.height || + (/px$/.test(cssHeight) && parseInt(cssHeight, 10)) || + 400; + + // override some options + extend(options.chart, { + animation: false, + renderTo: sandbox, + forExport: true, + width: sourceWidth, + height: sourceHeight + }); + options.exporting.enabled = false; // hide buttons in print + delete options.data; // #3004 + + // prepare for replicating the chart + options.series = []; + each(chart.series, function (serie) { + seriesOptions = merge(serie.options, { + animation: false, // turn off animation + enableMouseTracking: false, + showCheckbox: false, + visible: serie.visible + }); + + if (!seriesOptions.isInternal) { // used for the navigator series that has its own option set + options.series.push(seriesOptions); + } + }); + + // Axis options must be merged in one by one, since it may be an array or an object (#2022, #3900) + if (additionalOptions) { + each(['xAxis', 'yAxis'], function (axisType) { + each(splat(additionalOptions[axisType]), function (axisOptions, i) { + options[axisType][i] = merge(options[axisType][i], axisOptions); + }); + }); + } + + // generate the chart copy + chartCopy = new Highcharts.Chart(options, chart.callback); + + // reflect axis extremes in the export + each(['xAxis', 'yAxis'], function (axisType) { + each(chart[axisType], function (axis, i) { + var axisCopy = chartCopy[axisType][i], + extremes = axis.getExtremes(), + userMin = extremes.userMin, + userMax = extremes.userMax; + + if (axisCopy && (userMin !== UNDEFINED || userMax !== UNDEFINED)) { + axisCopy.setExtremes(userMin, userMax, true, false); + } + }); + }); + + // get the SVG from the container's innerHTML + svg = chartCopy.container.innerHTML; + + // free up memory + options = null; + chartCopy.destroy(); + discardElement(sandbox); + + // sanitize + svg = this.sanitizeSVG(svg); + + // IE9 beta bugs with innerHTML. Test again with final IE9. + svg = svg.replace(/(url\(#highcharts-[0-9]+)"/g, '$1') + .replace(/"/g, "'"); + + return svg; + }, + + getSVGForExport: function (options, chartOptions) { + var chartExportingOptions = this.options.exporting; + + return this.getSVG(merge( + { chart: { borderRadius: 0 } }, + chartExportingOptions.chartOptions, + chartOptions, + { + exporting: { + sourceWidth: (options && options.sourceWidth) || chartExportingOptions.sourceWidth, + sourceHeight: (options && options.sourceHeight) || chartExportingOptions.sourceHeight + } + } + )); + }, + + /** + * Submit the SVG representation of the chart to the server + * @param {Object} options Exporting options. Possible members are url, type, width and formAttributes. + * @param {Object} chartOptions Additional chart options for the SVG representation of the chart + */ + exportChart: function (options, chartOptions) { + + var svg = this.getSVGForExport(options, chartOptions); + + // merge the options + options = merge(this.options.exporting, options); + + // do the post + Highcharts.post(options.url, { + filename: options.filename || 'chart', + type: options.type, + width: options.width || 0, // IE8 fails to post undefined correctly, so use 0 + scale: options.scale || 2, + svg: svg + }, options.formAttributes); + + }, + + /** + * Print the chart + */ + print: function () { + + var chart = this, + container = chart.container, + origDisplay = [], + origParent = container.parentNode, + body = doc.body, + childNodes = body.childNodes; + + if (chart.isPrinting) { // block the button while in printing mode + return; + } + + chart.isPrinting = true; + + fireEvent(chart, 'beforePrint'); + + // hide all body content + each(childNodes, function (node, i) { + if (node.nodeType === 1) { + origDisplay[i] = node.style.display; + node.style.display = NONE; + } + }); + + // pull out the chart + body.appendChild(container); + + // print + win.focus(); // #1510 + win.print(); + + // allow the browser to prepare before reverting + setTimeout(function () { + + // put the chart back in + origParent.appendChild(container); + + // restore all body content + each(childNodes, function (node, i) { + if (node.nodeType === 1) { + node.style.display = origDisplay[i]; + } + }); + + chart.isPrinting = false; + + fireEvent(chart, 'afterPrint'); + + }, 1000); + + }, + + /** + * Display a popup menu for choosing the export type + * + * @param {String} className An identifier for the menu + * @param {Array} items A collection with text and onclicks for the items + * @param {Number} x The x position of the opener button + * @param {Number} y The y position of the opener button + * @param {Number} width The width of the opener button + * @param {Number} height The height of the opener button + */ + contextMenu: function (className, items, x, y, width, height, button) { + var chart = this, + navOptions = chart.options.navigation, + menuItemStyle = navOptions.menuItemStyle, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + cacheName = 'cache-' + className, + menu = chart[cacheName], + menuPadding = mathMax(width, height), // for mouse leave detection + boxShadow = '3px 3px 10px #888', + innerMenu, + hide, + hideTimer, + menuStyle, + docMouseUpHandler = function (e) { + if (!chart.pointer.inClass(e.target, className)) { + hide(); + } + }; + + // create the menu only the first time + if (!menu) { + + // create a HTML element above the SVG + chart[cacheName] = menu = createElement(DIV, { + className: className + }, { + position: ABSOLUTE, + zIndex: 1000, + padding: menuPadding + PX + }, chart.container); + + innerMenu = createElement(DIV, null, + extend({ + MozBoxShadow: boxShadow, + WebkitBoxShadow: boxShadow, + boxShadow: boxShadow + }, navOptions.menuStyle), menu); + + // hide on mouse out + hide = function () { + css(menu, { display: NONE }); + if (button) { + button.setState(0); + } + chart.openMenu = false; + }; + + // Hide the menu some time after mouse leave (#1357) + addEvent(menu, 'mouseleave', function () { + hideTimer = setTimeout(hide, 500); + }); + addEvent(menu, 'mouseenter', function () { + clearTimeout(hideTimer); + }); + + + // Hide it on clicking or touching outside the menu (#2258, #2335, #2407) + addEvent(document, 'mouseup', docMouseUpHandler); + addEvent(chart, 'destroy', function () { + removeEvent(document, 'mouseup', docMouseUpHandler); + }); + + + // create the items + each(items, function (item) { + if (item) { + var element = item.separator ? + createElement('hr', null, null, innerMenu) : + createElement(DIV, { + onmouseover: function () { + css(this, navOptions.menuItemHoverStyle); + }, + onmouseout: function () { + css(this, menuItemStyle); + }, + onclick: function () { + hide(); + if (item.onclick) { + item.onclick.apply(chart, arguments); + } + }, + innerHTML: item.text || chart.options.lang[item.textKey] + }, extend({ + cursor: 'pointer' + }, menuItemStyle), innerMenu); + + + // Keep references to menu divs to be able to destroy them + chart.exportDivElements.push(element); + } + }); + + // Keep references to menu and innerMenu div to be able to destroy them + chart.exportDivElements.push(innerMenu, menu); + + chart.exportMenuWidth = menu.offsetWidth; + chart.exportMenuHeight = menu.offsetHeight; + } + + menuStyle = { display: 'block' }; + + // if outside right, right align it + if (x + chart.exportMenuWidth > chartWidth) { + menuStyle.right = (chartWidth - x - width - menuPadding) + PX; + } else { + menuStyle.left = (x - menuPadding) + PX; + } + // if outside bottom, bottom align it + if (y + height + chart.exportMenuHeight > chartHeight && button.alignOptions.verticalAlign !== 'top') { + menuStyle.bottom = (chartHeight - y - menuPadding) + PX; + } else { + menuStyle.top = (y + height - menuPadding) + PX; + } + + css(menu, menuStyle); + chart.openMenu = true; + }, + + /** + * Add the export button to the chart + */ + addButton: function (options) { + var chart = this, + renderer = chart.renderer, + btnOptions = merge(chart.options.navigation.buttonOptions, options), + onclick = btnOptions.onclick, + menuItems = btnOptions.menuItems, + symbol, + button, + symbolAttr = { + stroke: btnOptions.symbolStroke, + fill: btnOptions.symbolFill + }, + symbolSize = btnOptions.symbolSize || 12; + if (!chart.btnCount) { + chart.btnCount = 0; + } + + // Keeps references to the button elements + if (!chart.exportDivElements) { + chart.exportDivElements = []; + chart.exportSVGElements = []; + } + + if (btnOptions.enabled === false) { + return; + } + + + var attr = btnOptions.theme, + states = attr.states, + hover = states && states.hover, + select = states && states.select, + callback; + + delete attr.states; + + if (onclick) { + callback = function () { + onclick.apply(chart, arguments); + }; + + } else if (menuItems) { + callback = function () { + chart.contextMenu( + button.menuClassName, + menuItems, + button.translateX, + button.translateY, + button.width, + button.height, + button + ); + button.setState(2); + }; + } + + + if (btnOptions.text && btnOptions.symbol) { + attr.paddingLeft = Highcharts.pick(attr.paddingLeft, 25); + + } else if (!btnOptions.text) { + extend(attr, { + width: btnOptions.width, + height: btnOptions.height, + padding: 0 + }); + } + + button = renderer.button(btnOptions.text, 0, 0, callback, attr, hover, select) + .attr({ + title: chart.options.lang[btnOptions._titleKey], + 'stroke-linecap': 'round' + }); + button.menuClassName = options.menuClassName || PREFIX + 'menu-' + chart.btnCount++; + + if (btnOptions.symbol) { + symbol = renderer.symbol( + btnOptions.symbol, + btnOptions.symbolX - (symbolSize / 2), + btnOptions.symbolY - (symbolSize / 2), + symbolSize, + symbolSize + ) + .attr(extend(symbolAttr, { + 'stroke-width': btnOptions.symbolStrokeWidth || 1, + zIndex: 1 + })).add(button); + } + + button.add() + .align(extend(btnOptions, { + width: button.width, + x: Highcharts.pick(btnOptions.x, buttonOffset) // #1654 + }), true, 'spacingBox'); + + buttonOffset += (button.width + btnOptions.buttonSpacing) * (btnOptions.align === 'right' ? -1 : 1); + + chart.exportSVGElements.push(button, symbol); + + }, + + /** + * Destroy the buttons. + */ + destroyExport: function (e) { + var chart = e.target, + i, + elem; + + // Destroy the extra buttons added + for (i = 0; i < chart.exportSVGElements.length; i++) { + elem = chart.exportSVGElements[i]; + + // Destroy and null the svg/vml elements + if (elem) { // #1822 + elem.onclick = elem.ontouchstart = null; + chart.exportSVGElements[i] = elem.destroy(); + } + } + + // Destroy the divs for the menu + for (i = 0; i < chart.exportDivElements.length; i++) { + elem = chart.exportDivElements[i]; + + // Remove the event handler + removeEvent(elem, 'mouseleave'); + + // Remove inline events + chart.exportDivElements[i] = elem.onmouseout = elem.onmouseover = elem.ontouchstart = elem.onclick = null; + + // Destroy the div by moving to garbage bin + discardElement(elem); + } + } +}); + + +symbols.menu = function (x, y, width, height) { + var arr = [ + M, x, y + 2.5, + L, x + width, y + 2.5, + M, x, y + height / 2 + 0.5, + L, x + width, y + height / 2 + 0.5, + M, x, y + height - 1.5, + L, x + width, y + height - 1.5 + ]; + return arr; +}; + +// Add the buttons on chart load +Chart.prototype.callbacks.push(function (chart) { + var n, + exportingOptions = chart.options.exporting, + buttons = exportingOptions.buttons; + + buttonOffset = 0; + + if (exportingOptions.enabled !== false) { + + for (n in buttons) { + chart.addButton(buttons[n]); + } + + // Destroy the export elements at chart destroy + addEvent(chart, 'destroy', chart.destroyExport); + } + +}); + + +}(Highcharts)); diff --git a/media/js/modules/funnel.js b/media/js/modules/funnel.js index d33b042..6b4f42c 100644 --- a/media/js/modules/funnel.js +++ b/media/js/modules/funnel.js @@ -1,12 +1,13 @@ /* - Highcharts funnel module, Beta + Highcharts funnel module - (c) 2010-2012 Torstein Hønsi + (c) 2010-2014 Torstein Honsi License: www.highcharts.com/license */ -(function(d){var u=d.getOptions().plotOptions,p=d.seriesTypes,D=d.merge,z=function(){},A=d.each;u.funnel=D(u.pie,{center:["50%","50%"],width:"90%",neckWidth:"30%",height:"100%",neckHeight:"25%",dataLabels:{connectorWidth:1,connectorColor:"#606060"},size:!0,states:{select:{color:"#C0C0C0",borderColor:"#000000",shadow:!1}}});p.funnel=d.extendClass(p.pie,{type:"funnel",animate:z,translate:function(){var a=function(k,a){return/%$/.test(k)?a*parseInt(k,10)/100:parseInt(k,10)},g=0,e=this.chart,f=e.plotWidth, -e=e.plotHeight,h=0,c=this.options,C=c.center,b=a(C[0],f),d=a(C[0],e),p=a(c.width,f),i,q,j=a(c.height,e),r=a(c.neckWidth,f),s=a(c.neckHeight,e),v=j-s,a=this.data,w,x,u=c.dataLabels.position==="left"?1:0,y,m,B,n,l,t,o;this.getWidthAt=q=function(k){return k>j-s||j===s?r:r+(p-r)*((j-s-k)/(j-s))};this.getX=function(k,a){return b+(a?-1:1)*(q(k)/2+c.dataLabels.distance)};this.center=[b,d,j];this.centerX=b;A(a,function(a){g+=a.y});A(a,function(a){o=null;x=g?a.y/g:0;m=d-j/2+h*j;l=m+x*j;i=q(m);y=b-i/2;B=y+ -i;i=q(l);n=b-i/2;t=n+i;m>v?(y=n=b-r/2,B=t=b+r/2):l>v&&(o=l,i=q(v),n=b-i/2,t=n+i,l=v);w=["M",y,m,"L",B,m,t,l];o&&w.push(t,o,n,o);w.push(n,l,"Z");a.shapeType="path";a.shapeArgs={d:w};a.percentage=x*100;a.plotX=b;a.plotY=(m+(o||l))/2;a.tooltipPos=[b,a.plotY];a.slice=z;a.half=u;h+=x});this.setTooltipPoints()},drawPoints:function(){var a=this,g=a.options,e=a.chart.renderer;A(a.data,function(f){var h=f.graphic,c=f.shapeArgs;h?h.animate(c):f.graphic=e.path(c).attr({fill:f.color,stroke:g.borderColor,"stroke-width":g.borderWidth}).add(a.group)})}, -sortByAngle:z,drawDataLabels:function(){var a=this.data,g=this.options.dataLabels.distance,e,f,h,c=a.length,d,b;for(this.center[2]-=2*g;c--;)h=a[c],f=(e=h.half)?1:-1,b=h.plotY,d=this.getX(b,e),h.labelPos=[0,b,d+(g-5)*f,b,d+g*f,b,e?"right":"left",0];p.pie.prototype.drawDataLabels.call(this)}})})(Highcharts); +(function(b){var q=b.getOptions(),w=q.plotOptions,r=b.seriesTypes,G=b.merge,E=function(){},B=b.each,F=b.pick;w.funnel=G(w.pie,{animation:!1,center:["50%","50%"],width:"90%",neckWidth:"30%",height:"100%",neckHeight:"25%",reversed:!1,dataLabels:{connectorWidth:1,connectorColor:"#606060"},size:!0,states:{select:{color:"#C0C0C0",borderColor:"#000000",shadow:!1}}});r.funnel=b.extendClass(r.pie,{type:"funnel",animate:E,translate:function(){var a=function(i,a){return/%$/.test(i)?a*parseInt(i,10)/100:parseInt(i, +10)},C=0,e=this.chart,c=this.options,b=c.reversed,j=e.plotWidth,f=e.plotHeight,n=0,e=c.center,g=a(e[0],j),q=a(e[1],f),r=a(c.width,j),k,s,d=a(c.height,f),t=a(c.neckWidth,j),u=a(c.neckHeight,f),x=d-u,a=this.data,y,z,w=c.dataLabels.position==="left"?1:0,A,l,D,p,h,v,m;this.getWidthAt=s=function(i){return i>d-u||d===u?t:t+(r-t)*((d-u-i)/(d-u))};this.getX=function(i,a){return g+(a?-1:1)*(s(b?f-i:i)/2+c.dataLabels.distance)};this.center=[g,q,d];this.centerX=g;B(a,function(a){C+=a.y});B(a,function(a){m=null; +z=C?a.y/C:0;l=q-d/2+n*d;h=l+z*d;k=s(l);A=g-k/2;D=A+k;k=s(h);p=g-k/2;v=p+k;l>x?(A=p=g-t/2,D=v=g+t/2):h>x&&(m=h,k=s(x),p=g-k/2,v=p+k,h=x);b&&(l=d-l,h=d-h,m=m?d-m:null);y=["M",A,l,"L",D,l,v,h];m&&y.push(v,m,p,m);y.push(p,h,"Z");a.shapeType="path";a.shapeArgs={d:y};a.percentage=z*100;a.plotX=g;a.plotY=(l+(m||h))/2;a.tooltipPos=[g,a.plotY];a.slice=E;a.half=w;n+=z})},drawPoints:function(){var a=this,b=a.options,e=a.chart.renderer;B(a.data,function(c){var o=c.options,j=c.graphic,f=c.shapeArgs;j?j.animate(f): +c.graphic=e.path(f).attr({fill:c.color,stroke:F(o.borderColor,b.borderColor),"stroke-width":F(o.borderWidth,b.borderWidth)}).add(a.group)})},sortByAngle:function(a){a.sort(function(a,b){return a.plotY-b.plotY})},drawDataLabels:function(){var a=this.data,b=this.options.dataLabels.distance,e,c,o,j=a.length,f,n;for(this.center[2]-=2*b;j--;)o=a[j],c=(e=o.half)?1:-1,n=o.plotY,f=this.getX(n,e),o.labelPos=[0,n,f+(b-5)*c,n,f+b*c,n,e?"right":"left",0];r.pie.prototype.drawDataLabels.call(this)}});q.plotOptions.pyramid= +b.merge(q.plotOptions.funnel,{neckWidth:"0%",neckHeight:"0%",reversed:!0});b.seriesTypes.pyramid=b.extendClass(b.seriesTypes.funnel,{type:"pyramid"})})(Highcharts); diff --git a/media/js/modules/funnel.src.js b/media/js/modules/funnel.src.js new file mode 100644 index 0000000..e58d109 --- /dev/null +++ b/media/js/modules/funnel.src.js @@ -0,0 +1,310 @@ +/** + * @license + * Highcharts funnel module + * + * (c) 2010-2014 Torstein Honsi + * + * License: www.highcharts.com/license + */ + +/*global Highcharts */ +(function (Highcharts) { + +'use strict'; + +// create shortcuts +var defaultOptions = Highcharts.getOptions(), + defaultPlotOptions = defaultOptions.plotOptions, + seriesTypes = Highcharts.seriesTypes, + merge = Highcharts.merge, + noop = function () {}, + each = Highcharts.each, + pick = Highcharts.pick; + +// set default options +defaultPlotOptions.funnel = merge(defaultPlotOptions.pie, { + animation: false, + center: ['50%', '50%'], + width: '90%', + neckWidth: '30%', + height: '100%', + neckHeight: '25%', + reversed: false, + dataLabels: { + //position: 'right', + connectorWidth: 1, + connectorColor: '#606060' + }, + size: true, // to avoid adapting to data label size in Pie.drawDataLabels + states: { + select: { + color: '#C0C0C0', + borderColor: '#000000', + shadow: false + } + } +}); + + +seriesTypes.funnel = Highcharts.extendClass(seriesTypes.pie, { + + type: 'funnel', + animate: noop, + + /** + * Overrides the pie translate method + */ + translate: function () { + + var + // Get positions - either an integer or a percentage string must be given + getLength = function (length, relativeTo) { + return (/%$/).test(length) ? + relativeTo * parseInt(length, 10) / 100 : + parseInt(length, 10); + }, + + sum = 0, + series = this, + chart = series.chart, + options = series.options, + reversed = options.reversed, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + cumulative = 0, // start at top + center = options.center, + centerX = getLength(center[0], plotWidth), + centerY = getLength(center[1], plotHeight), + width = getLength(options.width, plotWidth), + tempWidth, + getWidthAt, + height = getLength(options.height, plotHeight), + neckWidth = getLength(options.neckWidth, plotWidth), + neckHeight = getLength(options.neckHeight, plotHeight), + neckY = height - neckHeight, + data = series.data, + path, + fraction, + half = options.dataLabels.position === 'left' ? 1 : 0, + + x1, + y1, + x2, + x3, + y3, + x4, + y5; + + // Return the width at a specific y coordinate + series.getWidthAt = getWidthAt = function (y) { + return y > height - neckHeight || height === neckHeight ? + neckWidth : + neckWidth + (width - neckWidth) * ((height - neckHeight - y) / (height - neckHeight)); + }; + series.getX = function (y, half) { + return centerX + (half ? -1 : 1) * ((getWidthAt(reversed ? plotHeight - y : y) / 2) + options.dataLabels.distance); + }; + + // Expose + series.center = [centerX, centerY, height]; + series.centerX = centerX; + + /* + * Individual point coordinate naming: + * + * x1,y1 _________________ x2,y1 + * \ / + * \ / + * \ / + * \ / + * \ / + * x3,y3 _________ x4,y3 + * + * Additional for the base of the neck: + * + * | | + * | | + * | | + * x3,y5 _________ x4,y5 + */ + + + + + // get the total sum + each(data, function (point) { + sum += point.y; + }); + + each(data, function (point) { + // set start and end positions + y5 = null; + fraction = sum ? point.y / sum : 0; + y1 = centerY - height / 2 + cumulative * height; + y3 = y1 + fraction * height; + //tempWidth = neckWidth + (width - neckWidth) * ((height - neckHeight - y1) / (height - neckHeight)); + tempWidth = getWidthAt(y1); + x1 = centerX - tempWidth / 2; + x2 = x1 + tempWidth; + tempWidth = getWidthAt(y3); + x3 = centerX - tempWidth / 2; + x4 = x3 + tempWidth; + + // the entire point is within the neck + if (y1 > neckY) { + x1 = x3 = centerX - neckWidth / 2; + x2 = x4 = centerX + neckWidth / 2; + + // the base of the neck + } else if (y3 > neckY) { + y5 = y3; + + tempWidth = getWidthAt(neckY); + x3 = centerX - tempWidth / 2; + x4 = x3 + tempWidth; + + y3 = neckY; + } + + if (reversed) { + y1 = height - y1; + y3 = height - y3; + y5 = (y5 ? height - y5 : null); + } + // save the path + path = [ + 'M', + x1, y1, + 'L', + x2, y1, + x4, y3 + ]; + if (y5) { + path.push(x4, y5, x3, y5); + } + path.push(x3, y3, 'Z'); + + // prepare for using shared dr + point.shapeType = 'path'; + point.shapeArgs = { d: path }; + + + // for tooltips and data labels + point.percentage = fraction * 100; + point.plotX = centerX; + point.plotY = (y1 + (y5 || y3)) / 2; + + // Placement of tooltips and data labels + point.tooltipPos = [ + centerX, + point.plotY + ]; + + // Slice is a noop on funnel points + point.slice = noop; + + // Mimicking pie data label placement logic + point.half = half; + + cumulative += fraction; + }); + }, + /** + * Draw a single point (wedge) + * @param {Object} point The point object + * @param {Object} color The color of the point + * @param {Number} brightness The brightness relative to the color + */ + drawPoints: function () { + var series = this, + options = series.options, + chart = series.chart, + renderer = chart.renderer; + + each(series.data, function (point) { + var pointOptions = point.options, + graphic = point.graphic, + shapeArgs = point.shapeArgs; + + if (!graphic) { // Create the shapes + point.graphic = renderer.path(shapeArgs). + attr({ + fill: point.color, + stroke: pick(pointOptions.borderColor, options.borderColor), + 'stroke-width': pick(pointOptions.borderWidth, options.borderWidth) + }). + add(series.group); + + } else { // Update the shapes + graphic.animate(shapeArgs); + } + }); + }, + + /** + * Funnel items don't have angles (#2289) + */ + sortByAngle: function (points) { + points.sort(function (a, b) { + return a.plotY - b.plotY; + }); + }, + + /** + * Extend the pie data label method + */ + drawDataLabels: function () { + var data = this.data, + labelDistance = this.options.dataLabels.distance, + leftSide, + sign, + point, + i = data.length, + x, + y; + + // In the original pie label anticollision logic, the slots are distributed + // from one labelDistance above to one labelDistance below the pie. In funnels + // we don't want this. + this.center[2] -= 2 * labelDistance; + + // Set the label position array for each point. + while (i--) { + point = data[i]; + leftSide = point.half; + sign = leftSide ? 1 : -1; + y = point.plotY; + x = this.getX(y, leftSide); + + // set the anchor point for data labels + point.labelPos = [ + 0, // first break of connector + y, // a/a + x + (labelDistance - 5) * sign, // second break, right outside point shape + y, // a/a + x + labelDistance * sign, // landing point for connector + y, // a/a + leftSide ? 'right' : 'left', // alignment + 0 // center angle + ]; + } + + seriesTypes.pie.prototype.drawDataLabels.call(this); + } + +}); + +/** + * Pyramid series type. + * A pyramid series is a special type of funnel, without neck and reversed by default. + */ +defaultOptions.plotOptions.pyramid = Highcharts.merge(defaultOptions.plotOptions.funnel, { + neckWidth: '0%', + neckHeight: '0%', + reversed: true +}); +Highcharts.seriesTypes.pyramid = Highcharts.extendClass(Highcharts.seriesTypes.funnel, { + type: 'pyramid' +}); + +}(Highcharts)); diff --git a/media/js/modules/heatmap.js b/media/js/modules/heatmap.js index 9c3c8d1..2fc8805 100644 --- a/media/js/modules/heatmap.js +++ b/media/js/modules/heatmap.js @@ -1,2 +1,22 @@ -(function(a){var k=a.seriesTypes,l=a.each;k.heatmap=a.extendClass(k.map,{colorKey:"z",useMapGeometry:!1,pointArrayMap:["y","z"],translate:function(){var c=this,a=c.options,i=Number.MAX_VALUE,j=Number.MIN_VALUE;c.generatePoints();l(c.data,function(b){var e=b.x,f=b.y,d=b.z,g=(a.colsize||1)/2,h=(a.rowsize||1)/2;b.path=["M",e-g,f-h,"L",e+g,f-h,"L",e+g,f+h,"L",e-g,f+h,"Z"];b.shapeType="path";b.shapeArgs={d:c.translatePath(b.path)};typeof d==="number"&&(d>j?j=d:d=d)&&(c===void 0||a<=c)){e=g.color;if(b)b.dataClass=j;break}}else{this.isLog&&(a=this.val2lin(a));e=1-(this.max-a)/(this.max-this.min||1);for(j=c.length;j--;)if(e>c[j][0])break;d=c[j]||c[j+1];c=c[j+1]||d;e=1-(c[0]-e)/(c[0]-d[0]||1);e=this.tweenColors(d.color,c.color,e)}return e},getOffset:function(){var a= +this.legendGroup,b=this.chart.axisOffset[this.side];if(a){m.prototype.getOffset.call(this);if(!this.axisGroup.parentGroup)this.axisGroup.add(a),this.gridGroup.add(a),this.labelGroup.add(a),this.added=!0,this.labelLeft=0,this.labelRight=this.width;this.chart.axisOffset[this.side]=b}},setLegendColor:function(){var a,b=this.options;a=this.reversed;a=this.horiz?[+a,0,+!a,0]:[0,+!a,0,+a];this.legendColor={linearGradient:{x1:a[0],y1:a[1],x2:a[2],y2:a[3]},stops:b.stops||[[0,b.minColor],[1,b.maxColor]]}}, +drawLegendSymbol:function(a,b){var e=a.padding,c=a.options,d=this.horiz,f=l(c.symbolWidth,d?200:12),g=l(c.symbolHeight,d?12:200),j=l(c.labelPadding,d?16:30),c=l(c.itemDistance,10);this.setLegendColor();b.legendSymbol=this.chart.renderer.rect(0,a.baseline-11,f,g).attr({zIndex:1}).add(b.legendGroup);b.legendSymbol.getBBox();this.legendItemWidth=f+e+(d?c:j);this.legendItemHeight=g+e+(d?j:0)},setState:o,visible:!0,setVisible:o,getSeriesExtremes:function(){var a;if(this.series.length)a=this.series[0], +this.dataMin=a.valueMin,this.dataMax=a.valueMax},drawCrosshair:function(a,b){var e=b&&b.plotX,c=b&&b.plotY,d,f=this.pos,g=this.len;if(b)d=this.toPixels(b[b.series.colorKey]),df+g&&(d=f+g+2),b.plotX=d,b.plotY=this.len-d,m.prototype.drawCrosshair.call(this,a,b),b.plotX=e,b.plotY=c,this.cross&&this.cross.attr({fill:this.crosshair.color}).add(this.legendGroup)},getPlotLinePath:function(a,b,e,c,d){return typeof d==="number"?this.horiz?["M",d-4,this.top-6,"L",d+4,this.top-6,d,this.top,"Z"]:["M", +this.left,d,"L",this.left-6,d+6,this.left-6,d-6,"Z"]:m.prototype.getPlotLinePath.call(this,a,b,e,c)},update:function(a,b){i(this.series,function(a){a.isDirtyData=!0});m.prototype.update.call(this,a,b);this.legendItem&&(this.setLegendColor(),this.chart.legend.colorizeItem(this,!0))},getDataClassLegendSymbols:function(){var a=this,b=this.chart,e=this.legendItems,c=b.options.legend,d=c.valueDecimals,f=c.valueSuffix||"",g;e.length||i(this.dataClasses,function(c,m){var k=!0,l=c.from,n=c.to;g="";l===void 0? +g="< ":n===void 0&&(g="> ");l!==void 0&&(g+=h.numberFormat(l,d)+f);l!==void 0&&n!==void 0&&(g+=" - ");n!==void 0&&(g+=h.numberFormat(n,d)+f);e.push(s({chart:b,name:g,options:{},drawLegendSymbol:t.drawRectangle,visible:!0,setState:o,setVisible:function(){k=this.visible=!k;i(a.series,function(a){i(a.points,function(a){a.dataClass===m&&a.setVisible(k)})});b.legend.colorizeItem(this,k)}},c))});return e},name:""});i(["fill","stroke"],function(a){HighchartsAdapter.addAnimSetter(a,function(b){b.elem.attr(a, +q.prototype.tweenColors(k(b.start),k(b.end),b.pos))})});w(r.prototype,"getAxes",function(a){var b=this.options.colorAxis;a.call(this);this.colorAxis=[];b&&new q(this,b)});w(x.prototype,"getAllItems",function(a){var b=[],e=this.chart.colorAxis[0];e&&(e.options.dataClasses?b=b.concat(e.getDataClassLegendSymbols()):b.push(e),i(e.series,function(a){a.options.showInLegend=!1}));return b.concat(a.call(this))});r={pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth",fill:"color",dashstyle:"dashStyle"}, +pointArrayMap:["value"],axisTypes:["xAxis","yAxis","colorAxis"],optionalAxis:"colorAxis",trackerGroups:["group","markerGroup","dataLabelsGroup"],getSymbol:o,parallelArrays:["x","y","value"],colorKey:"value",translateColors:function(){var a=this,b=this.options.nullColor,e=this.colorAxis,c=this.colorKey;i(this.data,function(d){var f=d[c];if(f=f===null?b:e&&f!==void 0?e.toColor(f,d):d.color||a.color)d.color=f})}};v.plotOptions.heatmap=n(v.plotOptions.scatter,{animation:!1,borderWidth:0,nullColor:"#F8F8F8", +dataLabels:{formatter:function(){return this.point.value},inside:!0,verticalAlign:"middle",crop:!1,overflow:!1,padding:0},marker:null,pointRange:null,tooltip:{pointFormat:"{point.x}, {point.y}: {point.value}
"},states:{normal:{animation:!0},hover:{halo:!1,brightness:0.2}}});p.heatmap=y(p.scatter,n(r,{type:"heatmap",pointArrayMap:["y","value"],hasPointSpecificOptions:!0,supportsDrilldown:!0,getExtremesFromAll:!0,init:function(){var a;p.scatter.prototype.init.apply(this,arguments);a=this.options; +this.pointRange=a.pointRange=l(a.pointRange,a.colsize||1);this.yAxis.axisPointRange=a.rowsize||1},translate:function(){var a=this.options,b=this.xAxis,e=this.yAxis;this.generatePoints();i(this.points,function(c){var d=(a.colsize||1)/2,f=(a.rowsize||1)/2,g=Math.round(b.len-b.translate(c.x-d,0,1,0,1)),d=Math.round(b.len-b.translate(c.x+d,0,1,0,1)),h=Math.round(e.translate(c.y-f,0,1,0,1)),f=Math.round(e.translate(c.y+f,0,1,0,1));c.plotX=c.clientX=(g+d)/2;c.plotY=(h+f)/2;c.shapeType="rect";c.shapeArgs= +{x:Math.min(g,d),y:Math.min(h,f),width:Math.abs(d-g),height:Math.abs(f-h)}});this.translateColors();this.chart.hasRendered&&i(this.points,function(a){a.shapeArgs.fill=a.options.color||a.color})},drawPoints:p.column.prototype.drawPoints,animate:o,getBox:o,drawLegendSymbol:t.drawRectangle,getExtremes:function(){u.prototype.getExtremes.call(this,this.valueData);this.valueMin=this.dataMin;this.valueMax=this.dataMax;u.prototype.getExtremes.call(this)}}))})(Highcharts); diff --git a/media/js/modules/heatmap.src.js b/media/js/modules/heatmap.src.js new file mode 100644 index 0000000..af7d351 --- /dev/null +++ b/media/js/modules/heatmap.src.js @@ -0,0 +1,660 @@ +/** + * @license Highcharts JS v4.1.5 (2015-04-13) + * + * (c) 2011-2014 Torstein Honsi + * + * License: www.highcharts.com/license + */ + +/*global HighchartsAdapter*/ +(function (Highcharts) { + + +var UNDEFINED, + Axis = Highcharts.Axis, + Chart = Highcharts.Chart, + Color = Highcharts.Color, + Legend = Highcharts.Legend, + LegendSymbolMixin = Highcharts.LegendSymbolMixin, + Series = Highcharts.Series, + + defaultOptions = Highcharts.getOptions(), + each = Highcharts.each, + extend = Highcharts.extend, + extendClass = Highcharts.extendClass, + merge = Highcharts.merge, + pick = Highcharts.pick, + seriesTypes = Highcharts.seriesTypes, + wrap = Highcharts.wrap, + noop = function () {}; + + + + +/** + * The ColorAxis object for inclusion in gradient legends + */ +var ColorAxis = Highcharts.ColorAxis = function () { + this.isColorAxis = true; + this.init.apply(this, arguments); +}; +extend(ColorAxis.prototype, Axis.prototype); +extend(ColorAxis.prototype, { + defaultColorAxisOptions: { + lineWidth: 0, + gridLineWidth: 1, + tickPixelInterval: 72, + startOnTick: true, + endOnTick: true, + offset: 0, + marker: { + animation: { + duration: 50 + }, + color: 'gray', + width: 0.01 + }, + labels: { + overflow: 'justify' + }, + minColor: '#EFEFFF', + maxColor: '#003875', + tickLength: 5 + }, + init: function (chart, userOptions) { + var horiz = chart.options.legend.layout !== 'vertical', + options; + + // Build the options + options = merge(this.defaultColorAxisOptions, { + side: horiz ? 2 : 1, + reversed: !horiz + }, userOptions, { + isX: horiz, + opposite: !horiz, + showEmpty: false, + title: null, + isColor: true + }); + + Axis.prototype.init.call(this, chart, options); + + // Base init() pushes it to the xAxis array, now pop it again + //chart[this.isXAxis ? 'xAxis' : 'yAxis'].pop(); + + // Prepare data classes + if (userOptions.dataClasses) { + this.initDataClasses(userOptions); + } + this.initStops(userOptions); + + // Override original axis properties + this.isXAxis = true; + this.horiz = horiz; + this.zoomEnabled = false; + }, + + /* + * Return an intermediate color between two colors, according to pos where 0 + * is the from color and 1 is the to color. + * NOTE: Changes here should be copied + * to the same function in drilldown.src.js and solid-gauge-src.js. + */ + tweenColors: function (from, to, pos) { + // Check for has alpha, because rgba colors perform worse due to lack of + // support in WebKit. + var hasAlpha, + ret; + + // Unsupported color, return to-color (#3920) + if (!to.rgba.length || !from.rgba.length) { + ret = to.raw || 'none'; + + // Interpolate + } else { + from = from.rgba; + to = to.rgba; + hasAlpha = (to[3] !== 1 || from[3] !== 1); + ret = (hasAlpha ? 'rgba(' : 'rgb(') + + Math.round(to[0] + (from[0] - to[0]) * (1 - pos)) + ',' + + Math.round(to[1] + (from[1] - to[1]) * (1 - pos)) + ',' + + Math.round(to[2] + (from[2] - to[2]) * (1 - pos)) + + (hasAlpha ? (',' + (to[3] + (from[3] - to[3]) * (1 - pos))) : '') + ')'; + } + return ret; + }, + + initDataClasses: function (userOptions) { + var axis = this, + chart = this.chart, + dataClasses, + colorCounter = 0, + options = this.options, + len = userOptions.dataClasses.length; + this.dataClasses = dataClasses = []; + this.legendItems = []; + + each(userOptions.dataClasses, function (dataClass, i) { + var colors; + + dataClass = merge(dataClass); + dataClasses.push(dataClass); + if (!dataClass.color) { + if (options.dataClassColor === 'category') { + colors = chart.options.colors; + dataClass.color = colors[colorCounter++]; + // loop back to zero + if (colorCounter === colors.length) { + colorCounter = 0; + } + } else { + dataClass.color = axis.tweenColors( + Color(options.minColor), + Color(options.maxColor), + len < 2 ? 0.5 : i / (len - 1) // #3219 + ); + } + } + }); + }, + + initStops: function (userOptions) { + this.stops = userOptions.stops || [ + [0, this.options.minColor], + [1, this.options.maxColor] + ]; + each(this.stops, function (stop) { + stop.color = Color(stop[1]); + }); + }, + + /** + * Extend the setOptions method to process extreme colors and color + * stops. + */ + setOptions: function (userOptions) { + Axis.prototype.setOptions.call(this, userOptions); + + this.options.crosshair = this.options.marker; + this.coll = 'colorAxis'; + }, + + setAxisSize: function () { + var symbol = this.legendSymbol, + chart = this.chart, + x, + y, + width, + height; + + if (symbol) { + this.left = x = symbol.attr('x'); + this.top = y = symbol.attr('y'); + this.width = width = symbol.attr('width'); + this.height = height = symbol.attr('height'); + this.right = chart.chartWidth - x - width; + this.bottom = chart.chartHeight - y - height; + + this.len = this.horiz ? width : height; + this.pos = this.horiz ? x : y; + } + }, + + /** + * Translate from a value to a color + */ + toColor: function (value, point) { + var pos, + stops = this.stops, + from, + to, + color, + dataClasses = this.dataClasses, + dataClass, + i; + + if (dataClasses) { + i = dataClasses.length; + while (i--) { + dataClass = dataClasses[i]; + from = dataClass.from; + to = dataClass.to; + if ((from === UNDEFINED || value >= from) && (to === UNDEFINED || value <= to)) { + color = dataClass.color; + if (point) { + point.dataClass = i; + } + break; + } + } + + } else { + + if (this.isLog) { + value = this.val2lin(value); + } + pos = 1 - ((this.max - value) / ((this.max - this.min) || 1)); + i = stops.length; + while (i--) { + if (pos > stops[i][0]) { + break; + } + } + from = stops[i] || stops[i + 1]; + to = stops[i + 1] || from; + + // The position within the gradient + pos = 1 - (to[0] - pos) / ((to[0] - from[0]) || 1); + + color = this.tweenColors( + from.color, + to.color, + pos + ); + } + return color; + }, + + getOffset: function () { + var group = this.legendGroup, + sideOffset = this.chart.axisOffset[this.side]; + + if (group) { + + Axis.prototype.getOffset.call(this); + + if (!this.axisGroup.parentGroup) { + + // Move the axis elements inside the legend group + this.axisGroup.add(group); + this.gridGroup.add(group); + this.labelGroup.add(group); + + this.added = true; + + this.labelLeft = 0; + this.labelRight = this.width; + } + // Reset it to avoid color axis reserving space + this.chart.axisOffset[this.side] = sideOffset; + } + }, + + /** + * Create the color gradient + */ + setLegendColor: function () { + var grad, + horiz = this.horiz, + options = this.options, + reversed = this.reversed; + + grad = horiz ? [+reversed, 0, +!reversed, 0] : [0, +!reversed, 0, +reversed]; // #3190 + this.legendColor = { + linearGradient: { x1: grad[0], y1: grad[1], x2: grad[2], y2: grad[3] }, + stops: options.stops || [ + [0, options.minColor], + [1, options.maxColor] + ] + }; + }, + + /** + * The color axis appears inside the legend and has its own legend symbol + */ + drawLegendSymbol: function (legend, item) { + var padding = legend.padding, + legendOptions = legend.options, + horiz = this.horiz, + box, + width = pick(legendOptions.symbolWidth, horiz ? 200 : 12), + height = pick(legendOptions.symbolHeight, horiz ? 12 : 200), + labelPadding = pick(legendOptions.labelPadding, horiz ? 16 : 30), + itemDistance = pick(legendOptions.itemDistance, 10); + + this.setLegendColor(); + + // Create the gradient + item.legendSymbol = this.chart.renderer.rect( + 0, + legend.baseline - 11, + width, + height + ).attr({ + zIndex: 1 + }).add(item.legendGroup); + box = item.legendSymbol.getBBox(); + + // Set how much space this legend item takes up + this.legendItemWidth = width + padding + (horiz ? itemDistance : labelPadding); + this.legendItemHeight = height + padding + (horiz ? labelPadding : 0); + }, + /** + * Fool the legend + */ + setState: noop, + visible: true, + setVisible: noop, + getSeriesExtremes: function () { + var series; + if (this.series.length) { + series = this.series[0]; + this.dataMin = series.valueMin; + this.dataMax = series.valueMax; + } + }, + drawCrosshair: function (e, point) { + var plotX = point && point.plotX, + plotY = point && point.plotY, + crossPos, + axisPos = this.pos, + axisLen = this.len; + + if (point) { + crossPos = this.toPixels(point[point.series.colorKey]); + if (crossPos < axisPos) { + crossPos = axisPos - 2; + } else if (crossPos > axisPos + axisLen) { + crossPos = axisPos + axisLen + 2; + } + + point.plotX = crossPos; + point.plotY = this.len - crossPos; + Axis.prototype.drawCrosshair.call(this, e, point); + point.plotX = plotX; + point.plotY = plotY; + + if (this.cross) { + this.cross + .attr({ + fill: this.crosshair.color + }) + .add(this.legendGroup); + } + } + }, + getPlotLinePath: function (a, b, c, d, pos) { + if (typeof pos === 'number') { // crosshairs only // #3969 pos can be 0 !! + return this.horiz ? + ['M', pos - 4, this.top - 6, 'L', pos + 4, this.top - 6, pos, this.top, 'Z'] : + ['M', this.left, pos, 'L', this.left - 6, pos + 6, this.left - 6, pos - 6, 'Z']; + } else { + return Axis.prototype.getPlotLinePath.call(this, a, b, c, d); + } + }, + + update: function (newOptions, redraw) { + each(this.series, function (series) { + series.isDirtyData = true; // Needed for Axis.update when choropleth colors change + }); + Axis.prototype.update.call(this, newOptions, redraw); + if (this.legendItem) { + this.setLegendColor(); + this.chart.legend.colorizeItem(this, true); + } + }, + + /** + * Get the legend item symbols for data classes + */ + getDataClassLegendSymbols: function () { + var axis = this, + chart = this.chart, + legendItems = this.legendItems, + legendOptions = chart.options.legend, + valueDecimals = legendOptions.valueDecimals, + valueSuffix = legendOptions.valueSuffix || '', + name; + + if (!legendItems.length) { + each(this.dataClasses, function (dataClass, i) { + var vis = true, + from = dataClass.from, + to = dataClass.to; + + // Assemble the default name. This can be overridden by legend.options.labelFormatter + name = ''; + if (from === UNDEFINED) { + name = '< '; + } else if (to === UNDEFINED) { + name = '> '; + } + if (from !== UNDEFINED) { + name += Highcharts.numberFormat(from, valueDecimals) + valueSuffix; + } + if (from !== UNDEFINED && to !== UNDEFINED) { + name += ' - '; + } + if (to !== UNDEFINED) { + name += Highcharts.numberFormat(to, valueDecimals) + valueSuffix; + } + + // Add a mock object to the legend items + legendItems.push(extend({ + chart: chart, + name: name, + options: {}, + drawLegendSymbol: LegendSymbolMixin.drawRectangle, + visible: true, + setState: noop, + setVisible: function () { + vis = this.visible = !vis; + each(axis.series, function (series) { + each(series.points, function (point) { + if (point.dataClass === i) { + point.setVisible(vis); + } + }); + }); + + chart.legend.colorizeItem(this, vis); + } + }, dataClass)); + }); + } + return legendItems; + }, + name: '' // Prevents 'undefined' in legend in IE8 +}); + +/** + * Handle animation of the color attributes directly + */ +each(['fill', 'stroke'], function (prop) { + HighchartsAdapter.addAnimSetter(prop, function (fx) { + fx.elem.attr(prop, ColorAxis.prototype.tweenColors(Color(fx.start), Color(fx.end), fx.pos)); + }); +}); + +/** + * Extend the chart getAxes method to also get the color axis + */ +wrap(Chart.prototype, 'getAxes', function (proceed) { + + var options = this.options, + colorAxisOptions = options.colorAxis; + + proceed.call(this); + + this.colorAxis = []; + if (colorAxisOptions) { + proceed = new ColorAxis(this, colorAxisOptions); // Fake assignment for jsLint + } +}); + + +/** + * Wrap the legend getAllItems method to add the color axis. This also removes the + * axis' own series to prevent them from showing up individually. + */ +wrap(Legend.prototype, 'getAllItems', function (proceed) { + var allItems = [], + colorAxis = this.chart.colorAxis[0]; + + if (colorAxis) { + + // Data classes + if (colorAxis.options.dataClasses) { + allItems = allItems.concat(colorAxis.getDataClassLegendSymbols()); + // Gradient legend + } else { + // Add this axis on top + allItems.push(colorAxis); + } + + // Don't add the color axis' series + each(colorAxis.series, function (series) { + series.options.showInLegend = false; + }); + } + + return allItems.concat(proceed.call(this)); +});/** + * Mixin for maps and heatmaps + */ +var colorSeriesMixin = { + + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + 'stroke-width': 'borderWidth', + fill: 'color', + dashstyle: 'dashStyle' + }, + pointArrayMap: ['value'], + axisTypes: ['xAxis', 'yAxis', 'colorAxis'], + optionalAxis: 'colorAxis', + trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], + getSymbol: noop, + parallelArrays: ['x', 'y', 'value'], + colorKey: 'value', + + /** + * In choropleth maps, the color is a result of the value, so this needs translation too + */ + translateColors: function () { + var series = this, + nullColor = this.options.nullColor, + colorAxis = this.colorAxis, + colorKey = this.colorKey; + + each(this.data, function (point) { + var value = point[colorKey], + color; + + color = value === null ? nullColor : (colorAxis && value !== undefined) ? colorAxis.toColor(value, point) : point.color || series.color; + + if (color) { + point.color = color; + } + }); + } +}; +/** + * Extend the default options with map options + */ +defaultOptions.plotOptions.heatmap = merge(defaultOptions.plotOptions.scatter, { + animation: false, + borderWidth: 0, + nullColor: '#F8F8F8', + dataLabels: { + formatter: function () { // #2945 + return this.point.value; + }, + inside: true, + verticalAlign: 'middle', + crop: false, + overflow: false, + padding: 0 // #3837 + }, + marker: null, + pointRange: null, // dynamically set to colsize by default + tooltip: { + pointFormat: '{point.x}, {point.y}: {point.value}
' + }, + states: { + normal: { + animation: true + }, + hover: { + halo: false, // #3406, halo is not required on heatmaps + brightness: 0.2 + } + } +}); + +// The Heatmap series type +seriesTypes.heatmap = extendClass(seriesTypes.scatter, merge(colorSeriesMixin, { + type: 'heatmap', + pointArrayMap: ['y', 'value'], + hasPointSpecificOptions: true, + supportsDrilldown: true, + getExtremesFromAll: true, + + /** + * Override the init method to add point ranges on both axes. + */ + init: function () { + var options; + seriesTypes.scatter.prototype.init.apply(this, arguments); + + options = this.options; + this.pointRange = options.pointRange = pick(options.pointRange, options.colsize || 1); // #3758, prevent resetting in setData + this.yAxis.axisPointRange = options.rowsize || 1; // general point range + }, + translate: function () { + var series = this, + options = series.options, + xAxis = series.xAxis, + yAxis = series.yAxis; + + series.generatePoints(); + + each(series.points, function (point) { + var xPad = (options.colsize || 1) / 2, + yPad = (options.rowsize || 1) / 2, + x1 = Math.round(xAxis.len - xAxis.translate(point.x - xPad, 0, 1, 0, 1)), + x2 = Math.round(xAxis.len - xAxis.translate(point.x + xPad, 0, 1, 0, 1)), + y1 = Math.round(yAxis.translate(point.y - yPad, 0, 1, 0, 1)), + y2 = Math.round(yAxis.translate(point.y + yPad, 0, 1, 0, 1)); + + // Set plotX and plotY for use in K-D-Tree and more + point.plotX = point.clientX = (x1 + x2) / 2; + point.plotY = (y1 + y2) / 2; + + point.shapeType = 'rect'; + point.shapeArgs = { + x: Math.min(x1, x2), + y: Math.min(y1, y2), + width: Math.abs(x2 - x1), + height: Math.abs(y2 - y1) + }; + }); + + series.translateColors(); + + // Make sure colors are updated on colorAxis update (#2893) + if (this.chart.hasRendered) { + each(series.points, function (point) { + point.shapeArgs.fill = point.options.color || point.color; // #3311 + }); + } + }, + drawPoints: seriesTypes.column.prototype.drawPoints, + animate: noop, + getBox: noop, + drawLegendSymbol: LegendSymbolMixin.drawRectangle, + + getExtremes: function () { + // Get the extremes from the value data + Series.prototype.getExtremes.call(this, this.valueData); + this.valueMin = this.dataMin; + this.valueMax = this.dataMax; + + // Get the extremes from the y data + Series.prototype.getExtremes.call(this); + } + +})); + + +}(Highcharts)); diff --git a/media/js/modules/no-data-to-display.js b/media/js/modules/no-data-to-display.js index 1771776..46715ee 100644 --- a/media/js/modules/no-data-to-display.js +++ b/media/js/modules/no-data-to-display.js @@ -1,12 +1,12 @@ /* - Highcharts JS v3.0.7 (2013-10-24) + Highcharts JS v4.1.5 (2015-04-13) Plugin for displaying a message when there is no data visible in chart. - (c) 2010-2013 Highsoft AS - Author: Øystein Moseng + (c) 2010-2014 Highsoft AS + Author: Oystein Moseng License: www.highcharts.com/license */ -(function(c){function f(){return!!this.points.length}function g(){this.hasData()?this.hideNoData():this.showNoData()}var d=c.seriesTypes,e=c.Chart.prototype,h=c.getOptions(),i=c.extend;i(h.lang,{noData:"No data to display"});h.noData={position:{x:0,y:0,align:"center",verticalAlign:"middle"},attr:{},style:{fontWeight:"bold",fontSize:"12px",color:"#60606a"}};d.pie.prototype.hasData=f;if(d.gauge)d.gauge.prototype.hasData=f;if(d.waterfall)d.waterfall.prototype.hasData=f;c.Series.prototype.hasData=function(){return this.dataMax!== -void 0&&this.dataMin!==void 0};e.showNoData=function(a){var b=this.options,a=a||b.lang.noData,b=b.noData;if(!this.noDataLabel)this.noDataLabel=this.renderer.label(a,0,0,null,null,null,null,null,"no-data").attr(b.attr).css(b.style).add(),this.noDataLabel.align(i(this.noDataLabel.getBBox(),b.position),!1,"plotBox")};e.hideNoData=function(){if(this.noDataLabel)this.noDataLabel=this.noDataLabel.destroy()};e.hasData=function(){for(var a=this.series,b=a.length;b--;)if(a[b].hasData()&&!a[b].options.isInternal)return!0; -return!1};e.callbacks.push(function(a){c.addEvent(a,"load",g);c.addEvent(a,"redraw",g)})})(Highcharts); +(function(c){function i(){return!!this.points.length}function e(){this.hasData()?this.hideNoData():this.showNoData()}var f=c.seriesTypes,d=c.Chart.prototype,g=c.getOptions(),h=c.extend,j=c.each;h(g.lang,{noData:"No data to display"});g.noData={position:{x:0,y:0,align:"center",verticalAlign:"middle"},attr:{},style:{fontWeight:"bold",fontSize:"12px",color:"#60606a"}};j(["pie","gauge","waterfall","bubble"],function(a){if(f[a])f[a].prototype.hasData=i});c.Series.prototype.hasData=function(){return this.visible&& +this.dataMax!==void 0&&this.dataMin!==void 0};d.showNoData=function(a){var b=this.options,a=a||b.lang.noData,b=b.noData;if(!this.noDataLabel)this.noDataLabel=this.renderer.label(a,0,0,null,null,null,null,null,"no-data").attr(b.attr).css(b.style).add(),this.noDataLabel.align(h(this.noDataLabel.getBBox(),b.position),!1,"plotBox")};d.hideNoData=function(){if(this.noDataLabel)this.noDataLabel=this.noDataLabel.destroy()};d.hasData=function(){for(var a=this.series,b=a.length;b--;)if(a[b].hasData()&&!a[b].options.isInternal)return!0; +return!1};d.callbacks.push(function(a){c.addEvent(a,"load",e);c.addEvent(a,"redraw",e)})})(Highcharts); diff --git a/media/js/modules/no-data-to-display.src.js b/media/js/modules/no-data-to-display.src.js new file mode 100644 index 0000000..a4ab2ef --- /dev/null +++ b/media/js/modules/no-data-to-display.src.js @@ -0,0 +1,125 @@ +/** + * @license Highcharts JS v4.1.5 (2015-04-13) + * Plugin for displaying a message when there is no data visible in chart. + * + * (c) 2010-2014 Highsoft AS + * Author: Oystein Moseng + * + * License: www.highcharts.com/license + */ + +(function (H) { + + var seriesTypes = H.seriesTypes, + chartPrototype = H.Chart.prototype, + defaultOptions = H.getOptions(), + extend = H.extend, + each = H.each; + + // Add language option + extend(defaultOptions.lang, { + noData: 'No data to display' + }); + + // Add default display options for message + defaultOptions.noData = { + position: { + x: 0, + y: 0, + align: 'center', + verticalAlign: 'middle' + }, + attr: { + }, + style: { + fontWeight: 'bold', + fontSize: '12px', + color: '#60606a' + } + }; + + /** + * Define hasData functions for series. These return true if there are data points on this series within the plot area + */ + function hasDataPie() { + return !!this.points.length; /* != 0 */ + } + + each(['pie', 'gauge', 'waterfall', 'bubble'], function (type) { + if (seriesTypes[type]) { + seriesTypes[type].prototype.hasData = hasDataPie; + } + }); + + H.Series.prototype.hasData = function () { + return this.visible && this.dataMax !== undefined && this.dataMin !== undefined; // #3703 + }; + + /** + * Display a no-data message. + * + * @param {String} str An optional message to show in place of the default one + */ + chartPrototype.showNoData = function (str) { + var chart = this, + options = chart.options, + text = str || options.lang.noData, + noDataOptions = options.noData; + + if (!chart.noDataLabel) { + chart.noDataLabel = chart.renderer.label(text, 0, 0, null, null, null, null, null, 'no-data') + .attr(noDataOptions.attr) + .css(noDataOptions.style) + .add(); + chart.noDataLabel.align(extend(chart.noDataLabel.getBBox(), noDataOptions.position), false, 'plotBox'); + } + }; + + /** + * Hide no-data message + */ + chartPrototype.hideNoData = function () { + var chart = this; + if (chart.noDataLabel) { + chart.noDataLabel = chart.noDataLabel.destroy(); + } + }; + + /** + * Returns true if there are data points within the plot area now + */ + chartPrototype.hasData = function () { + var chart = this, + series = chart.series, + i = series.length; + + while (i--) { + if (series[i].hasData() && !series[i].options.isInternal) { + return true; + } + } + + return false; + }; + + /** + * Show no-data message if there is no data in sight. Otherwise, hide it. + */ + function handleNoData() { + var chart = this; + if (chart.hasData()) { + chart.hideNoData(); + } else { + chart.showNoData(); + } + } + + /** + * Add event listener to handle automatic display of no-data message + */ + chartPrototype.callbacks.push(function (chart) { + H.addEvent(chart, 'load', handleNoData); + H.addEvent(chart, 'redraw', handleNoData); + }); + +}(Highcharts)); diff --git a/media/js/modules/solid-gauge.js b/media/js/modules/solid-gauge.js new file mode 100644 index 0000000..5648c08 --- /dev/null +++ b/media/js/modules/solid-gauge.js @@ -0,0 +1,14 @@ +/* + Highcharts JS v4.1.5 (2015-04-13) + Solid angular gauge module + + (c) 2010-2014 Torstein Honsi + + License: www.highcharts.com/license +*/ +(function(b){var q=b.getOptions().plotOptions,r=b.pInt,s=b.pick,j=b.each,k;q.solidgauge=b.merge(q.gauge,{colorByPoint:!0});k={initDataClasses:function(a){var c=this,e=this.chart,d,o=0,f=this.options;this.dataClasses=d=[];j(a.dataClasses,function(g,h){var p,g=b.merge(g);d.push(g);if(!g.color)f.dataClassColor==="category"?(p=e.options.colors,g.color=p[o++],o===p.length&&(o=0)):g.color=c.tweenColors(b.Color(f.minColor),b.Color(f.maxColor),h/(a.dataClasses.length-1))})},initStops:function(a){this.stops= +a.stops||[[0,this.options.minColor],[1,this.options.maxColor]];j(this.stops,function(a){a.color=b.Color(a[1])})},toColor:function(a,c){var e,d=this.stops,b,f=this.dataClasses,g,h;if(f)for(h=f.length;h--;){if(g=f[h],b=g.from,d=g.to,(b===void 0||a>=b)&&(d===void 0||a<=d)){e=g.color;if(c)c.dataClass=h;break}}else{this.isLog&&(a=this.val2lin(a));e=1-(this.max-a)/(this.max-this.min);for(h=d.length;h--;)if(e>d[h][0])break;b=d[h]||d[h+1];d=d[h+1]||b;e=1-(d[0]-e)/(d[0]-b[0]||1);e=this.tweenColors(b.color, +d.color,e)}return e},tweenColors:function(a,c,b){var d;!c.rgba.length||!a.rgba.length?a=c.raw||"none":(a=a.rgba,c=c.rgba,d=c[3]!==1||a[3]!==1,a=(d?"rgba(":"rgb(")+Math.round(c[0]+(a[0]-c[0])*(1-b))+","+Math.round(c[1]+(a[1]-c[1])*(1-b))+","+Math.round(c[2]+(a[2]-c[2])*(1-b))+(d?","+(c[3]+(a[3]-c[3])*(1-b)):"")+")");return a}};j(["fill","stroke"],function(a){HighchartsAdapter.addAnimSetter(a,function(c){c.elem.attr(a,k.tweenColors(b.Color(c.start),b.Color(c.end),c.pos))})});b.seriesTypes.solidgauge= +b.extendClass(b.seriesTypes.gauge,{type:"solidgauge",bindAxes:function(){var a;b.seriesTypes.gauge.prototype.bindAxes.call(this);a=this.yAxis;b.extend(a,k);a.options.dataClasses&&a.initDataClasses(a.options);a.initStops(a.options)},drawPoints:function(){var a=this,c=a.yAxis,e=c.center,d=a.options,o=a.chart.renderer,f=d.overshoot,g=f&&typeof f==="number"?f/180*Math.PI:0;b.each(a.points,function(b){var f=b.graphic,i=c.startAngleRad+c.translate(b.y,null,null,null,!0),j=r(s(b.options.radius,d.radius, +100))*e[2]/200,l=r(s(b.options.innerRadius,d.innerRadius,60))*e[2]/200,m=c.toColor(b.y,b);m==="none"&&(m=b.color||a.color||"none");if(m!=="none")b.color=m;i=Math.max(c.startAngleRad-g,Math.min(c.endAngleRad+g,i));d.wrap===!1&&(i=Math.max(c.startAngleRad,Math.min(c.endAngleRad,i)));var i=i*180/Math.PI,n=i/(180/Math.PI),k=c.startAngleRad,i=Math.min(n,k),n=Math.max(n,k);n-i>2*Math.PI&&(n=i+2*Math.PI);b.shapeArgs=l={x:e[0],y:e[1],r:j,innerR:l,start:i,end:n,fill:m};b.startR=j;f?(b=l.d,f.animate(l),l.d= +b):b.graphic=o.arc(l).attr({stroke:d.borderColor||"none","stroke-width":d.borderWidth||0,fill:m,"sweep-flag":0}).add(a.group)})},animate:function(a){if(!a)this.startAngleRad=this.yAxis.startAngleRad,b.seriesTypes.pie.prototype.animate.call(this,a)}})})(Highcharts); diff --git a/media/js/modules/solid-gauge.src.js b/media/js/modules/solid-gauge.src.js new file mode 100644 index 0000000..358ce7e --- /dev/null +++ b/media/js/modules/solid-gauge.src.js @@ -0,0 +1,267 @@ +/** + * @license Highcharts JS v4.1.5 (2015-04-13) + * Solid angular gauge module + * + * (c) 2010-2014 Torstein Honsi + * + * License: www.highcharts.com/license + */ + +/*global Highcharts, HighchartsAdapter*/ +(function (H) { + "use strict"; + + var defaultPlotOptions = H.getOptions().plotOptions, + pInt = H.pInt, + pick = H.pick, + each = H.each, + colorAxisMethods, + UNDEFINED; + + // The default options + defaultPlotOptions.solidgauge = H.merge(defaultPlotOptions.gauge, { + colorByPoint: true + }); + + + // These methods are defined in the ColorAxis object, and copied here. + // If we implement an AMD system we should make ColorAxis a dependency. + colorAxisMethods = { + + + initDataClasses: function (userOptions) { + var axis = this, + chart = this.chart, + dataClasses, + colorCounter = 0, + options = this.options; + this.dataClasses = dataClasses = []; + + each(userOptions.dataClasses, function (dataClass, i) { + var colors; + + dataClass = H.merge(dataClass); + dataClasses.push(dataClass); + if (!dataClass.color) { + if (options.dataClassColor === 'category') { + colors = chart.options.colors; + dataClass.color = colors[colorCounter++]; + // loop back to zero + if (colorCounter === colors.length) { + colorCounter = 0; + } + } else { + dataClass.color = axis.tweenColors(H.Color(options.minColor), H.Color(options.maxColor), i / (userOptions.dataClasses.length - 1)); + } + } + }); + }, + + initStops: function (userOptions) { + this.stops = userOptions.stops || [ + [0, this.options.minColor], + [1, this.options.maxColor] + ]; + each(this.stops, function (stop) { + stop.color = H.Color(stop[1]); + }); + }, + /** + * Translate from a value to a color + */ + toColor: function (value, point) { + var pos, + stops = this.stops, + from, + to, + color, + dataClasses = this.dataClasses, + dataClass, + i; + + if (dataClasses) { + i = dataClasses.length; + while (i--) { + dataClass = dataClasses[i]; + from = dataClass.from; + to = dataClass.to; + if ((from === UNDEFINED || value >= from) && (to === UNDEFINED || value <= to)) { + color = dataClass.color; + if (point) { + point.dataClass = i; + } + break; + } + } + + } else { + + if (this.isLog) { + value = this.val2lin(value); + } + pos = 1 - ((this.max - value) / (this.max - this.min)); + i = stops.length; + while (i--) { + if (pos > stops[i][0]) { + break; + } + } + from = stops[i] || stops[i + 1]; + to = stops[i + 1] || from; + + // The position within the gradient + pos = 1 - (to[0] - pos) / ((to[0] - from[0]) || 1); + + color = this.tweenColors( + from.color, + to.color, + pos + ); + } + return color; + }, + /* + * Return an intermediate color between two colors, according to pos where 0 + * is the from color and 1 is the to color. + */ + tweenColors: function (from, to, pos) { + // Check for has alpha, because rgba colors perform worse due to lack of + // support in WebKit. + var hasAlpha, + ret; + + // Unsupported color, return to-color (#3920) + if (!to.rgba.length || !from.rgba.length) { + ret = to.raw || 'none'; + + // Interpolate + } else { + from = from.rgba; + to = to.rgba; + hasAlpha = (to[3] !== 1 || from[3] !== 1); + ret = (hasAlpha ? 'rgba(' : 'rgb(') + + Math.round(to[0] + (from[0] - to[0]) * (1 - pos)) + ',' + + Math.round(to[1] + (from[1] - to[1]) * (1 - pos)) + ',' + + Math.round(to[2] + (from[2] - to[2]) * (1 - pos)) + + (hasAlpha ? (',' + (to[3] + (from[3] - to[3]) * (1 - pos))) : '') + ')'; + } + return ret; + } + }; + + /** + * Handle animation of the color attributes directly + */ + each(['fill', 'stroke'], function (prop) { + HighchartsAdapter.addAnimSetter(prop, function (fx) { + fx.elem.attr(prop, colorAxisMethods.tweenColors(H.Color(fx.start), H.Color(fx.end), fx.pos)); + }); + }); + + // The series prototype + H.seriesTypes.solidgauge = H.extendClass(H.seriesTypes.gauge, { + type: 'solidgauge', + + bindAxes: function () { + var axis; + H.seriesTypes.gauge.prototype.bindAxes.call(this); + + axis = this.yAxis; + H.extend(axis, colorAxisMethods); + + // Prepare data classes + if (axis.options.dataClasses) { + axis.initDataClasses(axis.options); + } + axis.initStops(axis.options); + }, + + /** + * Draw the points where each point is one needle + */ + drawPoints: function () { + var series = this, + yAxis = series.yAxis, + center = yAxis.center, + options = series.options, + renderer = series.chart.renderer, + overshoot = options.overshoot, + overshootVal = overshoot && typeof overshoot === 'number' ? overshoot / 180 * Math.PI : 0; + + H.each(series.points, function (point) { + var graphic = point.graphic, + rotation = yAxis.startAngleRad + yAxis.translate(point.y, null, null, null, true), + radius = (pInt(pick(point.options.radius, options.radius, 100)) * center[2]) / 200, // docs: series.data.radius http://jsfiddle.net/highcharts/7nwebu4b/ + innerRadius = (pInt(pick(point.options.innerRadius, options.innerRadius, 60)) * center[2]) / 200, // docs: series .data.innerRadius + shapeArgs, + d, + toColor = yAxis.toColor(point.y, point), + fromColor; + + if (toColor === 'none') { // #3708 + toColor = point.color || series.color || 'none'; + } + if (toColor !== 'none') { + fromColor = point.color; + point.color = toColor; + } + + // Handle overshoot and clipping to axis max/min + rotation = Math.max(yAxis.startAngleRad - overshootVal, Math.min(yAxis.endAngleRad + overshootVal, rotation)); + + // Handle the wrap option + if (options.wrap === false) { + rotation = Math.max(yAxis.startAngleRad, Math.min(yAxis.endAngleRad, rotation)); + } + rotation = rotation * 180 / Math.PI; + + var angle1 = rotation / (180 / Math.PI), + angle2 = yAxis.startAngleRad, + minAngle = Math.min(angle1, angle2), + maxAngle = Math.max(angle1, angle2); + + if (maxAngle - minAngle > 2 * Math.PI) { + maxAngle = minAngle + 2 * Math.PI; + } + + point.shapeArgs = shapeArgs = { + x: center[0], + y: center[1], + r: radius, + innerR: innerRadius, + start: minAngle, + end: maxAngle, + fill: toColor + }; + point.startR = radius; // For PieSeries.animate + + if (graphic) { + d = shapeArgs.d; + graphic.animate(shapeArgs); + shapeArgs.d = d; // animate alters it + } else { + point.graphic = renderer.arc(shapeArgs) + .attr({ + stroke: options.borderColor || 'none', + 'stroke-width': options.borderWidth || 0, + fill: toColor, + 'sweep-flag': 0 + }) + .add(series.group); + } + }); + }, + + /** + * Extend the pie slice animation by animating from start angle and up + */ + animate: function (init) { + + if (!init) { + this.startAngleRad = this.yAxis.startAngleRad; + H.seriesTypes.pie.prototype.animate.call(this, init); + } + } + }); + +}(Highcharts)); diff --git a/media/js/modules/treemap.js b/media/js/modules/treemap.js new file mode 100644 index 0000000..3366fce --- /dev/null +++ b/media/js/modules/treemap.js @@ -0,0 +1,29 @@ +/* + Highcharts JS v4.1.5 (2015-04-13) + + (c) 2014 Highsoft AS + Authors: Jon Arild Nygard / Oystein Moseng + + License: www.highcharts.com/license +*/ +(function(f){var i=f.seriesTypes,l=f.merge,t=f.extendClass,u=f.getOptions().plotOptions,v=function(){},m=f.each,s=f.pick,n=f.Series,q=f.Color;u.treemap=l(u.scatter,{showInLegend:!1,marker:!1,borderColor:"#E0E0E0",borderWidth:1,dataLabels:{enabled:!0,defer:!1,verticalAlign:"middle",formatter:function(){return this.point.name||this.point.id},inside:!0},tooltip:{headerFormat:"",pointFormat:"{point.name}: {point.value}