UNPKG

dashboard

Version:

Create dashboards with gadgets on node.js

2,027 lines (1,779 loc) 170 kB
// ==ClosureCompiler== // @compilation_level SIMPLE_OPTIMIZATIONS /** * @license Highcharts JS v1.2.5 (2010-04-13) * * (c) 2010 Torstein Hønsi * * License: www.highcharts.com/license */ /* * Roadmap * * 2.0 (summer 2010) * - Rewritten rendering engine to SVG/VML * - Save as image * - Print chart - open in a popup and call window.print. * - Improvements to the toolbar object to allow the two above * * 2.1 (summer/autumn 2010) * - Logarithmic axis * - Built-in table parser * - Base value for column, bar and area series * - Automatic margin size based on axis label size and legend. Anti-collision logic for labels. * - Floating columns and bars * - Candlestick charts * - Stock charts? * - Radar charts? * * 2.2 (autumn 2010) * - Improve pies: shadow, better dataLabels, 3D view. * */ (function() { // encapsulated variables var // abstracts to make compiled code smaller undefined, doc = document, win = window, math = Math, mathRound = math.round, mathFloor = math.floor, mathMax = math.max, mathAbs = math.abs, mathCos = math.cos, mathSin = math.sin, // some variables userAgent = navigator.userAgent, isIE = /msie/i.test(userAgent) && !win.opera, isWebKit = /AppleWebKit/.test(userAgent), styleTag, canvasCounter = 0, colorCounter, symbolCounter, symbolSizes = {}, idCounter = 0, timeFactor = 1, // 1 = JavaScript time, 1000 = Unix time garbageBin, // some constants for frequently used strings DIV = 'div', ABSOLUTE = 'absolute', RELATIVE = 'relative', HIDDEN = 'hidden', HIGHCHARTS_HIDDEN = 'highcharts-' + HIDDEN, VISIBLE = 'visible', PX = 'px', // time methods, changed based on whether or not UTC is used makeTime, getMinutes, getHours, getDay, getDate, getMonth, getFullYear, setMinutes, setHours, setDate, setMonth, setFullYear, // check for a custom HighchartsAdapter defined prior to this file globalAdapter = win.HighchartsAdapter, adapter = globalAdapter || {}, // Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object // and all the utility functions will be null. In that case they are populated by the // default adapters below. each = adapter.each, grep = adapter.grep, map = adapter.map, merge = adapter.merge, hyphenate = adapter.hyphenate, addEvent = adapter.addEvent, fireEvent = adapter.fireEvent, animate = adapter.animate, getAjax = adapter.getAjax, // lookup over the types and the associated classes seriesTypes = {}; // the jQuery adapter if (!globalAdapter && win.jQuery) { var jQ = jQuery; each = function(arr, fn) { for (var i = 0, len = arr.length; i < len; i++) { if (fn.call(arr[i], arr[i], i, arr) === false) { return i; } } }; grep = jQ.grep; map = function(arr, fn){ //return jQuery.map(arr, fn); var results = []; for (var i = 0, len = arr.length; i < len; i++) { results[i] = fn.call(arr[i], arr[i], i, arr); } return results; } merge = function(){ var args = arguments; return jQ.extend(true, null, args[0], args[1], args[2], args[3]); } hyphenate = function (str){ return str.replace(/([A-Z])/g, function(a, b){ return '-'+ b.toLowerCase() }); } addEvent = function (el, event, fn){ jQ(el).bind(event, fn); } fireEvent = function(el, type, eventArguments, defaultFunction) { var event = jQ.Event(type), detachedType = 'detached'+ type; extend(event, eventArguments); // Prevent jQuery from triggering the object method that is named the // same as the event. For example, if the event is 'select', jQuery // attempts calling el.select and it goes into a loop. if (el[type]) { el[detachedType] = el[type]; el[type] = null; } // trigger it jQ(el).trigger(event); // attach the method if (el[detachedType]) { el[type] = el[detachedType]; el[detachedType] = null; } if (defaultFunction && !event.isDefaultPrevented()) { defaultFunction(event); } } animate = function (el, params, options) { jQ(el).animate(params, options); } getAjax = function (url, callback) { jQ.get(url, null, callback); } jQ.extend( jQ.easing, { easeOutQuad: function (x, t, b, c, d) { return -c *(t/=d)*(t-2) + b; } }); // the MooTools adapter } else if (!globalAdapter && win.MooTools) { each = $each; map = function (arr, fn){ return arr.map(fn); } grep = function(arr, fn) { return arr.filter(fn) } merge = $merge; hyphenate = function (str){ return str.hyphenate(); } addEvent = function (el, type, fn) { // if the addEvent method is not defined, el is a custom Highcharts object // like series or point if (!el.addEvent) { if (el.nodeName) el = $(el); // a dynamically generated node else extend(el, new Events()); // a custom object } el.addEvent(type, fn); } fireEvent = function(el, event, eventArguments, defaultFunction) { // create an event object that keeps all functions event = new Event({ type: event, target: el }); event = extend (event, eventArguments); // override the preventDefault function to be able to use // this for custom events event.preventDefault = function() { defaultFunction = null; } // if fireEvent is not available on the object, there hasn't been added // any events to it above if (el.fireEvent) el.fireEvent(event.type, event); // fire the default if it is passed and it is not prevented above if (defaultFunction) defaultFunction(event); } animate = function (el, params, options){ var myEffect = new Fx.Morph($(el), extend(options, { transition: Fx.Transitions.Quad.easeInOut })); myEffect.start(params); } getAjax = function (url, callback) { (new Request({ url: url, method: 'get', onSuccess: callback })).send(); } } /** * Check if an element is an array, and if not, make it into an array. Like * MooTools' $.splat. */ function splat(obj) { if (!obj || obj.constructor != Array) obj = [obj]; return obj; } /** * Returns true if the object is not null or undefined. Like MooTools' $.defined. * @param {Object} obj */ function defined (obj) { return obj !== undefined && obj !== null; } /** * Return the first value that is defined. Like MooTools' $.pick. */ function pick() { var args = arguments, i, arg; for (i = 0; i < args.length; i++) { arg = args[i]; if (defined(arg)) return arg; }; } /** * Dynamically add a CSS rule to the page * @param {String} selector * @param {Object} declaration * @param {Boolean} print Whether to add the styles only to print media. IE only. */ function addCSSRule(selector, declaration, print) { var key, serialized = '', styleSheets, last, media = print ? 'print' : '', createStyleTag = function(print) { return createElement( 'style', { type: 'text/css', media: print ? 'print' : '' }, null, doc.getElementsByTagName('HEAD')[0] ); }; // add the style tag the first time if (!styleTag) styleTag = createStyleTag(); // serialize the declaration for (key in declaration) serialized += hyphenate(key) +':'+ declaration[key] + ';'; if (!isIE) { // create a text node in the style tag styleTag.appendChild( doc.createTextNode( selector + " {" + serialized + "}\n" ) ); } else { // get the last stylesheet and add rules var styleSheets = doc.styleSheets, index, styleSheet; if (print) { // only in IE for now createStyleTag(true); } index = styleSheets.length - 1; while (index >= 0 && styleSheets[index].media != media) index--; styleSheet = styleSheets[index]; styleSheet.addRule(selector, serialized); } } /** * Extend an object with the members of another * @param {Object} a The object to be extended * @param {Object} b The object to add to the first one */ function extend(a, b) { if (!a) a = {}; for (var n in b) a[n] = b[n]; return a; } /** * Merge the default options with custom options and return the new options structure * @param {Object} options The new custom options */ function setOptions(options) { defaultOptions = merge(defaultOptions, options); // apply UTC setTimeMethods(); return defaultOptions; } /** * Discard an element by moving it to the bin and delete * @param {Object} The HTML node to discard */ function discardElement(element) { // create a garbage bin element, not part of the DOM if (!garbageBin) garbageBin = createElement(DIV); // move the node and empty bin if (element) garbageBin.appendChild(element); garbageBin.innerHTML = ''; } var defaultFont = 'normal 12px "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', defaultLabelOptions = { enabled: true, // rotation: 0, align: 'center', x: 0, y: 15, /*formatter: function() { return this.value; },*/ style: { color: '#666', font: defaultFont.replace('12px', '11px') //'10px bold "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif' } }, defaultOptions = { colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE', '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'], symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], lang: { loading: 'Loading...', months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], decimalPoint: '.', resetZoom: 'Reset zoom', resetZoomTitle: 'Reset zoom level 1:1', thousandsSep: ',' }, global: { useUTC: true }, chart: { //alignTicks: false, //className: null, /*events: { * load, * selection * }, */ margin: [50, 50, 60, 80], borderColor: '#4572A7', //borderWidth: 0, borderRadius: 5, defaultSeriesType: 'line', ignoreHiddenSeries: true, //inverted: false, //shadow: false, //style: {}, //backgroundColor: null, //plotBackgroundColor: null, plotBorderColor: '#C0C0C0' //plotBorderWidth: 0, //plotShadow: false, //zoomType: '' }, title: { text: 'Chart title', style: { textAlign: 'center', color: '#3E576F', font: defaultFont.replace('12px', '16px'), margin: '10px 0 0 0' } }, subtitle: { text: '', style: { textAlign: 'center', color: '#6D869F', font: defaultFont, margin: 0 } }, plotOptions: { line: { // base series options allowPointSelect: false, //allowDrag: false, // point dragging - not yet implemented //dragType: 'y', showCheckbox: false, animation: true, //cursor: 'default', //enableMouseTracking: true, events: {}, lineWidth: 2, shadow: true, // stacking: null, marker: { enabled: true, symbol: 'auto', lineWidth: 0, radius: 4, lineColor: '#FFFFFF', fillColor: 'auto', states: { // states for a single point hover: { //radius: base + 2 }, select: { fillColor: '#FFFFFF', lineColor: 'auto', lineWidth: 2 } } }, point: { events: {} }, dataLabels: merge(defaultLabelOptions, { enabled: false, y: -6, formatter: function() { return this.y; } }), //pointStart: 0, //pointInterval: 1, showInLegend: true, states: { // states for the entire series hover: { //enabled: false, lineWidth: 3, marker: { // lineWidth: base + 1, // radius: base + 1 } }, select: { marker: {} } } } }, labels: { //items: [], style: { position: ABSOLUTE, color: '#3E576F', font: defaultFont } }, legend: { enabled: true, layout: 'horizontal', labelFormatter: function() { return this.name }, //borderWidth: 0, borderColor: '#909090', borderRadius: 5, shadow: true, //backgroundColor: null, style: { bottom: '10px', left: '80px', padding: '5px' }, itemStyle: { listStyle: 'none', margin: 0, padding: '0 2em 0 0', // make room for the checkbox font: defaultFont, cursor: 'pointer', color: '#3E576F', position: RELATIVE // to allow absolute placement of the checkboxes }, itemHoverStyle: { color: '#000' }, itemHiddenStyle: { color: '#CCC' }, itemCheckboxStyle: { position: ABSOLUTE, right: 0 }, //reversed: false, // docs symbolWidth: 16, symbolPadding: 5 }, loading: { hideDuration: 100, labelStyle: { font: defaultFont.replace('normal', 'bold'), position: RELATIVE, top: '1em' }, showDuration: 100, style: { position: ABSOLUTE, backgroundColor: 'white', opacity: 0.5, textAlign: 'center' } }, tooltip: { enabled: true, formatter: function() { var pThis = this, series = pThis.series, xAxis = series.xAxis, x = pThis.x; return '<b>'+ (pThis.point.name || series.name) +'</b><br/>'+ (defined(x) ? 'X value: '+ (xAxis && xAxis.options.type == 'datetime' ? dateFormat('%Y-%m-%d %H:%M:%S', x) : x) +'<br/>': '')+ 'Y value: '+ pThis.y; }, backgroundColor: 'rgba(255, 255, 255, .85)', borderWidth: 2, borderRadius: 5, shadow: true, snap: 10, style: { color: '#333333', font: defaultFont, fontSize: '9pt', padding: '5px', whiteSpace: 'nowrap' } }, toolbar: { itemStyle: { color: '#4572A7', cursor: 'pointer', margin: '20px', font: defaultFont } }, credits: { enabled: true, text: 'Highcharts.com', href: 'http://www.highcharts.com', style: { position: ABSOLUTE, right: '10px', bottom: '5px', color: '#999', textDecoration: 'none', font: defaultFont.replace('12px', '10px') }, target: '_self' } }; // Axis defaults //defaultOptions.xAxis = merge(defaultOptions.axis); var defaultXAxisOptions = { // alternateGridColor: null, // categories: [], dateTimeLabelFormats: { second: '%H:%M:%S', minute: '%H:%M', hour: '%H:%M', day: '%e. %b', week: '%e. %b', month: '%b \'%y', year: '%Y' }, endOnTick: false, gridLineColor: '#C0C0C0', // gridLineWidth: 0, // reversed: false, labels: defaultLabelOptions, lineColor: '#C0D0E0', lineWidth: 1, max: null, min: null, maxZoom: null, minorGridLineColor: '#E0E0E0', minorGridLineWidth: 1, minorTickColor: '#A0A0A0', //minorTickInterval: null, minorTickLength: 2, minorTickPosition: 'outside', // inside or outside minorTickWidth: 1, //plotBands: [], //plotLines: [], //reversed: false, showFirstLabel: true, showLastLabel: false, startOfWeek: 1, startOnTick: false, tickColor: '#C0D0E0', tickInterval: 'auto', tickLength: 5, tickmarkPlacement: 'between', // on or between tickPixelInterval: 100, tickPosition: 'outside', tickWidth: 1, title: { enabled: false, text: 'X-values', align: 'middle', // low, middle or high margin: 35, //rotation: 0, //side: 'outside', style: { color: '#6D869F', font: defaultFont.replace('normal', 'bold') } }, type: 'linear' // linear or datetime }, defaultYAxisOptions = merge(defaultXAxisOptions, { endOnTick: true, gridLineWidth: 1, tickPixelInterval: 72, showLastLabel: true, labels: { align: 'right', x: -8, y: 3 }, lineWidth: 0, maxPadding: 0.05, minPadding: 0.05, startOnTick: true, tickWidth: 0, title: { enabled: true, margin: 40, rotation: 270, text: 'Y-values' } }), defaultLeftAxisOptions = { labels: { align: 'right', x: -8, y: 3 }, title: { rotation: 270 } }, defaultRightAxisOptions = { labels: { align: 'left', x: 8, y: 3 }, title: { rotation: 90 } }, defaultBottomAxisOptions = { // horizontal axis labels: { align: 'center', x: 0, y: 14 }, title: { rotation: 0 } }, defaultTopAxisOptions = merge(defaultBottomAxisOptions, { labels: { y: -5 } }); // Series defaults var defaultPlotOptions = defaultOptions.plotOptions, defaultSeriesOptions = defaultPlotOptions.line; //defaultPlotOptions.line = merge(defaultSeriesOptions); defaultPlotOptions.spline = merge(defaultSeriesOptions); defaultPlotOptions.scatter = merge(defaultSeriesOptions, { //dragType: 'xy', // n/a lineWidth: 0, states: { hover: { lineWidth: 0 } } }); defaultPlotOptions.area = merge(defaultSeriesOptions, { // lineColor: null, // overrides color, but lets fillColor be unaltered // fillOpacity: .75, fillColor: 'auto' }); defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); defaultPlotOptions.column = merge(defaultSeriesOptions, { borderColor: '#FFFFFF', borderWidth: 1, borderRadius: 0, groupPadding: 0.2, pointPadding: 0.1, states: { hover: { brightness: 0.1, shadow: false }, select: { color: '#C0C0C0', borderColor: '#000000', shadow: false } } }); defaultPlotOptions.bar = merge(defaultPlotOptions.column, { dataLabels: { align: 'left', x: 5, y: 0 } }); defaultPlotOptions.pie = merge(defaultSeriesOptions, { //dragType: '', // n/a borderColor: '#FFFFFF', borderWidth: 1, center: ['50%', '50%'], legendType: 'point', size: '90%', slicedOffset: 10, states: { hover: { brightness: 0.1, shadow: false } } }); // set the default time methods setTimeMethods(); // class-like inheritance function extendClass(parent, members) { var object = function(){}; object.prototype = new parent(); extend(object.prototype, members); return object; } /* function reverseArray(arr) { var reversed = []; for (var i = arr.length - 1; i >= 0; i--) reversed.push( arr[i]); return reversed; } */ // return a deep value without throwing an error /*function deepStructure(obj, path) { // split the path into an array path = path.split('.'), i = 0; // recursively set obj to the path while (path[i] && obj) obj = obj[path[i++]]; if (i == path.length) return obj; }*/ /** * Create a color from a string or configuration object * @param {Object} val */ function setColor(val, ctx) { if (typeof val == 'string') { return val; } else if (val.linearGradient) { var gradient = ctx.createLinearGradient.apply(ctx, val.linearGradient); each (val.stops, function(stop) { gradient.addColorStop(stop[0], stop[1]); }); return gradient; } } var Color = function(input) { var rgba = [], result; function parse(input) { // rgba if((result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(input))) rgba = [parseInt(result[1]), parseInt(result[2]), parseInt(result[3]), parseFloat(result[4])]; // hex else if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input))) rgba = [parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16), 1]; } function get() { // it's NaN if gradient colors on a column chart if (rgba && !isNaN(rgba[0])) return 'rgba('+ rgba.join(',') +')'; else return input; } function brighten(alpha) { if (typeof alpha == 'number' && alpha != 0) { for (var i = 0; i < 3; i++) { rgba[i] += parseInt(alpha * 255); if (rgba[i] < 0) rgba[i] = 0; if (rgba[i] > 255) rgba[i] = 255; } } return this; } function setOpacity(alpha) { rgba[3] = alpha; return this; } parse(input); // public methods return { get: get, brighten: brighten, setOpacity: setOpacity }; }; //defaultMarkers = ['circle']; function createElement (tag, attribs, styles, parent, nopad) { var el = doc.createElement(tag); if (attribs) extend(el, attribs); if (nopad) setStyles(el, {padding: 0, border: 'none', margin: 0}); if (styles) setStyles(el, styles); if (parent) parent.appendChild(el); return el; }; function setStyles (el, styles) { //for (var x in styles) el.style[x] = styles[x]; if (isIE) { if (styles.opacity !== undefined) styles.filter = 'alpha(opacity='+ (styles.opacity * 100) +')'; } extend(el.style, styles); }; /** * Format a number and return a string based on input settings * @param {Number} number The input number to format * @param {Number} decimals The amount of decimals * @param {String} decPoint The decimal point, defaults to the one given in the lang options * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options */ function numberFormat (number, decimals, decPoint, thousandsSep) { var lang = defaultOptions.lang, // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/ n = number, c = isNaN(decimals = mathAbs(decimals)) ? 2 : decimals, d = decPoint === undefined ? lang.decimalPoint : decPoint, t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep, s = n < 0 ? "-" : "", i = parseInt(n = mathAbs(+n || 0).toFixed(c)) + "", j = (j = i.length) > 3 ? j % 3 : 0; return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + mathAbs(n - i).toFixed(c).slice(2) : ""); }; /** * Based on http://www.php.net/manual/en/function.strftime.php * @param {String} format * @param {Number} timestamp * @param {Boolean} capitalize */ function dateFormat(format, timestamp, capitalize) { function pad (number) { return number.toString().replace(/^([0-9])$/, '0$1'); } if (!defined(timestamp)) return 'Invalid date'; var date = new Date(timestamp * timeFactor), // get the basic time values hours = date[getHours](), day = date[getDay](), dayOfMonth = date[getDate](), month = date[getMonth](), fullYear = date[getFullYear](), lang = defaultOptions.lang, langWeekdays = lang.weekdays, langMonths = lang.months, // list all format keys replacements = { // Day 'a': langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon' 'A': langWeekdays[day], // Long weekday, like 'Monday' 'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31 'e': dayOfMonth, // Day of the month, 1 through 31 // Week (none implemented) // Month 'b': langMonths[month].substr(0, 3), // Short month, like 'Jan' 'B': langMonths[month], // Long month, like 'January' 'm': pad(month + 1), // Two digit month number, 01 through 12 // Year 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009 'Y': fullYear, // Four digits year, like 2009 // Time 'H': pad(hours), // Two digits hours in 24h format, 00 through 23 'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11 'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12 'M': pad(date[getMinutes]()), // Two digits minutes, 00 through 59 'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM 'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM 'S': pad(date.getSeconds()) // Two digits seconds, 00 through 59 }; // do the replaces for (var key in replacements) format = format.replace('%'+ key, replacements[key]); // Optionally capitalize the string and return return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format; }; /** * Set the time methods globally based on the useUTC option. Time method can be either * local time or UTC (default). */ function setTimeMethods() { var useUTC = defaultOptions.global.useUTC; makeTime = useUTC ? Date.UTC : function(year, month, date, hours, minutes, seconds) { return new Date( year, month, pick(date, 1), pick(hours, 0), pick(minutes, 0), pick(seconds, 0) ).getTime(); }; getMinutes = useUTC ? 'getUTCMinutes' : 'getMinutes'; getHours = useUTC ? 'getUTCHours' : 'getHours'; getDay = useUTC ? 'getUTCDay' : 'getDay'; getDate = useUTC ? 'getUTCDate' : 'getDate'; getMonth = useUTC ? 'getUTCMonth' : 'getMonth'; getFullYear = useUTC ? 'getUTCFullYear' : 'getFullYear'; setMinutes = useUTC ? 'setUTCMinutes' : 'setMinutes'; setHours = useUTC ? 'setUTCHours' : 'setHours'; setDate = useUTC ? 'setUTCDate' : 'setDate'; setMonth = useUTC ? 'setUTCMonth' : 'setMonth'; setFullYear = useUTC ? 'setUTCFullYear' : 'setFullYear'; }; /** * Return the absolute page position of an element * @param {Object} el */ function getPosition (el) { var p = { x: el.offsetLeft, y: el.offsetTop }; while (el.offsetParent) { el = el.offsetParent; p.x += el.offsetLeft; p.y += el.offsetTop; if (el != doc.body && el != doc.documentElement) { p.x -= el.scrollLeft; p.y -= el.scrollTop; } } return p; } var Layer = function (name, appendTo, props, styles) { var layer = this, div, appendToStyle = appendTo.style; props = extend({ className: 'highcharts-'+ name }, props); styles = extend({ width: appendToStyle.width, //appendTo.offsetWidth + PX, height: appendToStyle.height, //appendTo.offsetHeight + PX, position: ABSOLUTE, top: 0, left: 0, margin: 0, padding: 0, border: 'none' }, styles); div = createElement(DIV, props, styles, appendTo); extend(layer, { div: div, width: parseInt(styles.width), height: parseInt(styles.height) }); // initial SVG string layer.svg = isIE ? '' : '<?xml version="1.0" encoding="utf-8"?>'+ '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" '+ 'xmlns:xlink="http://www.w3.org/1999/xlink" width="'+ layer.width +'px" height="'+ layer.height +'">'; // save it for later layer.basicSvg = layer.svg; } Layer.prototype = { getCtx: function() { if (!this.ctx) { var cvs = createElement('canvas', { id: 'highcharts-canvas-' + idCounter++, width: this.width, height: this.height }, { position: ABSOLUTE }, this.div); if (isIE) { G_vmlCanvasManager.initElement(cvs); cvs = doc.getElementById(cvs.id); } this.ctx = cvs.getContext('2d'); } return this.ctx; }, getSvg: function() { if (!this.svgObject) { var layer = this, div = layer.div, width = layer.width, height = layer.height; if (isIE) { // create xmlns if excanvas hasn't done it if (!doc.namespaces["g_vml_"]) { doc.namespaces.add("g_vml_", "urn:schemas-microsoft-com:vml"); // setup default css doc.createStyleSheet().cssText = "g_vml_\\:*{behavior:url(#default#VML)}"; } this.svgObject = createElement(DIV, null, { width: width + PX, height: height + PX, position: ABSOLUTE }, div); } else { // create an object and inject SVG into it this.svgObject = createElement('object', { width: width, height: height, type: 'image/svg+xml' }, { position : ABSOLUTE, left: 0, top: 0 }, div); } } return this.svgObject; }, drawLine: function(x1, y1, x2, y2, color, width) { var ctx = this.getCtx(), xBefore = x1; // normalize to a crisp line if (x1 == x2) x1 = x2 = mathRound(x1) + (width % 2 / 2); if (y1 == y2) y1 = y2 = mathRound(y1) + (width % 2 / 2); // draw path ctx.lineWidth = width; ctx.lineCap = 'round'; /* If this is not set, plotBands appear like 'square' in Firefox/Win. Reason unknown. */ ctx.beginPath(); ctx.moveTo(x1, y1); ctx.strokeStyle = color; ctx.lineTo(x2, y2); ctx.closePath(); ctx.stroke(); }, drawPolyLine: function(points, color, width, shadow, fillColor) { var ctx = this.getCtx(), shadowLine = []; // the shadow if (shadow && width) { each (points, function(point) { // add 1px offset shadowLine.push(point === undefined ? point : point + 1); }); for (var i = 1; i <= 3; i++) // three lines of differing thickness and opacity this.drawPolyLine(shadowLine, 'rgba(0, 0, 0, '+ (0.05 * i) +')', 6 - 2 * i); } // the line path ctx.beginPath(); for (i = 0; i < points.length; i += 2) ctx[i == 0 ? 'moveTo' : 'lineTo'](points[i], points[i + 1]); // common properties extend(ctx, { lineWidth: width, lineJoin: 'round' }); // stroke if (color && width) { ctx.strokeStyle = setColor(color, ctx); ctx.stroke(); } // fill if (fillColor) { ctx.fillStyle = setColor(fillColor, ctx); ctx.fill(); } }, drawRect: function(x, y, w, h, color, width, radius, fill, shadow, image) { // must (?) be done twice to apply both stroke and fill in excanvas var drawPath = function() { var ret; if (w > 0 && h > 0) { // zero or negative dimensions break Opera 10 ctx.beginPath(); if (!radius) { ctx.rect(x, y, w, h); } else { ctx.moveTo(x, y + radius); ctx.lineTo(x, y + h - radius); ctx.quadraticCurveTo(x, y + h, x + radius, y + h); // change: use bezier ctx.lineTo(x + w - radius, y + h); ctx.quadraticCurveTo(x + w, y + h, x + w, y + h - radius); ctx.lineTo(x + w, y + radius); ctx.quadraticCurveTo(x + w, y , x + w - radius, y); ctx.lineTo(x + radius, y); ctx.quadraticCurveTo(x , y, x, y + radius); } ctx.closePath(); ret = true; } return ret; }; var ctx = this.getCtx(), normalizer = (width || 0) % 2 / 2; // normalize for sharp edges x = mathRound(x) + normalizer; y = mathRound(y) + normalizer; w = mathRound(w - 2 * normalizer); h = mathRound(h - 2 * normalizer); // apply the drop shadow if (shadow) for (var i = 1; i <= 3; i++) { this.drawRect(x + 1, y + 1, w, h, 'rgba(0, 0, 0, '+ (0.05 * i) +')', 6 - 2 * i, radius); } // apply the background image behind everything if (image) ctx.drawImage(image, x, y, w, h); if (drawPath()) { if (fill) { ctx.fillStyle = setColor(fill, ctx); ctx.fill(); // draw path again if (win.G_vmlCanvasManager) drawPath(); } if (width) { ctx.strokeStyle = setColor(color, ctx); ctx.lineWidth = width; ctx.stroke(); } } }, drawSymbol: function(symbol, x, y, radius, lineWidth, lineColor, fillColor) { var ctx = this.getCtx(), imageRegex = /^url\((.*?)\)$/; ctx.beginPath(); if (symbol == 'square') { var len = 0.707 * radius; ctx.moveTo(x - len, y - len); ctx.lineTo(x + len, y - len); ctx.lineTo(x + len, y + len); ctx.lineTo(x - len, y + len); ctx.lineTo(x - len, y - len); } else if (symbol == 'triangle') { y++; ctx.moveTo(x, y - 1.33 * radius); ctx.lineTo(x + radius, y + 0.67 * radius); ctx.lineTo(x - radius, y + 0.67 * radius); ctx.lineTo(x, y - 1.33 * radius); } else if (symbol == 'triangle-down') { y--; ctx.moveTo(x, y + 1.33 * radius); ctx.lineTo(x - radius, y - 0.67 * radius); ctx.lineTo(x + radius, y - 0.67 * radius); ctx.lineTo(x, y + 1.33 * radius); } else if (symbol == 'diamond') { ctx.moveTo(x, y - radius); ctx.lineTo(x + radius, y); ctx.lineTo(x, y + radius); ctx.lineTo(x - radius, y); ctx.lineTo(x, y - radius); } else if (imageRegex.test(symbol)) { createElement('img', { onload: function() { var img = this, size = symbolSizes[img.src] || [img.width, img.height]; setStyles(img, { left: mathRound(x - size[0] / 2) + PX, top: mathRound(y - size[1] / 2) + PX, visibility: VISIBLE }) // Bug workaround: Opera (10.01) fails to get size the second time symbolSizes[img.src] = size; }, src: symbol.match(imageRegex)[1] }, { position: ABSOLUTE, visibility: isIE ? VISIBLE : HIDDEN // hide until left and top are set in Gecko }, this.div); } else { // default: circle ctx.arc(x, y, radius, 0, 2*math.PI, true); } if (fillColor) { ctx.fillStyle = fillColor; ctx.fill(); // draw path again //if (isIE) ctx.arc(x, y, radius, 0, 2*Math.PI, true); } if (lineColor && lineWidth) { ctx.strokeStyle = lineColor || "rgb(100, 100, 255)"; ctx.lineWidth = lineWidth || 2; ctx.stroke(); } }, drawHtml: function(html, attributes, styles) { createElement( DIV, extend(attributes, { innerHTML: html }), extend(styles, { position: ABSOLUTE}), this.div ); }, /** * Add text and draw it. For those browsers adding the text to an SVG object, * it is better for performance to add all strings before the object * is created. This function takes the same arguments as addText. * * @param {string} str * @param {number} x * @param {number} y * @param {object} style * @param {number} rotation * @param {string} align */ drawText: function() { this.addText.apply(this, arguments); this.strokeText(); }, addText: function(str, x, y, style, rotation, align) { if (str || str === 0) { // declare variables var layer = this, hasObject, div = layer.div, CSStransform, css = '', style = style || {}, fill = style.color || '#000000', align = align || 'left', fontSize = parseInt(style.fontSize || style.font.replace(/^[a-z ]+/, '')), span, spanWidth, transformOriginX; // prepare style for (var key in style) css += hyphenate(key) +':'+ style[key] +';'; // what transform property is supported? each (['MozTransform', 'WebkitTransform', 'transform'], function(str) { if (str in div.style) CSStransform = str; }); // if the text is not rotated, or if the browser supports CSS transform, // write a simple span if (!rotation || CSStransform) { span = createElement('span', { innerHTML: str }, extend(style, { position: ABSOLUTE, left: x + PX, whiteSpace: 'nowrap', bottom: mathRound(layer.height - y - fontSize * 0.25) + PX, color: fill }), div); // fix the position according to align and rotation spanWidth = span.offsetWidth; if (align == 'right') setStyles(span, { left: (x - spanWidth) + PX }); else if (align == 'center') setStyles(span, { left: mathRound(x - spanWidth / 2) + PX }); if (rotation) { // ... and CSStransform transformOriginX = { left: 0, center: 50, right: 100 }[align] span.style[CSStransform] = 'rotate('+ rotation +'deg)'; span.style[CSStransform +'Origin'] = transformOriginX +'% 100%'; } } else if (isIE) { // to achieve rotated text, the ie text is drawn on a vector line that // is extrapolated to the left or right or both depending on the // alignment of the text hasObject = true; var radians = (rotation || 0) * math.PI * 2 / 360, // deg to rad costheta = mathCos(radians), sintheta = mathSin(radians), length = layer.width, // the text is not likely to be longer than this baselineCorrection = fontSize / 3 || 3, left = align == 'left', right = align == 'right', x1 = left ? x : x - length * costheta, x2 = right ? x : x + length * costheta, y1 = left ? y : y - length * sintheta, y2 = right ? y : y + length * sintheta; // IE seems to always draw the text with v-text-align middle, so we need // to correct for that by moving the path x1 += baselineCorrection * sintheta; x2 += baselineCorrection * sintheta; y1 -= baselineCorrection * costheta; y2 -= baselineCorrection * costheta; if (mathAbs(x1 - x2) < 0.1) x1 += 0.1; // strange IE painting bug if (mathAbs(y1 - y2) < 0.1) y1 += 0.1; // strange IE painting bug layer.svg += '<g_vml_:line from="'+ x1 +', '+ y1 +'" to="'+ x2 +', '+ y2 +'" stroked="false">'+ '<g_vml_:fill on="true" color="'+ fill +'"/>'+ '<g_vml_:path textpathok="true"/>'+ '<g_vml_:textpath on="true" string="'+ str +'" '+ 'style="v-text-align:'+ align + ';'+ css +'"/>'+ '</g_vml_:line>'; // svg browsers } else { hasObject = true; layer.svg += '<g>'+ '<text transform="translate('+ x +','+ y + ') rotate('+ (rotation || 0) +')" '+ 'style="fill:'+ fill +';text-anchor:'+ { left: 'start', center: 'middle', right: 'end' }[align] + ';'+ css.replace(/"/g, "'") +'">'+ str+ '</text>'+ '</g>'; } layer.hasObject = hasObject; } }, /* Experimental text rendering using canvas text. Speed and possibly weight are the advantages. Excanvas trunk supports canvas text, but not current version (2009-11-03). Older Gecko and Webkit browsers and Opera needs SVG approach, so all in all there is not much weight spared by this one. Furthermore, CanvasText looks crappy in Firefox, but on the other hand, SVG object make the tooltip animation slow. 2009-11-07: Preliminary conclusion: - IE: Use VML on a textpath. The con is that IE8 renders all text as bold italic. Possibly experiment with CSS text rotation for whole 90 degrees if that's supported by IE8. - Firefox >= 3.5 (Gecko 1.9.1 was it?) and Webkit > ?: Use CSS transforms to rotate the text. Canvas and textFill would be a better and faster alternative, but the text is very badly drawn in Firefox. When that's fixed in future versions, go for Canvas and textFill instead. - Opera, older Gecko and older Webkit: Use SVG. It is slow, and all the text has to be added before written to the SVG object. Hence the addText and strokeText functions. When Opera starts supporting textFill or text rotate, use that instead. _addText: function(str, x, y, style, rotation, anchor) { if (str || str === 0) { // declare variables var css = '', style = style || {}, fill = style.color || '#000000', anchor = anchor || 'start', ctx, span, font = (style.font || '') +' '+ (style.fontSize || '') +' '+ (style.fontWeight || '') +' '+ (style.fontFamily || ''), align = { start: 'left', middle: 'center', end: 'right' }[anchor], rotation = (rotation || 0) * math.PI * 2 / 360, // deg to rad fontSize = parseInt(style.fontSize || font); // prepare style for (var key in style) css += hyphenate(key) +':'+ style[key] +';'; if (rotation) { var ctx = this.getCtx(); ctx.font = font; ctx.fillStyle = fill; ctx.textAlign = align; ctx.translate(x, y); ctx.rotate(rotation); ctx.fillText(str, 0, 0); ctx.rotate(-rotation); ctx.translate(-x, -y); } else { } } },*/ strokeText: function() { if (this.hasObject) { var svgObject = this.getSvg(), svg = this.svg; if (isIE) { svgObject.innerHTML = svg; } else { svgObject.data = 'data:image/svg+xml,'+ svg +'</svg>'; // append it again for Chrome to update if (isWebKit) this.div.appendChild(svgObject); } } }, clear: function() { var layer = this, div = this.div, childNodes = div.childNodes, node; if (layer.ctx) layer.ctx.clearRect(0, 0, layer.width, layer.height); if (layer.svgObject) { discardElement(layer.svgObject); layer.svgObject = null; layer.svg = layer.basicSvg; } // remove all spans for (var i = childNodes.length - 1; i >= 0; i--) { node = childNodes[i]; if (/(SPAN|IMG)/.test(node.tagName)) discardElement(node); } }, hide: function() { setStyles (this.div, { display: 'none' }) //jQuery(this.div).fadeOut(250); // A possible way to fade out in IE would be to get all the shapes // in the layer and change the opacities of their FillColor and StrokeColor /*var shapes = this.div.getElementsByTagName('shape'); each (shapes, function(shape) { //shape.style.filter = 'alpha(opacity=59)'; }) var layer = this; setTimeout(function() { layer.div.style.visibility = HIDDEN }, 2000);*/ }, show: function() { setStyles (this.div, { display: '' }) //jQuery(this.div).fadeIn(50); }, /** * Discard layer DOM elements and null the reference */ destroy: function() { discardElement(this.div); return null; } }; function Chart (options) { /** * Add a series dynamically after time * * @param {Object} options The config options * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true. * * @return {Object} series The newly created series object */ function addSeries(options, redraw) { var series; redraw = pick(redraw, true); // defaults to true fireEvent(chart, 'addSeries', { options: options }, function() { series = initSeries(options); series.isDirty = true; chart.isDirty = true; // the series array is out of sync with the display if (redraw) chart.redraw(); }); return series; }; /** * Redraw legend, axes or series based on updated data */ function redraw() { var redrawLegend = chart.isDirty; // handle updated data in the series each (series, function(serie) { if (serie.isDirty) { // prepare the data so axis can read it serie.cleanData(); serie.getSegments(); if (serie.options.legendType == 'point') redrawLegend = true; } }); // reset maxTicks maxTicks = null; if (hasCartesianSeries) { // set axes scales each (axes, function(axis) { axis.setScale(); }) adjustTickAmounts(); // redraw axes each (axes, function(axis) { if (axis.isDirty) axis.redraw(); }) } // redraw affected series each (series, function(serie) { if (serie.isDirty && serie.visible) serie.redraw(); }); // handle added or removed series if (redrawLegend) { // series or pie points are added or removed // draw legend graphics if (legend && legend.renderHTML) { legend.renderHTML(true); legend.drawGraphics(true); } chart.isDirty = false; } // hide tooltip and hover states if (tracker && tracker.resetTracker) tracker.resetTracker(); // fire the event fireEvent(chart, 'redraw'); } /** * Initialize an individual series, called internally before render time */ function initSeries(options) { var type = options.type || optionsChart.defaultSeriesType, typeClass = seriesTypes[type], serie, hasRendered = chart.hasRendered; // an inverted chart can't take a column series and vice versa if (hasRendered) { if (inverted && type == 'column') typeClass = BarSeries; else if (!inverted && type == 'bar') typeClass = ColumnSeries; } serie = new typeClass(); serie.init(chart, options); // set internal chart properties if (!hasRendered && serie.inverted) inverted = true; if (serie.isCartesian) hasCartesianSeries = serie.isCartesian; series.push(serie); return serie; } /** * Dim the chart and show a loading text or symbol */ function showLoading() { var loadingOptions = options.loading; // create the layer at the first call if (!loadingLayer) { loadingLayer = createElement(DIV, { className: 'highcharts-loading' }, extend(loadingOptions.style, { left: marginLeft + PX, top: marginTop + PX, width: plotWidth + PX, height: plotHeight + PX, zIndex: 10, display: 'none' }), container); createElement('span', { innerHTML: options.lang.loading }, loadingOptions.labelStyle, loadingLayer); } // show it setStyles(loadingLayer, { display: '' }); animate(loadingLayer, { opacity: loadingOptions.style.opacity }, { duration: loadingOptions.showDuration }); } /** * Hide the loading layer */ function hideLoading() { animate(loadingLayer, { opacity: 0 }, { duration: options.loading.hideDuration, complete: function() { setStyles(loadingLayer, { display: 'none' }); } }); } /** * Get an axis, series or point object by id. * @param id {String} The id as given in the configuration options */ function get(id) { var i, j, match, data; // search axes for (i = 0; i < axes.length; i++) { if (axes[i].options.id == id) return axes[i]; } // search series for (i = 0; i < series.length; i++) { if (series[i].options.id == id) return series[i]; } // search points for (i = 0; i < series.length; i++) { data = series[i].data; for (j = 0; j < data.length; j++) { if (data[j].id == id) return data[j]; } } return null; } /** * Update the chart's position after it has been moved, to match * the mouse positions with the chart */ function updatePosition() { var container = doc.getElementById(containerId); if (container) { position = getPosition(container); } } /** * Create the Axis instances based on the config options */ function getAxes() { var xAxisOptions = options.xAxis || {}, yAxisOptions = options.yAxis || {}, axis; // make sure the options are arrays and add some members xAxisOptions = splat(xAxisOptions); each(xAxisOptions, function(axis, i) { axis.index = i; axis.isX = true; }); yAxisOptions = splat(yAxisOptions); each(yAxisOptions, function(axis, i) { axis.index = i; }); // concatenate all axis options into one array axes = xAxisOptions.concat(yAxisOptions); // loop the options and construct axis objects chart.xAxis = []; chart.yAxis = []; axes = map (axes, function(axisOptions) { axis = new Axis(chart, axisOptions); chart[axis.isXAxis ? 'xAxis' : 'yAxis'].push(axis); return axis; }); adjustTickAmounts(); }; /** * Adjust all axes tick amounts */ function adjustTickAmounts() { if (optionsChart.alignTicks !== false) each (axes, function(axis) { axis.adjustTickAmount(); }); } /** * Get the currently selected points from all series */ function getSelectedPoints() { var points = []; each(series, function(serie) { points = points.concat( grep( serie.data, function(point) { return point.selected; })); }); return points; }; /** * Get the currently selected series */ function getSelectedSeries() { return grep (series, function (serie) { return serie.selected; }); } /** * Zoom into a given portion of the chart given by axis coordinates * @param {Object} event */ function zoom(event) { var lang = defaultOptions.lang; // add button to reset selection chart.toolbar.add('zoom', lang.resetZoom, lang.resetZoomTitle, function() { //zoom(false); fireEvent(chart, 'selection', { resetSelection: true }, zoom); chart.toolbar.remove('zoom'); }); // if zoom is called with no arguments, reset the axes if (!event || event.resetSelection) each(axes, function(axis) { axis.setExtremes(null, null, false); }); // else, zoom in on all axes else { each (event.xAxis.concat(event.yAxis), function(axisData) { var axis = axisData.axis; // don't zoom more than maxZoom if (chart.tracker[axis.isXAxis ? 'zoomX' : 'zoomY']) axis.setExtremes(axisData.min, axisData.max, false); }); } // redraw chart redraw(); } /** * Function: (private) showTitle * * Show the title and subtitle of the chart */ function showTitle () { var title = options.title, subtitle = options.subtitle; if (!chart.titleLayer) { var titleLayer = new Layer('title-layer', container, null, { zIndex: 2 }); // title if (title && title.text) createElement('h2', { className: 'highcharts-title', innerHTML: title.text }, title.style, titleLayer.div); // subtitle if (subtitle && subtitle.text) createElement('h3', { className: 'highcharts-subtitle', innerHTML: subtitle.text }, subtitle.style, titleLayer.div); chart.titleLayer = titleLayer; } } /** * Load graphics and data required to draw the chart */ function checkResources() { var allLoaded = true; for (var n in chart.resources) { if (!chart.resources[n]) allLoaded = false; } if (allLoaded) resourcesLoaded(); }; /** * Prepare for first rendering after all data are loaded */ function resourcesLoaded() { getAxes(); // Prepare for the axis sizes each(series, function(serie) { serie.translate(); serie.setTooltipPoints(); /*if (options.tooltip.enabled) */ serie.createArea(); }); chart.render = render; setTimeout(function() { // IE(7) needs timeout render(); fireEvent(chart, 'load'); }, 0); } /** * Get the containing element, determine the size and create the inner container * div to hold the chart */ function getContainer() { renderTo = optionsChart.renderTo; containerId = 'highcharts-'+ idCounter++; if (typeof renderTo == 'string') { renderTo = doc.getElementById(renderTo); } // remove previous chart renderTo.innerHTML = ''; // If the container doesn't have an offsetWidth, it has or is a child of a node // that has display:none. We need to temporarily move it out to a visible // state to determine the size, else the legend and tooltips won't render // properly if (!renderTo.offsetWidth) { renderToClone = renderTo.cloneNode(0); setStyles(renderToClone, { position: ABSOLUTE, top: '-9999px', display: '' }); doc.body.appendChild(renderToClone); } // get the width and height var renderToOffsetHeight = (renderToClone || renderTo).offsetHeight; chartWidth = optionsChart.width || (renderToClone || renderTo).offsetWidth || 600; chartHeight = optionsChart.height || // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: (renderToOffsetHeight > marginTop + marginBottom ? renderToOffsetHeight : 0) || 400; // create the inner container container = createElement(DIV, { className: 'highcharts-container' + (optionsChart.className ? ' '+ optionsChart.className : ''), id: containerId }, extend({ position: RELATIVE, overflow: HIDDEN, width: chartWidth + PX, height: chartHeight + PX, textAlign: 'left' }, optionsChart.style), renderToClone || renderTo ); } /** * Render all graphics for the chart */ function render () { var mgn, div, i, labels = options.labels, credits = options.credits; // Chart area mgn = 2 * (optionsChart.borderWidth || 0) + (optionsChart.shadow ? 8 : 0); backgroundLayer.drawRect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, optionsChart.borderColor, optionsChart.borderWidth, optionsChart.borderRadius, optionsChart.backgroundColor, optionsChart.shadow); // Plot background backgroundLayer.drawRect( marginLeft, marginTop, plotWidth, plotHeight, null, null,