UNPKG

@pgaubatz/npm-registry-couchapp

Version:
2,144 lines (1,890 loc) 258 kB
// ==ClosureCompiler== // @compilation_level SIMPLE_OPTIMIZATIONS /** * @license Highcharts JS v2.1.2 (2011-01-12) * * (c) 2009-2010 Torstein Hønsi * * License: www.highcharts.com/license */ // JSLint options: /*jslint forin: true */ /*global document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $ */ (function() { // encapsulated variables var doc = document, win = window, math = Math, mathRound = math.round, mathFloor = math.floor, mathCeil = math.ceil, mathMax = math.max, mathMin = math.min, mathAbs = math.abs, mathCos = math.cos, mathSin = math.sin, mathPI = math.PI, deg2rad = mathPI * 2 / 360, // some variables userAgent = navigator.userAgent, isIE = /msie/i.test(userAgent) && !win.opera, docMode8 = doc.documentMode == 8, isWebKit = /AppleWebKit/.test(userAgent), hasSVG = win.SVGAngle || doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1"), SVG_NS = 'http://www.w3.org/2000/svg', hasTouch = 'ontouchstart' in doc.documentElement, colorCounter, symbolCounter, symbolSizes = {}, idCounter = 0, timeFactor = 1, // 1 = JavaScript time, 1000 = Unix time garbageBin, defaultOptions, dateFormat, // function globalAnimation, pathAnim, // some constants for frequently used strings UNDEFINED, DIV = 'div', ABSOLUTE = 'absolute', RELATIVE = 'relative', HIDDEN = 'hidden', PREFIX = 'highcharts-', VISIBLE = 'visible', PX = 'px', NONE = 'none', M = 'M', L = 'L', /* * Empirical lowest possible opacities for TRACKER_FILL * IE6: 0.002 * IE7: 0.002 * IE8: 0.002 * IE9: 0.00000000001 (unlimited) * FF: 0.00000000001 (unlimited) * Chrome: 0.000001 * Safari: 0.000001 * Opera: 0.00000000001 (unlimited) */ TRACKER_FILL = 'rgba(192,192,192,'+ (hasSVG ? 0.000001 : 0.002) +')', // invisible but clickable NORMAL_STATE = '', HOVER_STATE = 'hover', SELECT_STATE = 'select', // 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, removeEvent = adapter.removeEvent, fireEvent = adapter.fireEvent, animate = adapter.animate, stop = adapter.stop, // lookup over the types and the associated classes seriesTypes = {}, hoverChart; /** * 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; } /** * Shortcut for parseInt * @param {Object} s */ function pInt(s, mag) { return parseInt(s, mag || 10); } /** * Check for string * @param {Object} s */ function isString(s) { return typeof s == 'string'; } /** * Check for object * @param {Object} obj */ function isObject(obj) { return typeof obj == 'object'; } /** * Check for number * @param {Object} n */ function isNumber(n) { return typeof n == 'number'; } /** * Remove last occurence of an item from an array * @param {Array} arr * @param {Mixed} item */ function erase(arr, item) { var i = arr.length; while (i--) { if (arr[i] == item) { arr.splice(i, 1); break; } } //return arr; } /** * Returns true if the object is not null or undefined. Like MooTools' $.defined. * @param {Object} obj */ function defined (obj) { return obj !== UNDEFINED && obj !== null; } /** * Set or get an attribute or an object of attributes. Can't use jQuery attr because * it attempts to set expando properties on the SVG element, which is not allowed. * * @param {Object} elem The DOM element to receive the attribute(s) * @param {String|Object} prop The property or an abject of key-value pairs * @param {String} value The value if a single property is set */ function attr(elem, prop, value) { var key, setAttribute = 'setAttribute', ret; // if the prop is a string if (isString(prop)) { // set the value if (defined(value)) { elem[setAttribute](prop, value); // get the value } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo... ret = elem.getAttribute(prop); } // else if prop is defined, it is a hash of key/value pairs } else if (defined(prop) && isObject(prop)) { for (key in prop) { elem[setAttribute](key, prop[key]); } } return ret; } /** * 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; } /** * Return the first value that is defined. Like MooTools' $.pick. */ function pick() { var args = arguments, i, arg, length = args.length; for (i = 0; i < length; i++) { arg = args[i]; if (typeof arg !== 'undefined' && arg !== null) { return arg; } } } /** * Make a style string from a JS object * @param {Object} style */ function serializeCSS(style) { var s = '', key; // serialize the declaration for (key in style) { s += hyphenate(key) +':'+ style[key] + ';'; } return s; } /** * Set CSS on a give element * @param {Object} el * @param {Object} styles */ function css (el, styles) { if (isIE) { if (styles && styles.opacity !== UNDEFINED) { styles.filter = 'alpha(opacity='+ (styles.opacity * 100) +')'; } } extend(el.style, styles); } /** * Utility function to create element with attributes and styles * @param {Object} tag * @param {Object} attribs * @param {Object} styles * @param {Object} parent * @param {Object} nopad */ function createElement (tag, attribs, styles, parent, nopad) { var el = doc.createElement(tag); if (attribs) { extend(el, attribs); } if (nopad) { css(el, {padding: 0, border: NONE, margin: 0}); } if (styles) { css(el, styles); } if (parent) { parent.appendChild(el); } return el; } /** * Set the global animation to either a given value, or fall back to the * given chart's animation option * @param {Object} animation * @param {Object} chart */ function setAnimation(animation, chart) { globalAnimation = pick(animation, chart.animation); } /* * Define the adapter for frameworks. If an external adapter is not defined, * Highcharts reverts to the built-in jQuery adapter. */ if (globalAdapter && globalAdapter.init) { globalAdapter.init(); } if (!globalAdapter && win.jQuery) { var jQ = jQuery; /** * Utility for iterating over an array. Parameters are reversed compared to jQuery. * @param {Array} arr * @param {Function} fn */ 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; } } }; /** * Filter an array */ grep = jQ.grep; /** * Map an array * @param {Array} arr * @param {Function} fn */ 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; }; /** * Deep merge two objects and return a third object */ merge = function(){ var args = arguments; return jQ.extend(true, null, args[0], args[1], args[2], args[3]); }; /** * Convert a camelCase string to a hyphenated string * @param {String} str */ hyphenate = function (str) { return str.replace(/([A-Z])/g, function(a, b){ return '-'+ b.toLowerCase(); }); }; /** * Add an event listener * @param {Object} el A HTML element or custom object * @param {String} event The event type * @param {Function} fn The event handler */ addEvent = function (el, event, fn){ jQ(el).bind(event, fn); }; /** * Remove event added with addEvent * @param {Object} el The object * @param {String} eventType The event type. Leave blank to remove all events. * @param {Function} handler The function to remove */ removeEvent = function(el, eventType, handler) { // workaround for jQuery issue with unbinding custom events: // http://forum.jquery.com/topic/javascript-error-when-unbinding-a-custom-event-using-jquery-1-4-2 var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent'; if (doc[func] && !el[func]) { el[func] = function() {}; } jQ(el).unbind(eventType, handler); }; /** * Fire an event on a custom object * @param {Object} el * @param {String} type * @param {Object} eventArguments * @param {Function} defaultFunction */ 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 a HTML element or SVG element wrapper * @param {Object} el * @param {Object} params * @param {Object} options jQuery-like animation options: duration, easing, callback */ animate = function (el, params, options) { var $el = jQ(el); if (params.d) { el.toD = params.d; // keep the array form for paths, used in jQ.fx.step.d params.d = 1; // because in jQuery, animating to an array has a different meaning } $el.stop(); $el.animate(params, options); }; /** * Stop running animation */ stop = function (el) { jQ(el).stop(); }; // extend jQuery jQ.extend( jQ.easing, { easeOutQuad: function (x, t, b, c, d) { return -c *(t/=d)*(t-2) + b; } }); // extend the animate function to allow SVG animations var oldStepDefault = jQuery.fx.step._default, oldCur = jQuery.fx.prototype.cur; // do the step jQ.fx.step._default = function(fx){ var elem = fx.elem; if (elem.attr) { // is SVG element wrapper elem.attr(fx.prop, fx.now); } else { oldStepDefault.apply(this, arguments); } }; // animate paths jQ.fx.step.d = function(fx) { var elem = fx.elem; // Normally start and end should be set in state == 0, but sometimes, // for reasons unknown, this doesn't happen. Perhaps state == 0 is skipped // in these cases if (!fx.started) { var ends = pathAnim.init(elem, elem.d, elem.toD); fx.start = ends[0]; fx.end = ends[1]; fx.started = true; } // interpolate each value of the path elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD)); }; // get the current value jQ.fx.prototype.cur = function() { var elem = this.elem, r; if (elem.attr) { // is SVG element wrapper r = elem.attr(this.prop); } else { r = oldCur.apply(this, arguments); } return r; }; } /** * Add a global listener for mousemove events */ /*addEvent(doc, 'mousemove', function(e) { if (globalMouseMove) { globalMouseMove(e); } });*/ /** * Path interpolation algorithm used across adapters */ pathAnim = { /** * Prepare start and end values so that the path can be animated one to one */ init: function(elem, fromD, toD) { fromD = fromD || ''; var shift = elem.shift, bezier = fromD.indexOf('C') > -1, numParams = bezier ? 7 : 3, endLength, slice, i, start = fromD.split(' '), end = [].concat(toD), // copy startBaseLine, endBaseLine, sixify = function(arr) { // in splines make move points have six parameters like bezier curves i = arr.length; while (i--) { if (arr[i] == M) { arr.splice(i + 1, 0, arr[i+1], arr[i+2], arr[i+1], arr[i+2]); } } }; if (bezier) { sixify(start); sixify(end); } // pull out the base lines before padding if (elem.isArea) { startBaseLine = start.splice(start.length - 6, 6); endBaseLine = end.splice(end.length - 6, 6); } // if shifting points, prepend a dummy point to the end path if (shift) { end = [].concat(end).splice(0, numParams).concat(end); elem.shift = false; // reset for following animations } // copy and append last point until the length matches the end length if (start.length) { endLength = end.length; while (start.length < endLength) { //bezier && sixify(start); slice = [].concat(start).splice(start.length - numParams, numParams); if (bezier) { // disable first control point slice[numParams - 6] = slice[numParams - 2]; slice[numParams - 5] = slice[numParams - 1]; } start = start.concat(slice); } } if (startBaseLine) { // append the base lines for areas start = start.concat(startBaseLine); end = end.concat(endBaseLine); } return [start, end]; }, /** * Interpolate each value of the path and return the array */ step: function(start, end, pos, complete) { var ret = [], i = start.length, startVal; if (pos == 1) { // land on the final path without adjustment points appended in the ends ret = complete; } else if (i == end.length && pos < 1) { while (i--) { startVal = parseFloat(start[i]); ret[i] = isNaN(startVal) ? // a letter instruction like M or L start[i] : pos * (parseFloat(end[i] - startVal)) + startVal; } } else { // if animation is finished or length not matching, land on right value ret = end; } return ret; } }; /** * 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'; } /** * 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; } /** * Get the updated default options. Merely exposing defaultOptions for outside modules * isn't enough because the setOptions method creates a new object. */ function getOptions() { 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 = ''; } /* **************************************************************************** * Handle the options * *****************************************************************************/ var defaultLabelOptions = { enabled: true, // rotation: 0, align: 'center', x: 0, y: 15, /*formatter: function() { return this.value; },*/ style: { color: '#666', fontSize: '11px', lineHeight: '14px' } }; 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: { //animation: true, //alignTicks: false, //reflow: true, //className: null, //events: { load, selection }, //margin: [null], //marginTop: null, //marginRight: null, //marginBottom: null, //marginLeft: null, borderColor: '#4572A7', //borderWidth: 0, borderRadius: 5, defaultSeriesType: 'line', ignoreHiddenSeries: true, //inverted: false, //shadow: false, spacingTop: 10, spacingRight: 10, spacingBottom: 15, spacingLeft: 10, style: { fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font fontSize: '12px' }, backgroundColor: '#FFFFFF', //plotBackgroundColor: null, plotBorderColor: '#C0C0C0' //plotBorderWidth: 0, //plotShadow: false, //zoomType: '' }, title: { text: 'Chart title', align: 'center', // floating: false, // margin: 15, // x: 0, // verticalAlign: 'top', y: 15, // docs style: { color: '#3E576F', fontSize: '16px' } }, subtitle: { text: '', align: 'center', // floating: false // x: 0, // verticalAlign: 'top', y: 30, // docs style: { color: '#6D869F' } }, plotOptions: { line: { // base series options allowPointSelect: false, showCheckbox: false, animation: { duration: 1000 }, //cursor: 'default', //dashStyle: null, //enableMouseTracking: true, events: {}, lineWidth: 2, shadow: true, // stacking: null, marker: { enabled: true, //symbol: null, lineWidth: 0, radius: 4, lineColor: '#FFFFFF', //fillColor: null, states: { // states for a single point hover: { //radius: base + 2 }, select: { fillColor: '#FFFFFF', lineColor: '#000000', 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: base + 1, marker: { // lineWidth: base + 1, // radius: base + 1 } }, select: { marker: {} } }, stickyTracking: true //zIndex: null } }, labels: { //items: [], style: { //font: defaultFont, position: ABSOLUTE, color: '#3E576F' } }, legend: { enabled: true, align: 'center', //floating: false, layout: 'horizontal', labelFormatter: function() { return this.name; }, // lineHeight: 16, // docs: deprecated borderWidth: 1, borderColor: '#909090', borderRadius: 5, // margin: 10, // reversed: false, shadow: false, // backgroundColor: null, style: { padding: '5px' }, itemStyle: { cursor: 'pointer', color: '#3E576F' }, itemHoverStyle: { cursor: 'pointer', color: '#000000' }, itemHiddenStyle: { color: '#C0C0C0' }, itemCheckboxStyle: { position: ABSOLUTE, width: '13px', // for IE precision height: '13px' }, // itemWidth: undefined, symbolWidth: 16, symbolPadding: 5, verticalAlign: 'bottom', // width: undefined, x: 0, // docs y: 0 // docs }, loading: { hideDuration: 100, labelStyle: { fontWeight: 'bold', position: RELATIVE, top: '1em' }, showDuration: 100, style: { position: ABSOLUTE, backgroundColor: 'white', opacity: 0.5, textAlign: 'center' } }, tooltip: { enabled: true, //crosshairs: null, backgroundColor: 'rgba(255, 255, 255, .85)', borderWidth: 2, borderRadius: 5, //formatter: defaultFormatter, shadow: true, //shared: false, snap: hasTouch ? 25 : 10, style: { color: '#333333', fontSize: '12px', padding: '5px', whiteSpace: 'nowrap' } }, toolbar: { itemStyle: { color: '#4572A7', cursor: 'pointer' } }, credits: { enabled: true, text: 'Highcharts.com', href: 'http://www.highcharts.com', position: { align: 'right', x: -10, verticalAlign: 'bottom', y: -5 }, style: { cursor: 'pointer', color: '#909090', fontSize: '10px' } } }; // Axis defaults var defaultXAxisOptions = { // allowDecimals: null, // 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', // gridLineDashStyle: 'solid', // docs // gridLineWidth: 0, // reversed: false, labels: defaultLabelOptions, // { step: null }, lineColor: '#C0D0E0', lineWidth: 1, //linkedTo: null, max: null, min: null, minPadding: 0.01, maxPadding: 0.01, //maxZoom: null, minorGridLineColor: '#E0E0E0', // minorGridLineDashStyle: null, minorGridLineWidth: 1, minorTickColor: '#A0A0A0', //minorTickInterval: null, minorTickLength: 2, minorTickPosition: 'outside', // inside or outside //minorTickWidth: 0, //opposite: false, //offset: 0, //plotBands: [{ // events: {}, // zIndex: 1, // labels: { align, x, verticalAlign, y, style, rotation, textAlign } //}], //plotLines: [{ // events: {} // dashStyle: {} // zIndex: // labels: { align, x, verticalAlign, y, style, rotation, textAlign } //}], //reversed: false, // showFirstLabel: true, // showLastLabel: false, startOfWeek: 1, startOnTick: false, tickColor: '#C0D0E0', //tickInterval: null, tickLength: 5, tickmarkPlacement: 'between', // on or between tickPixelInterval: 100, tickPosition: 'outside', tickWidth: 1, title: { //text: null, align: 'middle', // low, middle or high //margin: 0 for horizontal, 10 for vertical axes, //rotation: 0, //side: 'outside', style: { color: '#6D869F', //font: defaultFont.replace('normal', 'bold') fontWeight: 'bold' } //x: 0, //y: 0 }, 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: { rotation: 270, text: 'Y-values' } }), defaultLeftAxisOptions = { labels: { align: 'right', x: -8, y: null // docs }, title: { rotation: 270 } }, defaultRightAxisOptions = { labels: { align: 'left', x: 8, y: null // docs }, title: { rotation: 90 } }, defaultBottomAxisOptions = { // horizontal axis labels: { align: 'center', x: 0, y: 14 // staggerLines: null }, title: { rotation: 0 } }, defaultTopAxisOptions = merge(defaultBottomAxisOptions, { labels: { y: -5 // staggerLines: null } }); // Series defaults var defaultPlotOptions = defaultOptions.plotOptions, defaultSeriesOptions = defaultPlotOptions.line; //defaultPlotOptions.line = merge(defaultSeriesOptions); defaultPlotOptions.spline = merge(defaultSeriesOptions); defaultPlotOptions.scatter = merge(defaultSeriesOptions, { lineWidth: 0, states: { hover: { lineWidth: 0 } } }); defaultPlotOptions.area = merge(defaultSeriesOptions, { // threshold: 0, // lineColor: null, // overrides color, but lets fillColor be unaltered // fillOpacity: 0.75, // fillColor: null }); defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); defaultPlotOptions.column = merge(defaultSeriesOptions, { borderColor: '#FFFFFF', borderWidth: 1, borderRadius: 0, //colorByPoint: undefined, groupPadding: 0.2, marker: null, // point options are specified in the base options pointPadding: 0.1, //pointWidth: null, minPointLength: 0, 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%'], colorByPoint: true, // always true for pies dataLabels: { // align: null, // connectorWidth: 1, // connectorColor: '#606060', // connectorPadding: 5, distance: 30, enabled: true, formatter: function() { return this.point.name; }, y: 5 }, //innerSize: 0, legendType: 'point', marker: null, // point options are specified in the base options size: '75%', showInLegend: false, slicedOffset: 10, states: { hover: { brightness: 0.1, shadow: false } } }); // set the default time methods setTimeMethods(); /** * Extend a prototyped class by new members * @param {Object} parent * @param {Object} members */ function extendClass(parent, members) { var object = function(){}; object.prototype = new parent(); extend(object.prototype, members); return object; } /** * Handle color operations. The object methods are chainable. * @param {String} input The input color in either rbga or hex format */ var Color = function(input) { // declare variables var rgba = [], result; /** * Parse the input color to rgba array * @param {String} input */ function init(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 = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)]; } // hex else if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input))) { rgba = [pInt(result[1],16), pInt(result[2],16), pInt(result[3],16), 1]; } } /** * Return the color a specified format * @param {String} format */ function get(format) { var ret; // it's NaN if gradient colors on a column chart if (rgba && !isNaN(rgba[0])) { if (format == 'rgb') { ret = 'rgb('+ rgba[0] +','+ rgba[1] +','+ rgba[2] +')'; } else if (format == 'a') { ret = rgba[3]; } else { ret = 'rgba('+ rgba.join(',') +')'; } } else { ret = input; } return ret; } /** * Brighten the color * @param {Number} alpha */ function brighten(alpha) { if (isNumber(alpha) && alpha !== 0) { var i; for (i = 0; i < 3; i++) { rgba[i] += pInt(alpha * 255); if (rgba[i] < 0) { rgba[i] = 0; } if (rgba[i] > 255) { rgba[i] = 255; } } } return this; } /** * Set the color's opacity to a given alpha value * @param {Number} alpha */ function setOpacity(alpha) { rgba[3] = alpha; return this; } // initialize: parse the input init(input); // public methods return { get: get, brighten: brighten, setOpacity: setOpacity }; }; /** * 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 = pInt(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 */ dateFormat = function (format, timestamp, capitalize) { function pad (number) { return number.toString().replace(/^([0-9])$/, '0$1'); } if (!defined(timestamp) || isNaN(timestamp)) { return 'Invalid date'; } format = pick(format, '%Y-%m-%d %H:%M:%S'); 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; }; /** * Loop up the node tree and add offsetWidth and offsetHeight to get the * total page offset for a given element. Used by Opera and iOS on hover and * all browsers on point click. * * @param {Object} el * */ function getPosition (el) { var p = { left: el.offsetLeft, top: el.offsetTop }; while ((el = el.offsetParent)) { p.left += el.offsetLeft; p.top += el.offsetTop; if (el != doc.body && el != doc.documentElement) { p.left -= el.scrollLeft; p.top -= el.scrollTop; } } return p; } /** * A wrapper object for SVG elements */ function SVGElement () {} SVGElement.prototype = { /** * Initialize the SVG renderer * @param {Object} renderer * @param {String} nodeName */ init: function(renderer, nodeName) { this.element = doc.createElementNS(SVG_NS, nodeName); this.renderer = renderer; }, /** * Animate a given attribute * @param {Object} params * @param {Number} options The same options as in jQuery animation * @param {Function} complete Function to perform at the end of animation */ animate: function(params, options, complete) { var animOptions = pick(options, globalAnimation, true); if (animOptions) { animOptions = merge(animOptions); if (complete) { // allows using a callback with the global animation without overwriting it animOptions.complete = complete; } animate(this, params, animOptions); } else { this.attr(params); if (complete) { complete(); } } }, /** * Set or get a given attribute * @param {Object|String} hash * @param {Mixed|Undefined} val */ attr: function(hash, val) { var key, value, i, child, element = this.element, nodeName = element.nodeName, renderer = this.renderer, skipAttr, shadows = this.shadows, hasSetSymbolSize, ret = this; // single key-value pair if (isString(hash) && defined(val)) { key = hash; hash = {}; hash[key] = val; } // used as a getter: first argument is a string, second is undefined if (isString(hash)) { key = hash; if (nodeName == 'circle') { key = { x: 'cx', y: 'cy' }[key] || key; } else if (key == 'strokeWidth') { key = 'stroke-width'; } ret = attr(element, key) || this[key] || 0; if (key != 'd' && key != 'visibility') { // 'd' is string in animation step ret = parseFloat(ret); } // setter } else { for (key in hash) { skipAttr = false; // reset value = hash[key]; // paths if (key == 'd') { if (value && value.join) { // join path value = value.join(' '); } if (/(NaN| {2}|^$)/.test(value)) { value = 'M 0 0'; } this.d = value; // shortcut for animations // update child tspans x values } else if (key == 'x' && nodeName == 'text') { for (i = 0; i < element.childNodes.length; i++ ) { child = element.childNodes[i]; // if the x values are equal, the tspan represents a linebreak if (attr(child, 'x') == attr(element, 'x')) { //child.setAttribute('x', value); attr(child, 'x', value); } } if (this.rotation) { attr(element, 'transform', 'rotate('+ this.rotation +' '+ value +' '+ pInt(hash.y || attr(element, 'y')) +')'); } // apply gradients } else if (key == 'fill') { value = renderer.color(value, element, key); // circle x and y } else if (nodeName == 'circle' && (key == 'x' || key == 'y')) { key = { x: 'cx', y: 'cy' }[key] || key; // translation and text rotation } else if (key == 'translateX' || key == 'translateY' || key == 'rotation' || key == 'verticalAlign') { this[key] = value; this.updateTransform(); skipAttr = true; // apply opacity as subnode (required by legacy WebKit and Batik) } else if (key == 'stroke') { value = renderer.color(value, element, key); // emulate VML's dashstyle implementation } else if (key == 'dashstyle') { key = 'stroke-dasharray'; if (value) { value = value.toLowerCase() .replace('shortdashdotdot', '3,1,1,1,1,1,') .replace('shortdashdot', '3,1,1,1') .replace('shortdot', '1,1,') .replace('shortdash', '3,1,') .replace('longdash', '8,3,') .replace(/dot/g, '1,3,') .replace('dash', '4,3,') .replace(/,$/, '') .split(','); // ending comma i = value.length; while (i--) { value[i] = pInt(value[i]) * hash['stroke-width']; } value = value.join(','); } // special } else if (key == 'isTracker') { this[key] = value; // IE9/MooTools combo: MooTools returns objects instead of numbers and IE9 Beta 2 // is unable to cast them. Test again with final IE9. } else if (key == 'width') { value = pInt(value); // Text alignment } else if (key == 'align') { key = 'text-anchor'; value = { left: 'start', center: 'middle', right: 'end' }[value]; } // jQuery animate changes case if (key == 'strokeWidth') { key = 'stroke-width'; } // Chrome/Win < 6 bug (http://code.google.com/p/chromium/issues/detail?id=15461) if (isWebKit && key == 'stroke-width' && value === 0) { value = 0.000001; } // symbols if (this.symbolName && /^(x|y|r|start|end|innerR)/.test(key)) { if (!hasSetSymbolSize) { this.symbolAttr(hash); hasSetSymbolSize = true; } skipAttr = true; } // let the shadow follow the main element if (shadows && /^(width|height|visibility|x|y|d)$/.test(key)) { i = shadows.length; while (i--) { attr(shadows[i], key, value); } } /* trows errors in Chrome if ((key == 'width' || key == 'height') && nodeName == 'rect' && value < 0) { console.log(element); } */ if (key == 'text') { // only one node allowed this.textStr = value; renderer.buildText(this); } else if (!skipAttr) { //element.setAttribute(key, value); attr(element, key, value); } } } return ret; }, /** * If one of the symbol size affecting parameters are changed, * check all the others only once for each call to an element's * .attr() method * @param {Object} hash */ symbolAttr: function(hash) { var wrapper = this; wrapper.x = pick(hash.x, wrapper.x); wrapper.y = pick(hash.y, wrapper.y); // mootools animation bug needs parseFloat wrapper.r = pick(hash.r, wrapper.r); wrapper.start = pick(hash.start, wrapper.start); wrapper.end = pick(hash.end, wrapper.end); wrapper.width = pick(hash.width, wrapper.width); wrapper.height = pick(hash.height, wrapper.height); wrapper.innerR = pick(hash.innerR, wrapper.innerR); wrapper.attr({ d: wrapper.renderer.symbols[wrapper.symbolName](wrapper.x, wrapper.y, wrapper.r, { start: wrapper.start, end: wrapper.end, width: wrapper.width, height: wrapper.height, innerR: wrapper.innerR }) }); }, /** * Apply a clipping path to this object * @param {String} id */ clip: function(clipRect) { return this.attr('clip-path', 'url('+ this.renderer.url +'#'+ clipRect.id +')'); }, /** * Set styles for the element * @param {Object} styles */ css: function(styles) { var elemWrapper = this, elem = elemWrapper.element; // convert legacy if (styles && styles.color) { styles.fill = styles.color; } // save the styles in an object styles = extend( elemWrapper.styles, styles ); // serialize and set style attribute if (isIE && !hasSVG) { // legacy IE doesn't support setting style attribute css(elemWrapper.element, styles); } else { elemWrapper.attr({ style: serializeCSS(styles) }); } // store object elemWrapper.styles = styles; // re-build text if (styles.width && elem.nodeName == 'text' && elemWrapper.added) { elemWrapper.renderer.buildText(elemWrapper); } return elemWrapper; }, /** * Add an event listener * @param {String} eventType * @param {Function} handler */ on: function(eventType, handler) { var fn = handler; // touch if (hasTouch && eventType == 'click') { eventType = 'touchstart'; fn = function(e) { e.preventDefault(); handler(); } } // simplest possible event model for internal use this.element['on'+ eventType] = fn; return this; }, /** * Move an object and its children by x and y values * @param {Number} x * @param {Number} y */ translate: function(x, y) { return this.attr({ translateX: x, translateY: y }); }, /** * Invert a group, rotate and flip */ invert: function() { var wrapper = this; wrapper.inverted = true; wrapper.updateTransform(); return wrapper; }, /** * Private method to update the transform attribute based on internal * properties */ updateTransform: function() { var wrapper = this, translateX = wrapper.translateX || 0, translateY = wrapper.translateY || 0, inverted = wrapper.inverted, rotation = wrapper.rotation, transform = []; // flipping affects translate as adjustment for flipping around the group's axis if (inverted) { translateX += wrapper.attr('width'); translateY += wrapper.attr('height'); } // apply translate if (translateX || translateY) { transform.push('translate('+ translateX +','+ translateY +')'); } // apply rotation if (inverted) { transform.push('rotate(90) scale(-1,1)'); } else if (rotation) { // text rotation transform.push('rotate('+ rotation +' '+ wrapper.x +' '+ wrapper.y +')'); } if (transform.length) { attr(wrapper.element, 'transform', transform.join(' ')); } }, /** * Bring the element to the front */ toFront: function() { var element = this.element; element.parentNode.appendChild(element); return this; }, /** * Break down alignment options like align, verticalAlign, x and y * to x and y relative to the chart. * * @param {Object} alignOptions * @param {Boolean} alignByTranslate * @param {Object} box The box to align to, needs a width and height * */ align: function(alignOptions, alignByTranslate, box) { if (!alignOptions) { // called on resize alignOptions = this.alignOptions; alignByTranslate = this.alignByTranslate; } else { // first call on instanciate this.alignOptions = alignOptions; this.alignByTranslate = alignByTranslate; if (!box) { // boxes other than renderer handle this internally this.renderer.alignedObjects.push(this); } } box = pick(box, this.renderer); var align = alignOptions.align, vAlign = alignOptions.verticalAlign, x = (box.x || 0) + (alignOptions.x || 0), // default: left align y = (box.y || 0) + (alignOptions.y || 0), // default: top align attribs = {}; // align if (/^(right|center)$/.test(align)) { x += (box.width - (alignOptions.width || 0) ) / { right: 1, center: 2 }[align]; } attribs[alignByTranslate ? 'translateX' : 'x'] = x; // vertical align if (/^(bottom|middle)$/.test(vAlign)) { y += (box.height - (alignOptions.height || 0)) / ({ bottom: 1, middle: 2 }[vAlign] || 1); } attribs[alignByTranslate ? 'translateY' : 'y'] = y; // animate only if already placed this[this.placed ? 'animate' : 'attr'](attribs); this.placed = true; return this; }, /** * Get the bounding box (width, height, x and y) for the element */ getBBox: function() { var bBox, width, height, rotation = this.rotation, rad = rotation * deg2rad; try { // fails in Firefox if the container has display: none // use extend because IE9 is not allowed to change width and height in case // of rotation (below) bBox = extend({}, this.element.getBBox()); } catch(e) { bBox = { width: 0, height: 0 }; } width = bBox.width; height = bBox.height; // adjust for rotated text if (rotation) { bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad)); bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad)); } return bBox; }, /* * * Manually compute width and height of rotated text from non-rotated. Shared by SVG and VML * @param {Object} bBox * @param {number} rotation * / rotateBBox: function(bBox, rotation) { var rad = rotation * math.PI * 2 / 360, // radians width = bBox.width, height = bBox.height; },*/ /** * Show the element */ show: function() { return this.attr({ visibility: VISIBLE }); }, /** * Hide the element */ hide: function() { return this.attr({ visibility: HIDDEN }); }, /** * Add the element * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined * to append the element to the renderer.box. */ add: function(parent) { var renderer = this.renderer, parentWrapper = parent || renderer, parentNode = parentWrapper.element || renderer.box, childNodes = parentNode.childNodes, element = this.element, zIndex = attr(element, 'zIndex'), textStr = this.textStr, otherElement, otherZIndex, i; // mark as inverted this.parentInverted = parent && parent.inverted; // mark the container as having z indexed children if (zIndex) { parentWrapper.handleZ = true; zIndex = pInt(zIndex); } // insert according to this and other elements' zIndex if (parentWrapper.handleZ) { // this element or any of its siblings has a z index for (i = 0; i < childNodes.length; i++) { otherElement = childNodes[i]; otherZIndex = attr(otherElement, 'zIndex'); if (otherElement != element && ( // insert before the first element with a higher zIndex pInt(otherZIndex) > zIndex || // if no zIndex given, insert before the first element with a zIndex (!defined(zIndex) && defined(otherZIndex)) )) { parentNode.insertBefore(element, otherElement); return this; } } } // operations before adding if (textStr !== undefined) { renderer.buildText(this); this.added = true; } // default: append at the end parentNode.appendChild(element); return this; }, /** * Destroy the element and element wrapper */ destroy: function() { var wrapper = this, element = wrapper.element || {}, shadows = wrapper.shadows, parentNode = element.parentNode, key; // remove events element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = null; stop(wrapper); // stop running animations // remove element if (parentNode) { parentNode.removeChild(element); } // destroy shadows if (shadows) { each(shadows, function(shadow) { parentNode = shadow.parentNode; if (parentNode) { // the entire chart HTML can be overwritten parentNode.removeChild(shadow); } }); } // remove from alignObjects erase(wrapper.renderer.alignedObjects, wrapper); for (key in wrapper) { delete wrapper[key]; } return null; }, /** * Empty a group element */ empty: function() { var element = this.element, childNodes = element.childNodes, i = childNodes.length; while (i--) { element.removeChild(childNodes[i]); } }, /** * Add a shadow to the element. Must be done after the element is added to the DOM * @param {Boolean} apply */ shadow: function(apply) { var shadows = [], i, shadow, element = this.element, // compensate for inverted plot area transform = this.parentInverted ? '(-1,-1)' : '(1,1)'; if (apply) { for (i = 1; i <= 3; i++) { shadow = element.cloneNode(0); attr(shadow, { 'isShadow': 'true', 'stroke': 'rgb(0, 0, 0)', 'stroke-opacity': 0.05 * i, 'stroke-width': 7 - 2 * i, 'transform': 'translate'+ transform, 'fill': NONE }); element.parentNode.insertBefore(shadow, element); shadows.push(shadow); } this.shadows = shadows; } return this; } }; /** * The default SVG renderer */ var SVGRenderer = function() { this.init.apply(this, arguments); }; SVGRenderer.prototype = { /** * Initialize the SVGRenderer * @param {Object} container * @param {Number} width * @param {Number} height */ init: function(container, width, height) { var renderer = this, loc = location, boxWrapper; renderer.Element = SVGElement; boxWrapper = renderer.createElement('svg') .attr({ xmlns: SVG_NS, version: '1.1' }); container.appendChild(boxWrapper.element); // object properties renderer.box = boxWrapper.element; renderer.boxWrapper = boxWrapper; renderer.alignedObjects = []; renderer.url = isIE ? '' : loc.href.replace(/#.*?$/, ''); // page url used for internal references renderer.defs = this.createElement('defs').add(); renderer.setSize(width, height, false); }, /** * Create a wrapper for an SVG element * @param {Object} nodeName */ createElement: function(nodeName) { var wrapper = new this.Element(); wrapper.init(this, nodeName); return wrapper; }, /** * Parse a simple HTML string into SVG tspans * * @param {Object} textNode The parent text SVG node */ buildText: function(wrapper) { var textNode = wrapper.element, lines = pick(wrapper.textStr, '').toString() .replace(/<(b|strong)>/g, '<span style="font-weight:bold">') .replace(/<(i|em)>/g, '<span style="font-style:italic">') .replace(/<a/g, '<span') .replace(/<\/(b|strong|i|em|a)>/g, '</span>') .split(/<br[^>]?>/g), childNodes = textNode.childNodes, styleRegex = /style="([^"]+)"/, hrefRegex = /href="([^"]+)"/, parentX = attr(textNode, 'x'), textStyles = wrapper.styles, width = textStyles && pInt(textStyles.width), textLineHeight = textStyles && textStyles.lineHeight, lastLine, i = childNodes.length; // remove old text while (i--) { textNode.removeChild(childNodes[i]); } if (width) { this.box.appendChild(textNode); // attach it to the DOM to read offset width } each(lines, function(line, lineNo) { var spans, spanNo = 0, lineHeight; line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||'); spans = line.split('|||'); each(spans, function (span) { if (span !== '' || spans.length == 1) { var attributes = {}, tspan = doc.createElementNS(SVG_NS, 'tspan'); if (styleRegex.test(span)) { attr( tspan, 'style', span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2') ); } if (hrefRegex.test(span)) { attr(tspan, 'onclick', 'location.href=\"'+ span.match(hrefRegex)[1] +'\"'); css(tspan, { cursor: 'pointer' }); } span = span.replace(/<(.|\n)*?>/g, '') || ' '; tspan.appendChild(doc.createTextNode(span)); // WebKit needs a string //console