+++ /dev/null
-// ==ClosureCompiler==\r
-// @compilation_level SIMPLE_OPTIMIZATIONS\r
-\r
-/**\r
- * @license Highcharts JS v2.1.6 (2011-07-08)\r
- * \r
- * (c) 2009-2011 Torstein Hønsi\r
- * \r
- * License: www.highcharts.com/license\r
- */\r
-\r
-// JSLint options:\r
-/*jslint forin: true */\r
-/*global document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $ */\r
- \r
-(function() {\r
-// encapsulated variables\r
-var doc = document,\r
- win = window,\r
- math = Math,\r
- mathRound = math.round,\r
- mathFloor = math.floor,\r
- mathCeil = math.ceil,\r
- mathMax = math.max,\r
- mathMin = math.min,\r
- mathAbs = math.abs,\r
- mathCos = math.cos,\r
- mathSin = math.sin,\r
- mathPI = math.PI,\r
- deg2rad = mathPI * 2 / 360,\r
- \r
- \r
- // some variables\r
- userAgent = navigator.userAgent,\r
- isIE = /msie/i.test(userAgent) && !win.opera,\r
- docMode8 = doc.documentMode === 8,\r
- isWebKit = /AppleWebKit/.test(userAgent),\r
- isFirefox = /Firefox/.test(userAgent),\r
- //hasSVG = win.SVGAngle || doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1"),\r
- hasSVG = !!doc.createElementNS && !!doc.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGRect,\r
- SVG_NS = 'http://www.w3.org/2000/svg',\r
- Renderer,\r
- hasTouch = doc.documentElement.ontouchstart !== undefined,\r
- symbolSizes = {},\r
- idCounter = 0,\r
- timeFactor = 1, // 1 = JavaScript time, 1000 = Unix time\r
- garbageBin,\r
- defaultOptions,\r
- dateFormat, // function\r
- globalAnimation,\r
- pathAnim,\r
- \r
- \r
- // some constants for frequently used strings\r
- UNDEFINED,\r
- DIV = 'div',\r
- ABSOLUTE = 'absolute',\r
- RELATIVE = 'relative',\r
- HIDDEN = 'hidden',\r
- PREFIX = 'highcharts-',\r
- VISIBLE = 'visible',\r
- PX = 'px',\r
- NONE = 'none',\r
- M = 'M',\r
- L = 'L',\r
- /*\r
- * Empirical lowest possible opacities for TRACKER_FILL\r
- * IE6: 0.002\r
- * IE7: 0.002\r
- * IE8: 0.002\r
- * IE9: 0.00000000001 (unlimited)\r
- * FF: 0.00000000001 (unlimited)\r
- * Chrome: 0.000001\r
- * Safari: 0.000001\r
- * Opera: 0.00000000001 (unlimited)\r
- */\r
- TRACKER_FILL = 'rgba(192,192,192,'+ (hasSVG ? 0.000001 : 0.002) +')', // invisible but clickable\r
- NORMAL_STATE = '',\r
- HOVER_STATE = 'hover',\r
- SELECT_STATE = 'select',\r
- \r
- // time methods, changed based on whether or not UTC is used\r
- makeTime,\r
- getMinutes,\r
- getHours,\r
- getDay,\r
- getDate,\r
- getMonth,\r
- getFullYear,\r
- setMinutes,\r
- setHours,\r
- setDate,\r
- setMonth,\r
- setFullYear,\r
- \r
- // check for a custom HighchartsAdapter defined prior to this file\r
- globalAdapter = win.HighchartsAdapter,\r
- adapter = globalAdapter || {}, \r
- \r
- // Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object\r
- // and all the utility functions will be null. In that case they are populated by the \r
- // default adapters below.\r
- each = adapter.each,\r
- grep = adapter.grep,\r
- map = adapter.map,\r
- merge = adapter.merge,\r
- addEvent = adapter.addEvent,\r
- removeEvent = adapter.removeEvent,\r
- fireEvent = adapter.fireEvent,\r
- animate = adapter.animate,\r
- stop = adapter.stop,\r
- \r
- // lookup over the types and the associated classes\r
- seriesTypes = {},\r
- hoverChart;\r
- \r
-/**\r
- * Extend an object with the members of another\r
- * @param {Object} a The object to be extended\r
- * @param {Object} b The object to add to the first one\r
- */\r
-function extend(a, b) {\r
- var n;\r
- if (!a) {\r
- a = {};\r
- }\r
- for (n in b) {\r
- a[n] = b[n];\r
- }\r
- return a;\r
-}\r
-\r
-/**\r
- * Shortcut for parseInt\r
- * @param {Object} s\r
- */\r
-function pInt(s, mag) {\r
- return parseInt(s, mag || 10);\r
-}\r
-\r
-/**\r
- * Check for string\r
- * @param {Object} s\r
- */\r
-function isString(s) {\r
- return typeof s === 'string';\r
-}\r
-\r
-/**\r
- * Check for object\r
- * @param {Object} obj\r
- */\r
-function isObject(obj) {\r
- return typeof obj === 'object';\r
-}\r
-\r
-/**\r
- * Check for number\r
- * @param {Object} n\r
- */\r
-function isNumber(n) {\r
- return typeof n === 'number';\r
-}\r
-\r
-function log2lin(num) {\r
- return math.log(num) / math.LN10;\r
-}\r
-function lin2log(num) {\r
- return math.pow(10, num);\r
-}\r
-\r
-/**\r
- * Remove last occurence of an item from an array\r
- * @param {Array} arr\r
- * @param {Mixed} item\r
- */\r
-function erase(arr, item) {\r
- var i = arr.length;\r
- while (i--) {\r
- if (arr[i] === item) {\r
- arr.splice(i, 1);\r
- break;\r
- }\r
- }\r
- //return arr;\r
-}\r
-\r
-/**\r
- * Returns true if the object is not null or undefined. Like MooTools' $.defined.\r
- * @param {Object} obj\r
- */\r
-function defined (obj) {\r
- return obj !== UNDEFINED && obj !== null;\r
-}\r
-\r
-/**\r
- * Set or get an attribute or an object of attributes. Can't use jQuery attr because\r
- * it attempts to set expando properties on the SVG element, which is not allowed.\r
- * \r
- * @param {Object} elem The DOM element to receive the attribute(s)\r
- * @param {String|Object} prop The property or an abject of key-value pairs\r
- * @param {String} value The value if a single property is set\r
- */\r
-function attr(elem, prop, value) {\r
- var key,\r
- setAttribute = 'setAttribute',\r
- ret;\r
- \r
- // if the prop is a string\r
- if (isString(prop)) {\r
- // set the value\r
- if (defined(value)) {\r
-\r
- elem[setAttribute](prop, value);\r
- \r
- // get the value\r
- } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo...\r
- ret = elem.getAttribute(prop);\r
- }\r
- \r
- // else if prop is defined, it is a hash of key/value pairs\r
- } else if (defined(prop) && isObject(prop)) {\r
- for (key in prop) {\r
- elem[setAttribute](key, prop[key]);\r
- }\r
- }\r
- return ret;\r
-}\r
-/**\r
- * Check if an element is an array, and if not, make it into an array. Like\r
- * MooTools' $.splat.\r
- */\r
-function splat(obj) {\r
- if (!obj || obj.constructor !== Array) {\r
- obj = [obj];\r
- }\r
- return obj; \r
-}\r
-\r
-\r
-\r
-/**\r
- * Return the first value that is defined. Like MooTools' $.pick.\r
- */\r
-function pick() {\r
- var args = arguments,\r
- i,\r
- arg,\r
- length = args.length;\r
- for (i = 0; i < length; i++) {\r
- arg = args[i];\r
- if (typeof arg !== 'undefined' && arg !== null) {\r
- return arg;\r
- }\r
- }\r
-}\r
-\r
-/**\r
- * Set CSS on a given element\r
- * @param {Object} el\r
- * @param {Object} styles Style object with camel case property names\r
- */\r
-function css (el, styles) {\r
- if (isIE) {\r
- if (styles && styles.opacity !== UNDEFINED) {\r
- styles.filter = 'alpha(opacity='+ (styles.opacity * 100) +')';\r
- }\r
- }\r
- extend(el.style, styles);\r
-}\r
-\r
-/* *\r
- * Get CSS value on a given element\r
- * @param {Object} el DOM object\r
- * @param {String} styleProp Camel cased CSS propery\r
- * /\r
-function getStyle (el, styleProp) {\r
- var ret,\r
- CURRENT_STYLE = 'currentStyle',\r
- GET_COMPUTED_STYLE = 'getComputedStyle';\r
- if (el[CURRENT_STYLE]) {\r
- ret = el[CURRENT_STYLE][styleProp];\r
- } else if (win[GET_COMPUTED_STYLE]) {\r
- ret = win[GET_COMPUTED_STYLE](el, null).getPropertyValue(hyphenate(styleProp));\r
- }\r
- return ret;\r
-}*/\r
-\r
-/**\r
- * Utility function to create element with attributes and styles\r
- * @param {Object} tag\r
- * @param {Object} attribs\r
- * @param {Object} styles\r
- * @param {Object} parent\r
- * @param {Object} nopad\r
- */\r
-function createElement (tag, attribs, styles, parent, nopad) {\r
- var el = doc.createElement(tag);\r
- if (attribs) {\r
- extend(el, attribs);\r
- }\r
- if (nopad) {\r
- css(el, {padding: 0, border: NONE, margin: 0});\r
- }\r
- if (styles) {\r
- css(el, styles);\r
- }\r
- if (parent) {\r
- parent.appendChild(el);\r
- } \r
- return el;\r
-}\r
-\r
-/**\r
- * Extend a prototyped class by new members\r
- * @param {Object} parent\r
- * @param {Object} members\r
- */\r
-function extendClass(parent, members) {\r
- var object = function(){};\r
- object.prototype = new parent();\r
- extend(object.prototype, members);\r
- return object;\r
-}\r
-\r
-/**\r
- * Format a number and return a string based on input settings\r
- * @param {Number} number The input number to format\r
- * @param {Number} decimals The amount of decimals\r
- * @param {String} decPoint The decimal point, defaults to the one given in the lang options\r
- * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options\r
- */\r
-function numberFormat (number, decimals, decPoint, thousandsSep) {\r
- var lang = defaultOptions.lang,\r
- // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/\r
- n = number, c = isNaN(decimals = mathAbs(decimals)) ? 2 : decimals,\r
- d = decPoint === undefined ? lang.decimalPoint : decPoint,\r
- t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep, s = n < 0 ? "-" : "",\r
- i = String(pInt(n = mathAbs(+n || 0).toFixed(c))),\r
- j = i.length > 3 ? i.length % 3 : 0;\r
- \r
- return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) +\r
- (c ? d + mathAbs(n - i).toFixed(c).slice(2) : "");\r
-}\r
-\r
-/**\r
- * Based on http://www.php.net/manual/en/function.strftime.php \r
- * @param {String} format\r
- * @param {Number} timestamp\r
- * @param {Boolean} capitalize\r
- */\r
-dateFormat = function (format, timestamp, capitalize) {\r
- function pad (number) {\r
- return number.toString().replace(/^([0-9])$/, '0$1');\r
- }\r
- \r
- if (!defined(timestamp) || isNaN(timestamp)) {\r
- return 'Invalid date';\r
- }\r
- format = pick(format, '%Y-%m-%d %H:%M:%S');\r
- \r
- var date = new Date(timestamp * timeFactor),\r
- key, // used in for constuct below\r
- // get the basic time values\r
- hours = date[getHours](),\r
- day = date[getDay](),\r
- dayOfMonth = date[getDate](),\r
- month = date[getMonth](),\r
- fullYear = date[getFullYear](),\r
- lang = defaultOptions.lang,\r
- langWeekdays = lang.weekdays,\r
- langMonths = lang.months,\r
- /* // uncomment this and the 'W' format key below to enable week numbers\r
- weekNumber = function() { \r
- var clone = new Date(date.valueOf()),\r
- day = clone[getDay]() == 0 ? 7 : clone[getDay](),\r
- dayNumber;\r
- clone.setDate(clone[getDate]() + 4 - day);\r
- dayNumber = mathFloor((clone.getTime() - new Date(clone[getFullYear](), 0, 1, -6)) / 86400000);\r
- return 1 + mathFloor(dayNumber / 7);\r
- },\r
- */\r
- \r
- // list all format keys\r
- replacements = {\r
-\r
- // Day\r
- 'a': langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon'\r
- 'A': langWeekdays[day], // Long weekday, like 'Monday'\r
- 'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31 \r
- 'e': dayOfMonth, // Day of the month, 1 through 31 \r
- \r
- // Week (none implemented)\r
- //'W': weekNumber(),\r
- \r
- // Month\r
- 'b': langMonths[month].substr(0, 3), // Short month, like 'Jan'\r
- 'B': langMonths[month], // Long month, like 'January'\r
- 'm': pad(month + 1), // Two digit month number, 01 through 12\r
- \r
- // Year\r
- 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009\r
- 'Y': fullYear, // Four digits year, like 2009\r
- \r
- // Time\r
- 'H': pad(hours), // Two digits hours in 24h format, 00 through 23\r
- 'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11\r
- 'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12\r
- 'M': pad(date[getMinutes]()), // Two digits minutes, 00 through 59\r
- 'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM\r
- 'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM\r
- 'S': pad(date.getSeconds()) // Two digits seconds, 00 through 59\r
- \r
- };\r
-\r
-\r
- // do the replaces\r
- for (key in replacements) {\r
- format = format.replace('%'+ key, replacements[key]);\r
- }\r
- \r
- // Optionally capitalize the string and return\r
- return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format;\r
-};\r
-\r
-/**\r
- * Loop up the node tree and add offsetWidth and offsetHeight to get the\r
- * total page offset for a given element. Used by Opera and iOS on hover and\r
- * all browsers on point click.\r
- * \r
- * @param {Object} el\r
- * \r
- */\r
-function getPosition (el) {\r
- var p = { left: el.offsetLeft, top: el.offsetTop };\r
- el = el.offsetParent;\r
- while (el) {\r
- p.left += el.offsetLeft;\r
- p.top += el.offsetTop;\r
- if (el !== doc.body && el !== doc.documentElement) {\r
- p.left -= el.scrollLeft;\r
- p.top -= el.scrollTop;\r
- }\r
- el = el.offsetParent;\r
- }\r
- return p;\r
-}\r
-\r
-/**\r
- * Helper class that contains variuos counters that are local to the chart.\r
- */\r
-function ChartCounters() {\r
- this.color = 0;\r
- this.symbol = 0;\r
-}\r
-\r
-ChartCounters.prototype = {\r
- /**\r
- * Wraps the color counter if it reaches the specified length.\r
- */\r
- wrapColor: function(length) {\r
- if (this.color >= length) {\r
- this.color = 0;\r
- }\r
- },\r
-\r
- /**\r
- * Wraps the symbol counter if it reaches the specified length.\r
- */\r
- wrapSymbol: function(length) {\r
- if (this.symbol >= length) {\r
- this.symbol = 0;\r
- }\r
- }\r
-};\r
-\r
-/**\r
- * Set the global animation to either a given value, or fall back to the \r
- * given chart's animation option\r
- * @param {Object} animation\r
- * @param {Object} chart\r
- */\r
-function setAnimation(animation, chart) {\r
- globalAnimation = pick(animation, chart.animation);\r
-}\r
-\r
-/* \r
- * Define the adapter for frameworks. If an external adapter is not defined, \r
- * Highcharts reverts to the built-in jQuery adapter.\r
- */\r
-if (globalAdapter && globalAdapter.init) {\r
- globalAdapter.init();\r
-} \r
-if (!globalAdapter && win.jQuery) {\r
- var jQ = jQuery;\r
- \r
- /**\r
- * Utility for iterating over an array. Parameters are reversed compared to jQuery.\r
- * @param {Array} arr\r
- * @param {Function} fn\r
- */\r
- each = function(arr, fn) {\r
- var i = 0, \r
- len = arr.length;\r
- for (; i < len; i++) {\r
- if (fn.call(arr[i], arr[i], i, arr) === false) {\r
- return i;\r
- }\r
- }\r
- };\r
- \r
- /**\r
- * Filter an array\r
- */\r
- grep = jQ.grep;\r
- \r
- /**\r
- * Map an array\r
- * @param {Array} arr\r
- * @param {Function} fn\r
- */\r
- map = function(arr, fn){\r
- //return jQuery.map(arr, fn);\r
- var results = [],\r
- i = 0, len = arr.length;\r
- for (; i < len; i++) {\r
- results[i] = fn.call(arr[i], arr[i], i, arr);\r
- }\r
- return results;\r
- \r
- };\r
- \r
- /**\r
- * Deep merge two objects and return a third object\r
- */\r
- merge = function(){\r
- var args = arguments;\r
- return jQ.extend(true, null, args[0], args[1], args[2], args[3]);\r
- };\r
- \r
- /**\r
- * Add an event listener\r
- * @param {Object} el A HTML element or custom object\r
- * @param {String} event The event type\r
- * @param {Function} fn The event handler\r
- */\r
- addEvent = function (el, event, fn){\r
- jQ(el).bind(event, fn);\r
- };\r
- \r
- /**\r
- * Remove event added with addEvent\r
- * @param {Object} el The object\r
- * @param {String} eventType The event type. Leave blank to remove all events.\r
- * @param {Function} handler The function to remove\r
- */\r
- removeEvent = function(el, eventType, handler) {\r
- // workaround for jQuery issue with unbinding custom events:\r
- // http://forum.jquery.com/topic/javascript-error-when-unbinding-a-custom-event-using-jquery-1-4-2\r
- var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent';\r
- if (doc[func] && !el[func]) {\r
- el[func] = function() {};\r
- }\r
- \r
- jQ(el).unbind(eventType, handler);\r
- };\r
- \r
- /**\r
- * Fire an event on a custom object\r
- * @param {Object} el\r
- * @param {String} type\r
- * @param {Object} eventArguments\r
- * @param {Function} defaultFunction\r
- */\r
- fireEvent = function(el, type, eventArguments, defaultFunction) {\r
- var event = jQ.Event(type),\r
- detachedType = 'detached'+ type;\r
- extend(event, eventArguments);\r
- \r
- // Prevent jQuery from triggering the object method that is named the\r
- // same as the event. For example, if the event is 'select', jQuery\r
- // attempts calling el.select and it goes into a loop.\r
- if (el[type]) {\r
- el[detachedType] = el[type];\r
- el[type] = null; \r
- }\r
- \r
- // trigger it\r
- jQ(el).trigger(event);\r
- \r
- // attach the method\r
- if (el[detachedType]) {\r
- el[type] = el[detachedType];\r
- el[detachedType] = null;\r
- }\r
- \r
- if (defaultFunction && !event.isDefaultPrevented()) {\r
- defaultFunction(event);\r
- } \r
- };\r
-\r
- /**\r
- * Animate a HTML element or SVG element wrapper\r
- * @param {Object} el\r
- * @param {Object} params\r
- * @param {Object} options jQuery-like animation options: duration, easing, callback\r
- */\r
- animate = function (el, params, options) {\r
- var $el = jQ(el);\r
- if (params.d) {\r
- el.toD = params.d; // keep the array form for paths, used in jQ.fx.step.d\r
- params.d = 1; // because in jQuery, animating to an array has a different meaning\r
- }\r
- \r
- $el.stop();\r
- $el.animate(params, options);\r
- \r
- };\r
- /**\r
- * Stop running animation\r
- */\r
- stop = function (el) {\r
- jQ(el).stop();\r
- };\r
- \r
- \r
- // extend jQuery\r
- jQ.extend( jQ.easing, {\r
- easeOutQuad: function (x, t, b, c, d) {\r
- return -c *(t/=d)*(t-2) + b;\r
- }\r
- });\r
- \r
- // extend the animate function to allow SVG animations\r
- var oldStepDefault = jQuery.fx.step._default, \r
- oldCur = jQuery.fx.prototype.cur;\r
- \r
- // do the step\r
- jQ.fx.step._default = function(fx){\r
- var elem = fx.elem;\r
- if (elem.attr) { // is SVG element wrapper\r
- elem.attr(fx.prop, fx.now);\r
- } else {\r
- oldStepDefault.apply(this, arguments);\r
- }\r
- };\r
- // animate paths\r
- jQ.fx.step.d = function(fx) {\r
- var elem = fx.elem;\r
- \r
- \r
- // Normally start and end should be set in state == 0, but sometimes,\r
- // for reasons unknown, this doesn't happen. Perhaps state == 0 is skipped\r
- // in these cases\r
- if (!fx.started) {\r
- var ends = pathAnim.init(elem, elem.d, elem.toD);\r
- fx.start = ends[0];\r
- fx.end = ends[1];\r
- fx.started = true;\r
- }\r
- \r
- \r
- // interpolate each value of the path\r
- elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD));\r
- \r
- };\r
- // get the current value\r
- jQ.fx.prototype.cur = function() {\r
- var elem = this.elem,\r
- r;\r
- if (elem.attr) { // is SVG element wrapper\r
- r = elem.attr(this.prop);\r
- } else {\r
- r = oldCur.apply(this, arguments);\r
- }\r
- return r;\r
- };\r
-}\r
-\r
-\r
-/**\r
- * Add a global listener for mousemove events\r
- */\r
-/*addEvent(doc, 'mousemove', function(e) {\r
- if (globalMouseMove) {\r
- globalMouseMove(e);\r
- }\r
-});*/\r
-\r
-/**\r
- * Path interpolation algorithm used across adapters\r
- */\r
-pathAnim = {\r
- /**\r
- * Prepare start and end values so that the path can be animated one to one\r
- */\r
- init: function(elem, fromD, toD) {\r
- fromD = fromD || '';\r
- var shift = elem.shift,\r
- bezier = fromD.indexOf('C') > -1,\r
- numParams = bezier ? 7 : 3,\r
- endLength,\r
- slice,\r
- i,\r
- start = fromD.split(' '),\r
- end = [].concat(toD), // copy\r
- startBaseLine,\r
- endBaseLine,\r
- sixify = function(arr) { // in splines make move points have six parameters like bezier curves\r
- i = arr.length;\r
- while (i--) {\r
- if (arr[i] === M) {\r
- arr.splice(i + 1, 0, arr[i+1], arr[i+2], arr[i+1], arr[i+2]);\r
- }\r
- }\r
- };\r
- \r
- if (bezier) {\r
- sixify(start);\r
- sixify(end);\r
- }\r
- \r
- // pull out the base lines before padding\r
- if (elem.isArea) { \r
- startBaseLine = start.splice(start.length - 6, 6);\r
- endBaseLine = end.splice(end.length - 6, 6);\r
- }\r
- \r
- // if shifting points, prepend a dummy point to the end path\r
- if (shift) {\r
-\r
- end = [].concat(end).splice(0, numParams).concat(end);\r
- elem.shift = false; // reset for following animations\r
- }\r
- \r
- // copy and append last point until the length matches the end length\r
- if (start.length) {\r
- endLength = end.length;\r
- while (start.length < endLength) { \r
- \r
- //bezier && sixify(start); \r
- slice = [].concat(start).splice(start.length - numParams, numParams);\r
- if (bezier) { // disable first control point\r
- slice[numParams - 6] = slice[numParams - 2];\r
- slice[numParams - 5] = slice[numParams - 1];\r
- }\r
- start = start.concat(slice);\r
- }\r
- }\r
- \r
- if (startBaseLine) { // append the base lines for areas\r
- start = start.concat(startBaseLine);\r
- end = end.concat(endBaseLine);\r
- }\r
- return [start, end];\r
- },\r
- \r
- /**\r
- * Interpolate each value of the path and return the array\r
- */\r
- step: function(start, end, pos, complete) {\r
- var ret = [],\r
- i = start.length,\r
- startVal;\r
- \r
- if (pos === 1) { // land on the final path without adjustment points appended in the ends\r
- ret = complete;\r
- \r
- } else if (i === end.length && pos < 1) {\r
- while (i--) {\r
- startVal = parseFloat(start[i]);\r
- ret[i] = \r
- isNaN(startVal) ? // a letter instruction like M or L\r
- start[i] :\r
- pos * (parseFloat(end[i] - startVal)) + startVal;\r
- \r
- }\r
- } else { // if animation is finished or length not matching, land on right value\r
- ret = end;\r
- }\r
- return ret;\r
- }\r
-};\r
-\r
-/**\r
- * Set the time methods globally based on the useUTC option. Time method can be either \r
- * local time or UTC (default).\r
- */\r
-function setTimeMethods() {\r
- var useUTC = defaultOptions.global.useUTC;\r
- \r
- makeTime = useUTC ? Date.UTC : function(year, month, date, hours, minutes, seconds) {\r
- return new Date(\r
- year, \r
- month, \r
- pick(date, 1), \r
- pick(hours, 0), \r
- pick(minutes, 0), \r
- pick(seconds, 0)\r
- ).getTime();\r
- };\r
- getMinutes = useUTC ? 'getUTCMinutes' : 'getMinutes';\r
- getHours = useUTC ? 'getUTCHours' : 'getHours';\r
- getDay = useUTC ? 'getUTCDay' : 'getDay';\r
- getDate = useUTC ? 'getUTCDate' : 'getDate';\r
- getMonth = useUTC ? 'getUTCMonth' : 'getMonth';\r
- getFullYear = useUTC ? 'getUTCFullYear' : 'getFullYear';\r
- setMinutes = useUTC ? 'setUTCMinutes' : 'setMinutes';\r
- setHours = useUTC ? 'setUTCHours' : 'setHours';\r
- setDate = useUTC ? 'setUTCDate' : 'setDate';\r
- setMonth = useUTC ? 'setUTCMonth' : 'setMonth';\r
- setFullYear = useUTC ? 'setUTCFullYear' : 'setFullYear';\r
- \r
-}\r
-\r
-/**\r
- * Merge the default options with custom options and return the new options structure\r
- * @param {Object} options The new custom options\r
- */\r
-function setOptions(options) {\r
- defaultOptions = merge(defaultOptions, options);\r
- \r
- // apply UTC\r
- setTimeMethods();\r
- \r
- return defaultOptions;\r
-}\r
-\r
-/**\r
- * Get the updated default options. Merely exposing defaultOptions for outside modules\r
- * isn't enough because the setOptions method creates a new object.\r
- */\r
-function getOptions() {\r
- return defaultOptions;\r
-}\r
-\r
-/**\r
- * Discard an element by moving it to the bin and delete\r
- * @param {Object} The HTML node to discard\r
- */\r
-function discardElement(element) {\r
- // create a garbage bin element, not part of the DOM\r
- if (!garbageBin) {\r
- garbageBin = createElement(DIV);\r
- }\r
- \r
- // move the node and empty bin\r
- if (element) {\r
- garbageBin.appendChild(element);\r
- }\r
- garbageBin.innerHTML = '';\r
-}\r
-\r
-/* ****************************************************************************\r
- * Handle the options *\r
- *****************************************************************************/\r
-var \r
-\r
-defaultLabelOptions = {\r
- enabled: true,\r
- // rotation: 0,\r
- align: 'center',\r
- x: 0,\r
- y: 15,\r
- /*formatter: function() {\r
- return this.value;\r
- },*/\r
- style: {\r
- color: '#666',\r
- fontSize: '11px',\r
- lineHeight: '14px'\r
- }\r
-};\r
-\r
-defaultOptions = {\r
- colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE', \r
- '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'],\r
- symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],\r
- lang: {\r
- loading: 'Loading...',\r
- months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', \r
- 'August', 'September', 'October', 'November', 'December'],\r
- weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],\r
- decimalPoint: '.',\r
- resetZoom: 'Reset zoom',\r
- resetZoomTitle: 'Reset zoom level 1:1',\r
- thousandsSep: ','\r
- },\r
- global: {\r
- useUTC: true\r
- },\r
- chart: {\r
- //animation: true,\r
- //alignTicks: false,\r
- //reflow: true,\r
- //className: null,\r
- //events: { load, selection },\r
- //margin: [null],\r
- //marginTop: null,\r
- //marginRight: null,\r
- //marginBottom: null,\r
- //marginLeft: null,\r
- borderColor: '#4572A7',\r
- //borderWidth: 0,\r
- borderRadius: 5, \r
- defaultSeriesType: 'line',\r
- ignoreHiddenSeries: true,\r
- //inverted: false,\r
- //shadow: false,\r
- spacingTop: 10,\r
- spacingRight: 10,\r
- spacingBottom: 15,\r
- spacingLeft: 10,\r
- style: {\r
- fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font\r
- fontSize: '12px'\r
- },\r
- backgroundColor: '#FFFFFF',\r
- //plotBackgroundColor: null,\r
- plotBorderColor: '#C0C0C0'\r
- //plotBorderWidth: 0,\r
- //plotShadow: false,\r
- //zoomType: ''\r
- },\r
- title: {\r
- text: 'Chart title',\r
- align: 'center',\r
- // floating: false,\r
- // margin: 15,\r
- // x: 0,\r
- // verticalAlign: 'top',\r
- y: 15, // docs\r
- style: {\r
- color: '#3E576F',\r
- fontSize: '16px'\r
- }\r
-\r
- },\r
- subtitle: {\r
- text: '',\r
- align: 'center',\r
- // floating: false\r
- // x: 0,\r
- // verticalAlign: 'top',\r
- y: 30, // docs\r
- style: {\r
- color: '#6D869F'\r
- }\r
- },\r
- \r
- plotOptions: {\r
- line: { // base series options\r
- allowPointSelect: false,\r
- showCheckbox: false,\r
- animation: {\r
- duration: 1000\r
- },\r
- // connectNulls: false, // docs\r
- //cursor: 'default',\r
- //dashStyle: null,\r
- //enableMouseTracking: true,\r
- events: {},\r
- //legendIndex: 0, // docs (+ pie points)\r
- lineWidth: 2,\r
- shadow: true,\r
- // stacking: null,\r
- marker: { \r
- enabled: true,\r
- //symbol: null, \r
- lineWidth: 0,\r
- radius: 4,\r
- lineColor: '#FFFFFF',\r
- //fillColor: null, \r
- states: { // states for a single point\r
- hover: {\r
- //radius: base + 2\r
- },\r
- select: {\r
- fillColor: '#FFFFFF',\r
- lineColor: '#000000',\r
- lineWidth: 2\r
- } \r
- }\r
- },\r
- point: {\r
- events: {}\r
- },\r
- dataLabels: merge(defaultLabelOptions, {\r
- enabled: false,\r
- y: -6,\r
- formatter: function() {\r
- return this.y;\r
- }\r
- }),\r
- \r
- //pointStart: 0,\r
- //pointInterval: 1,\r
- showInLegend: true,\r
- states: { // states for the entire series\r
- hover: {\r
- //enabled: false,\r
- //lineWidth: base + 1,\r
- marker: {\r
- // lineWidth: base + 1,\r
- // radius: base + 1\r
- }\r
- },\r
- select: {\r
- marker: {}\r
- }\r
- },\r
- stickyTracking: true\r
- //zIndex: null\r
- }\r
- },\r
- labels: {\r
- //items: [],\r
- style: {\r
- //font: defaultFont,\r
- position: ABSOLUTE,\r
- color: '#3E576F'\r
- }\r
- },\r
- legend: {\r
- enabled: true,\r
- align: 'center',\r
- //floating: false,\r
- layout: 'horizontal',\r
- labelFormatter: function() {\r
- return this.name;\r
- },\r
- // lineHeight: 16, // docs: deprecated\r
- borderWidth: 1,\r
- borderColor: '#909090',\r
- borderRadius: 5,\r
- // margin: 10,\r
- // reversed: false,\r
- shadow: false,\r
- // backgroundColor: null,\r
- style: {\r
- padding: '5px'\r
- },\r
- itemStyle: {\r
- cursor: 'pointer',\r
- color: '#3E576F'\r
- },\r
- itemHoverStyle: {\r
- cursor: 'pointer',\r
- color: '#000000'\r
- },\r
- itemHiddenStyle: {\r
- color: '#C0C0C0'\r
- },\r
- itemCheckboxStyle: {\r
- position: ABSOLUTE,\r
- width: '13px', // for IE precision\r
- height: '13px'\r
- },\r
- // itemWidth: undefined,\r
- symbolWidth: 16,\r
- symbolPadding: 5,\r
- verticalAlign: 'bottom',\r
- // width: undefined,\r
- x: 0, // docs\r
- y: 0 // docs\r
- },\r
- \r
- loading: {\r
- hideDuration: 100,\r
- labelStyle: {\r
- fontWeight: 'bold',\r
- position: RELATIVE,\r
- top: '1em'\r
- },\r
- showDuration: 100,\r
- style: {\r
- position: ABSOLUTE,\r
- backgroundColor: 'white',\r
- opacity: 0.5,\r
- textAlign: 'center'\r
- }\r
- },\r
- \r
- tooltip: {\r
- enabled: true,\r
- //crosshairs: null,\r
- backgroundColor: 'rgba(255, 255, 255, .85)',\r
- borderWidth: 2,\r
- borderRadius: 5,\r
- //formatter: defaultFormatter,\r
- shadow: true,\r
- //shared: false,\r
- snap: hasTouch ? 25 : 10,\r
- style: {\r
- color: '#333333',\r
- fontSize: '12px',\r
- padding: '5px',\r
- whiteSpace: 'nowrap'\r
- }\r
- },\r
- \r
- toolbar: {\r
- itemStyle: {\r
- color: '#4572A7',\r
- cursor: 'pointer'\r
- }\r
- },\r
- \r
- credits: {\r
- enabled: true,\r
- text: 'Highcharts.com',\r
- href: 'http://www.highcharts.com',\r
- position: {\r
- align: 'right',\r
- x: -10,\r
- verticalAlign: 'bottom',\r
- y: -5\r
- },\r
- style: {\r
- cursor: 'pointer',\r
- color: '#909090',\r
- fontSize: '10px'\r
- }\r
- }\r
-};\r
-\r
-// Axis defaults\r
-var defaultXAxisOptions = {\r
- // allowDecimals: null,\r
- // alternateGridColor: null,\r
- // categories: [],\r
- dateTimeLabelFormats: {\r
- second: '%H:%M:%S',\r
- minute: '%H:%M',\r
- hour: '%H:%M',\r
- day: '%e. %b',\r
- week: '%e. %b',\r
- month: '%b \'%y',\r
- year: '%Y'\r
- },\r
- endOnTick: false,\r
- gridLineColor: '#C0C0C0',\r
- // gridLineDashStyle: 'solid', // docs\r
- // gridLineWidth: 0,\r
- // reversed: false,\r
- \r
- labels: defaultLabelOptions,\r
- // { step: null },\r
- lineColor: '#C0D0E0',\r
- lineWidth: 1,\r
- //linkedTo: null,\r
- max: null,\r
- min: null,\r
- minPadding: 0.01,\r
- maxPadding: 0.01,\r
- //maxZoom: null,\r
- minorGridLineColor: '#E0E0E0',\r
- // minorGridLineDashStyle: null,\r
- minorGridLineWidth: 1,\r
- minorTickColor: '#A0A0A0',\r
- //minorTickInterval: null,\r
- minorTickLength: 2,\r
- minorTickPosition: 'outside', // inside or outside\r
- //minorTickWidth: 0,\r
- //opposite: false,\r
- //offset: 0,\r
- //plotBands: [{\r
- // events: {},\r
- // zIndex: 1,\r
- // labels: { align, x, verticalAlign, y, style, rotation, textAlign }\r
- //}],\r
- //plotLines: [{\r
- // events: {}\r
- // dashStyle: {}\r
- // zIndex:\r
- // labels: { align, x, verticalAlign, y, style, rotation, textAlign }\r
- //}],\r
- //reversed: false,\r
- // showFirstLabel: true,\r
- // showLastLabel: false,\r
- startOfWeek: 1, \r
- startOnTick: false,\r
- tickColor: '#C0D0E0',\r
- //tickInterval: null,\r
- tickLength: 5,\r
- tickmarkPlacement: 'between', // on or between\r
- tickPixelInterval: 100,\r
- tickPosition: 'outside',\r
- tickWidth: 1,\r
- title: {\r
- //text: null,\r
- align: 'middle', // low, middle or high\r
- //margin: 0 for horizontal, 10 for vertical axes,\r
- //rotation: 0,\r
- //side: 'outside',\r
- style: {\r
- color: '#6D869F',\r
- //font: defaultFont.replace('normal', 'bold')\r
- fontWeight: 'bold'\r
- }\r
- //x: 0,\r
- //y: 0\r
- },\r
- type: 'linear' // linear, logarithmic or datetime // docs\r
-},\r
-\r
-defaultYAxisOptions = merge(defaultXAxisOptions, {\r
- endOnTick: true,\r
- gridLineWidth: 1,\r
- tickPixelInterval: 72,\r
- showLastLabel: true,\r
- labels: {\r
- align: 'right',\r
- x: -8,\r
- y: 3\r
- },\r
- lineWidth: 0,\r
- maxPadding: 0.05,\r
- minPadding: 0.05,\r
- startOnTick: true,\r
- tickWidth: 0,\r
- title: {\r
- rotation: 270,\r
- text: 'Y-values'\r
- },\r
- stackLabels: {\r
- enabled: false,\r
- //align: dynamic,\r
- //y: dynamic,\r
- //x: dynamic,\r
- //verticalAlign: dynamic,\r
- //textAlign: dynamic,\r
- //rotation: 0,\r
- formatter: function() {\r
- return this.total;\r
- },\r
- style: defaultLabelOptions.style\r
- }\r
-}),\r
-\r
-defaultLeftAxisOptions = {\r
- labels: {\r
- align: 'right',\r
- x: -8,\r
- y: null // docs\r
- },\r
- title: {\r
- rotation: 270\r
- }\r
-},\r
-defaultRightAxisOptions = {\r
- labels: {\r
- align: 'left',\r
- x: 8,\r
- y: null // docs\r
- },\r
- title: {\r
- rotation: 90\r
- }\r
-},\r
-defaultBottomAxisOptions = { // horizontal axis\r
- labels: {\r
- align: 'center',\r
- x: 0,\r
- y: 14\r
- // staggerLines: null\r
- },\r
- title: {\r
- rotation: 0\r
- }\r
-},\r
-defaultTopAxisOptions = merge(defaultBottomAxisOptions, {\r
- labels: {\r
- y: -5\r
- // staggerLines: null\r
- }\r
-});\r
-\r
-\r
- \r
-\r
-// Series defaults\r
-var defaultPlotOptions = defaultOptions.plotOptions, \r
- defaultSeriesOptions = defaultPlotOptions.line; \r
-//defaultPlotOptions.line = merge(defaultSeriesOptions);\r
-defaultPlotOptions.spline = merge(defaultSeriesOptions);\r
-defaultPlotOptions.scatter = merge(defaultSeriesOptions, {\r
- lineWidth: 0,\r
- states: {\r
- hover: {\r
- lineWidth: 0\r
- }\r
- }\r
-});\r
-defaultPlotOptions.area = merge(defaultSeriesOptions, {\r
- // threshold: 0,\r
- // lineColor: null, // overrides color, but lets fillColor be unaltered\r
- // fillOpacity: 0.75,\r
- // fillColor: null\r
-\r
-});\r
-defaultPlotOptions.areaspline = merge(defaultPlotOptions.area);\r
-defaultPlotOptions.column = merge(defaultSeriesOptions, {\r
- borderColor: '#FFFFFF',\r
- borderWidth: 1,\r
- borderRadius: 0,\r
- //colorByPoint: undefined,\r
- groupPadding: 0.2,\r
- marker: null, // point options are specified in the base options\r
- pointPadding: 0.1,\r
- //pointWidth: null,\r
- minPointLength: 0, \r
- states: {\r
- hover: {\r
- brightness: 0.1,\r
- shadow: false\r
- },\r
- select: {\r
- color: '#C0C0C0',\r
- borderColor: '#000000',\r
- shadow: false\r
- }\r
- },\r
- dataLabels: {\r
- y: null,\r
- verticalAlign: null\r
- }\r
-});\r
-defaultPlotOptions.bar = merge(defaultPlotOptions.column, {\r
- dataLabels: {\r
- align: 'left',\r
- x: 5,\r
- y: 0\r
- }\r
-});\r
-defaultPlotOptions.pie = merge(defaultSeriesOptions, {\r
- //dragType: '', // n/a\r
- borderColor: '#FFFFFF',\r
- borderWidth: 1,\r
- center: ['50%', '50%'],\r
- colorByPoint: true, // always true for pies\r
- dataLabels: {\r
- // align: null,\r
- // connectorWidth: 1,\r
- // connectorColor: '#606060',\r
- // connectorPadding: 5,\r
- distance: 30,\r
- enabled: true,\r
- formatter: function() {\r
- return this.point.name;\r
- },\r
- y: 5\r
- },\r
- //innerSize: 0,\r
- legendType: 'point',\r
- marker: null, // point options are specified in the base options\r
- size: '75%',\r
- showInLegend: false,\r
- slicedOffset: 10,\r
- states: {\r
- hover: {\r
- brightness: 0.1,\r
- shadow: false\r
- }\r
- }\r
- \r
-});\r
-\r
-// set the default time methods\r
-setTimeMethods();\r
-\r
-\r
-/**\r
- * Handle color operations. The object methods are chainable.\r
- * @param {String} input The input color in either rbga or hex format\r
- */\r
-var Color = function(input) {\r
- // declare variables\r
- var rgba = [], result;\r
- \r
- /**\r
- * Parse the input color to rgba array\r
- * @param {String} input\r
- */\r
- function init(input) {\r
- \r
- // rgba\r
- 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);\r
- if (result) {\r
- rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)];\r
- }\r
-\r
- // hex\r
- else {\r
- result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input);\r
- if (result) {\r
- rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1];\r
- }\r
- }\r
- \r
- }\r
- /**\r
- * Return the color a specified format\r
- * @param {String} format\r
- */\r
- function get(format) {\r
- var ret;\r
- \r
- // it's NaN if gradient colors on a column chart\r
- if (rgba && !isNaN(rgba[0])) {\r
- if (format === 'rgb') {\r
- ret = 'rgb('+ rgba[0] +','+ rgba[1] +','+ rgba[2] +')';\r
- } else if (format === 'a') {\r
- ret = rgba[3];\r
- } else {\r
- ret = 'rgba('+ rgba.join(',') +')';\r
- }\r
- } else {\r
- ret = input;\r
- }\r
- return ret;\r
- }\r
- \r
- /**\r
- * Brighten the color\r
- * @param {Number} alpha\r
- */\r
- function brighten(alpha) {\r
- if (isNumber(alpha) && alpha !== 0) {\r
- var i;\r
- for (i = 0; i < 3; i++) {\r
- rgba[i] += pInt(alpha * 255);\r
- \r
- if (rgba[i] < 0) {\r
- rgba[i] = 0;\r
- }\r
- if (rgba[i] > 255) {\r
- rgba[i] = 255;\r
- }\r
- }\r
- }\r
- return this;\r
- }\r
- /**\r
- * Set the color's opacity to a given alpha value\r
- * @param {Number} alpha\r
- */\r
- function setOpacity(alpha) {\r
- rgba[3] = alpha;\r
- return this;\r
- } \r
- \r
- // initialize: parse the input\r
- init(input);\r
- \r
- // public methods\r
- return {\r
- get: get,\r
- brighten: brighten,\r
- setOpacity: setOpacity\r
- };\r
-};\r
-\r
-/**\r
- * A wrapper object for SVG elements \r
- */\r
-function SVGElement () {}\r
-\r
-SVGElement.prototype = {\r
- /**\r
- * Initialize the SVG renderer\r
- * @param {Object} renderer\r
- * @param {String} nodeName\r
- */\r
- init: function(renderer, nodeName) {\r
- this.element = doc.createElementNS(SVG_NS, nodeName);\r
- this.renderer = renderer;\r
- },\r
- /**\r
- * Animate a given attribute\r
- * @param {Object} params\r
- * @param {Number} options The same options as in jQuery animation\r
- * @param {Function} complete Function to perform at the end of animation\r
- */\r
- animate: function(params, options, complete) {\r
- var animOptions = pick(options, globalAnimation, true);\r
- if (animOptions) {\r
- animOptions = merge(animOptions);\r
- if (complete) { // allows using a callback with the global animation without overwriting it\r
- animOptions.complete = complete;\r
- }\r
- animate(this, params, animOptions);\r
- } else {\r
- this.attr(params);\r
- if (complete) {\r
- complete();\r
- }\r
- }\r
- },\r
- /**\r
- * Set or get a given attribute\r
- * @param {Object|String} hash\r
- * @param {Mixed|Undefined} val\r
- */\r
- attr: function(hash, val) {\r
- var key, \r
- value, \r
- i, \r
- child,\r
- element = this.element,\r
- nodeName = element.nodeName,\r
- renderer = this.renderer,\r
- skipAttr,\r
- shadows = this.shadows,\r
- hasSetSymbolSize,\r
- ret = this;\r
- \r
- // single key-value pair\r
- if (isString(hash) && defined(val)) {\r
- key = hash;\r
- hash = {};\r
- hash[key] = val;\r
- }\r
- \r
- // used as a getter: first argument is a string, second is undefined\r
- if (isString(hash)) {\r
- key = hash;\r
- if (nodeName === 'circle') {\r
- key = { x: 'cx', y: 'cy' }[key] || key;\r
- } else if (key === 'strokeWidth') {\r
- key = 'stroke-width';\r
- }\r
- ret = attr(element, key) || this[key] || 0;\r
- \r
- if (key !== 'd' && key !== 'visibility') { // 'd' is string in animation step\r
- ret = parseFloat(ret);\r
- }\r
- \r
- // setter\r
- } else {\r
- \r
- for (key in hash) {\r
- skipAttr = false; // reset\r
- value = hash[key];\r
- \r
- // paths\r
- if (key === 'd') {\r
- if (value && value.join) { // join path\r
- value = value.join(' ');\r
- } \r
- if (/(NaN| {2}|^$)/.test(value)) {\r
- value = 'M 0 0';\r
- }\r
- this.d = value; // shortcut for animations\r
- \r
- // update child tspans x values\r
- } else if (key === 'x' && nodeName === 'text') { \r
- for (i = 0; i < element.childNodes.length; i++ ) {\r
- child = element.childNodes[i];\r
- // if the x values are equal, the tspan represents a linebreak\r
- if (attr(child, 'x') === attr(element, 'x')) {\r
- //child.setAttribute('x', value);\r
- attr(child, 'x', value);\r
- }\r
- }\r
- \r
- if (this.rotation) {\r
- attr(element, 'transform', 'rotate('+ this.rotation +' '+ value +' '+\r
- pInt(hash.y || attr(element, 'y')) +')');\r
- }\r
- \r
- // apply gradients\r
- } else if (key === 'fill') {\r
- value = renderer.color(value, element, key);\r
- \r
- // circle x and y\r
- } else if (nodeName === 'circle' && (key === 'x' || key === 'y')) {\r
- key = { x: 'cx', y: 'cy' }[key] || key;\r
- \r
- // translation and text rotation\r
- } else if (key === 'translateX' || key === 'translateY' || key === 'rotation' || key === 'verticalAlign') {\r
- this[key] = value;\r
- this.updateTransform();\r
- skipAttr = true;\r
- \r
- // apply opacity as subnode (required by legacy WebKit and Batik)\r
- } else if (key === 'stroke') {\r
- value = renderer.color(value, element, key);\r
- \r
- // emulate VML's dashstyle implementation\r
- } else if (key === 'dashstyle') {\r
- key = 'stroke-dasharray';\r
- value = value && value.toLowerCase();\r
- if (value === 'solid') {\r
- value = NONE;\r
- } else if (value) {\r
- value = value\r
- .replace('shortdashdotdot', '3,1,1,1,1,1,')\r
- .replace('shortdashdot', '3,1,1,1')\r
- .replace('shortdot', '1,1,')\r
- .replace('shortdash', '3,1,')\r
- .replace('longdash', '8,3,')\r
- .replace(/dot/g, '1,3,')\r
- .replace('dash', '4,3,')\r
- .replace(/,$/, '')\r
- .split(','); // ending comma\r
- \r
- i = value.length;\r
- while (i--) {\r
- value[i] = pInt(value[i]) * hash['stroke-width'];\r
- }\r
- \r
- value = value.join(',');\r
- } \r
- \r
- // special\r
- } else if (key === 'isTracker') {\r
- this[key] = value;\r
- \r
- // IE9/MooTools combo: MooTools returns objects instead of numbers and IE9 Beta 2\r
- // is unable to cast them. Test again with final IE9.\r
- } else if (key === 'width') {\r
- value = pInt(value);\r
- \r
- // Text alignment\r
- } else if (key === 'align') {\r
- key = 'text-anchor';\r
- value = { left: 'start', center: 'middle', right: 'end' }[value];\r
- }\r
- \r
- \r
- \r
- // jQuery animate changes case\r
- if (key === 'strokeWidth') {\r
- key = 'stroke-width';\r
- }\r
- \r
- // Chrome/Win < 6 bug (http://code.google.com/p/chromium/issues/detail?id=15461) \r
- if (isWebKit && key === 'stroke-width' && value === 0) {\r
- value = 0.000001;\r
- }\r
- \r
- // symbols\r
- if (this.symbolName && /^(x|y|r|start|end|innerR)/.test(key)) {\r
- \r
- \r
- if (!hasSetSymbolSize) {\r
- this.symbolAttr(hash);\r
- hasSetSymbolSize = true;\r
- }\r
- skipAttr = true;\r
- }\r
- \r
- // let the shadow follow the main element\r
- if (shadows && /^(width|height|visibility|x|y|d)$/.test(key)) {\r
- i = shadows.length;\r
- while (i--) {\r
- attr(shadows[i], key, value);\r
- } \r
- }\r
- \r
- // validate heights\r
- if ((key === 'width' || key === 'height') && nodeName === 'rect' && value < 0) {\r
- value = 0;\r
- }\r
- \r
- if (key === 'text') {\r
- // only one node allowed\r
- this.textStr = value;\r
- if (this.added) {\r
- renderer.buildText(this);\r
- }\r
- } else if (!skipAttr) {\r
- //element.setAttribute(key, value);\r
- attr(element, key, value);\r
- }\r
- \r
- }\r
- \r
- }\r
- return ret;\r
- },\r
- \r
- /**\r
- * If one of the symbol size affecting parameters are changed,\r
- * check all the others only once for each call to an element's\r
- * .attr() method\r
- * @param {Object} hash\r
- */\r
- symbolAttr: function(hash) {\r
- var wrapper = this;\r
- \r
- each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR'], function(key) {\r
- wrapper[key] = pick(hash[key], wrapper[key]);\r
- });\r
- \r
- wrapper.attr({ \r
- d: wrapper.renderer.symbols[wrapper.symbolName](\r
- mathRound(wrapper.x * 2) / 2, // Round to halves. Issue #274.\r
- mathRound(wrapper.y * 2) / 2, \r
- wrapper.r, \r
- {\r
- start: wrapper.start, \r
- end: wrapper.end,\r
- width: wrapper.width, \r
- height: wrapper.height,\r
- innerR: wrapper.innerR\r
- })\r
- });\r
- },\r
- \r
- /**\r
- * Apply a clipping path to this object\r
- * @param {String} id\r
- */\r
- clip: function(clipRect) {\r
- return this.attr('clip-path', 'url('+ this.renderer.url +'#'+ clipRect.id +')');\r
- },\r
- \r
- /**\r
- * Calculate the coordinates needed for drawing a rectangle crisply and return the\r
- * calculated attributes\r
- * @param {Number} strokeWidth\r
- * @param {Number} x\r
- * @param {Number} y\r
- * @param {Number} width\r
- * @param {Number} height\r
- */\r
- crisp: function(strokeWidth, x, y, width, height) {\r
- \r
- var wrapper = this,\r
- key,\r
- attr = {},\r
- values = {},\r
- normalizer;\r
- \r
- strokeWidth = strokeWidth || wrapper.strokeWidth || 0;\r
- normalizer = strokeWidth % 2 / 2;\r
-\r
- // normalize for crisp edges\r
- values.x = mathFloor(x || wrapper.x || 0) + normalizer;\r
- values.y = mathFloor(y || wrapper.y || 0) + normalizer;\r
- values.width = mathFloor((width || wrapper.width || 0) - 2 * normalizer);\r
- values.height = mathFloor((height || wrapper.height || 0) - 2 * normalizer);\r
- values.strokeWidth = strokeWidth;\r
- \r
- for (key in values) {\r
- if (wrapper[key] !== values[key]) { // only set attribute if changed\r
- wrapper[key] = attr[key] = values[key];\r
- }\r
- }\r
- \r
- return attr;\r
- },\r
- \r
- /**\r
- * Set styles for the element\r
- * @param {Object} styles\r
- */\r
- css: function(styles) {\r
- var elemWrapper = this,\r
- elem = elemWrapper.element,\r
- textWidth = styles && styles.width && elem.nodeName === 'text',\r
- n,\r
- serializedCss = '',\r
- hyphenate = function(a, b){ return '-'+ b.toLowerCase(); };\r
- \r
- // convert legacy\r
- if (styles && styles.color) {\r
- styles.fill = styles.color;\r
- }\r
-\r
- // Merge the new styles with the old ones\r
- styles = extend(\r
- elemWrapper.styles,\r
- styles\r
- );\r
- \r
- \r
- // store object\r
- elemWrapper.styles = styles;\r
- \r
- \r
- // serialize and set style attribute\r
- if (isIE && !hasSVG) { // legacy IE doesn't support setting style attribute\r
- if (textWidth) {\r
- delete styles.width;\r
- } \r
- css(elemWrapper.element, styles); \r
- } else {\r
- for (n in styles) {\r
- serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':'+ styles[n] + ';';\r
- }\r
- elemWrapper.attr({\r
- style: serializedCss\r
- });\r
- } \r
- \r
- \r
- // re-build text\r
- if (textWidth && elemWrapper.added) {\r
- elemWrapper.renderer.buildText(elemWrapper);\r
- }\r
- \r
- return elemWrapper;\r
- },\r
- \r
- /**\r
- * Add an event listener\r
- * @param {String} eventType\r
- * @param {Function} handler\r
- */\r
- on: function(eventType, handler) {\r
- var fn = handler;\r
- // touch\r
- if (hasTouch && eventType === 'click') {\r
- eventType = 'touchstart';\r
- fn = function(e) {\r
- e.preventDefault();\r
- handler();\r
- };\r
- }\r
- // simplest possible event model for internal use\r
- this.element['on'+ eventType] = fn;\r
- return this;\r
- },\r
- \r
- \r
- /**\r
- * Move an object and its children by x and y values\r
- * @param {Number} x\r
- * @param {Number} y\r
- */\r
- translate: function(x, y) {\r
- return this.attr({\r
- translateX: x,\r
- translateY: y\r
- });\r
- },\r
- \r
- /**\r
- * Invert a group, rotate and flip\r
- */\r
- invert: function() {\r
- var wrapper = this;\r
- wrapper.inverted = true;\r
- wrapper.updateTransform();\r
- return wrapper;\r
- },\r
- \r
- /**\r
- * Private method to update the transform attribute based on internal \r
- * properties\r
- */\r
- updateTransform: function() {\r
- var wrapper = this,\r
- translateX = wrapper.translateX || 0,\r
- translateY = wrapper.translateY || 0,\r
- inverted = wrapper.inverted,\r
- rotation = wrapper.rotation,\r
- transform = [];\r
- \r
- // flipping affects translate as adjustment for flipping around the group's axis\r
- if (inverted) {\r
- translateX += wrapper.attr('width');\r
- translateY += wrapper.attr('height');\r
- }\r
- \r
- // apply translate\r
- if (translateX || translateY) {\r
- transform.push('translate('+ translateX +','+ translateY +')');\r
- }\r
- \r
- // apply rotation\r
- if (inverted) {\r
- transform.push('rotate(90) scale(-1,1)');\r
- } else if (rotation) { // text rotation\r
- transform.push('rotate('+ rotation +' '+ wrapper.x +' '+ wrapper.y +')');\r
- }\r
- \r
- if (transform.length) {\r
- attr(wrapper.element, 'transform', transform.join(' '));\r
- }\r
- },\r
- /**\r
- * Bring the element to the front\r
- */\r
- toFront: function() {\r
- var element = this.element;\r
- element.parentNode.appendChild(element);\r
- return this;\r
- },\r
- \r
- \r
- /**\r
- * Break down alignment options like align, verticalAlign, x and y \r
- * to x and y relative to the chart.\r
- * \r
- * @param {Object} alignOptions\r
- * @param {Boolean} alignByTranslate\r
- * @param {Object} box The box to align to, needs a width and height\r
- * \r
- */\r
- align: function(alignOptions, alignByTranslate, box) {\r
- var elemWrapper = this;\r
- \r
- if (!alignOptions) { // called on resize\r
- alignOptions = elemWrapper.alignOptions;\r
- alignByTranslate = elemWrapper.alignByTranslate;\r
- } else { // first call on instanciate\r
- elemWrapper.alignOptions = alignOptions;\r
- elemWrapper.alignByTranslate = alignByTranslate;\r
- if (!box) { // boxes other than renderer handle this internally\r
- elemWrapper.renderer.alignedObjects.push(elemWrapper);\r
- }\r
- }\r
- \r
- box = pick(box, elemWrapper.renderer);\r
- \r
- var align = alignOptions.align,\r
- vAlign = alignOptions.verticalAlign,\r
- x = (box.x || 0) + (alignOptions.x || 0), // default: left align\r
- y = (box.y || 0) + (alignOptions.y || 0), // default: top align\r
- attribs = {};\r
- \r
- \r
- // align\r
- if (/^(right|center)$/.test(align)) {\r
- x += (box.width - (alignOptions.width || 0) ) /\r
- { right: 1, center: 2 }[align];\r
- }\r
- attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x);\r
- \r
- \r
- // vertical align\r
- if (/^(bottom|middle)$/.test(vAlign)) {\r
- y += (box.height - (alignOptions.height || 0)) /\r
- ({ bottom: 1, middle: 2 }[vAlign] || 1);\r
- \r
- }\r
- attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y);\r
- \r
- // animate only if already placed\r
- elemWrapper[elemWrapper.placed ? 'animate' : 'attr'](attribs);\r
- elemWrapper.placed = true;\r
- elemWrapper.alignAttr = attribs;\r
- \r
- return elemWrapper;\r
- },\r
- \r
- /**\r
- * Get the bounding box (width, height, x and y) for the element\r
- */\r
- getBBox: function() { \r
- var bBox,\r
- width,\r
- height,\r
- rotation = this.rotation,\r
- rad = rotation * deg2rad;\r
- \r
- try { // fails in Firefox if the container has display: none\r
- // use extend because IE9 is not allowed to change width and height in case \r
- // of rotation (below)\r
- bBox = extend({}, this.element.getBBox());\r
- } catch(e) {\r
- bBox = { width: 0, height: 0 };\r
- }\r
- width = bBox.width;\r
- height = bBox.height;\r
- \r
- // adjust for rotated text\r
- if (rotation) {\r
- bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad));\r
- bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad));\r
- }\r
- \r
- return bBox;\r
- },\r
- \r
- /* *\r
- * Manually compute width and height of rotated text from non-rotated. Shared by SVG and VML\r
- * @param {Object} bBox\r
- * @param {number} rotation\r
- * /\r
- rotateBBox: function(bBox, rotation) {\r
- var rad = rotation * math.PI * 2 / 360, // radians\r
- width = bBox.width,\r
- height = bBox.height;\r
- \r
- \r
- },*/\r
- \r
- /**\r
- * Show the element\r
- */\r
- show: function() {\r
- return this.attr({ visibility: VISIBLE });\r
- },\r
- \r
- /**\r
- * Hide the element\r
- */\r
- hide: function() {\r
- return this.attr({ visibility: HIDDEN });\r
- },\r
- \r
- /**\r
- * Add the element\r
- * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined\r
- * to append the element to the renderer.box.\r
- */ \r
- add: function(parent) {\r
- \r
- var renderer = this.renderer,\r
- parentWrapper = parent || renderer,\r
- parentNode = parentWrapper.element || renderer.box,\r
- childNodes = parentNode.childNodes,\r
- element = this.element,\r
- zIndex = attr(element, 'zIndex'),\r
- otherElement,\r
- otherZIndex,\r
- i;\r
- \r
- // mark as inverted\r
- this.parentInverted = parent && parent.inverted;\r
- \r
- // build formatted text\r
- if (this.textStr !== undefined) {\r
- renderer.buildText(this);\r
- }\r
- \r
- // mark the container as having z indexed children\r
- if (zIndex) {\r
- parentWrapper.handleZ = true;\r
- zIndex = pInt(zIndex);\r
- }\r
-\r
- // insert according to this and other elements' zIndex\r
- if (parentWrapper.handleZ) { // this element or any of its siblings has a z index\r
- for (i = 0; i < childNodes.length; i++) {\r
- otherElement = childNodes[i];\r
- otherZIndex = attr(otherElement, 'zIndex');\r
- if (otherElement !== element && (\r
- // insert before the first element with a higher zIndex\r
- pInt(otherZIndex) > zIndex || \r
- // if no zIndex given, insert before the first element with a zIndex\r
- (!defined(zIndex) && defined(otherZIndex)) \r
- \r
- )) {\r
- parentNode.insertBefore(element, otherElement);\r
- return this;\r
- }\r
- }\r
- }\r
- \r
- // default: append at the end\r
- parentNode.appendChild(element);\r
- \r
- this.added = true;\r
- \r
- return this;\r
- },\r
-\r
- /**\r
- * Destroy the element and element wrapper\r
- */\r
- destroy: function() {\r
- var wrapper = this,\r
- element = wrapper.element || {},\r
- shadows = wrapper.shadows,\r
- parentNode = element.parentNode,\r
- key;\r
- \r
- // remove events\r
- element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = null;\r
- stop(wrapper); // stop running animations\r
- \r
- // remove element\r
- if (parentNode) {\r
- parentNode.removeChild(element);\r
- }\r
- \r
- // destroy shadows\r
- if (shadows) {\r
- each(shadows, function(shadow) {\r
- parentNode = shadow.parentNode;\r
- if (parentNode) { // the entire chart HTML can be overwritten\r
- parentNode.removeChild(shadow);\r
- } \r
- });\r
- }\r
- \r
- // remove from alignObjects\r
- erase(wrapper.renderer.alignedObjects, wrapper);\r
- \r
- for (key in wrapper) {\r
- delete wrapper[key];\r
- }\r
- \r
- return null;\r
- },\r
- \r
- /**\r
- * Empty a group element\r
- */\r
- empty: function() {\r
- var element = this.element,\r
- childNodes = element.childNodes,\r
- i = childNodes.length;\r
- \r
- while (i--) {\r
- element.removeChild(childNodes[i]);\r
- }\r
- },\r
- \r
- /**\r
- * Add a shadow to the element. Must be done after the element is added to the DOM\r
- * @param {Boolean} apply\r
- */\r
- shadow: function(apply, group) {\r
- var shadows = [],\r
- i,\r
- shadow,\r
- element = this.element,\r
- \r
- // compensate for inverted plot area\r
- transform = this.parentInverted ? '(-1,-1)' : '(1,1)';\r
- \r
- \r
- if (apply) {\r
- for (i = 1; i <= 3; i++) {\r
- shadow = element.cloneNode(0);\r
- attr(shadow, {\r
- 'isShadow': 'true',\r
- 'stroke': 'rgb(0, 0, 0)',\r
- 'stroke-opacity': 0.05 * i,\r
- 'stroke-width': 7 - 2 * i,\r
- 'transform': 'translate'+ transform,\r
- 'fill': NONE\r
- });\r
- \r
- if (group) {\r
- group.element.appendChild(shadow);\r
- } else {\r
- element.parentNode.insertBefore(shadow, element);\r
- }\r
- \r
- shadows.push(shadow);\r
- }\r
- \r
- this.shadows = shadows;\r
- }\r
- return this;\r
- \r
- }\r
-};\r
-\r
-/**\r
- * The default SVG renderer\r
- */\r
-var SVGRenderer = function() {\r
- this.init.apply(this, arguments);\r
-};\r
-SVGRenderer.prototype = {\r
- \r
- Element: SVGElement,\r
- \r
- /**\r
- * Initialize the SVGRenderer\r
- * @param {Object} container\r
- * @param {Number} width\r
- * @param {Number} height\r
- * @param {Boolean} forExport\r
- */\r
- init: function(container, width, height, forExport) {\r
- var renderer = this,\r
- loc = location,\r
- boxWrapper;\r
- \r
- boxWrapper = renderer.createElement('svg')\r
- .attr({\r
- xmlns: SVG_NS,\r
- version: '1.1'\r
- });\r
- container.appendChild(boxWrapper.element);\r
- \r
- // object properties\r
- renderer.box = boxWrapper.element;\r
- renderer.boxWrapper = boxWrapper;\r
- renderer.alignedObjects = [];\r
- renderer.url = isIE ? '' : loc.href.replace(/#.*?$/, ''); // page url used for internal references\r
- renderer.defs = this.createElement('defs').add();\r
- renderer.forExport = forExport;\r
- \r
- renderer.setSize(width, height, false);\r
- \r
- },\r
- \r
- \r
- /**\r
- * Create a wrapper for an SVG element\r
- * @param {Object} nodeName\r
- */\r
- createElement: function(nodeName) {\r
- var wrapper = new this.Element();\r
- wrapper.init(this, nodeName);\r
- return wrapper;\r
- },\r
- \r
- \r
- /** \r
- * Parse a simple HTML string into SVG tspans\r
- * \r
- * @param {Object} textNode The parent text SVG node\r
- */\r
- buildText: function(wrapper) {\r
- var textNode = wrapper.element,\r
- lines = pick(wrapper.textStr, '').toString()\r
- .replace(/<(b|strong)>/g, '<span style="font-weight:bold">')\r
- .replace(/<(i|em)>/g, '<span style="font-style:italic">')\r
- .replace(/<a/g, '<span')\r
- .replace(/<\/(b|strong|i|em|a)>/g, '</span>')\r
- .split(/<br.*?>/g),\r
- childNodes = textNode.childNodes,\r
- styleRegex = /style="([^"]+)"/,\r
- hrefRegex = /href="([^"]+)"/,\r
- parentX = attr(textNode, 'x'),\r
- textStyles = wrapper.styles,\r
- reverse = isFirefox && textStyles && textStyles.HcDirection === 'rtl' && \r
- !this.forExport && pInt(userAgent.split('Firefox/')[1]) < 4, // issue #38\r
- arr,\r
- width = textStyles && pInt(textStyles.width),\r
- textLineHeight = textStyles && textStyles.lineHeight,\r
- lastLine,\r
- GET_COMPUTED_STYLE = 'getComputedStyle',\r
- i = childNodes.length;\r
- \r
- // remove old text\r
- while (i--) {\r
- textNode.removeChild(childNodes[i]);\r
- }\r
- \r
- if (width && !wrapper.added) {\r
- this.box.appendChild(textNode); // attach it to the DOM to read offset width\r
- }\r
- \r
- each(lines, function(line, lineNo) {\r
- var spans, spanNo = 0, lineHeight;\r
- \r
- line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||');\r
- spans = line.split('|||');\r
- \r
- each(spans, function (span) {\r
- if (span !== '' || spans.length === 1) {\r
- var attributes = {},\r
- tspan = doc.createElementNS(SVG_NS, 'tspan');\r
- if (styleRegex.test(span)) {\r
- attr(\r
- tspan, \r
- 'style', \r
- span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2')\r
- );\r
- }\r
- if (hrefRegex.test(span)) {\r
- attr(tspan, 'onclick', 'location.href=\"'+ span.match(hrefRegex)[1] +'\"');\r
- css(tspan, { cursor: 'pointer' });\r
- }\r
- \r
- span = (span.replace(/<(.|\n)*?>/g, '') || ' ')\r
- .replace(/</g, '<')\r
- .replace(/>/g, '>');\r
- \r
- // issue #38 workaround.\r
- if (reverse) {\r
- arr = [];\r
- i = span.length;\r
- while (i--) {\r
- arr.push(span.charAt(i));\r
- }\r
- span = arr.join('');\r
- }\r
- \r
- // add the text node\r
- tspan.appendChild(doc.createTextNode(span));\r
- \r
- if (!spanNo) { // first span in a line, align it to the left\r
- attributes.x = parentX;\r
- } else {\r
- // Firefox ignores spaces at the front or end of the tspan\r
- attributes.dx = 3; // space\r
- }\r
- \r
- // first span on subsequent line, add the line height\r
- if (!spanNo) { \r
- if (lineNo) {\r
- \r
- // allow getting the right offset height in exporting in IE\r
- if (!hasSVG && wrapper.renderer.forExport) {\r
- css(tspan, { display: 'block' });\r
- }\r
- \r
- // Webkit and opera sometimes return 'normal' as the line height. In that\r
- // case, webkit uses offsetHeight, while Opera falls back to 18\r
- lineHeight = win[GET_COMPUTED_STYLE] &&\r
- pInt(win[GET_COMPUTED_STYLE](lastLine, null).getPropertyValue('line-height'));\r
- \r
- if (!lineHeight || isNaN(lineHeight)) {\r
- lineHeight = textLineHeight || lastLine.offsetHeight || 18;\r
- }\r
- attr(tspan, 'dy', lineHeight);\r
- }\r
- lastLine = tspan; // record for use in next line \r
- }\r
- \r
- // add attributes\r
- attr(tspan, attributes);\r
- \r
- // append it\r
- textNode.appendChild(tspan);\r
- \r
- spanNo++;\r
- \r
- // check width and apply soft breaks\r
- if (width) {\r
- var words = span.replace(/-/g, '- ').split(' '),\r
- tooLong,\r
- actualWidth,\r
- rest = [];\r
- \r
- while (words.length || rest.length) {\r
- actualWidth = textNode.getBBox().width;\r
- tooLong = actualWidth > width;\r
- if (!tooLong || words.length === 1) { // new line needed\r
- words = rest;\r
- rest = [];\r
- if (words.length) {\r
- tspan = doc.createElementNS(SVG_NS, 'tspan');\r
- attr(tspan, {\r
- dy: textLineHeight || 16,\r
- x: parentX\r
- });\r
- textNode.appendChild(tspan);\r
- \r
- if (actualWidth > width) { // a single word is pressing it out\r
- width = actualWidth;\r
- }\r
- }\r
- } else { // append to existing line tspan\r
- tspan.removeChild(tspan.firstChild);\r
- rest.unshift(words.pop()); \r
- }\r
- if (words.length) {\r
- tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));\r
- }\r
- }\r
- }\r
- }\r
- });\r
- });\r
- \r
- \r
- },\r
- \r
- /**\r
- * Make a straight line crisper by not spilling out to neighbour pixels\r
- * @param {Array} points\r
- * @param {Number} width \r
- */\r
- crispLine: function(points, width) {\r
- // points format: [M, 0, 0, L, 100, 0]\r
- // normalize to a crisp line\r
- if (points[1] === points[4]) {\r
- points[1] = points[4] = mathRound(points[1]) + (width % 2 / 2);\r
- }\r
- if (points[2] === points[5]) {\r
- points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2);\r
- }\r
- return points;\r
- },\r
- \r
- \r
- /**\r
- * Draw a path\r
- * @param {Array} path An SVG path in array form\r
- */\r
- path: function (path) {\r
- return this.createElement('path').attr({ \r
- d: path, \r
- fill: NONE\r
- });\r
- },\r
- \r
- /**\r
- * Draw and return an SVG circle\r
- * @param {Number} x The x position\r
- * @param {Number} y The y position\r
- * @param {Number} r The radius\r
- */\r
- circle: function (x, y, r) {\r
- var attr = isObject(x) ?\r
- x :\r
- {\r
- x: x,\r
- y: y,\r
- r: r\r
- };\r
- \r
- return this.createElement('circle').attr(attr);\r
- },\r
- \r
- /**\r
- * Draw and return an arc\r
- * @param {Number} x X position\r
- * @param {Number} y Y position\r
- * @param {Number} r Radius\r
- * @param {Number} innerR Inner radius like used in donut charts\r
- * @param {Number} start Starting angle\r
- * @param {Number} end Ending angle\r
- */\r
- arc: function (x, y, r, innerR, start, end) {\r
- // arcs are defined as symbols for the ability to set \r
- // attributes in attr and animate\r
- \r
- if (isObject(x)) {\r
- y = x.y;\r
- r = x.r;\r
- innerR = x.innerR;\r
- start = x.start;\r
- end = x.end;\r
- x = x.x;\r
- }\r
- \r
- return this.symbol('arc', x || 0, y || 0, r || 0, {\r
- innerR: innerR || 0,\r
- start: start || 0,\r
- end: end || 0\r
- });\r
- },\r
- \r
- /**\r
- * Draw and return a rectangle\r
- * @param {Number} x Left position\r
- * @param {Number} y Top position\r
- * @param {Number} width\r
- * @param {Number} height\r
- * @param {Number} r Border corner radius\r
- * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing\r
- */\r
- rect: function (x, y, width, height, r, strokeWidth) {\r
- if (isObject(x)) {\r
- y = x.y;\r
- width = x.width;\r
- height = x.height;\r
- r = x.r;\r
- strokeWidth = x.strokeWidth;\r
- x = x.x; \r
- }\r
- var wrapper = this.createElement('rect').attr({\r
- rx: r,\r
- ry: r,\r
- fill: NONE\r
- });\r
- \r
- return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0)));\r
- },\r
- \r
- /**\r
- * Resize the box and re-align all aligned elements\r
- * @param {Object} width\r
- * @param {Object} height\r
- * @param {Boolean} animate\r
- * \r
- */\r
- setSize: function(width, height, animate) {\r
- var renderer = this,\r
- alignedObjects = renderer.alignedObjects,\r
- i = alignedObjects.length;\r
- \r
- renderer.width = width;\r
- renderer.height = height;\r
- \r
- renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({\r
- width: width,\r
- height: height\r
- }); \r
- \r
- while (i--) {\r
- alignedObjects[i].align();\r
- }\r
- },\r
- \r
- /**\r
- * Create a group\r
- * @param {String} name The group will be given a class name of 'highcharts-{name}'.\r
- * This can be used for styling and scripting.\r
- */\r
- g: function(name) {\r
- return this.createElement('g').attr(\r
- defined(name) && { 'class': PREFIX + name }\r
- );\r
- },\r
- \r
- /**\r
- * Display an image\r
- * @param {String} src\r
- * @param {Number} x\r
- * @param {Number} y\r
- * @param {Number} width\r
- * @param {Number} height\r
- */\r
- image: function(src, x, y, width, height) {\r
- var attribs = {\r
- preserveAspectRatio: NONE \r
- },\r
- elemWrapper;\r
- \r
- // optional properties\r
- if (arguments.length > 1) {\r
- extend(attribs, {\r
- x: x,\r
- y: y,\r
- width: width,\r
- height: height\r
- });\r
- }\r
- \r
- elemWrapper = this.createElement('image').attr(attribs); \r
- \r
- // set the href in the xlink namespace\r
- if (elemWrapper.element.setAttributeNS) {\r
- elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink', \r
- 'href', src);\r
- } else {\r
- // could be exporting in IE\r
- // using href throws "not supported" in ie7 and under, requries regex shim to fix later\r
- elemWrapper.element.setAttribute('hc-svg-href', src);\r
- }\r
- \r
- return elemWrapper; \r
- },\r
- \r
- /**\r
- * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object.\r
- * \r
- * @param {Object} symbol\r
- * @param {Object} x\r
- * @param {Object} y\r
- * @param {Object} radius\r
- * @param {Object} options\r
- */\r
- symbol: function(symbol, x, y, radius, options) {\r
- \r
- var obj,\r
- \r
- // get the symbol definition function\r
- symbolFn = this.symbols[symbol],\r
- \r
- // check if there's a path defined for this symbol\r
- path = symbolFn && symbolFn(\r
- mathRound(x), \r
- mathRound(y),\r
- radius, \r
- options\r
- ),\r
- \r
- imageRegex = /^url\((.*?)\)$/,\r
- imageSrc,\r
- imageSize;\r
- \r
- if (path) {\r
- \r
- obj = this.path(path);\r
- // expando properties for use in animate and attr\r
- extend(obj, {\r
- symbolName: symbol,\r
- x: x,\r
- y: y,\r
- r: radius\r
- });\r
- if (options) {\r
- extend(obj, options);\r
- }\r
- \r
- \r
- // image symbols\r
- } else if (imageRegex.test(symbol)) {\r
- \r
- var centerImage = function(img, size) {\r
- img.attr({\r
- width: size[0],\r
- height: size[1]\r
- }).translate(\r
- -mathRound(size[0] / 2),\r
- -mathRound(size[1] / 2)\r
- );\r
- };\r
- \r
- imageSrc = symbol.match(imageRegex)[1];\r
- imageSize = symbolSizes[imageSrc];\r
- \r
- // create the image synchronously, add attribs async\r
- obj = this.image(imageSrc)\r
- .attr({\r
- x: x,\r
- y: y\r
- });\r
-\r
- if (imageSize) {\r
- centerImage(obj, imageSize);\r
- } else {\r
- // initialize image to be 0 size so export will still function if there's no cached sizes\r
- obj.attr({ width: 0, height: 0 });\r
-\r
- // create a dummy JavaScript image to get the width and height \r
- createElement('img', {\r
- onload: function() {\r
- var img = this;\r
- centerImage(obj, symbolSizes[imageSrc] = [img.width, img.height]);\r
- },\r
- src: imageSrc\r
- });\r
- }\r
- \r
- // default circles\r
- } else {\r
- obj = this.circle(x, y, radius);\r
- }\r
- \r
- return obj;\r
- },\r
- \r
- /**\r
- * An extendable collection of functions for defining symbol paths.\r
- */\r
- symbols: {\r
- 'square': function (x, y, radius) {\r
- var len = 0.707 * radius;\r
- return [\r
- M, x-len, y-len,\r
- L, x+len, y-len,\r
- x+len, y+len,\r
- x-len, y+len,\r
- 'Z'\r
- ];\r
- },\r
- \r
- 'triangle': function (x, y, radius) {\r
- return [\r
- M, x, y-1.33 * radius,\r
- L, x+radius, y + 0.67 * radius,\r
- x-radius, y + 0.67 * radius,\r
- 'Z'\r
- ];\r
- },\r
- \r
- 'triangle-down': function (x, y, radius) {\r
- return [\r
- M, x, y + 1.33 * radius,\r
- L, x-radius, y-0.67 * radius,\r
- x+radius, y-0.67 * radius,\r
- 'Z'\r
- ];\r
- },\r
- 'diamond': function (x, y, radius) {\r
- return [\r
- M, x, y-radius,\r
- L, x+radius, y,\r
- x, y+radius,\r
- x-radius, y,\r
- 'Z'\r
- ];\r
- },\r
- 'arc': function (x, y, radius, options) {\r
- var start = options.start,\r
- end = options.end - 0.000001, // to prevent cos and sin of start and end from becoming equal on 360 arcs\r
- innerRadius = options.innerR,\r
- cosStart = mathCos(start),\r
- sinStart = mathSin(start),\r
- cosEnd = mathCos(end),\r
- sinEnd = mathSin(end),\r
- longArc = options.end - start < mathPI ? 0 : 1;\r
- \r
- return [\r
- M,\r
- x + radius * cosStart,\r
- y + radius * sinStart,\r
- 'A', // arcTo\r
- radius, // x radius\r
- radius, // y radius\r
- 0, // slanting\r
- longArc, // long or short arc\r
- 1, // clockwise\r
- x + radius * cosEnd,\r
- y + radius * sinEnd,\r
- L, \r
- x + innerRadius * cosEnd, \r
- y + innerRadius * sinEnd,\r
- 'A', // arcTo\r
- innerRadius, // x radius\r
- innerRadius, // y radius\r
- 0, // slanting\r
- longArc, // long or short arc\r
- 0, // clockwise\r
- x + innerRadius * cosStart,\r
- y + innerRadius * sinStart,\r
- \r
- 'Z' // close\r
- ];\r
- }\r
- },\r
- \r
- /**\r
- * Define a clipping rectangle\r
- * @param {String} id\r
- * @param {Number} x\r
- * @param {Number} y\r
- * @param {Number} width\r
- * @param {Number} height\r
- */\r
- clipRect: function (x, y, width, height) {\r
- var wrapper,\r
- id = PREFIX + idCounter++,\r
- \r
- clipPath = this.createElement('clipPath').attr({\r
- id: id\r
- }).add(this.defs);\r
- \r
- wrapper = this.rect(x, y, width, height, 0).add(clipPath);\r
- wrapper.id = id;\r
- \r
- return wrapper;\r
- },\r
- \r
- \r
- /**\r
- * Take a color and return it if it's a string, make it a gradient if it's a\r
- * gradient configuration object\r
- * \r
- * @param {Object} color The color or config object\r
- */\r
- color: function(color, elem, prop) {\r
- var colorObject,\r
- regexRgba = /^rgba/;\r
- if (color && color.linearGradient) {\r
- var renderer = this,\r
- strLinearGradient = 'linearGradient',\r
- linearGradient = color[strLinearGradient],\r
- id = PREFIX + idCounter++,\r
- gradientObject,\r
- stopColor,\r
- stopOpacity;\r
- gradientObject = renderer.createElement(strLinearGradient).attr({\r
- id: id,\r
- gradientUnits: 'userSpaceOnUse',\r
- x1: linearGradient[0],\r
- y1: linearGradient[1],\r
- x2: linearGradient[2],\r
- y2: linearGradient[3]\r
- }).add(renderer.defs);\r
- \r
- each(color.stops, function(stop) {\r
- if (regexRgba.test(stop[1])) {\r
- colorObject = Color(stop[1]);\r
- stopColor = colorObject.get('rgb');\r
- stopOpacity = colorObject.get('a');\r
- } else {\r
- stopColor = stop[1];\r
- stopOpacity = 1;\r
- }\r
- renderer.createElement('stop').attr({\r
- offset: stop[0],\r
- 'stop-color': stopColor,\r
- 'stop-opacity': stopOpacity\r
- }).add(gradientObject);\r
- });\r
- \r
- return 'url('+ this.url +'#'+ id +')';\r
- \r
- // Webkit and Batik can't show rgba.\r
- } else if (regexRgba.test(color)) {\r
- colorObject = Color(color);\r
- attr(elem, prop +'-opacity', colorObject.get('a'));\r
- \r
- return colorObject.get('rgb');\r
- \r
- \r
- } else {\r
- return color;\r
- }\r
- \r
- },\r
- \r
- \r
- /**\r
- * Add text to the SVG object\r
- * @param {String} str\r
- * @param {Number} x Left position\r
- * @param {Number} y Top position\r
- */\r
- text: function(str, x, y) {\r
- \r
- // declare variables\r
- var defaultChartStyle = defaultOptions.chart.style,\r
- wrapper;\r
- \r
- x = mathRound(pick(x, 0));\r
- y = mathRound(pick(y, 0));\r
- \r
- wrapper = this.createElement('text')\r
- .attr({\r
- x: x,\r
- y: y,\r
- text: str \r
- })\r
- .css({\r
- fontFamily: defaultChartStyle.fontFamily,\r
- fontSize: defaultChartStyle.fontSize\r
- });\r
- \r
- wrapper.x = x;\r
- wrapper.y = y;\r
- return wrapper;\r
- }\r
-}; // end SVGRenderer\r
-\r
-// general renderer\r
-Renderer = SVGRenderer;\r
-\r
-\r
-\r
-/* **************************************************************************** \r
- * * \r
- * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE *\r
- * *\r
- * For applications and websites that don't need IE support, like platform *\r
- * targeted mobile apps and web apps, this code can be removed. *\r
- * *\r
- *****************************************************************************/\r
-var VMLRenderer;\r
-if (!hasSVG) {\r
-\r
-/**\r
- * The VML element wrapper.\r
- */\r
-var VMLElement = extendClass( SVGElement, {\r
- \r
- /**\r
- * Initialize a new VML element wrapper. It builds the markup as a string\r
- * to minimize DOM traffic.\r
- * @param {Object} renderer\r
- * @param {Object} nodeName\r
- */\r
- init: function(renderer, nodeName) {\r
- var markup = ['<', nodeName, ' filled="f" stroked="f"'],\r
- style = ['position: ', ABSOLUTE, ';'];\r
- \r
- // divs and shapes need size\r
- if (nodeName === 'shape' || nodeName === DIV) {\r
- style.push('left:0;top:0;width:10px;height:10px;');\r
- }\r
- if (docMode8) {\r
- style.push('visibility: ', nodeName === DIV ? HIDDEN : VISIBLE);\r
- }\r
- \r
- markup.push(' style="', style.join(''), '"/>');\r
- \r
- // create element with default attributes and style\r
- if (nodeName) {\r
- markup = nodeName === DIV || nodeName === 'span' || nodeName === 'img' ? \r
- markup.join('')\r
- : renderer.prepVML(markup);\r
- this.element = createElement(markup);\r
- }\r
- \r
- this.renderer = renderer;\r
- },\r
- \r
- /**\r
- * Add the node to the given parent\r
- * @param {Object} parent\r
- */\r
- add: function(parent) {\r
- var wrapper = this,\r
- renderer = wrapper.renderer,\r
- element = wrapper.element,\r
- box = renderer.box,\r
- inverted = parent && parent.inverted,\r
- \r
- // get the parent node\r
- parentNode = parent ? \r
- parent.element || parent : \r
- box;\r
- \r
- \r
- // if the parent group is inverted, apply inversion on all children\r
- if (inverted) { // only on groups\r
- renderer.invertChild(element, parentNode); \r
- }\r
- \r
- // issue #140 workaround - related to #61 and #74\r
- if (docMode8 && parentNode.gVis === HIDDEN) {\r
- css(element, { visibility: HIDDEN });\r
- }\r
- \r
- // append it\r
- parentNode.appendChild(element);\r
- \r
- // align text after adding to be able to read offset\r
- wrapper.added = true;\r
- if (wrapper.alignOnAdd) {\r
- wrapper.updateTransform();\r
- } \r
- \r
- return wrapper;\r
- },\r
- \r
- /**\r
- * Get or set attributes\r
- */\r
- attr: function(hash, val) {\r
- var key, \r
- value, \r
- i, \r
- element = this.element || {},\r
- elemStyle = element.style,\r
- nodeName = element.nodeName,\r
- renderer = this.renderer,\r
- symbolName = this.symbolName,\r
- childNodes,\r
- hasSetSymbolSize,\r
- shadows = this.shadows,\r
- skipAttr,\r
- ret = this;\r
- \r
- // single key-value pair\r
- if (isString(hash) && defined(val)) {\r
- key = hash;\r
- hash = {};\r
- hash[key] = val;\r
- }\r
- \r
- // used as a getter, val is undefined\r
- if (isString(hash)) {\r
- key = hash;\r
- if (key === 'strokeWidth' || key === 'stroke-width') {\r
- ret = this.strokeweight;\r
- } else {\r
- ret = this[key];\r
- }\r
- \r
- // setter\r
- } else { \r
- for (key in hash) {\r
- value = hash[key];\r
- skipAttr = false;\r
- \r
- // prepare paths\r
- // symbols\r
- if (symbolName && /^(x|y|r|start|end|width|height|innerR)/.test(key)) {\r
- // if one of the symbol size affecting parameters are changed,\r
- // check all the others only once for each call to an element's\r
- // .attr() method\r
- if (!hasSetSymbolSize) {\r
- this.symbolAttr(hash); \r
- \r
- hasSetSymbolSize = true;\r
- } \r
- \r
- skipAttr = true;\r
- \r
- } else if (key === 'd') {\r
- value = value || [];\r
- this.d = value.join(' '); // used in getter for animation\r
- \r
- // convert paths \r
- i = value.length;\r
- var convertedPath = [];\r
- while (i--) { \r
- \r
- // Multiply by 10 to allow subpixel precision.\r
- // Substracting half a pixel seems to make the coordinates\r
- // align with SVG, but this hasn't been tested thoroughly\r
- if (isNumber(value[i])) {\r
- convertedPath[i] = mathRound(value[i] * 10) - 5;\r
- }\r
- // close the path\r
- else if (value[i] === 'Z') {\r
- convertedPath[i] = 'x';\r
- } \r
- else {\r
- convertedPath[i] = value[i];\r
- }\r
- \r
- }\r
- value = convertedPath.join(' ') || 'x'; \r
- element.path = value;\r
- \r
- // update shadows\r
- if (shadows) {\r
- i = shadows.length;\r
- while (i--) {\r
- shadows[i].path = value;\r
- }\r
- }\r
- skipAttr = true;\r
- \r
- // directly mapped to css\r
- } else if (key === 'zIndex' || key === 'visibility') {\r
- \r
- // issue 61 workaround\r
- if (docMode8 && key === 'visibility' && nodeName === 'DIV') {\r
- element.gVis = value;\r
- childNodes = element.childNodes;\r
- i = childNodes.length;\r
- while (i--) {\r
- css(childNodes[i], { visibility: value });\r
- }\r
- if (value === VISIBLE) { // issue 74\r
- value = null;\r
- }\r
- }\r
- \r
- if (value) {\r
- elemStyle[key] = value;\r
- }\r
- \r
- \r
- \r
- skipAttr = true;\r
- \r
- // width and height\r
- } else if (/^(width|height)$/.test(key)) {\r
- \r
- \r
- // clipping rectangle special\r
- if (this.updateClipping) {\r
- this[key] = value;\r
- this.updateClipping();\r
- \r
- } else {\r
- // normal\r
- elemStyle[key] = value;\r
- }\r
- \r
- skipAttr = true;\r
- \r
- // x and y \r
- } else if (/^(x|y)$/.test(key)) {\r
-\r
- this[key] = value; // used in getter\r
- \r
- if (element.tagName === 'SPAN') {\r
- this.updateTransform();\r
- \r
- } else {\r
- elemStyle[{ x: 'left', y: 'top' }[key]] = value;\r
- }\r
- \r
- // class name\r
- } else if (key === 'class') {\r
- // IE8 Standards mode has problems retrieving the className\r
- element.className = value;\r
- \r
- // stroke\r
- } else if (key === 'stroke') {\r
- \r
- value = renderer.color(value, element, key); \r
- \r
- key = 'strokecolor';\r
- \r
- // stroke width\r
- } else if (key === 'stroke-width' || key === 'strokeWidth') {\r
- element.stroked = value ? true : false;\r
- key = 'strokeweight';\r
- this[key] = value; // used in getter, issue #113\r
- if (isNumber(value)) {\r
- value += PX;\r
- }\r
- \r
- // dashStyle \r
- } else if (key === 'dashstyle') {\r
- var strokeElem = element.getElementsByTagName('stroke')[0] ||\r
- createElement(renderer.prepVML(['<stroke/>']), null, null, element);\r
- strokeElem[key] = value || 'solid';\r
- this.dashstyle = value; /* because changing stroke-width will change the dash length\r
- and cause an epileptic effect */ \r
- skipAttr = true;\r
- \r
- // fill\r
- } else if (key === 'fill') {\r
- \r
- if (nodeName === 'SPAN') { // text color\r
- elemStyle.color = value;\r
- } else {\r
- element.filled = value !== NONE ? true : false;\r
- \r
- value = renderer.color(value, element, key);\r
- \r
- key = 'fillcolor';\r
- }\r
- \r
- // translation for animation\r
- } else if (key === 'translateX' || key === 'translateY' || key === 'rotation' || key === 'align') {\r
- if (key === 'align') {\r
- key = 'textAlign';\r
- }\r
- this[key] = value;\r
- this.updateTransform();\r
- \r
- skipAttr = true;\r
- }\r
- \r
- // text for rotated and non-rotated elements\r
- else if (key === 'text') {\r
- this.bBox = null;\r
- element.innerHTML = value;\r
- skipAttr = true;\r
- } \r
- \r
- \r
- // let the shadow follow the main element\r
- if (shadows && key === 'visibility') {\r
- i = shadows.length;\r
- while (i--) {\r
- shadows[i].style[key] = value;\r
- }\r
- }\r
- \r
- \r
- \r
- if (!skipAttr) {\r
- if (docMode8) { // IE8 setAttribute bug\r
- element[key] = value;\r
- } else {\r
- attr(element, key, value);\r
- }\r
- }\r
- } \r
- }\r
- return ret;\r
- },\r
- \r
- /**\r
- * Set the element's clipping to a predefined rectangle\r
- * \r
- * @param {String} id The id of the clip rectangle\r
- */\r
- clip: function(clipRect) {\r
- var wrapper = this,\r
- clipMembers = clipRect.members;\r
- \r
- clipMembers.push(wrapper);\r
- wrapper.destroyClip = function() {\r
- erase(clipMembers, wrapper);\r
- };\r
- return wrapper.css(clipRect.getCSS(wrapper.inverted));\r
- },\r
- \r
- /**\r
- * Set styles for the element\r
- * @param {Object} styles\r
- */\r
- css: function(styles) {\r
- var wrapper = this,\r
- element = wrapper.element,\r
- textWidth = styles && element.tagName === 'SPAN' && styles.width;\r
- \r
- /*if (textWidth) {\r
- extend(styles, {\r
- display: 'block',\r
- whiteSpace: 'normal'\r
- }); \r
- }*/\r
- if (textWidth) {\r
- delete styles.width;\r
- wrapper.textWidth = textWidth;\r
- wrapper.updateTransform(); \r
- }\r
- \r
- wrapper.styles = extend(wrapper.styles, styles);\r
- css(wrapper.element, styles);\r
- \r
- return wrapper;\r
- },\r
- \r
- /**\r
- * Extend element.destroy by removing it from the clip members array\r
- */\r
- destroy: function() {\r
- var wrapper = this;\r
- \r
- if (wrapper.destroyClip) {\r
- wrapper.destroyClip();\r
- }\r
- \r
- SVGElement.prototype.destroy.apply(wrapper);\r
- },\r
- \r
- /**\r
- * Remove all child nodes of a group, except the v:group element\r
- */\r
- empty: function() {\r
- var element = this.element,\r
- childNodes = element.childNodes,\r
- i = childNodes.length,\r
- node;\r
- \r
- while (i--) {\r
- node = childNodes[i];\r
- node.parentNode.removeChild(node);\r
- }\r
- },\r
- \r
- /**\r
- * VML override for calculating the bounding box based on offsets\r
- * \r
- * @return {Object} A hash containing values for x, y, width and height\r
- */\r
- \r
- getBBox: function() {\r
- var wrapper = this,\r
- element = wrapper.element,\r
- bBox = wrapper.bBox;\r
- \r
- if (!bBox) {\r
- // faking getBBox in exported SVG in legacy IE\r
- if (element.nodeName === 'text') {\r
- element.style.position = ABSOLUTE;\r
- }\r
- \r
- bBox = wrapper.bBox = {\r
- x: element.offsetLeft,\r
- y: element.offsetTop,\r
- width: element.offsetWidth,\r
- height: element.offsetHeight\r
- };\r
- }\r
- return bBox;\r
- \r
- },\r
- \r
- /**\r
- * Add an event listener. VML override for normalizing event parameters.\r
- * @param {String} eventType\r
- * @param {Function} handler\r
- */\r
- on: function(eventType, handler) {\r
- // simplest possible event model for internal use\r
- this.element['on'+ eventType] = function() {\r
- var evt = win.event;\r
- evt.target = evt.srcElement;\r
- handler(evt);\r
- };\r
- return this;\r
- },\r
- \r
- \r
- /**\r
- * VML override private method to update elements based on internal \r
- * properties based on SVG transform\r
- */\r
- updateTransform: function(hash) { \r
- // aligning non added elements is expensive\r
- if (!this.added) {\r
- this.alignOnAdd = true;\r
- return;\r
- }\r
- \r
- var wrapper = this,\r
- elem = wrapper.element,\r
- translateX = wrapper.translateX || 0,\r
- translateY = wrapper.translateY || 0,\r
- x = wrapper.x || 0,\r
- y = wrapper.y || 0,\r
- align = wrapper.textAlign || 'left',\r
- alignCorrection = { left: 0, center: 0.5, right: 1 }[align],\r
- nonLeft = align && align !== 'left';\r
- \r
- // apply translate\r
- if (translateX || translateY) {\r
- wrapper.css({\r
- marginLeft: translateX,\r
- marginTop: translateY\r
- });\r
- }\r
- \r
- // apply inversion\r
- if (wrapper.inverted) { // wrapper is a group\r
- each(elem.childNodes, function(child) {\r
- wrapper.renderer.invertChild(child, elem);\r
- });\r
- }\r
- \r
- if (elem.tagName === 'SPAN') {\r
- \r
- var width, height,\r
- rotation = wrapper.rotation,\r
- lineHeight,\r
- radians = 0,\r
- costheta = 1,\r
- sintheta = 0,\r
- quad,\r
- textWidth = pInt(wrapper.textWidth),\r
- xCorr = wrapper.xCorr || 0,\r
- yCorr = wrapper.yCorr || 0,\r
- currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth].join(',');\r
- \r
- if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed\r
- \r
- if (defined(rotation)) {\r
- radians = rotation * deg2rad; // deg to rad\r
- costheta = mathCos(radians);\r
- sintheta = mathSin(radians); \r
- \r
- // Adjust for alignment and rotation.\r
- // Test case: http://highcharts.com/tests/?file=text-rotation\r
- css(elem, {\r
- filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, \r
- ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, \r
- ', sizingMethod=\'auto expand\')'].join('') : NONE\r
- });\r
- }\r
- \r
- width = elem.offsetWidth;\r
- height = elem.offsetHeight;\r
- \r
- // update textWidth\r
- if (width > textWidth) {\r
- css(elem, {\r
- width: textWidth +PX,\r
- display: 'block',\r
- whiteSpace: 'normal'\r
- });\r
- width = textWidth;\r
- }\r
- \r
- // correct x and y\r
- lineHeight = mathRound((pInt(elem.style.fontSize) || 12) * 1.2);\r
- xCorr = costheta < 0 && -width;\r
- yCorr = sintheta < 0 && -height;\r
- \r
- // correct for lineHeight and corners spilling out after rotation\r
- quad = costheta * sintheta < 0;\r
- xCorr += sintheta * lineHeight * (quad ? 1 - alignCorrection : alignCorrection);\r
- yCorr -= costheta * lineHeight * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);\r
- \r
- // correct for the length/height of the text\r
- if (nonLeft) {\r
- xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);\r
- if (rotation) {\r
- yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);\r
- }\r
- css(elem, {\r
- textAlign: align\r
- });\r
- }\r
- \r
- // record correction\r
- wrapper.xCorr = xCorr;\r
- wrapper.yCorr = yCorr; \r
- }\r
- \r
- // apply position with correction\r
- css(elem, {\r
- left: x + xCorr,\r
- top: y + yCorr\r
- });\r
- \r
- // record current text transform\r
- wrapper.cTT = currentTextTransform;\r
- }\r
- },\r
- \r
- /**\r
- * Apply a drop shadow by copying elements and giving them different strokes \r
- * @param {Boolean} apply\r
- */\r
- shadow: function(apply, group) {\r
- var shadows = [],\r
- i,\r
- element = this.element,\r
- renderer = this.renderer,\r
- shadow,\r
- elemStyle = element.style,\r
- markup,\r
- path = element.path;\r
- \r
- // some times empty paths are not strings\r
- if (path && typeof path.value !== 'string') {\r
- path = 'x';\r
- }\r
- \r
- if (apply) {\r
- for (i = 1; i <= 3; i++) {\r
- markup = ['<shape isShadow="true" strokeweight="', ( 7 - 2 * i ) ,\r
- '" filled="false" path="', path,\r
- '" coordsize="100,100" style="', element.style.cssText, '" />'];\r
- shadow = createElement(renderer.prepVML(markup),\r
- null, {\r
- left: pInt(elemStyle.left) + 1,\r
- top: pInt(elemStyle.top) + 1\r
- }\r
- );\r
- \r
- // apply the opacity\r
- markup = ['<stroke color="black" opacity="', (0.05 * i), '"/>'];\r
- createElement(renderer.prepVML(markup), null, null, shadow); \r
- \r
- \r
- // insert it\r
- if (group) {\r
- group.element.appendChild(shadow);\r
- } else {\r
- element.parentNode.insertBefore(shadow, element);\r
- }\r
- \r
- // record it\r
- shadows.push(shadow); \r
- \r
- }\r
- \r
- this.shadows = shadows;\r
- }\r
- return this;\r
- \r
- }\r
-});\r
- \r
-/**\r
- * The VML renderer\r
- */\r
-VMLRenderer = function() {\r
- this.init.apply(this, arguments);\r
-};\r
-VMLRenderer.prototype = merge( SVGRenderer.prototype, { // inherit SVGRenderer\r
- \r
- Element: VMLElement,\r
- isIE8: userAgent.indexOf('MSIE 8.0') > -1,\r
- \r
-\r
- /**\r
- * Initialize the VMLRenderer\r
- * @param {Object} container\r
- * @param {Number} width\r
- * @param {Number} height\r
- */\r
- init: function(container, width, height) {\r
- var renderer = this,\r
- boxWrapper;\r
-\r
- renderer.alignedObjects = [];\r
- \r
- boxWrapper = renderer.createElement(DIV);\r
- container.appendChild(boxWrapper.element);\r
- \r
- \r
- // generate the containing box\r
- renderer.box = boxWrapper.element;\r
- renderer.boxWrapper = boxWrapper;\r
- \r
- \r
- renderer.setSize(width, height, false);\r
- \r
- // The only way to make IE6 and IE7 print is to use a global namespace. However,\r
- // with IE8 the only way to make the dynamic shapes visible in screen and print mode\r
- // seems to be to add the xmlns attribute and the behaviour style inline. \r
- if (!doc.namespaces.hcv) { \r
- \r
- doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml');\r
- \r
- // setup default css\r
- doc.createStyleSheet().cssText = \r
- 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke'+\r
- '{ behavior:url(#default#VML); display: inline-block; } ';\r
- \r
- } \r
- },\r
- \r
- /**\r
- * Define a clipping rectangle. In VML it is accomplished by storing the values\r
- * for setting the CSS style to all associated members.\r
- * \r
- * @param {Number} x\r
- * @param {Number} y\r
- * @param {Number} width\r
- * @param {Number} height\r
- */\r
- clipRect: function (x, y, width, height) {\r
- \r
- // create a dummy element\r
- var clipRect = this.createElement();\r
- \r
- // mimic a rectangle with its style object for automatic updating in attr\r
- return extend(clipRect, {\r
- members: [],\r
- left: x,\r
- top: y,\r
- width: width,\r
- height: height,\r
- getCSS: function(inverted) {\r
- var rect = this,//clipRect.element.style,\r
- top = rect.top,\r
- left = rect.left,\r
- right = left + rect.width,\r
- bottom = top + rect.height,\r
- ret = {\r
- clip: 'rect('+ \r
- mathRound(inverted ? left : top) + 'px,'+ \r
- mathRound(inverted ? bottom : right) + 'px,'+ \r
- mathRound(inverted ? right : bottom) + 'px,'+ \r
- mathRound(inverted ? top : left) +'px)'\r
- };\r
- \r
- // issue 74 workaround\r
- if (!inverted && docMode8) {\r
- extend(ret, {\r
- width: right +PX,\r
- height: bottom +PX\r
- });\r
- }\r
- return ret;\r
- },\r
- \r
- // used in attr and animation to update the clipping of all members\r
- updateClipping: function() {\r
- each(clipRect.members, function(member) {\r
- member.css(clipRect.getCSS(member.inverted));\r
- });\r
- }\r
- });\r
- \r
- },\r
- \r
- \r
- /**\r
- * Take a color and return it if it's a string, make it a gradient if it's a\r
- * gradient configuration object, and apply opacity.\r
- * \r
- * @param {Object} color The color or config object\r
- */\r
- color: function(color, elem, prop) {\r
- var colorObject,\r
- regexRgba = /^rgba/,\r
- markup;\r
- \r
- if (color && color.linearGradient) {\r
- \r
- var stopColor, \r
- stopOpacity,\r
- linearGradient = color.linearGradient,\r
- angle,\r
- color1,\r
- opacity1,\r
- color2,\r
- opacity2; \r
- \r
- each(color.stops, function(stop, i) {\r
- if (regexRgba.test(stop[1])) {\r
- colorObject = Color(stop[1]);\r
- stopColor = colorObject.get('rgb');\r
- stopOpacity = colorObject.get('a');\r
- } else {\r
- stopColor = stop[1];\r
- stopOpacity = 1;\r
- }\r
- \r
- if (!i) { // first\r
- color1 = stopColor;\r
- opacity1 = stopOpacity;\r
- } else {\r
- color2 = stopColor;\r
- opacity2 = stopOpacity;\r
- }\r
- });\r
- \r
- \r
- \r
- // calculate the angle based on the linear vector\r
- angle = 90 - math.atan(\r
- (linearGradient[3] - linearGradient[1]) / // y vector\r
- (linearGradient[2] - linearGradient[0]) // x vector\r
- ) * 180 / mathPI;\r
- \r
- // when colors attribute is used, the meanings of opacity and o:opacity2\r
- // are reversed.\r
- markup = ['<', prop, ' colors="0% ', color1, ',100% ', color2, '" angle="', angle,\r
- '" opacity="', opacity2, '" o:opacity2="', opacity1,\r
- '" type="gradient" focus="100%" />'];\r
- createElement(this.prepVML(markup), null, null, elem);\r
- \r
- \r
- \r
- // if the color is an rgba color, split it and add a fill node\r
- // to hold the opacity component\r
- } else if (regexRgba.test(color) && elem.tagName !== 'IMG') {\r
- \r
- colorObject = Color(color);\r
- \r
- markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>'];\r
- createElement(this.prepVML(markup), null, null, elem);\r
- \r
- return colorObject.get('rgb');\r
- \r
- \r
- } else {\r
- return color;\r
- }\r
- \r
- },\r
- \r
- /**\r
- * Take a VML string and prepare it for either IE8 or IE6/IE7. \r
- * @param {Array} markup A string array of the VML markup to prepare\r
- */\r
- prepVML: function(markup) {\r
- var vmlStyle = 'display:inline-block;behavior:url(#default#VML);',\r
- isIE8 = this.isIE8;\r
- \r
- markup = markup.join('');\r
- \r
- if (isIE8) { // add xmlns and style inline\r
- markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />');\r
- if (markup.indexOf('style="') === -1) {\r
- markup = markup.replace('/>', ' style="'+ vmlStyle +'" />');\r
- } else {\r
- markup = markup.replace('style="', 'style="'+ vmlStyle);\r
- }\r
-\r
- } else { // add namespace\r
- markup = markup.replace('<', '<hcv:');\r
- }\r
-\r
- return markup;\r
- },\r
- \r
- /**\r
- * Create rotated and aligned text\r
- * @param {String} str\r
- * @param {Number} x\r
- * @param {Number} y\r
- */\r
- text: function(str, x, y) {\r
- \r
- var defaultChartStyle = defaultOptions.chart.style; \r
- \r
- return this.createElement('span')\r
- .attr({\r
- text: str,\r
- x: mathRound(x),\r
- y: mathRound(y)\r
- })\r
- .css({\r
- whiteSpace: 'nowrap',\r
- fontFamily: defaultChartStyle.fontFamily,\r
- fontSize: defaultChartStyle.fontSize\r
- });\r
- },\r
- \r
- /**\r
- * Create and return a path element\r
- * @param {Array} path\r
- */\r
- path: function (path) {\r
- // create the shape\r
- return this.createElement('shape').attr({\r
- // subpixel precision down to 0.1 (width and height = 10px)\r
- coordsize: '100 100',\r
- d: path\r
- });\r
- },\r
- \r
- /**\r
- * Create and return a circle element. In VML circles are implemented as\r
- * shapes, which is faster than v:oval\r
- * @param {Number} x\r
- * @param {Number} y\r
- * @param {Number} r\r
- */\r
- circle: function(x, y, r) {\r
- return this.symbol('circle').attr({ x: x, y: y, r: r});\r
- },\r
- \r
- /**\r
- * Create a group using an outer div and an inner v:group to allow rotating \r
- * and flipping. A simple v:group would have problems with positioning\r
- * child HTML elements and CSS clip.\r
- * \r
- * @param {String} name The name of the group\r
- */\r
- g: function(name) {\r
- var wrapper,\r
- attribs;\r
- \r
- // set the class name \r
- if (name) {\r
- attribs = { 'className': PREFIX + name, 'class': PREFIX + name };\r
- }\r
- \r
- // the div to hold HTML and clipping \r
- wrapper = this.createElement(DIV).attr(attribs);\r
- \r
- return wrapper;\r
- },\r
- \r
- /**\r
- * VML override to create a regular HTML image\r
- * @param {String} src\r
- * @param {Number} x\r
- * @param {Number} y\r
- * @param {Number} width\r
- * @param {Number} height\r
- */\r
- image: function(src, x, y, width, height) {\r
- var obj = this.createElement('img')\r
- .attr({ src: src });\r
- \r
- if (arguments.length > 1) {\r
- obj.css({\r
- left: x,\r
- top: y,\r
- width: width,\r
- height: height\r
- });\r
- }\r
- return obj;\r
- },\r
- \r
- /**\r
- * VML uses a shape for rect to overcome bugs and rotation problems\r
- */\r
- rect: function(x, y, width, height, r, strokeWidth) {\r
- \r
- if (isObject(x)) {\r
- y = x.y;\r
- width = x.width;\r
- height = x.height;\r
- r = x.r;\r
- strokeWidth = x.strokeWidth;\r
- x = x.x;\r
- }\r
- var wrapper = this.symbol('rect');\r
- wrapper.r = r;\r
- \r
- return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0)));\r
- },\r
- \r
- /**\r
- * In the VML renderer, each child of an inverted div (group) is inverted\r
- * @param {Object} element\r
- * @param {Object} parentNode\r
- */\r
- invertChild: function(element, parentNode) {\r
- var parentStyle = parentNode.style;\r
- \r
- css(element, { \r
- flip: 'x',\r
- left: pInt(parentStyle.width) - 10,\r
- top: pInt(parentStyle.height) - 10,\r
- rotation: -90\r
- });\r
- },\r
- \r
- /**\r
- * Symbol definitions that override the parent SVG renderer's symbols\r
- * \r
- */\r
- symbols: {\r
- // VML specific arc function\r
- arc: function (x, y, radius, options) {\r
- var start = options.start,\r
- end = options.end,\r
- cosStart = mathCos(start),\r
- sinStart = mathSin(start),\r
- cosEnd = mathCos(end),\r
- sinEnd = mathSin(end),\r
- innerRadius = options.innerR,\r
- circleCorrection = 0.07 / radius,\r
- innerCorrection = (innerRadius && 0.1 / innerRadius) || 0;\r
- \r
- if (end - start === 0) { // no angle, don't show it. \r
- return ['x'];\r
- \r
- //} else if (end - start == 2 * mathPI) { // full circle\r
- } else if (2 * mathPI - end + start < circleCorrection) { // full circle\r
- // empirical correction found by trying out the limits for different radii\r
- cosEnd = - circleCorrection;\r
- } else if (end - start < innerCorrection) { // issue #186, another mysterious VML arc problem\r
- cosEnd = mathCos(start + innerCorrection);\r
- }\r
- \r
- return [\r
- 'wa', // clockwise arc to\r
- x - radius, // left\r
- y - radius, // top\r
- x + radius, // right\r
- y + radius, // bottom\r
- x + radius * cosStart, // start x\r
- y + radius * sinStart, // start y\r
- x + radius * cosEnd, // end x\r
- y + radius * sinEnd, // end y\r
- \r
- \r
- 'at', // anti clockwise arc to\r
- x - innerRadius, // left\r
- y - innerRadius, // top\r
- x + innerRadius, // right\r
- y + innerRadius, // bottom\r
- x + innerRadius * cosEnd, // start x\r
- y + innerRadius * sinEnd, // start y\r
- x + innerRadius * cosStart, // end x\r
- y + innerRadius * sinStart, // end y\r
- \r
- 'x', // finish path\r
- 'e' // close\r
- ];\r
- \r
- },\r
- // Add circle symbol path. This performs significantly faster than v:oval.\r
- circle: function (x, y, r) {\r
- return [\r
- 'wa', // clockwisearcto\r
- x - r, // left\r
- y - r, // top\r
- x + r, // right\r
- y + r, // bottom\r
- x + r, // start x\r
- y, // start y\r
- x + r, // end x\r
- y, // end y\r
- //'x', // finish path\r
- 'e' // close\r
- ];\r
- },\r
- /** \r
- * Add rectangle symbol path which eases rotation and omits arcsize problems\r
- * compared to the built-in VML roundrect shape\r
- * \r
- * @param {Number} left Left position\r
- * @param {Number} top Top position\r
- * @param {Number} r Border radius\r
- * @param {Object} options Width and height\r
- */\r
- \r
- rect: function (left, top, r, options) {\r
- if (!defined(options)) {\r
- return [];\r
- }\r
- var width = options.width,\r
- height = options.height,\r
- right = left + width,\r
- bottom = top + height;\r
- \r
- r = mathMin(r, width, height);\r
- \r
- return [\r
- M,\r
- left + r, top,\r
- \r
- L,\r
- right - r, top,\r
- 'wa',\r
- right - 2 * r, top,\r
- right, top + 2 * r,\r
- right - r, top,\r
- right, top + r,\r
- \r
- L,\r
- right, bottom - r,\r
- 'wa',\r
- right - 2 * r, bottom - 2 * r,\r
- right, bottom,\r
- right, bottom - r,\r
- right - r, bottom,\r
- \r
- L,\r
- left + r, bottom,\r
- 'wa',\r
- left, bottom - 2 * r,\r
- left + 2 * r, bottom, \r
- left + r, bottom,\r
- left, bottom - r,\r
- \r
- L,\r
- left, top + r,\r
- 'wa',\r
- left, top,\r
- left + 2 * r, top + 2 * r,\r
- left, top + r,\r
- left + r, top,\r
- \r
- \r
- 'x',\r
- 'e'\r
- ];\r
- \r
- }\r
- }\r
-});\r
-\r
-// general renderer\r
-Renderer = VMLRenderer;\r
-}\r
-/* **************************************************************************** \r
- * * \r
- * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE *\r
- * *\r
- *****************************************************************************/\r
- \r
-\r
-/**\r
- * The chart class\r
- * @param {Object} options\r
- * @param {Function} callback Function to run when the chart has loaded\r
- */\r
-function Chart (options, callback) {\r
- \r
- defaultXAxisOptions = merge(defaultXAxisOptions, defaultOptions.xAxis);\r
- defaultYAxisOptions = merge(defaultYAxisOptions, defaultOptions.yAxis);\r
- defaultOptions.xAxis = defaultOptions.yAxis = null;\r
- \r
- // Handle regular options\r
- options = merge(defaultOptions, options);\r
- \r
- // Define chart variables\r
- var optionsChart = options.chart,\r
- optionsMargin = optionsChart.margin,\r
- margin = isObject(optionsMargin) ?\r
- optionsMargin : \r
- [optionsMargin, optionsMargin, optionsMargin, optionsMargin],\r
- optionsMarginTop = pick(optionsChart.marginTop, margin[0]),\r
- optionsMarginRight = pick(optionsChart.marginRight, margin[1]),\r
- optionsMarginBottom = pick(optionsChart.marginBottom, margin[2]),\r
- optionsMarginLeft = pick(optionsChart.marginLeft, margin[3]),\r
- spacingTop = optionsChart.spacingTop,\r
- spacingRight = optionsChart.spacingRight,\r
- spacingBottom = optionsChart.spacingBottom,\r
- spacingLeft = optionsChart.spacingLeft,\r
- spacingBox, \r
- chartTitleOptions,\r
- chartSubtitleOptions,\r
- plotTop,\r
- marginRight,\r
- marginBottom,\r
- plotLeft,\r
- axisOffset,\r
- renderTo,\r
- renderToClone,\r
- container,\r
- containerId,\r
- containerWidth,\r
- containerHeight,\r
- chartWidth,\r
- chartHeight,\r
- oldChartWidth,\r
- oldChartHeight,\r
- chartBackground,\r
- plotBackground,\r
- plotBGImage,\r
- plotBorder,\r
- chart = this,\r
- chartEvents = optionsChart.events,\r
- runChartClick = chartEvents && !!chartEvents.click,\r
- eventType,\r
- isInsidePlot, // function\r
- tooltip,\r
- mouseIsDown,\r
- loadingDiv,\r
- loadingSpan,\r
- loadingShown,\r
- plotHeight,\r
- plotWidth,\r
- tracker,\r
- trackerGroup,\r
- placeTrackerGroup,\r
- legend,\r
- legendWidth,\r
- legendHeight,\r
- chartPosition,// = getPosition(container),\r
- hasCartesianSeries = optionsChart.showAxes,\r
- isResizing = 0,\r
- axes = [],\r
- maxTicks, // handle the greatest amount of ticks on grouped axes\r
- series = [], \r
- inverted,\r
- renderer,\r
- tooltipTick,\r
- tooltipInterval,\r
- hoverX,\r
- drawChartBox, // function\r
- getMargins, // function\r
- resetMargins, // function\r
- setChartSize, // function\r
- resize,\r
- zoom, // function\r
- zoomOut; // function\r
- \r
-\r
- /**\r
- * Create a new axis object\r
- * @param {Object} chart\r
- * @param {Object} options\r
- */\r
- function Axis (chart, options) {\r
-\r
- // Define variables\r
- var isXAxis = options.isX,\r
- opposite = options.opposite, // needed in setOptions \r
- horiz = inverted ? !isXAxis : isXAxis,\r
- side = horiz ? \r
- (opposite ? 0 /* top */ : 2 /* bottom */) :\r
- (opposite ? 1 /* right*/ : 3 /* left */ ),\r
- stacks = {};\r
- \r
- \r
- options = merge(\r
- isXAxis ? defaultXAxisOptions : defaultYAxisOptions,\r
- [defaultTopAxisOptions, defaultRightAxisOptions, \r
- defaultBottomAxisOptions, defaultLeftAxisOptions][side],\r
- options\r
- );\r
- \r
- var axis = this,\r
- type = options.type,\r
- isDatetimeAxis = type === 'datetime',\r
- isLog = type === 'logarithmic',\r
- offset = options.offset || 0,\r
- xOrY = isXAxis ? 'x' : 'y',\r
- axisLength,\r
- transA, // translation factor\r
- oldTransA, // used for prerendering\r
- transB = horiz ? plotLeft : marginBottom, // translation addend\r
- translate, // fn\r
- getPlotLinePath, // fn\r
- axisGroup,\r
- gridGroup,\r
- axisLine,\r
- dataMin,\r
- dataMax,\r
- associatedSeries,\r
- userMin,\r
- userMax,\r
- max = null,\r
- min = null,\r
- oldMin,\r
- oldMax,\r
- minPadding = options.minPadding,\r
- maxPadding = options.maxPadding,\r
- isLinked = defined(options.linkedTo),\r
- ignoreMinPadding, // can be set to true by a column or bar series\r
- ignoreMaxPadding,\r
- usePercentage,\r
- events = options.events,\r
- eventType,\r
- plotLinesAndBands = [],\r
- tickInterval,\r
- minorTickInterval,\r
- magnitude,\r
- tickPositions, // array containing predefined positions\r
- ticks = {},\r
- minorTicks = {},\r
- alternateBands = {},\r
- tickAmount,\r
- labelOffset,\r
- axisTitleMargin,// = options.title.margin,\r
- dateTimeLabelFormat,\r
- categories = options.categories,\r
- labelFormatter = options.labels.formatter || // can be overwritten by dynamic format\r
- function() {\r
- var value = this.value, \r
- ret;\r
- \r
- if (dateTimeLabelFormat) { // datetime axis\r
- ret = dateFormat(dateTimeLabelFormat, value);\r
- \r
- } else if (tickInterval % 1000000 === 0) { // use M abbreviation\r
- ret = (value / 1000000) +'M';\r
- \r
- } else if (tickInterval % 1000 === 0) { // use k abbreviation\r
- ret = (value / 1000) +'k';\r
- \r
- } else if (!categories && value >= 1000) { // add thousands separators\r
- ret = numberFormat(value, 0);\r
- \r
- } else { // strings (categories) and small numbers\r
- ret = value;\r
- }\r
- return ret;\r
- },\r
- \r
- staggerLines = horiz && options.labels.staggerLines,\r
- reversed = options.reversed,\r
- tickmarkOffset = (categories && options.tickmarkPlacement === 'between') ? 0.5 : 0; \r
-\r
- /**\r
- * The Tick class\r
- */\r
- function Tick(pos, minor) {\r
- var tick = this;\r
- tick.pos = pos;\r
- tick.minor = minor;\r
- tick.isNew = true; \r
- \r
- if (!minor) {\r
- tick.addLabel();\r
- }\r
- }\r
- Tick.prototype = {\r
- /**\r
- * Write the tick label\r
- */\r
- addLabel: function() {\r
- var pos = this.pos,\r
- labelOptions = options.labels,\r
- str,\r
- withLabel = !((pos === min && !pick(options.showFirstLabel, 1)) ||\r
- (pos === max && !pick(options.showLastLabel, 0))),\r
- width = (categories && horiz && categories.length && \r
- !labelOptions.step && !labelOptions.staggerLines &&\r
- !labelOptions.rotation &&\r
- plotWidth / categories.length) ||\r
- (!horiz && plotWidth / 2),\r
- css,\r
- label = this.label;\r
- \r
- \r
- // get the string\r
- str = labelFormatter.call({\r
- isFirst: pos === tickPositions[0],\r
- isLast: pos === tickPositions[tickPositions.length - 1],\r
- dateTimeLabelFormat: dateTimeLabelFormat,\r
- value: (categories && categories[pos] ? categories[pos] : pos)\r
- });\r
- \r
- \r
- // prepare CSS\r
- css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) +PX };\r
- css = extend(css, labelOptions.style);\r
- \r
- // first call\r
- if (label === UNDEFINED) {\r
- this.label = \r
- defined(str) && withLabel && labelOptions.enabled ?\r
- renderer.text(\r
- str,\r
- 0,\r
- 0\r
- )\r
- .attr({\r
- align: labelOptions.align,\r
- rotation: labelOptions.rotation\r
- })\r
- // without position absolute, IE export sometimes is wrong\r
- .css(css)\r
- .add(axisGroup):\r
- null;\r
- \r
- // update\r
- } else if (label) {\r
- label.attr({ text: str })\r
- .css(css);\r
- }\r
- },\r
- /**\r
- * Get the offset height or width of the label\r
- */\r
- getLabelSize: function() {\r
- var label = this.label;\r
- return label ? \r
- ((this.labelBBox = label.getBBox()))[horiz ? 'height' : 'width'] :\r
- 0;\r
- },\r
- /**\r
- * Put everything in place\r
- * \r
- * @param index {Number}\r
- * @param old {Boolean} Use old coordinates to prepare an animation into new position\r
- */\r
- render: function(index, old) {\r
- var tick = this,\r
- major = !tick.minor,\r
- label = tick.label,\r
- pos = tick.pos,\r
- labelOptions = options.labels,\r
- gridLine = tick.gridLine,\r
- gridLineWidth = major ? options.gridLineWidth : options.minorGridLineWidth,\r
- gridLineColor = major ? options.gridLineColor : options.minorGridLineColor,\r
- dashStyle = major ? \r
- options.gridLineDashStyle : \r
- options.minorGridLineDashStyle,\r
- gridLinePath,\r
- mark = tick.mark,\r
- markPath,\r
- tickLength = major ? options.tickLength : options.minorTickLength,\r
- tickWidth = major ? options.tickWidth : (options.minorTickWidth || 0),\r
- tickColor = major ? options.tickColor : options.minorTickColor,\r
- tickPosition = major ? options.tickPosition : options.minorTickPosition,\r
- step = labelOptions.step,\r
- cHeight = (old && oldChartHeight) || chartHeight,\r
- attribs,\r
- x,\r
- y;\r
- \r
- // get x and y position for ticks and labels\r
- x = horiz ? \r
- translate(pos + tickmarkOffset, null, null, old) + transB : \r
- plotLeft + offset + (opposite ? ((old && oldChartWidth) || chartWidth) - marginRight - plotLeft : 0);\r
- \r
- y = horiz ?\r
- cHeight - marginBottom + offset - (opposite ? plotHeight : 0) :\r
- cHeight - translate(pos + tickmarkOffset, null, null, old) - transB;\r
- \r
- // create the grid line\r
- if (gridLineWidth) {\r
- gridLinePath = getPlotLinePath(pos + tickmarkOffset, gridLineWidth, old);\r
- \r
- if (gridLine === UNDEFINED) {\r
- attribs = {\r
- stroke: gridLineColor,\r
- 'stroke-width': gridLineWidth\r
- };\r
- if (dashStyle) {\r
- attribs.dashstyle = dashStyle;\r
- }\r
- tick.gridLine = gridLine =\r
- gridLineWidth ?\r
- renderer.path(gridLinePath)\r
- .attr(attribs).add(gridGroup) :\r
- null;\r
- } \r
- if (gridLine && gridLinePath) {\r
- gridLine.animate({\r
- d: gridLinePath\r
- });\r
- }\r
- }\r
- \r
- // create the tick mark\r
- if (tickWidth) {\r
- \r
- // negate the length\r
- if (tickPosition === 'inside') {\r
- tickLength = -tickLength;\r
- }\r
- if (opposite) {\r
- tickLength = -tickLength;\r
- }\r
- \r
- markPath = renderer.crispLine([\r
- M, \r
- x, \r
- y, \r
- L, \r
- x + (horiz ? 0 : -tickLength), \r
- y + (horiz ? tickLength : 0)\r
- ], tickWidth);\r
- \r
- if (mark) { // updating\r
- mark.animate({\r
- d: markPath\r
- });\r
- } else { // first time\r
- tick.mark = renderer.path(\r
- markPath\r
- ).attr({\r
- stroke: tickColor,\r
- 'stroke-width': tickWidth\r
- }).add(axisGroup);\r
- }\r
- }\r
- \r
- // the label is created on init - now move it into place\r
- if (label && !isNaN(x)) {\r
- x = x + labelOptions.x - (tickmarkOffset && horiz ? \r
- tickmarkOffset * transA * (reversed ? -1 : 1) : 0); \r
- y = y + labelOptions.y - (tickmarkOffset && !horiz ? \r
- tickmarkOffset * transA * (reversed ? 1 : -1) : 0);\r
- \r
- // vertically centered\r
- if (!defined(labelOptions.y)) {\r
- y += pInt(label.styles.lineHeight) * 0.9 - label.getBBox().height / 2;\r
- }\r
- \r
- \r
- // correct for staggered labels\r
- if (staggerLines) {\r
- y += (index / (step || 1) % staggerLines) * 16;\r
- }\r
- // apply step\r
- if (step) {\r
- // show those indices dividable by step \r
- label[index % step ? 'hide' : 'show']();\r
- }\r
- \r
- label[tick.isNew ? 'attr' : 'animate']({\r
- x: x,\r
- y: y\r
- });\r
- }\r
- \r
- tick.isNew = false;\r
- },\r
- /**\r
- * Destructor for the tick prototype\r
- */\r
- destroy: function() {\r
- var tick = this,\r
- n;\r
- for (n in tick) {\r
- if (tick[n] && tick[n].destroy) {\r
- tick[n].destroy();\r
- }\r
- }\r
- }\r
- };\r
- \r
- /**\r
- * The object wrapper for plot lines and plot bands\r
- * @param {Object} options\r
- */\r
- function PlotLineOrBand(options) {\r
- var plotLine = this;\r
- if (options) {\r
- plotLine.options = options;\r
- plotLine.id = options.id;\r
- }\r
- \r
- //plotLine.render()\r
- return plotLine;\r
- }\r
- \r
- PlotLineOrBand.prototype = {\r
-\r
- /**\r
- * Render the plot line or plot band. If it is already existing,\r
- * move it.\r
- */\r
- render: function () {\r
- var plotLine = this,\r
- options = plotLine.options,\r
- optionsLabel = options.label,\r
- label = plotLine.label,\r
- width = options.width,\r
- to = options.to,\r
- toPath, // bands only\r
- from = options.from,\r
- dashStyle = options.dashStyle,\r
- svgElem = plotLine.svgElem,\r
- path = [],\r
- addEvent,\r
- eventType,\r
- xs,\r
- ys,\r
- x,\r
- y,\r
- color = options.color,\r
- zIndex = options.zIndex,\r
- events = options.events,\r
- attribs;\r
- \r
- // plot line\r
- if (width) {\r
- path = getPlotLinePath(options.value, width);\r
- attribs = {\r
- stroke: color,\r
- 'stroke-width': width\r
- };\r
- if (dashStyle) {\r
- attribs.dashstyle = dashStyle;\r
- }\r
- }\r
- \r
- // plot band\r
- else if (defined(from) && defined(to)) {\r
- // keep within plot area\r
- from = mathMax(from, min);\r
- to = mathMin(to, max);\r
- \r
- toPath = getPlotLinePath(to);\r
- path = getPlotLinePath(from);\r
- if (path && toPath) {\r
- path.push(\r
- toPath[4],\r
- toPath[5],\r
- toPath[1],\r
- toPath[2]\r
- );\r
- } else { // outside the axis area\r
- path = null;\r
- }\r
- attribs = {\r
- fill: color\r
- };\r
- } else {\r
- return;\r
- }\r
- // zIndex \r
- if (defined(zIndex)) {\r
- attribs.zIndex = zIndex;\r
- }\r
- \r
- // common for lines and bands\r
- if (svgElem) {\r
- if (path) {\r
- svgElem.animate({\r
- d: path\r
- }, null, svgElem.onGetPath);\r
- } else {\r
- svgElem.hide();\r
- svgElem.onGetPath = function() {\r
- svgElem.show();\r
- };\r
- }\r
- } else if (path && path.length) {\r
- plotLine.svgElem = svgElem = renderer.path(path)\r
- .attr(attribs).add();\r
- \r
- // events\r
- if (events) {\r
- addEvent = function(eventType) {\r
- svgElem.on(eventType, function(e) {\r
- events[eventType].apply(plotLine, [e]);\r
- });\r
- };\r
- for (eventType in events) {\r
- addEvent(eventType);\r
- }\r
- }\r
- }\r
- \r
- // the plot band/line label\r
- if (optionsLabel && defined(optionsLabel.text) && path && path.length && plotWidth > 0 && plotHeight > 0) {\r
- // apply defaults\r
- optionsLabel = merge({\r
- align: horiz && toPath && 'center',\r
- x: horiz ? !toPath && 4 : 10,\r
- verticalAlign : !horiz && toPath && 'middle',\r
- y: horiz ? toPath ? 16 : 10 : toPath ? 6 : -4,\r
- rotation: horiz && !toPath && 90\r
- }, optionsLabel);\r
- \r
- // add the SVG element\r
- if (!label) {\r
- plotLine.label = label = renderer.text(\r
- optionsLabel.text,\r
- 0,\r
- 0\r
- )\r
- .attr({\r
- align: optionsLabel.textAlign || optionsLabel.align,\r
- rotation: optionsLabel.rotation,\r
- zIndex: zIndex\r
- })\r
- .css(optionsLabel.style)\r
- .add();\r
- }\r
- \r
- // get the bounding box and align the label\r
- xs = [path[1], path[4], pick(path[6], path[1])];\r
- ys = [path[2], path[5], pick(path[7], path[2])];\r
- x = mathMin.apply(math, xs);\r
- y = mathMin.apply(math, ys);\r
- \r
- label.align(optionsLabel, false, {\r
- x: x,\r
- y: y,\r
- width: mathMax.apply(math, xs) - x,\r
- height: mathMax.apply(math, ys) - y\r
- });\r
- label.show();\r
- \r
- } else if (label) { // move out of sight\r
- label.hide();\r
- }\r
- \r
- // chainable\r
- return plotLine;\r
- },\r
- \r
- /**\r
- * Remove the plot line or band\r
- */\r
- destroy: function() {\r
- var obj = this,\r
- n;\r
- \r
- for (n in obj) {\r
- if (obj[n] && obj[n].destroy) {\r
- obj[n].destroy(); // destroy SVG wrappers\r
- }\r
- delete obj[n];\r
- }\r
- // remove it from the lookup\r
- erase(plotLinesAndBands, obj);\r
- }\r
- };\r
- \r
- /**\r
- * The class for stack items\r
- */\r
- function StackItem(options, isNegative, x) {\r
- var stackItem = this;\r
- \r
- // Tells if the stack is negative \r
- stackItem.isNegative = isNegative;\r
- \r
- // Save the options to be able to style the label\r
- stackItem.options = options;\r
- \r
- // Save the x value to be able to position the label later\r
- stackItem.x = x;\r
- \r
- // The align options and text align varies on whether the stack is negative and\r
- // if the chart is inverted or not.\r
- // First test the user supplied value, then use the dynamic.\r
- stackItem.alignOptions = {\r
- align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'),\r
- verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),\r
- y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)),\r
- x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0)\r
- };\r
- \r
- stackItem.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center');\r
- }\r
- \r
- StackItem.prototype = {\r
- /**\r
- * Sets the total of this stack. Should be called when a serie is hidden or shown\r
- * since that will affect the total of other stacks.\r
- */\r
- setTotal: function(total) {\r
- this.total = total;\r
- this.cum = total;\r
- },\r
-\r
- /**\r
- * Renders the stack total label and adds it to the stack label group.\r
- */\r
- render: function(group) {\r
- var stackItem = this, // aliased this\r
- str = stackItem.options.formatter.call(stackItem); // format the text in the label\r
-\r
- // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden\r
- if (stackItem.label) {\r
- stackItem.label.attr({text: str, visibility: HIDDEN});\r
- // Create new label\r
- } else {\r
- stackItem.label =\r
- chart.renderer.text(str, 0, 0) // dummy positions, actual position updated with setOffset method in columnseries\r
- .css(stackItem.options.style) // apply style\r
- .attr({align: stackItem.textAlign, // fix the text-anchor\r
- rotation: stackItem.options.rotation, // rotation\r
- visibility: HIDDEN }) // hidden until setOffset is called\r
- .add(group); // add to the labels-group\r
- }\r
- },\r
-\r
- /**\r
- * Sets the offset that the stack has from the x value and repositions the label.\r
- */\r
- setOffset: function(xOffset, xWidth) {\r
- var stackItem = this, // aliased this\r
- neg = stackItem.isNegative, // special treatment is needed for negative stacks\r
- y = axis.translate(stackItem.total), // stack value translated mapped to chart coordinates\r
- yZero = axis.translate(0), // stack origin\r
- h = mathAbs(y - yZero), // stack height\r
- x = chart.xAxis[0].translate(stackItem.x) + xOffset, // stack x position\r
- plotHeight = chart.plotHeight,\r
- stackBox = { // this is the box for the complete stack\r
- x: inverted ? (neg ? y : y - h) : x,\r
- y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y),\r
- width: inverted ? h : xWidth,\r
- height: inverted ? xWidth : h\r
- };\r
- \r
- if (stackItem.label) {\r
- stackItem.label\r
- .align(stackItem.alignOptions, null, stackBox) // align the label to the box\r
- .attr({visibility: VISIBLE}); // set visibility\r
- }\r
- }\r
- };\r
- \r
- /**\r
- * Get the minimum and maximum for the series of each axis \r
- */\r
- function getSeriesExtremes() {\r
- var posStack = [],\r
- negStack = [],\r
- run;\r
- \r
- // reset dataMin and dataMax in case we're redrawing\r
- dataMin = dataMax = null;\r
- \r
- // get an overview of what series are associated with this axis\r
- associatedSeries = [];\r
- \r
- each(series, function(serie) {\r
- run = false;\r
- \r
- \r
- // match this axis against the series' given or implicated axis\r
- each(['xAxis', 'yAxis'], function(strAxis) {\r
- if (\r
- // the series is a cartesian type, and...\r
- serie.isCartesian &&\r
- // we're in the right x or y dimension, and...\r
- ((strAxis === 'xAxis' && isXAxis) || (strAxis === 'yAxis' && !isXAxis)) && (\r
- // the axis number is given in the options and matches this axis index, or\r
- (serie.options[strAxis] === options.index) || \r
- // the axis index is not given\r
- (serie.options[strAxis] === UNDEFINED && options.index === 0)\r
- )\r
- ) {\r
- serie[strAxis] = axis;\r
- associatedSeries.push(serie);\r
- \r
- // the series is visible, run the min/max detection\r
- run = true; \r
- }\r
- });\r
- // ignore hidden series if opted \r
- if (!serie.visible && optionsChart.ignoreHiddenSeries) {\r
- run = false;\r
- } \r
- \r
- if (run) {\r
- \r
- var stacking,\r
- posPointStack,\r
- negPointStack,\r
- stackKey,\r
- negKey;\r
- \r
- if (!isXAxis) {\r
- stacking = serie.options.stacking;\r
- usePercentage = stacking === 'percent';\r
- \r
- // create a stack for this particular series type\r
- if (stacking) {\r
- stackKey = serie.type + pick(serie.options.stack, '');\r
- negKey = '-'+ stackKey;\r
- serie.stackKey = stackKey; // used in translate\r
- \r
- posPointStack = posStack[stackKey] || []; // contains the total values for each x\r
- posStack[stackKey] = posPointStack;\r
- \r
- negPointStack = negStack[negKey] || [];\r
- negStack[negKey] = negPointStack;\r
- }\r
- if (usePercentage) {\r
- dataMin = 0;\r
- dataMax = 99; \r
- }\r
- } \r
- if (serie.isCartesian) { // line, column etc. need axes, pie doesn't\r
- each(serie.data, function(point, i) {\r
- var pointX = point.x,\r
- pointY = point.y,\r
- isNegative = pointY < 0, \r
- pointStack = isNegative ? negPointStack : posPointStack,\r
- key = isNegative ? negKey : stackKey,\r
- totalPos,\r
- pointLow;\r
- \r
- // initial values\r
- if (dataMin === null) {\r
-\r
- // start out with the first point\r
- dataMin = dataMax = point[xOrY]; \r
- }\r
- \r
- // x axis\r
- if (isXAxis) {\r
- if (pointX > dataMax) {\r
- dataMax = pointX;\r
- } else if (pointX < dataMin) {\r
- dataMin = pointX;\r
- }\r
- }\r
- \r
- // y axis\r
- else if (defined(pointY)) {\r
- if (stacking) {\r
- pointStack[pointX] = \r
- defined(pointStack[pointX]) ? \r
- pointStack[pointX] + pointY : pointY;\r
- }\r
- totalPos = pointStack ? pointStack[pointX] : pointY;\r
- pointLow = pick(point.low, totalPos);\r
- if (!usePercentage) {\r
- if (totalPos > dataMax) {\r
- dataMax = totalPos;\r
- } else if (pointLow < dataMin) {\r
- dataMin = pointLow;\r
- }\r
- }\r
- if (stacking) { \r
- // add the series\r
- if (!stacks[key]) {\r
- stacks[key] = {};\r
- }\r
- \r
- // If the StackItem is there, just update the values,\r
- // if not, create one first\r
- if (!stacks[key][pointX]) {\r
- stacks[key][pointX] = new StackItem(options.stackLabels, isNegative, pointX);\r
- }\r
- stacks[key][pointX].setTotal(totalPos);\r
- }\r
- }\r
- });\r
- \r
- \r
- // For column, areas and bars, set the minimum automatically to zero\r
- // and prevent that minPadding is added in setScale\r
- if (/(area|column|bar)/.test(serie.type) && !isXAxis) {\r
- var threshold = 0; // use series.options.threshold?\r
- if (dataMin >= threshold) {\r
- dataMin = threshold;\r
- ignoreMinPadding = true;\r
- } else if (dataMax < threshold) {\r
- dataMax = threshold;\r
- ignoreMaxPadding = true;\r
- }\r
- }\r
- }\r
- }\r
- });\r
- \r
- }\r
- \r
- /**\r
- * Translate from axis value to pixel position on the chart, or back\r
- * \r
- */\r
- translate = function(val, backwards, cvsCoord, old, handleLog) {\r
- var sign = 1,\r
- cvsOffset = 0,\r
- localA = old ? oldTransA : transA,\r
- localMin = old ? oldMin : min,\r
- returnValue;\r
- \r
- if (!localA) {\r
- localA = transA;\r
- }\r
- \r
- if (cvsCoord) {\r
- sign *= -1; // canvas coordinates inverts the value\r
- cvsOffset = axisLength;\r
- }\r
- if (reversed) { // reversed axis\r
- sign *= -1; \r
- cvsOffset -= sign * axisLength;\r
- }\r
- \r
- if (backwards) { // reverse translation\r
- if (reversed) {\r
- val = axisLength - val;\r
- }\r
- returnValue = val / localA + localMin; // from chart pixel to value \r
- if (isLog && handleLog) {\r
- returnValue = lin2log(returnValue);\r
- } \r
- \r
- } else { // normal translation\r
- if (isLog && handleLog) {\r
- val = log2lin(val);\r
- }\r
- returnValue = sign * (val - localMin) * localA + cvsOffset; // from value to chart pixel\r
- }\r
- \r
- return returnValue;\r
- };\r
- \r
- /**\r
- * Create the path for a plot line that goes from the given value on \r
- * this axis, across the plot to the opposite side\r
- * @param {Number} value\r
- * @param {Number} lineWidth Used for calculation crisp line\r
- * @param {Number] old Use old coordinates (for resizing and rescaling)\r
- */\r
- getPlotLinePath = function(value, lineWidth, old) {\r
- var x1, \r
- y1, \r
- x2, \r
- y2,\r
- translatedValue = translate(value, null, null, old),\r
- cHeight = (old && oldChartHeight) || chartHeight,\r
- cWidth = (old && oldChartWidth) || chartWidth,\r
- skip;\r
- \r
- x1 = x2 = mathRound(translatedValue + transB);\r
- y1 = y2 = mathRound(cHeight - translatedValue - transB);\r
- \r
- if (isNaN(translatedValue)) { // no min or max\r
- skip = true;\r
- \r
- } else if (horiz) { \r
- y1 = plotTop;\r
- y2 = cHeight - marginBottom;\r
- if (x1 < plotLeft || x1 > plotLeft + plotWidth) {\r
- skip = true;\r
- }\r
- } else {\r
- x1 = plotLeft;\r
- x2 = cWidth - marginRight;\r
- if (y1 < plotTop || y1 > plotTop + plotHeight) {\r
- skip = true;\r
- }\r
- }\r
- return skip ? \r
- null : \r
- renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 0);\r
- };\r
- \r
- \r
- /**\r
- * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5\r
- * @param {Number} interval\r
- */\r
- function normalizeTickInterval(interval, multiples) {\r
- var normalized, i;\r
- \r
- // round to a tenfold of 1, 2, 2.5 or 5\r
- magnitude = multiples ? 1 : math.pow(10, mathFloor(math.log(interval) / math.LN10));\r
- normalized = interval / magnitude;\r
- \r
- // multiples for a linear scale\r
- if (!multiples) {\r
- multiples = [1, 2, 2.5, 5, 10];\r
- //multiples = [1, 2, 2.5, 4, 5, 7.5, 10];\r
- \r
- // the allowDecimals option\r
- if (options.allowDecimals === false || isLog) {\r
- if (magnitude === 1) {\r
- multiples = [1, 2, 5, 10];\r
- } else if (magnitude <= 0.1) {\r
- multiples = [1 / magnitude];\r
- } \r
- }\r
- }\r
- \r
- // normalize the interval to the nearest multiple\r
- for (i = 0; i < multiples.length; i++) {\r
- interval = multiples[i];\r
- if (normalized <= (multiples[i] + (multiples[i+1] || multiples[i])) / 2) {\r
- break;\r
- }\r
- }\r
- \r
- // multiply back to the correct magnitude\r
- interval *= magnitude;\r
- \r
- return interval;\r
- }\r
- \r
- /**\r
- * Set the tick positions to a time unit that makes sense, for example\r
- * on the first of each month or on every Monday.\r
- */\r
- function setDateTimeTickPositions() {\r
- tickPositions = [];\r
- var i,\r
- useUTC = defaultOptions.global.useUTC,\r
- oneSecond = 1000 / timeFactor,\r
- oneMinute = 60000 / timeFactor,\r
- oneHour = 3600000 / timeFactor,\r
- oneDay = 24 * 3600000 / timeFactor,\r
- oneWeek = 7 * 24 * 3600000 / timeFactor,\r
- oneMonth = 30 * 24 * 3600000 / timeFactor,\r
- oneYear = 31556952000 / timeFactor,\r
- \r
- units = [[\r
- 'second', // unit name\r
- oneSecond, // fixed incremental unit\r
- [1, 2, 5, 10, 15, 30] // allowed multiples\r
- ], [\r
- 'minute', // unit name\r
- oneMinute, // fixed incremental unit\r
- [1, 2, 5, 10, 15, 30] // allowed multiples\r
- ], [\r
- 'hour', // unit name\r
- oneHour, // fixed incremental unit\r
- [1, 2, 3, 4, 6, 8, 12] // allowed multiples\r
- ], [\r
- 'day', // unit name\r
- oneDay, // fixed incremental unit\r
- [1, 2] // allowed multiples\r
- ], [\r
- 'week', // unit name\r
- oneWeek, // fixed incremental unit\r
- [1, 2] // allowed multiples\r
- ], [\r
- 'month',\r
- oneMonth,\r
- [1, 2, 3, 4, 6]\r
- ], [\r
- 'year',\r
- oneYear,\r
- null\r
- ]],\r
- \r
- unit = units[6], // default unit is years\r
- interval = unit[1], \r
- multiples = unit[2];\r
- \r
- // loop through the units to find the one that best fits the tickInterval\r
- for (i = 0; i < units.length; i++) {\r
- unit = units[i];\r
- interval = unit[1];\r
- multiples = unit[2];\r
- \r
- \r
- if (units[i+1]) {\r
- // lessThan is in the middle between the highest multiple and the next unit.\r
- var lessThan = (interval * multiples[multiples.length - 1] + \r
- units[i + 1][1]) / 2;\r
- \r
- // break and keep the current unit\r
- if (tickInterval <= lessThan) {\r
- break;\r
- }\r
- }\r
- }\r
- \r
- // prevent 2.5 years intervals, though 25, 250 etc. are allowed\r
- if (interval === oneYear && tickInterval < 5 * interval) {\r
- multiples = [1, 2, 5];\r
- }\r
- \r
- // get the minimum value by flooring the date\r
- var multitude = normalizeTickInterval(tickInterval / interval, multiples),\r
- minYear, // used in months and years as a basis for Date.UTC()\r
- minDate = new Date(min * timeFactor);\r
- \r
- minDate.setMilliseconds(0);\r
- \r
- if (interval >= oneSecond) { // second\r
- minDate.setSeconds(interval >= oneMinute ? 0 :\r
- multitude * mathFloor(minDate.getSeconds() / multitude));\r
- }\r
- \r
- if (interval >= oneMinute) { // minute\r
- minDate[setMinutes](interval >= oneHour ? 0 :\r
- multitude * mathFloor(minDate[getMinutes]() / multitude));\r
- }\r
- \r
- if (interval >= oneHour) { // hour\r
- minDate[setHours](interval >= oneDay ? 0 :\r
- multitude * mathFloor(minDate[getHours]() / multitude));\r
- }\r
- \r
- if (interval >= oneDay) { // day\r
- minDate[setDate](interval >= oneMonth ? 1 :\r
- multitude * mathFloor(minDate[getDate]() / multitude));\r
- }\r
- \r
- if (interval >= oneMonth) { // month\r
- minDate[setMonth](interval >= oneYear ? 0 :\r
- multitude * mathFloor(minDate[getMonth]() / multitude));\r
- minYear = minDate[getFullYear]();\r
- }\r
- \r
- if (interval >= oneYear) { // year\r
- minYear -= minYear % multitude;\r
- minDate[setFullYear](minYear);\r
- }\r
- \r
- // week is a special case that runs outside the hierarchy\r
- if (interval === oneWeek) {\r
- // get start of current week, independent of multitude\r
- minDate[setDate](minDate[getDate]() - minDate[getDay]() + \r
- options.startOfWeek);\r
- }\r
- \r
- \r
- // get tick positions\r
- i = 1; // prevent crash just in case\r
- minYear = minDate[getFullYear]();\r
- var time = minDate.getTime() / timeFactor,\r
- minMonth = minDate[getMonth](),\r
- minDateDate = minDate[getDate]();\r
- \r
- // iterate and add tick positions at appropriate values\r
- while (time < max && i < plotWidth) {\r
- tickPositions.push(time);\r
- \r
- // if the interval is years, use Date.UTC to increase years\r
- if (interval === oneYear) {\r
- time = makeTime(minYear + i * multitude, 0) / timeFactor;\r
- \r
- // if the interval is months, use Date.UTC to increase months\r
- } else if (interval === oneMonth) {\r
- time = makeTime(minYear, minMonth + i * multitude) / timeFactor;\r
- \r
- // if we're using global time, the interval is not fixed as it jumps\r
- // one hour at the DST crossover\r
- } else if (!useUTC && (interval === oneDay || interval === oneWeek)) {\r
- time = makeTime(minYear, minMonth, minDateDate + \r
- i * multitude * (interval === oneDay ? 1 : 7));\r
- \r
- // else, the interval is fixed and we use simple addition\r
- } else {\r
- time += interval * multitude;\r
- }\r
- \r
- i++;\r
- }\r
- // push the last time\r
- tickPositions.push(time);\r
- \r
- \r
- // dynamic label formatter \r
- dateTimeLabelFormat = options.dateTimeLabelFormats[unit[0]];\r
- }\r
- \r
- /**\r
- * Fix JS round off float errors\r
- * @param {Number} num\r
- */\r
- function correctFloat(num) {\r
- var invMag, ret = num;\r
- magnitude = pick(magnitude, math.pow(10, mathFloor(math.log(tickInterval) / math.LN10)));\r
- \r
- if (magnitude < 1) {\r
- invMag = mathRound(1 / magnitude) * 10;\r
- ret = mathRound(num * invMag) / invMag;\r
- }\r
- return ret;\r
- }\r
- \r
- /**\r
- * Set the tick positions of a linear axis to round values like whole tens or every five.\r
- */\r
- function setLinearTickPositions() {\r
- \r
- var i,\r
- roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval),\r
- roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval);\r
- \r
- tickPositions = [];\r
- \r
- // populate the intermediate values\r
- i = correctFloat(roundedMin);\r
- while (i <= roundedMax) {\r
- tickPositions.push(i);\r
- i = correctFloat(i + tickInterval);\r
- }\r
- \r
- }\r
- \r
- /**\r
- * Set the tick positions to round values and optionally extend the extremes\r
- * to the nearest tick\r
- */\r
- function setTickPositions(secondPass) {\r
- var length,\r
- catPad,\r
- linkedParent,\r
- linkedParentExtremes,\r
- tickIntervalOption = options.tickInterval,\r
- tickPixelIntervalOption = options.tickPixelInterval,\r
- maxZoom = options.maxZoom || (\r
- isXAxis && !defined(options.min) && !defined(options.max) ? \r
- mathMin(chart.smallestInterval * 5, dataMax - dataMin) : \r
- null \r
- ),\r
- zoomOffset;\r
- \r
- \r
- axisLength = horiz ? plotWidth : plotHeight;\r
- \r
- // linked axis gets the extremes from the parent axis\r
- if (isLinked) {\r
- linkedParent = chart[isXAxis ? 'xAxis' : 'yAxis'][options.linkedTo];\r
- linkedParentExtremes = linkedParent.getExtremes();\r
- min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);\r
- max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax);\r
- }\r
- \r
- // initial min and max from the extreme data values\r
- else {\r
- min = pick(userMin, options.min, dataMin);\r
- max = pick(userMax, options.max, dataMax);\r
- }\r
- \r
- if (isLog) {\r
- min = log2lin(min);\r
- max = log2lin(max);\r
- }\r
- \r
- // maxZoom exceeded, just center the selection\r
- if (max - min < maxZoom) { \r
- zoomOffset = (maxZoom - max + min) / 2;\r
- // if min and max options have been set, don't go beyond it\r
- min = mathMax(min - zoomOffset, pick(options.min, min - zoomOffset), dataMin);\r
- max = mathMin(min + maxZoom, pick(options.max, min + maxZoom), dataMax);\r
- }\r
- \r
- // pad the values to get clear of the chart's edges\r
- if (!categories && !usePercentage && !isLinked && defined(min) && defined(max)) {\r
- length = (max - min) || 1;\r
- if (!defined(options.min) && !defined(userMin) && minPadding && (dataMin < 0 || !ignoreMinPadding)) { \r
- min -= length * minPadding; \r
- }\r
- if (!defined(options.max) && !defined(userMax) && maxPadding && (dataMax > 0 || !ignoreMaxPadding)) { \r
- max += length * maxPadding;\r
- }\r
- }\r
-\r
- // get tickInterval\r
- if (min === max) {\r
- tickInterval = 1;\r
- } else if (isLinked && !tickIntervalOption &&\r
- tickPixelIntervalOption === linkedParent.options.tickPixelInterval) {\r
- tickInterval = linkedParent.tickInterval;\r
- } else {\r
- tickInterval = pick(\r
- tickIntervalOption,\r
- categories ? // for categoried axis, 1 is default, for linear axis use tickPix \r
- 1 : \r
- (max - min) * tickPixelIntervalOption / axisLength\r
- );\r
- }\r
- \r
- if (!isDatetimeAxis && !defined(options.tickInterval)) { // linear\r
- tickInterval = normalizeTickInterval(tickInterval);\r
- }\r
- axis.tickInterval = tickInterval; // record for linked axis\r
- \r
- // get minorTickInterval\r
- minorTickInterval = options.minorTickInterval === 'auto' && tickInterval ?\r
- tickInterval / 5 : options.minorTickInterval;\r
- \r
- // find the tick positions\r
- if (isDatetimeAxis) {\r
- setDateTimeTickPositions();\r
- } else {\r
- setLinearTickPositions();\r
- }\r
- \r
- if (!isLinked) {\r
- // pad categorised axis to nearest half unit\r
- if (categories || (isXAxis && chart.hasColumn)) {\r
- catPad = (categories ? 1 : tickInterval) * 0.5;\r
- if (categories || !defined(pick(options.min, userMin))) {\r
- min -= catPad;\r
- }\r
- if (categories || !defined(pick(options.max, userMax))) {\r
- max += catPad;\r
- }\r
- }\r
- \r
- // reset min/max or remove extremes based on start/end on tick\r
- var roundedMin = tickPositions[0],\r
- roundedMax = tickPositions[tickPositions.length - 1];\r
- \r
- if (options.startOnTick) {\r
- min = roundedMin;\r
- } else if (min > roundedMin) {\r
- tickPositions.shift();\r
- }\r
- \r
- if (options.endOnTick) {\r
- max = roundedMax;\r
- } else if (max < roundedMax) {\r
- tickPositions.pop();\r
- }\r
- \r
- // record the greatest number of ticks for multi axis\r
- if (!maxTicks) { // first call, or maxTicks have been reset after a zoom operation\r
- maxTicks = {\r
- x: 0,\r
- y: 0\r
- };\r
- }\r
- \r
- if (!isDatetimeAxis && tickPositions.length > maxTicks[xOrY]) {\r
- maxTicks[xOrY] = tickPositions.length;\r
- }\r
- }\r
- \r
- \r
- }\r
- \r
- /**\r
- * When using multiple axes, adjust the number of ticks to match the highest\r
- * number of ticks in that group\r
- */ \r
- function adjustTickAmount() {\r
- \r
- if (maxTicks && !isDatetimeAxis && !categories && !isLinked) { // only apply to linear scale\r
- var oldTickAmount = tickAmount,\r
- calculatedTickAmount = tickPositions.length;\r
- \r
- // set the axis-level tickAmount to use below\r
- tickAmount = maxTicks[xOrY];\r
- \r
- if (calculatedTickAmount < tickAmount) {\r
- while (tickPositions.length < tickAmount) {\r
- tickPositions.push( correctFloat(\r
- tickPositions[tickPositions.length - 1] + tickInterval\r
- ));\r
- }\r
- transA *= (calculatedTickAmount - 1) / (tickAmount - 1);\r
- max = tickPositions[tickPositions.length - 1];\r
- \r
- }\r
- if (defined(oldTickAmount) && tickAmount !== oldTickAmount) {\r
- axis.isDirty = true; \r
- }\r
- }\r
- \r
- }\r
- \r
- /**\r
- * Set the scale based on data min and max, user set min and max or options\r
- * \r
- */\r
- function setScale() {\r
- var type, \r
- i;\r
- \r
- oldMin = min;\r
- oldMax = max;\r
- \r
- // get data extremes if needed\r
- getSeriesExtremes();\r
- \r
- // get fixed positions based on tickInterval\r
- setTickPositions();\r
- \r
- // the translation factor used in translate function\r
- oldTransA = transA;\r
- transA = axisLength / ((max - min) || 1);\r
- \r
- // reset stacks\r
- if (!isXAxis) {\r
- for (type in stacks) {\r
- for (i in stacks[type]) {\r
- stacks[type][i].cum = stacks[type][i].total;\r
- }\r
- }\r
- }\r
-\r
- // mark as dirty if it is not already set to dirty and extremes have changed\r
- if (!axis.isDirty) {\r
- axis.isDirty = (min !== oldMin || max !== oldMax);\r
- }\r
- \r
- }\r
- \r
- /**\r
- * Set the extremes and optionally redraw\r
- * @param {Number} newMin\r
- * @param {Number} newMax\r
- * @param {Boolean} redraw\r
- * @param {Boolean|Object} animation Whether to apply animation, and optionally animation\r
- * configuration\r
- * \r
- */\r
- function setExtremes(newMin, newMax, redraw, animation) {\r
- \r
- redraw = pick(redraw, true); // defaults to true\r
- \r
- fireEvent(axis, 'setExtremes', { // fire an event to enable syncing of multiple charts\r
- min: newMin,\r
- max: newMax\r
- }, function() { // the default event handler\r
- \r
- userMin = newMin;\r
- userMax = newMax;\r
- \r
- \r
- // redraw\r
- if (redraw) {\r
- chart.redraw(animation);\r
- }\r
- });\r
- \r
- }\r
- \r
- /**\r
- * Get the actual axis extremes\r
- */\r
- function getExtremes() {\r
- return {\r
- min: min,\r
- max: max,\r
- dataMin: dataMin,\r
- dataMax: dataMax,\r
- userMin: userMin,\r
- userMax: userMax\r
- };\r
- }\r
- \r
- /**\r
- * Get the zero plane either based on zero or on the min or max value.\r
- * Used in bar and area plots\r
- */\r
- function getThreshold(threshold) {\r
- if (min > threshold) {\r
- threshold = min;\r
- } else if (max < threshold) {\r
- threshold = max;\r
- }\r
- \r
- return translate(threshold, 0, 1);\r
- }\r
- \r
- /**\r
- * Add a plot band or plot line after render time\r
- * \r
- * @param options {Object} The plotBand or plotLine configuration object\r
- */\r
- function addPlotBandOrLine(options) {\r
- var obj = new PlotLineOrBand(options).render();\r
- plotLinesAndBands.push(obj);\r
- return obj;\r
- }\r
- \r
- /**\r
- * Render the tick labels to a preliminary position to get their sizes\r
- */\r
- function getOffset() {\r
- \r
- var hasData = associatedSeries.length && defined(min) && defined(max),\r
- titleOffset = 0,\r
- titleMargin = 0,\r
- axisTitleOptions = options.title,\r
- labelOptions = options.labels,\r
- directionFactor = [-1, 1, 1, -1][side],\r
- n;\r
- \r
- if (!axisGroup) {\r
- axisGroup = renderer.g('axis')\r
- .attr({ zIndex: 7 })\r
- .add();\r
- gridGroup = renderer.g('grid')\r
- .attr({ zIndex: 1 })\r
- .add();\r
- }\r
- \r
- labelOffset = 0; // reset\r
- \r
- if (hasData || isLinked) {\r
- each(tickPositions, function(pos) {\r
- if (!ticks[pos]) {\r
- ticks[pos] = new Tick(pos);\r
- } else {\r
- ticks[pos].addLabel(); // update labels depending on tick interval\r
- }\r
- \r
- // left side must be align: right and right side must have align: left for labels\r
- if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === labelOptions.align) {\r
- \r
- // get the highest offset\r
- labelOffset = mathMax(\r
- ticks[pos].getLabelSize(),\r
- labelOffset\r
- );\r
- }\r
- \r
- });\r
- \r
- if (staggerLines) {\r
- labelOffset += (staggerLines - 1) * 16;\r
- }\r
- \r
- } else { // doesn't have data\r
- for (n in ticks) {\r
- ticks[n].destroy();\r
- delete ticks[n];\r
- }\r
- }\r
- \r
- if (axisTitleOptions && axisTitleOptions.text) {\r
- if (!axis.axisTitle) {\r
- axis.axisTitle = renderer.text(\r
- axisTitleOptions.text,\r
- 0,\r
- 0\r
- )\r
- .attr({ \r
- zIndex: 7,\r
- rotation: axisTitleOptions.rotation || 0,\r
- align: \r
- axisTitleOptions.textAlign || \r
- { low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align]\r
- })\r
- .css(axisTitleOptions.style)\r
- .add();\r
- }\r
- \r
- titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width'];\r
- titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10);\r
- \r
- }\r
- \r
- // handle automatic or user set offset\r
- offset = directionFactor * (options.offset || axisOffset[side]);\r
- \r
- axisTitleMargin = \r
- labelOffset +\r
- (side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x']) + \r
- titleMargin;\r
- \r
- axisOffset[side] = mathMax(\r
- axisOffset[side], \r
- axisTitleMargin + titleOffset + directionFactor * offset\r
- );\r
- \r
- }\r
- \r
- /**\r
- * Render the axis\r
- */\r
- function render() {\r
- var axisTitleOptions = options.title,\r
- stackLabelOptions = options.stackLabels,\r
- alternateGridColor = options.alternateGridColor,\r
- lineWidth = options.lineWidth,\r
- lineLeft,\r
- lineTop,\r
- linePath,\r
- hasRendered = chart.hasRendered,\r
- slideInTicks = hasRendered && defined(oldMin) && !isNaN(oldMin),\r
- hasData = associatedSeries.length && defined(min) && defined(max);\r
- \r
- // update metrics\r
- axisLength = horiz ? plotWidth : plotHeight;\r
- transA = axisLength / ((max - min) || 1);\r
- transB = horiz ? plotLeft : marginBottom; // translation addend\r
- \r
- // If the series has data draw the ticks. Else only the line and title\r
- if (hasData || isLinked) {\r
- \r
- // minor ticks\r
- if (minorTickInterval && !categories) {\r
- var pos = min + (tickPositions[0] - min) % minorTickInterval;\r
- for (pos; pos <= max; pos += minorTickInterval) {\r
- if (!minorTicks[pos]) {\r
- minorTicks[pos] = new Tick(pos, true);\r
- }\r
- \r
- // render new ticks in old position\r
- if (slideInTicks && minorTicks[pos].isNew) {\r
- minorTicks[pos].render(null, true);\r
- }\r
- \r
- \r
- minorTicks[pos].isActive = true;\r
- minorTicks[pos].render();\r
- }\r
- }\r
- \r
- // major ticks\r
- each(tickPositions, function(pos, i) {\r
- // linked axes need an extra check to find out if \r
- if (!isLinked || (pos >= min && pos <= max)) {\r
- \r
- // render new ticks in old position\r
- if (slideInTicks && ticks[pos].isNew) {\r
- ticks[pos].render(i, true);\r
- }\r
- \r
- ticks[pos].isActive = true;\r
- ticks[pos].render(i);\r
- }\r
- });\r
- \r
- // alternate grid color\r
- if (alternateGridColor) {\r
- each(tickPositions, function(pos, i) {\r
- if (i % 2 === 0 && pos < max) {\r
- /*plotLinesAndBands.push(new PlotLineOrBand({\r
- from: pos,\r
- to: tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : max,\r
- color: alternateGridColor \r
- }));*/\r
- \r
- if (!alternateBands[pos]) {\r
- alternateBands[pos] = new PlotLineOrBand();\r
- }\r
- alternateBands[pos].options = {\r
- from: pos,\r
- to: tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : max,\r
- color: alternateGridColor \r
- };\r
- alternateBands[pos].render();\r
- alternateBands[pos].isActive = true;\r
- }\r
- });\r
- }\r
- \r
- // custom plot bands (behind grid lines)\r
- /*if (!hasRendered) { // only first time\r
- each(options.plotBands || [], function(plotBandOptions) {\r
- plotLinesAndBands.push(new PlotLineOrBand(\r
- extend({ zIndex: 1 }, plotBandOptions)\r
- ).render());\r
- });\r
- }*/\r
- \r
- \r
- \r
- \r
- // custom plot lines and bands\r
- if (!hasRendered) { // only first time\r
- each((options.plotLines || []).concat(options.plotBands || []), function(plotLineOptions) {\r
- plotLinesAndBands.push(new PlotLineOrBand(plotLineOptions).render());\r
- });\r
- }\r
- \r
- \r
- \r
- } // end if hasData\r
- \r
- // remove inactive ticks\r
- each([ticks, minorTicks, alternateBands], function(coll) {\r
- var pos;\r
- for (pos in coll) {\r
- if (!coll[pos].isActive) {\r
- coll[pos].destroy();\r
- delete coll[pos];\r
- } else {\r
- coll[pos].isActive = false; // reset\r
- }\r
- }\r
- });\r
- \r
- \r
- \r
- \r
- // Static items. As the axis group is cleared on subsequent calls\r
- // to render, these items are added outside the group. \r
- // axis line\r
- if (lineWidth) {\r
- lineLeft = plotLeft + (opposite ? plotWidth : 0) + offset;\r
- lineTop = chartHeight - marginBottom - (opposite ? plotHeight : 0) + offset;\r
- \r
- linePath = renderer.crispLine([\r
- M,\r
- horiz ? \r
- plotLeft: \r
- lineLeft,\r
- horiz ? \r
- lineTop: \r
- plotTop,\r
- L, \r
- horiz ? \r
- chartWidth - marginRight : \r
- lineLeft,\r
- horiz ? \r
- lineTop:\r
- chartHeight - marginBottom\r
- ], lineWidth);\r
- if (!axisLine) {\r
- axisLine = renderer.path(linePath)\r
- .attr({ \r
- stroke: options.lineColor, \r
- 'stroke-width': lineWidth,\r
- zIndex: 7\r
- })\r
- .add();\r
- } else {\r
- axisLine.animate({ d: linePath });\r
- }\r
- \r
- }\r
- \r
- if (axis.axisTitle) {\r
- // compute anchor points for each of the title align options\r
- var margin = horiz ? plotLeft : plotTop,\r
- fontSize = pInt(axisTitleOptions.style.fontSize || 12),\r
- // the position in the length direction of the axis\r
- alongAxis = { \r
- low: margin + (horiz ? 0 : axisLength), \r
- middle: margin + axisLength / 2, \r
- high: margin + (horiz ? axisLength : 0)\r
- }[axisTitleOptions.align],\r
- \r
- // the position in the perpendicular direction of the axis\r
- offAxis = (horiz ? plotTop + plotHeight : plotLeft) +\r
- (horiz ? 1 : -1) * // horizontal axis reverses the margin\r
- (opposite ? -1 : 1) * // so does opposite axes\r
- axisTitleMargin +\r
- //(isIE ? fontSize / 3 : 0)+ // preliminary fix for vml's centerline\r
- (side === 2 ? fontSize : 0);\r
- \r
- axis.axisTitle[hasRendered ? 'animate' : 'attr']({\r
- x: horiz ? \r
- alongAxis: \r
- offAxis + (opposite ? plotWidth : 0) + offset +\r
- (axisTitleOptions.x || 0), // x\r
- y: horiz ? \r
- offAxis - (opposite ? plotHeight : 0) + offset: \r
- alongAxis + (axisTitleOptions.y || 0) // y\r
- });\r
- \r
- }\r
- \r
- // Stacked totals:\r
- if (stackLabelOptions && stackLabelOptions.enabled) {\r
- var stackKey, oneStack, stackCategory,\r
- stackTotalGroup = axis.stackTotalGroup;\r
-\r
- // Create a separate group for the stack total labels\r
- if (!stackTotalGroup) {\r
- axis.stackTotalGroup = stackTotalGroup =\r
- renderer.g('stack-labels')\r
- .attr({ \r
- visibility: VISIBLE,\r
- zIndex: 6\r
- })\r
- .translate(plotLeft, plotTop)\r
- .add();\r
- }\r
-\r
- // Render each stack total\r
- for (stackKey in stacks) {\r
- oneStack = stacks[stackKey];\r
- for (stackCategory in oneStack) {\r
- oneStack[stackCategory].render(stackTotalGroup);\r
- }\r
- }\r
- }\r
- // End stacked totals\r
- \r
- axis.isDirty = false;\r
- }\r
- \r
- /**\r
- * Remove a plot band or plot line from the chart by id\r
- * @param {Object} id\r
- */\r
- function removePlotBandOrLine(id) {\r
- var i = plotLinesAndBands.length;\r
- while (i--) {\r
- if (plotLinesAndBands[i].id === id) {\r
- plotLinesAndBands[i].destroy();\r
- }\r
- }\r
- }\r
- \r
- /**\r
- * Redraw the axis to reflect changes in the data or axis extremes\r
- */\r
- function redraw() {\r
- \r
- // hide tooltip and hover states\r
- if (tracker.resetTracker) {\r
- tracker.resetTracker();\r
- }\r
- \r
- // render the axis\r
- render(); \r
- \r
- // move plot lines and bands\r
- each(plotLinesAndBands, function(plotLine) {\r
- plotLine.render();\r
- });\r
- \r
- // mark associated series as dirty and ready for redraw\r
- each(associatedSeries, function(series) {\r
- series.isDirty = true;\r
- });\r
- \r
- }\r
- \r
- /**\r
- * Set new axis categories and optionally redraw\r
- * @param {Array} newCategories\r
- * @param {Boolean} doRedraw\r
- */\r
- function setCategories(newCategories, doRedraw) {\r
- // set the categories\r
- axis.categories = categories = newCategories;\r
- \r
- // force reindexing tooltips\r
- each(associatedSeries, function(series) {\r
- series.translate();\r
- series.setTooltipPoints(true);\r
- });\r
- \r
- \r
- // optionally redraw\r
- axis.isDirty = true;\r
- \r
- if (pick(doRedraw, true)) {\r
- chart.redraw();\r
- }\r
- }\r
- \r
- \r
- \r
- // Run Axis\r
- \r
- // inverted charts have reversed xAxes as default\r
- if (inverted && isXAxis && reversed === UNDEFINED) {\r
- reversed = true;\r
- }\r
- \r
- \r
- // expose some variables\r
- extend(axis, {\r
- addPlotBand: addPlotBandOrLine,\r
- addPlotLine: addPlotBandOrLine,\r
- adjustTickAmount: adjustTickAmount,\r
- categories: categories,\r
- getExtremes: getExtremes,\r
- getPlotLinePath: getPlotLinePath,\r
- getThreshold: getThreshold,\r
- isXAxis: isXAxis,\r
- options: options,\r
- plotLinesAndBands: plotLinesAndBands,\r
- getOffset: getOffset,\r
- render: render,\r
- setCategories: setCategories,\r
- setExtremes: setExtremes,\r
- setScale: setScale,\r
- setTickPositions: setTickPositions,\r
- translate: translate,\r
- redraw: redraw,\r
- removePlotBand: removePlotBandOrLine,\r
- removePlotLine: removePlotBandOrLine,\r
- reversed: reversed,\r
- stacks: stacks\r
- });\r
- \r
- // register event listeners\r
- for (eventType in events) {\r
- addEvent(axis, eventType, events[eventType]);\r
- }\r
- \r
- // set min and max\r
- setScale();\r
- \r
- } // end Axis\r
- \r
- \r
- /**\r
- * The toolbar object\r
- * \r
- * @param {Object} chart \r
- */\r
- function Toolbar(chart) {\r
- var buttons = {};\r
- \r
- function add(id, text, title, fn) {\r
- if (!buttons[id]) {\r
- var button = renderer.text(\r
- text,\r
- 0,\r
- 0\r
- )\r
- .css(options.toolbar.itemStyle)\r
- .align({\r
- align: 'right',\r
- x: - marginRight - 20,\r
- y: plotTop + 30\r
- })\r
- .on('click', fn)\r
- /*.on('touchstart', function(e) {\r
- e.stopPropagation(); // don't fire the container event\r
- fn();\r
- })*/\r
- .attr({\r
- align: 'right', \r
- zIndex: 20\r
- })\r
- .add();\r
- buttons[id] = button;\r
- }\r
- }\r
- function remove(id) {\r
- discardElement(buttons[id].element);\r
- buttons[id] = null;\r
- }\r
- \r
- // public\r
- return {\r
- add: add,\r
- remove: remove\r
- };\r
- }\r
- \r
- /**\r
- * The tooltip object\r
- * @param {Object} options Tooltip options\r
- */\r
- function Tooltip (options) {\r
- var currentSeries,\r
- borderWidth = options.borderWidth,\r
- crosshairsOptions = options.crosshairs,\r
- crosshairs = [],\r
- style = options.style,\r
- shared = options.shared,\r
- padding = pInt(style.padding),\r
- boxOffLeft = borderWidth + padding, // off left/top position as IE can't \r
- //properly handle negative positioned shapes\r
- tooltipIsHidden = true,\r
- boxWidth,\r
- boxHeight,\r
- currentX = 0, \r
- currentY = 0;\r
- \r
- // remove padding CSS and apply padding on box instead\r
- style.padding = 0;\r
- \r
- // create the elements\r
- var group = renderer.g('tooltip')\r
- .attr({ zIndex: 8 })\r
- .add(),\r
- \r
- box = renderer.rect(boxOffLeft, boxOffLeft, 0, 0, options.borderRadius, borderWidth)\r
- .attr({\r
- fill: options.backgroundColor,\r
- 'stroke-width': borderWidth\r
- })\r
- .add(group)\r
- .shadow(options.shadow),\r
- label = renderer.text('', padding + boxOffLeft, pInt(style.fontSize) + padding + boxOffLeft)\r
- .attr({ zIndex: 1 })\r
- .css(style)\r
- .add(group);\r
- \r
- group.hide();\r
- \r
- /**\r
- * In case no user defined formatter is given, this will be used\r
- */\r
- function defaultFormatter() {\r
- var pThis = this,\r
- items = pThis.points || splat(pThis),\r
- xAxis = items[0].series.xAxis, \r
- x = pThis.x,\r
- isDateTime = xAxis && xAxis.options.type === 'datetime',\r
- useHeader = isString(x) || isDateTime,\r
- series,\r
- s;\r
- \r
- // build the header \r
- s = useHeader ? \r
- ['<span style="font-size: 10px">' +\r
- (isDateTime ? dateFormat('%A, %b %e, %Y', x) : x) +\r
- '</span>'] : [];\r
- \r
- // build the values\r
- each(items, function(item) {\r
- s.push(item.point.tooltipFormatter(useHeader));\r
- });\r
- return s.join('<br/>');\r
- }\r
- \r
- /**\r
- * Provide a soft movement for the tooltip\r
- * \r
- * @param {Number} finalX\r
- * @param {Number} finalY \r
- */\r
- function move(finalX, finalY) {\r
-\r
- currentX = tooltipIsHidden ? finalX : (2 * currentX + finalX) / 3;\r
- currentY = tooltipIsHidden ? finalY : (currentY + finalY) / 2;\r
- \r
- group.translate(currentX, currentY);\r
- \r
- \r
- // run on next tick of the mouse tracker\r
- if (mathAbs(finalX - currentX) > 1 || mathAbs(finalY - currentY) > 1) {\r
- tooltipTick = function() {\r
- move(finalX, finalY);\r
- };\r
- } else {\r
- tooltipTick = null;\r
- }\r
- }\r
- \r
- /**\r
- * Hide the tooltip\r
- */\r
- function hide() {\r
- if (!tooltipIsHidden) {\r
- var hoverPoints = chart.hoverPoints;\r
- \r
- group.hide();\r
- \r
- each(crosshairs, function(crosshair) {\r
- if (crosshair) {\r
- crosshair.hide();\r
- }\r
- });\r
- \r
- // hide previous hoverPoints and set new\r
- if (hoverPoints) {\r
- each(hoverPoints, function(point) {\r
- point.setState();\r
- });\r
- }\r
- chart.hoverPoints = null; \r
- \r
- \r
- tooltipIsHidden = true;\r
- }\r
- \r
- }\r
- \r
- /**\r
- * Refresh the tooltip's text and position. \r
- * @param {Object} point\r
- * \r
- */\r
- function refresh(point) {\r
- var x,\r
- y,\r
- boxX,\r
- boxY,\r
- show,\r
- bBox,\r
- plotX,\r
- plotY = 0,\r
- textConfig = {},\r
- text,\r
- pointConfig = [],\r
- tooltipPos = point.tooltipPos,\r
- formatter = options.formatter || defaultFormatter,\r
- hoverPoints = chart.hoverPoints;\r
- \r
- // shared tooltip, array is sent over\r
- if (shared) {\r
- \r
- // hide previous hoverPoints and set new\r
- if (hoverPoints) {\r
- each(hoverPoints, function(point) {\r
- point.setState();\r
- });\r
- }\r
- chart.hoverPoints = point;\r
- \r
- each(point, function(item, i) {\r
- /*var series = item.series,\r
- hoverPoint = series.hoverPoint;\r
- if (hoverPoint) {\r
- hoverPoint.setState();\r
- }\r
- series.hoverPoint = item;*/\r
- item.setState(HOVER_STATE);\r
- plotY += item.plotY; // for average\r
- \r
- pointConfig.push(item.getLabelConfig());\r
- });\r
- \r
- plotX = point[0].plotX;\r
- plotY = mathRound(plotY) / point.length; // mathRound because Opera 10 has problems here\r
- \r
- textConfig = {\r
- x: point[0].category\r
- };\r
- textConfig.points = pointConfig;\r
- point = point[0];\r
- \r
- // single point tooltip\r
- } else {\r
- textConfig = point.getLabelConfig();\r
- }\r
- text = formatter.call(textConfig);\r
- \r
- // register the current series\r
- currentSeries = point.series;\r
- \r
- // get the reference point coordinates (pie charts use tooltipPos)\r
- plotX = shared ? plotX : point.plotX;\r
- plotY = shared ? plotY : point.plotY;\r
- x = mathRound(tooltipPos ? tooltipPos[0] : (inverted ? plotWidth - plotY : plotX));\r
- y = mathRound(tooltipPos ? tooltipPos[1] : (inverted ? plotHeight - plotX : plotY));\r
- \r
- \r
- // hide tooltip if the point falls outside the plot\r
- show = shared || !point.series.isCartesian || isInsidePlot(x, y);\r
- \r
- // update the inner HTML\r
- if (text === false || !show) { \r
- hide();\r
- } else {\r
- \r
- // show it\r
- if (tooltipIsHidden) {\r
- group.show();\r
- tooltipIsHidden = false;\r
- }\r
- \r
- // update text\r
- label.attr({\r
- text: text\r
- });\r
- \r
- // get the bounding box\r
- bBox = label.getBBox();\r
- boxWidth = bBox.width + 2 * padding;\r
- boxHeight = bBox.height + 2 * padding;\r
-\r
- // set the size of the box\r
- box.attr({\r
- width: boxWidth,\r
- height: boxHeight,\r
- stroke: options.borderColor || point.color || currentSeries.color || '#606060'\r
- });\r
- \r
- // keep the box within the chart area\r
- boxX = x - boxWidth + plotLeft - 25;\r
- boxY = y - boxHeight + plotTop + 10;\r
- \r
- // it is too far to the left, adjust it\r
- if (boxX < 7) {\r
- boxX = plotLeft + x + 15;\r
- }\r
- \r
- \r
- if (boxY < 5) {\r
- boxY = 5; // above\r
- } else if (boxY + boxHeight > chartHeight) { \r
- boxY = chartHeight - boxHeight - 5; // below\r
- }\r
- \r
- // do the move\r
- move(mathRound(boxX - boxOffLeft), mathRound(boxY - boxOffLeft));\r
- \r
- \r
- }\r
- \r
- \r
- // crosshairs\r
- if (crosshairsOptions) {\r
- crosshairsOptions = splat(crosshairsOptions); // [x, y]\r
- \r
- var path, \r
- i = crosshairsOptions.length,\r
- attribs,\r
- axis;\r
- \r
- while (i--) {\r
- axis = point.series[i ? 'yAxis' : 'xAxis'];\r
- if (crosshairsOptions[i] && axis) {\r
- path = axis\r
- .getPlotLinePath(point[i ? 'y' : 'x'], 1);\r
- if (crosshairs[i]) {\r
- crosshairs[i].attr({ d: path, visibility: VISIBLE });\r
- \r
- } else {\r
- attribs = {\r
- 'stroke-width': crosshairsOptions[i].width || 1,\r
- stroke: crosshairsOptions[i].color || '#C0C0C0',\r
- zIndex: 2\r
- };\r
- if (crosshairsOptions[i].dashStyle) {\r
- attribs.dashstyle = crosshairsOptions[i].dashStyle;\r
- }\r
- crosshairs[i] = renderer.path(path)\r
- .attr(attribs)\r
- .add();\r
- }\r
- }\r
- } \r
- } \r
- }\r
- \r
-\r
- \r
- // public members\r
- return {\r
- shared: shared,\r
- refresh: refresh,\r
- hide: hide\r
- }; \r
- }\r
- \r
- /**\r
- * The mouse tracker object\r
- * @param {Object} chart\r
- * @param {Object} options\r
- */\r
- function MouseTracker (chart, options) {\r
-\r
- \r
- var mouseDownX, \r
- mouseDownY,\r
- hasDragged,\r
- selectionMarker,\r
- zoomType = optionsChart.zoomType,\r
- zoomX = /x/.test(zoomType),\r
- zoomY = /y/.test(zoomType),\r
- zoomHor = (zoomX && !inverted) || (zoomY && inverted),\r
- zoomVert = (zoomY && !inverted) || (zoomX && inverted);\r
- \r
- /**\r
- * Add crossbrowser support for chartX and chartY\r
- * @param {Object} e The event object in standard browsers\r
- */\r
- function normalizeMouseEvent(e) {\r
- var ePos,\r
- pageZoomFix = isWebKit && doc.width / doc.documentElement.clientWidth - 1,\r
- chartPosLeft,\r
- chartPosTop,\r
- chartX,\r
- chartY;\r
- \r
- // common IE normalizing\r
- e = e || win.event;\r
- if (!e.target) {\r
- e.target = e.srcElement;\r
- }\r
- \r
- // iOS\r
- ePos = e.touches ? e.touches.item(0) : e;\r
- \r
- // in certain cases, get mouse position\r
- if (e.type !== 'mousemove' || win.opera || pageZoomFix) { // only Opera needs position on mouse move, see below\r
- chartPosition = getPosition(container);\r
- chartPosLeft = chartPosition.left;\r
- chartPosTop = chartPosition.top;\r
- }\r
- \r
- // chartX and chartY\r
- if (isIE) { // IE including IE9 that has chartX but in a different meaning\r
- chartX = e.x;\r
- chartY = e.y;\r
- } else {\r
- if (ePos.layerX === UNDEFINED) { // Opera and iOS\r
- chartX = ePos.pageX - chartPosLeft;\r
- chartY = ePos.pageY - chartPosTop;\r
- } else {\r
- chartX = e.layerX;\r
- chartY = e.layerY;\r
- }\r
- }\r
- \r
- // correct for page zoom bug in WebKit\r
- if (pageZoomFix) {\r
- chartX += mathRound((pageZoomFix + 1) * chartPosLeft - chartPosLeft);\r
- chartY += mathRound((pageZoomFix + 1) * chartPosTop - chartPosTop);\r
- }\r
- \r
- return extend(e, {\r
- chartX: chartX,\r
- chartY: chartY\r
- });\r
- }\r
- \r
- /**\r
- * Get the click position in terms of axis values.\r
- * \r
- * @param {Object} e A mouse event\r
- */\r
- function getMouseCoordinates(e) {\r
- var coordinates = {\r
- xAxis: [],\r
- yAxis: []\r
- }; \r
- each(axes, function(axis, i) {\r
- var translate = axis.translate,\r
- isXAxis = axis.isXAxis,\r
- isHorizontal = inverted ? !isXAxis : isXAxis;\r
- \r
- coordinates[isXAxis ? 'xAxis' : 'yAxis'].push({\r
- axis: axis,\r
- value: translate(\r
- isHorizontal ? \r
- e.chartX - plotLeft : \r
- plotHeight - e.chartY + plotTop,\r
- true\r
- ) \r
- });\r
- });\r
- return coordinates;\r
- }\r
- \r
- /**\r
- * With line type charts with a single tracker, get the point closest to the mouse\r
- */\r
- function onmousemove (e) {\r
- var point,\r
- points,\r
- hoverPoint = chart.hoverPoint,\r
- hoverSeries = chart.hoverSeries,\r
- i,\r
- j,\r
- distance = chartWidth,\r
- index = inverted ? e.chartY : e.chartX - plotLeft; // wtf?\r
- \r
- // shared tooltip\r
- if (tooltip && options.shared) {\r
- points = [];\r
- \r
- // loop over all series and find the ones with points closest to the mouse\r
- i = series.length;\r
- for (j = 0; j < i; j++) {\r
- if (series[j].visible && series[j].tooltipPoints.length) {\r
- point = series[j].tooltipPoints[index];\r
- point._dist = mathAbs(index - point.plotX);\r
- distance = mathMin(distance, point._dist);\r
- points.push(point);\r
- }\r
- }\r
- // remove furthest points\r
- i = points.length;\r
- while (i--) {\r
- if (points[i]._dist > distance) {\r
- points.splice(i, 1);\r
- }\r
- }\r
- // refresh the tooltip if necessary\r
- if (points.length && (points[0].plotX !== hoverX)) {\r
- tooltip.refresh(points);\r
- hoverX = points[0].plotX;\r
- }\r
- }\r
- \r
- // separate tooltip and general mouse events\r
- if (hoverSeries && hoverSeries.tracker) { // only use for line-type series with common tracker\r
- \r
- // get the point\r
- point = hoverSeries.tooltipPoints[index];\r
- \r
- // a new point is hovered, refresh the tooltip\r
- if (point && point !== hoverPoint) {\r
- \r
- // trigger the events\r
- point.onMouseOver();\r
- \r
- } \r
- }\r
- }\r
- \r
- \r
- \r
- /**\r
- * Reset the tracking by hiding the tooltip, the hover series state and the hover point\r
- */\r
- function resetTracker() {\r
- var hoverSeries = chart.hoverSeries,\r
- hoverPoint = chart.hoverPoint; \r
-\r
- if (hoverPoint) {\r
- hoverPoint.onMouseOut();\r
- }\r
- \r
- if (hoverSeries) {\r
- hoverSeries.onMouseOut();\r
- }\r
- \r
- if (tooltip) {\r
- tooltip.hide();\r
- }\r
- \r
- hoverX = null;\r
- }\r
- \r
- /**\r
- * Mouse up or outside the plot area\r
- */\r
- function drop() {\r
- if (selectionMarker) {\r
- var selectionData = {\r
- xAxis: [],\r
- yAxis: []\r
- },\r
- selectionBox = selectionMarker.getBBox(),\r
- selectionLeft = selectionBox.x - plotLeft,\r
- selectionTop = selectionBox.y - plotTop;\r
- \r
- \r
- // a selection has been made\r
- if (hasDragged) {\r
- \r
- // record each axis' min and max\r
- each(axes, function(axis, i) {\r
- var translate = axis.translate,\r
- isXAxis = axis.isXAxis,\r
- isHorizontal = inverted ? !isXAxis : isXAxis,\r
- selectionMin = translate(\r
- isHorizontal ? \r
- selectionLeft : \r
- plotHeight - selectionTop - selectionBox.height, \r
- true,\r
- 0,\r
- 0,\r
- 1\r
- ),\r
- selectionMax = translate(\r
- isHorizontal ? \r
- selectionLeft + selectionBox.width : \r
- plotHeight - selectionTop, \r
- true,\r
- 0,\r
- 0,\r
- 1\r
- );\r
- \r
- selectionData[isXAxis ? 'xAxis' : 'yAxis'].push({\r
- axis: axis,\r
- min: mathMin(selectionMin, selectionMax), // for reversed axes,\r
- max: mathMax(selectionMin, selectionMax)\r
- });\r
- \r
- });\r
- fireEvent(chart, 'selection', selectionData, zoom);\r
-\r
- }\r
- selectionMarker = selectionMarker.destroy();\r
- }\r
- \r
- chart.mouseIsDown = mouseIsDown = hasDragged = false;\r
- removeEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop);\r
-\r
- }\r
- \r
- /**\r
- * Set the JS events on the container element\r
- */\r
- function setDOMEvents () {\r
- var lastWasOutsidePlot = true;\r
- \r
- /*\r
- * Record the starting position of a dragoperation\r
- */\r
- container.onmousedown = function(e) {\r
- e = normalizeMouseEvent(e);\r
- \r
- // issue #295, dragging not always working in Firefox\r
- if (!hasTouch && e.preventDefault) {\r
- e.preventDefault();\r
- }\r
- \r
- // record the start position\r
- chart.mouseIsDown = mouseIsDown = true;\r
- mouseDownX = e.chartX;\r
- mouseDownY = e.chartY;\r
- \r
- addEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop);\r
- };\r
- \r
- // The mousemove, touchmove and touchstart event handler\r
- var mouseMove = function(e) {\r
-\r
- // let the system handle multitouch operations like two finger scroll\r
- // and pinching\r
- if (e && e.touches && e.touches.length > 1) {\r
- return;\r
- }\r
- \r
- // normalize\r
- e = normalizeMouseEvent(e);\r
- if (!hasTouch) { // not for touch devices\r
- e.returnValue = false;\r
- }\r
- \r
- var chartX = e.chartX,\r
- chartY = e.chartY,\r
- isOutsidePlot = !isInsidePlot(chartX - plotLeft, chartY - plotTop);\r
- \r
- // on touch devices, only trigger click if a handler is defined\r
- if (hasTouch && e.type === 'touchstart') {\r
- if (attr(e.target, 'isTracker')) {\r
- if (!chart.runTrackerClick) {\r
- e.preventDefault();\r
- } \r
- } else if (!runChartClick && !isOutsidePlot) {\r
- e.preventDefault();\r
- }\r
- }\r
- \r
- // cancel on mouse outside\r
- if (isOutsidePlot) {\r
- \r
- if (!lastWasOutsidePlot) {\r
- // reset the tracker \r
- resetTracker(); \r
- }\r
- \r
- // drop the selection if any and reset mouseIsDown and hasDragged\r
- //drop();\r
- if (chartX < plotLeft) {\r
- chartX = plotLeft;\r
- } else if (chartX > plotLeft + plotWidth) {\r
- chartX = plotLeft + plotWidth;\r
- }\r
- \r
- if (chartY < plotTop) {\r
- chartY = plotTop;\r
- } else if (chartY > plotTop + plotHeight) {\r
- chartY = plotTop + plotHeight;\r
- } \r
- \r
- } \r
- \r
- if (mouseIsDown && e.type !== 'touchstart') { // make selection\r
- \r
- // determine if the mouse has moved more than 10px\r
- hasDragged = Math.sqrt(\r
- Math.pow(mouseDownX - chartX, 2) + \r
- Math.pow(mouseDownY - chartY, 2));\r
- if (hasDragged > 10) {\r
- \r
- // make a selection\r
- if (hasCartesianSeries && (zoomX || zoomY) && \r
- isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop)) {\r
- if (!selectionMarker) {\r
- selectionMarker = renderer.rect(\r
- plotLeft,\r
- plotTop,\r
- zoomHor ? 1 : plotWidth,\r
- zoomVert ? 1 : plotHeight,\r
- 0\r
- )\r
- .attr({\r
- fill: 'rgba(69,114,167,0.25)',\r
- zIndex: 7\r
- })\r
- .add();\r
- }\r
- }\r
- \r
- // adjust the width of the selection marker\r
- if (selectionMarker && zoomHor) {\r
- var xSize = chartX - mouseDownX;\r
- selectionMarker.attr({\r
- width: mathAbs(xSize),\r
- x: (xSize > 0 ? 0 : xSize) + mouseDownX\r
- });\r
- }\r
- // adjust the height of the selection marker\r
- if (selectionMarker && zoomVert) {\r
- var ySize = chartY - mouseDownY;\r
- selectionMarker.attr({\r
- height: mathAbs(ySize),\r
- y: (ySize > 0 ? 0 : ySize) + mouseDownY\r
- });\r
- }\r
- }\r
- \r
- } else if (!isOutsidePlot) {\r
- // show the tooltip\r
- onmousemove(e);\r
- }\r
- \r
- lastWasOutsidePlot = isOutsidePlot;\r
- \r
- // when outside plot, allow touch-drag by returning true\r
- return isOutsidePlot || !hasCartesianSeries;\r
- };\r
- \r
- /*\r
- * When the mouse enters the container, run mouseMove\r
- */\r
- container.onmousemove = mouseMove;\r
- \r
- /*\r
- * When the mouse leaves the container, hide the tracking (tooltip).\r
- */\r
- addEvent(container, 'mouseleave', resetTracker);\r
- \r
- \r
- container.ontouchstart = function(e) {\r
- // For touch devices, use touchmove to zoom\r
- if (zoomX || zoomY) {\r
- container.onmousedown(e);\r
- }\r
- // Show tooltip and prevent the lower mouse pseudo event\r
- mouseMove(e);\r
- };\r
- \r
- /*\r
- * Allow dragging the finger over the chart to read the values on touch \r
- * devices\r
- */\r
- container.ontouchmove = mouseMove;\r
- \r
- /*\r
- * Allow dragging the finger over the chart to read the values on touch \r
- * devices\r
- */\r
- container.ontouchend = function() {\r
- if (hasDragged) {\r
- resetTracker();\r
- }\r
- }; \r
- \r
- \r
- // MooTools 1.2.3 doesn't fire this in IE when using addEvent\r
- container.onclick = function(e) {\r
- var hoverPoint = chart.hoverPoint;\r
- e = normalizeMouseEvent(e);\r
- \r
- e.cancelBubble = true; // IE specific\r
- \r
- \r
- if (!hasDragged) {\r
- if (hoverPoint && attr(e.target, 'isTracker')) {\r
- var plotX = hoverPoint.plotX,\r
- plotY = hoverPoint.plotY;\r
- \r
- // add page position info\r
- extend(hoverPoint, {\r
- pageX: chartPosition.left + plotLeft + \r
- (inverted ? plotWidth - plotY : plotX),\r
- pageY: chartPosition.top + plotTop + \r
- (inverted ? plotHeight - plotX : plotY)\r
- });\r
- \r
- // the series click event\r
- fireEvent(hoverPoint.series, 'click', extend(e, {\r
- point: hoverPoint\r
- }));\r
- \r
- // the point click event\r
- hoverPoint.firePointEvent('click', e);\r
- \r
- } else { \r
- extend(e, getMouseCoordinates(e));\r
- \r
- // fire a click event in the chart\r
- if (isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {\r
- fireEvent(chart, 'click', e);\r
- }\r
- }\r
- \r
- \r
- }\r
- // reset mouseIsDown and hasDragged\r
- hasDragged = false;\r
- };\r
- \r
- }\r
- \r
- /**\r
- * Create the image map that listens for mouseovers\r
- */\r
- placeTrackerGroup = function() {\r
- \r
- // first create - plot positions is not final at this stage\r
- if (!trackerGroup) {\r
- chart.trackerGroup = trackerGroup = renderer.g('tracker')\r
- .attr({ zIndex: 9 })\r
- .add();\r
- \r
- // then position - this happens on load and after resizing and changing\r
- // axis or box positions\r
- } else { \r
- trackerGroup.translate(plotLeft, plotTop);\r
- if (inverted) {\r
- trackerGroup.attr({\r
- width: chart.plotWidth,\r
- height: chart.plotHeight\r
- }).invert();\r
- }\r
- } \r
- };\r
- \r
- \r
- // Run MouseTracker\r
- placeTrackerGroup();\r
- if (options.enabled) {\r
- chart.tooltip = tooltip = Tooltip(options);\r
- }\r
- \r
- setDOMEvents();\r
- \r
- // set the fixed interval ticking for the smooth tooltip\r
- tooltipInterval = setInterval(function() {\r
- if (tooltipTick) {\r
- tooltipTick();\r
- }\r
- }, 32);\r
- \r
- // expose properties\r
- extend(this, {\r
- zoomX: zoomX,\r
- zoomY: zoomY,\r
- resetTracker: resetTracker\r
- });\r
- }\r
- \r
- \r
- \r
- /**\r
- * The overview of the chart's series\r
- * @param {Object} chart\r
- */\r
- var Legend = function(chart) {\r
-\r
- var options = chart.options.legend;\r
- \r
- if (!options.enabled) {\r
- return;\r
- }\r
- \r
- var horizontal = options.layout === 'horizontal',\r
- symbolWidth = options.symbolWidth,\r
- symbolPadding = options.symbolPadding,\r
- allItems,\r
- style = options.style,\r
- itemStyle = options.itemStyle,\r
- itemHoverStyle = options.itemHoverStyle,\r
- itemHiddenStyle = options.itemHiddenStyle,\r
- padding = pInt(style.padding),\r
- rightPadding = 20,\r
- //lineHeight = options.lineHeight || 16,\r
- y = 18,\r
- initialItemX = 4 + padding + symbolWidth + symbolPadding,\r
- itemX,\r
- itemY,\r
- lastItemY,\r
- itemHeight = 0,\r
- box,\r
- legendBorderWidth = options.borderWidth,\r
- legendBackgroundColor = options.backgroundColor,\r
- legendGroup,\r
- offsetWidth,\r
- widthOption = options.width,\r
- series = chart.series,\r
- reversedLegend = options.reversed;\r
- \r
- \r
- \r
- /**\r
- * Set the colors for the legend item\r
- * @param {Object} item A Series or Point instance\r
- * @param {Object} visible Dimmed or colored\r
- */\r
- function colorizeItem(item, visible) {\r
- var legendItem = item.legendItem,\r
- legendLine = item.legendLine,\r
- legendSymbol = item.legendSymbol,\r
- hiddenColor = itemHiddenStyle.color,\r
- textColor = visible ? options.itemStyle.color : hiddenColor,\r
- lineColor = visible ? item.color : hiddenColor,\r
- symbolAttr = visible ? item.pointAttr[NORMAL_STATE] : {\r
- stroke: hiddenColor,\r
- fill: hiddenColor\r
- };\r
- \r
- if (legendItem) {\r
- legendItem.css({ fill: textColor });\r
- }\r
- if (legendLine) {\r
- legendLine.attr({ stroke: lineColor });\r
- }\r
- if (legendSymbol) {\r
- legendSymbol.attr(symbolAttr);\r
- }\r
- \r
- }\r
- \r
- /**\r
- * Position the legend item\r
- * @param {Object} item A Series or Point instance\r
- * @param {Object} visible Dimmed or colored\r
- */\r
- function positionItem(item, itemX, itemY) {\r
- var legendItem = item.legendItem,\r
- legendLine = item.legendLine,\r
- legendSymbol = item.legendSymbol,\r
- checkbox = item.checkbox;\r
- if (legendItem) {\r
- legendItem.attr({ \r
- x: itemX,\r
- y: itemY\r
- });\r
- }\r
- if (legendLine) {\r
- legendLine.translate(itemX, itemY - 4);\r
- }\r
- if (legendSymbol) {\r
- legendSymbol.attr({\r
- x: itemX + legendSymbol.xOff, \r
- y: itemY + legendSymbol.yOff\r
- });\r
- }\r
- if (checkbox) {\r
- checkbox.x = itemX;\r
- checkbox.y = itemY;\r
- }\r
- }\r
- \r
- /**\r
- * Destroy a single legend item\r
- * @param {Object} item The series or point\r
- */\r
- function destroyItem(item) {\r
- var checkbox = item.checkbox;\r
- \r
- // pull out from the array\r
- //erase(allItems, item);\r
- \r
- // destroy SVG elements\r
- each(['legendItem', 'legendLine', 'legendSymbol'], function(key) {\r
- if (item[key]) {\r
- item[key].destroy();\r
- }\r
- });\r
- \r
- if (checkbox) {\r
- discardElement(item.checkbox);\r
- }\r
- \r
- \r
- }\r
- \r
- \r
- /**\r
- * Position the checkboxes after the width is determined\r
- */ \r
- function positionCheckboxes() {\r
- each(allItems, function(item) {\r
- var checkbox = item.checkbox,\r
- alignAttr = legendGroup.alignAttr;\r
- if (checkbox) {\r
- css(checkbox, {\r
- left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 40) +PX,\r
- top: (alignAttr.translateY + checkbox.y - 11) + PX \r
- });\r
- }\r
- });\r
- }\r
- \r
- /**\r
- * Render a single specific legend item\r
- * @param {Object} item A series or point\r
- */\r
- function renderItem(item) {\r
- var bBox,\r
- itemWidth,\r
- legendSymbol,\r
- symbolX,\r
- symbolY,\r
- attribs,\r
- simpleSymbol,\r
- li = item.legendItem,\r
- series = item.series || item,\r
- i = allItems.length,\r
- itemOptions = series.options,\r
- strokeWidth = (itemOptions && itemOptions.borderWidth) || 0; \r
- \r
- if (!li) { // generate it once, later move it\r
- \r
- // let these series types use a simple symbol\r
- simpleSymbol = /^(bar|pie|area|column)$/.test(series.type);\r
- \r
- // generate the list item text\r
- item.legendItem = li = renderer.text(\r
- options.labelFormatter.call(item),\r
- 0, \r
- 0\r
- )\r
- .css(item.visible ? itemStyle : itemHiddenStyle)\r
- .on('mouseover', function() {\r
- item.setState(HOVER_STATE);\r
- li.css(itemHoverStyle);\r
- })\r
- .on('mouseout', function() {\r
- li.css(item.visible ? itemStyle : itemHiddenStyle);\r
- item.setState();\r
- })\r
- .on('click', function(event) {\r
- var strLegendItemClick = 'legendItemClick',\r
- fnLegendItemClick = function() {\r
- item.setVisible();\r
- };\r
- \r
- // click the name or symbol\r
- if (item.firePointEvent) { // point\r
- item.firePointEvent(strLegendItemClick, null, fnLegendItemClick);\r
- } else {\r
- fireEvent(item, strLegendItemClick, null, fnLegendItemClick);\r
- }\r
- })\r
- .attr({ zIndex: 2 })\r
- .add(legendGroup);\r
- \r
- // draw the line\r
- if (!simpleSymbol && itemOptions && itemOptions.lineWidth) {\r
- var attrs = {\r
- 'stroke-width': itemOptions.lineWidth,\r
- zIndex: 2\r
- };\r
- if (itemOptions.dashStyle) {\r
- attrs.dashstyle = itemOptions.dashStyle;\r
- }\r
- item.legendLine = renderer.path([\r
- M,\r
- -symbolWidth - symbolPadding, \r
- 0,\r
- L, \r
- -symbolPadding, \r
- 0\r
- ])\r
- .attr(attrs)\r
- .add(legendGroup);\r
- }\r
- \r
- // draw a simple symbol\r
- if (simpleSymbol) { // bar|pie|area|column\r
- \r
- legendSymbol = renderer.rect(\r
- (symbolX = -symbolWidth - symbolPadding),\r
- (symbolY = -11),\r
- symbolWidth,\r
- 12,\r
- 2\r
- ).attr({\r
- //'stroke-width': 0,\r
- zIndex: 3\r
- }).add(legendGroup);\r
- }\r
- \r
- // draw the marker\r
- else if (itemOptions && itemOptions.marker && itemOptions.marker.enabled) {\r
- legendSymbol = renderer.symbol(\r
- item.symbol,\r
- (symbolX = -symbolWidth / 2 - symbolPadding), \r
- (symbolY = -4),\r
- itemOptions.marker.radius\r
- )\r
- //.attr(item.pointAttr[NORMAL_STATE])\r
- .attr({ zIndex: 3 })\r
- .add(legendGroup);\r
- \r
- }\r
- if (legendSymbol) {\r
- legendSymbol.xOff = symbolX + (strokeWidth % 2 / 2);\r
- legendSymbol.yOff = symbolY + (strokeWidth % 2 / 2);\r
- }\r
- \r
- item.legendSymbol = legendSymbol;\r
- \r
- // colorize the items\r
- colorizeItem(item, item.visible);\r
- \r
- \r
- // add the HTML checkbox on top\r
- if (itemOptions && itemOptions.showCheckbox) {\r
- item.checkbox = createElement('input', {\r
- type: 'checkbox',\r
- checked: item.selected,\r
- defaultChecked: item.selected // required by IE7 \r
- }, options.itemCheckboxStyle, container);\r
- \r
- addEvent(item.checkbox, 'click', function(event) {\r
- var target = event.target;\r
- fireEvent(item, 'checkboxClick', { \r
- checked: target.checked \r
- }, \r
- function() {\r
- item.select();\r
- }\r
- );\r
- });\r
- }\r
- }\r
- \r
- \r
- // calculate the positions for the next line\r
- bBox = li.getBBox();\r
- \r
- itemWidth = item.legendItemWidth = \r
- options.itemWidth || symbolWidth + symbolPadding + bBox.width + rightPadding;\r
- itemHeight = bBox.height;\r
- \r
- // if the item exceeds the width, start a new line\r
- if (horizontal && itemX - initialItemX + itemWidth > \r
- (widthOption || (chartWidth - 2 * padding - initialItemX))) {\r
- itemX = initialItemX;\r
- itemY += itemHeight;\r
- } \r
- lastItemY = itemY;\r
- \r
- // position the newly generated or reordered items\r
- positionItem(item, itemX, itemY);\r
- \r
- // advance\r
- if (horizontal) {\r
- itemX += itemWidth;\r
- } else {\r
- itemY += itemHeight;\r
- }\r
- \r
- // the width of the widest item\r
- offsetWidth = widthOption || mathMax(\r
- horizontal ? itemX - initialItemX : itemWidth, \r
- offsetWidth\r
- );\r
- \r
- \r
- \r
- // add it all to an array to use below\r
- //allItems.push(item);\r
- }\r
-\r
- /**\r
- * Render the legend. This method can be called both before and after\r
- * chart.render. If called after, it will only rearrange items instead\r
- * of creating new ones.\r
- */\r
- function renderLegend() {\r
- itemX = initialItemX;\r
- itemY = y;\r
- offsetWidth = 0;\r
- lastItemY = 0;\r
- \r
- if (!legendGroup) {\r
- legendGroup = renderer.g('legend')\r
- .attr({ zIndex: 7 })\r
- .add();\r
- }\r
- \r
- \r
- // add each series or point\r
- allItems = [];\r
- each(series, function(serie) {\r
- var seriesOptions = serie.options;\r
- \r
- if (!seriesOptions.showInLegend) {\r
- return;\r
- }\r
- \r
- // use points or series for the legend item depending on legendType\r
- allItems = allItems.concat(seriesOptions.legendType === 'point' ?\r
- serie.data : \r
- serie\r
- );\r
- \r
- });\r
- \r
- // sort by legendIndex\r
- allItems.sort(function(a, b) {\r
- return (a.options.legendIndex || 0) - (b.options.legendIndex || 0);\r
- });\r
- \r
- // reversed legend\r
- if (reversedLegend) {\r
- allItems.reverse();\r
- }\r
- \r
- // render the items\r
- each(allItems, renderItem);\r
- \r
- \r
- \r
- // Draw the border\r
- legendWidth = widthOption || offsetWidth;\r
- legendHeight = lastItemY - y + itemHeight;\r
- \r
- if (legendBorderWidth || legendBackgroundColor) {\r
- legendWidth += 2 * padding;\r
- legendHeight += 2 * padding;\r
- \r
- if (!box) {\r
- box = renderer.rect(\r
- 0, \r
- 0,\r
- legendWidth,\r
- legendHeight,\r
- options.borderRadius,\r
- legendBorderWidth || 0\r
- ).attr({\r
- stroke: options.borderColor,\r
- 'stroke-width': legendBorderWidth || 0,\r
- fill: legendBackgroundColor || NONE\r
- })\r
- .add(legendGroup)\r
- .shadow(options.shadow);\r
- \r
- } else if (legendWidth > 0 && legendHeight > 0) {\r
- box.animate(\r
- box.crisp(null, null, null, legendWidth, legendHeight)\r
- );\r
- }\r
- \r
- // hide the border if no items\r
- box[allItems.length ? 'show' : 'hide']();\r
- }\r
- \r
- // 1.x compatibility: positioning based on style\r
- var props = ['left', 'right', 'top', 'bottom'],\r
- prop,\r
- i = 4;\r
- while(i--) {\r
- prop = props[i];\r
- if (style[prop] && style[prop] !== 'auto') {\r
- options[i < 2 ? 'align' : 'verticalAlign'] = prop;\r
- options[i < 2 ? 'x' : 'y'] = pInt(style[prop]) * (i % 2 ? -1 : 1);\r
- }\r
- }\r
- \r
- legendGroup.align(extend(options, {\r
- width: legendWidth,\r
- height: legendHeight\r
- }), true, spacingBox);\r
- \r
- if (!isResizing) {\r
- positionCheckboxes();\r
- }\r
- }\r
- \r
- \r
- // run legend\r
- renderLegend();\r
- \r
- // move checkboxes\r
- addEvent(chart, 'endResize', positionCheckboxes);\r
- \r
- // expose \r
- return {\r
- colorizeItem: colorizeItem,\r
- destroyItem: destroyItem,\r
- renderLegend: renderLegend\r
- };\r
- };\r
- \r
- \r
- \r
- \r
- \r
-\r
- /** \r
- * Initialize an individual series, called internally before render time\r
- */\r
- function initSeries(options) {\r
- var type = options.type || optionsChart.type || optionsChart.defaultSeriesType,\r
- typeClass = seriesTypes[type],\r
- serie,\r
- hasRendered = chart.hasRendered;\r
- \r
- // an inverted chart can't take a column series and vice versa\r
- if (hasRendered) {\r
- if (inverted && type === 'column') {\r
- typeClass = seriesTypes.bar;\r
- } else if (!inverted && type === 'bar') {\r
- typeClass = seriesTypes.column;\r
- }\r
- }\r
- \r
- serie = new typeClass();\r
- \r
- serie.init(chart, options);\r
- \r
- // set internal chart properties\r
- if (!hasRendered && serie.inverted) {\r
- inverted = true;\r
- }\r
- if (serie.isCartesian) {\r
- hasCartesianSeries = serie.isCartesian;\r
- }\r
- \r
- series.push(serie);\r
- \r
- return serie;\r
- }\r
-\r
- /**\r
- * Add a series dynamically after time\r
- * \r
- * @param {Object} options The config options\r
- * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true.\r
- * @param {Boolean|Object} animation Whether to apply animation, and optionally animation\r
- * configuration\r
- * \r
- * @return {Object} series The newly created series object\r
- */\r
- function addSeries(options, redraw, animation) {\r
- var series;\r
- \r
- if (options) {\r
- setAnimation(animation, chart);\r
- redraw = pick(redraw, true); // defaults to true\r
- \r
- fireEvent(chart, 'addSeries', { options: options }, function() {\r
- series = initSeries(options);\r
- series.isDirty = true;\r
- \r
- chart.isDirtyLegend = true; // the series array is out of sync with the display\r
- if (redraw) {\r
- chart.redraw();\r
- }\r
- });\r
- }\r
- \r
- return series;\r
- }\r
- \r
- /**\r
- * Check whether a given point is within the plot area\r
- * \r
- * @param {Number} x Pixel x relative to the coordinateSystem\r
- * @param {Number} y Pixel y relative to the coordinateSystem\r
- */\r
- isInsidePlot = function(x, y) {\r
- return x >= 0 &&\r
- x <= plotWidth &&\r
- y >= 0 &&\r
- y <= plotHeight;\r
- };\r
- \r
- /**\r
- * Adjust all axes tick amounts\r
- */\r
- function adjustTickAmounts() {\r
- if (optionsChart.alignTicks !== false) {\r
- each(axes, function(axis) {\r
- axis.adjustTickAmount();\r
- });\r
- }\r
- maxTicks = null;\r
- }\r
-\r
- /**\r
- * Redraw legend, axes or series based on updated data\r
- * \r
- * @param {Boolean|Object} animation Whether to apply animation, and optionally animation\r
- * configuration\r
- */\r
- function redraw(animation) {\r
- var redrawLegend = chart.isDirtyLegend,\r
- hasStackedSeries,\r
- isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed?\r
- seriesLength = series.length,\r
- i = seriesLength,\r
- clipRect = chart.clipRect,\r
- serie;\r
- \r
- setAnimation(animation, chart);\r
- \r
- // link stacked series\r
- while (i--) {\r
- serie = series[i];\r
- if (serie.isDirty && serie.options.stacking) {\r
- hasStackedSeries = true;\r
- break;\r
- }\r
- }\r
- if (hasStackedSeries) { // mark others as dirty\r
- i = seriesLength;\r
- while (i--) {\r
- serie = series[i];\r
- if (serie.options.stacking) {\r
- serie.isDirty = true;\r
- }\r
- }\r
- }\r
- \r
- // handle updated data in the series \r
- each(series, function(serie) {\r
- if (serie.isDirty) { // prepare the data so axis can read it\r
- serie.cleanData();\r
- serie.getSegments();\r
- \r
- if (serie.options.legendType === 'point') {\r
- redrawLegend = true;\r
- }\r
- }\r
- });\r
- \r
- // handle added or removed series\r
- if (redrawLegend && legend.renderLegend) { // series or pie points are added or removed\r
- // draw legend graphics\r
- legend.renderLegend();\r
- \r
- chart.isDirtyLegend = false;\r
- }\r
- \r
- if (hasCartesianSeries) {\r
- if (!isResizing) {\r
- \r
- // reset maxTicks\r
- maxTicks = null; \r
- \r
- // set axes scales\r
- each(axes, function(axis) {\r
- axis.setScale();\r
- });\r
- }\r
- adjustTickAmounts();\r
- getMargins();\r
- \r
- // redraw axes\r
- each(axes, function(axis) {\r
- if (axis.isDirty || isDirtyBox) {\r
- axis.redraw();\r
- isDirtyBox = true; // always redraw box to reflect changes in the axis labels \r
- }\r
- });\r
- \r
- \r
- }\r
- \r
- // the plot areas size has changed\r
- if (isDirtyBox) {\r
- drawChartBox();\r
- placeTrackerGroup();\r
- \r
- // move clip rect\r
- if (clipRect) {\r
- stop(clipRect);\r
- clipRect.animate({ // for chart resize\r
- width: chart.plotSizeX,\r
- height: chart.plotSizeY\r
- });\r
- }\r
- \r
- }\r
- \r
- \r
- // redraw affected series\r
- each(series, function(serie) {\r
- if (serie.isDirty && serie.visible && \r
- (!serie.isCartesian || serie.xAxis)) { // issue #153 \r
- serie.redraw();\r
- }\r
- });\r
- \r
- \r
- // hide tooltip and hover states\r
- if (tracker && tracker.resetTracker) {\r
- tracker.resetTracker();\r
- }\r
- \r
- // fire the event\r
- fireEvent(chart, 'redraw');\r
- }\r
- \r
- \r
- \r
- /**\r
- * Dim the chart and show a loading text or symbol\r
- * @param {String} str An optional text to show in the loading label instead of the default one\r
- */\r
- function showLoading(str) {\r
- var loadingOptions = options.loading;\r
-\r
- // create the layer at the first call\r
- if (!loadingDiv) {\r
- loadingDiv = createElement(DIV, {\r
- className: 'highcharts-loading'\r
- }, extend(loadingOptions.style, {\r
- left: plotLeft + PX,\r
- top: plotTop + PX,\r
- width: plotWidth + PX,\r
- height: plotHeight + PX,\r
- zIndex: 10,\r
- display: NONE\r
- }), container);\r
- \r
- loadingSpan = createElement(\r
- 'span', \r
- null, \r
- loadingOptions.labelStyle, \r
- loadingDiv\r
- );\r
-\r
- }\r
- \r
- // update text\r
- loadingSpan.innerHTML = str || options.lang.loading;\r
- \r
- // show it\r
- if (!loadingShown) {\r
- css(loadingDiv, { opacity: 0, display: '' });\r
- animate(loadingDiv, {\r
- opacity: loadingOptions.style.opacity\r
- }, {\r
- duration: loadingOptions.showDuration\r
- });\r
- loadingShown = true;\r
- }\r
- }\r
- /**\r
- * Hide the loading layer\r
- */\r
- function hideLoading() {\r
- animate(loadingDiv, {\r
- opacity: 0\r
- }, {\r
- duration: options.loading.hideDuration, \r
- complete: function() {\r
- css(loadingDiv, { display: NONE });\r
- }\r
- });\r
- loadingShown = false;\r
- }\r
- \r
- /**\r
- * Get an axis, series or point object by id.\r
- * @param id {String} The id as given in the configuration options\r
- */\r
- function get(id) {\r
- var i,\r
- j,\r
- data;\r
- \r
- // search axes\r
- for (i = 0; i < axes.length; i++) {\r
- if (axes[i].options.id === id) {\r
- return axes[i];\r
- }\r
- }\r
- \r
- // search series\r
- for (i = 0; i < series.length; i++) {\r
- if (series[i].options.id === id) {\r
- return series[i];\r
- }\r
- }\r
- \r
- // search points\r
- for (i = 0; i < series.length; i++) {\r
- data = series[i].data;\r
- for (j = 0; j < data.length; j++) {\r
- if (data[j].id === id) {\r
- return data[j];\r
- }\r
- }\r
- }\r
- return null; \r
- }\r
- \r
- /** \r
- * Create the Axis instances based on the config options\r
- */\r
- function getAxes() {\r
- var xAxisOptions = options.xAxis || {},\r
- yAxisOptions = options.yAxis || {},\r
- axis;\r
- \r
- // make sure the options are arrays and add some members\r
- xAxisOptions = splat(xAxisOptions);\r
- each(xAxisOptions, function(axis, i) {\r
- axis.index = i; \r
- axis.isX = true;\r
- });\r
- \r
- yAxisOptions = splat(yAxisOptions);\r
- each(yAxisOptions, function(axis, i) {\r
- axis.index = i;\r
- });\r
- \r
- // concatenate all axis options into one array\r
- axes = xAxisOptions.concat(yAxisOptions);\r
- \r
- // loop the options and construct axis objects\r
- chart.xAxis = [];\r
- chart.yAxis = [];\r
- axes = map(axes, function(axisOptions) {\r
- axis = new Axis(chart, axisOptions);\r
- chart[axis.isXAxis ? 'xAxis' : 'yAxis'].push(axis);\r
- \r
- return axis;\r
- });\r
- \r
- adjustTickAmounts();\r
- }\r
-\r
- \r
- /**\r
- * Get the currently selected points from all series\r
- */\r
- function getSelectedPoints() {\r
- var points = [];\r
- each(series, function(serie) {\r
- points = points.concat( grep( serie.data, function(point) {\r
- return point.selected;\r
- }));\r
- });\r
- return points;\r
- }\r
- \r
- /**\r
- * Get the currently selected series\r
- */\r
- function getSelectedSeries() {\r
- return grep(series, function (serie) {\r
- return serie.selected;\r
- });\r
- }\r
- \r
- /**\r
- * Zoom out to 1:1\r
- */\r
- zoomOut = function () {\r
- fireEvent(chart, 'selection', { resetSelection: true }, zoom);\r
- chart.toolbar.remove('zoom');\r
-\r
- };\r
- /**\r
- * Zoom into a given portion of the chart given by axis coordinates\r
- * @param {Object} event\r
- */\r
- zoom = function (event) {\r
- \r
- // add button to reset selection\r
- var lang = defaultOptions.lang,\r
- animate = chart.pointCount < 100;\r
- chart.toolbar.add('zoom', lang.resetZoom, lang.resetZoomTitle, zoomOut);\r
- \r
- // if zoom is called with no arguments, reset the axes\r
- if (!event || event.resetSelection) {\r
- each(axes, function(axis) {\r
- axis.setExtremes(null, null, false, animate);\r
- });\r
- }\r
- \r
- // else, zoom in on all axes\r
- else {\r
- each(event.xAxis.concat(event.yAxis), function(axisData) {\r
- var axis = axisData.axis;\r
- \r
- // don't zoom more than maxZoom\r
- if (chart.tracker[axis.isXAxis ? 'zoomX' : 'zoomY']) {\r
- axis.setExtremes(axisData.min, axisData.max, false, animate);\r
- }\r
- });\r
- }\r
- \r
- // redraw chart\r
- redraw();\r
- };\r
- \r
- /**\r
- * Show the title and subtitle of the chart\r
- * \r
- * @param titleOptions {Object} New title options\r
- * @param subtitleOptions {Object} New subtitle options\r
- * \r
- */\r
- function setTitle (titleOptions, subtitleOptions) {\r
- \r
- chartTitleOptions = merge(options.title, titleOptions);\r
- chartSubtitleOptions = merge(options.subtitle, subtitleOptions);\r
- \r
- // add title and subtitle\r
- each([\r
- ['title', titleOptions, chartTitleOptions],\r
- ['subtitle', subtitleOptions, chartSubtitleOptions]\r
- ], function(arr) {\r
- var name = arr[0],\r
- title = chart[name],\r
- titleOptions = arr[1],\r
- chartTitleOptions = arr[2];\r
- \r
- if (title && titleOptions) {\r
- title.destroy(); // remove old\r
- title = null;\r
- }\r
- if (chartTitleOptions && chartTitleOptions.text && !title) {\r
- chart[name] = renderer.text(\r
- chartTitleOptions.text, \r
- 0,\r
- 0\r
- )\r
- .attr({\r
- align: chartTitleOptions.align,\r
- 'class': 'highcharts-'+ name,\r
- zIndex: 1\r
- })\r
- .css(chartTitleOptions.style)\r
- .add()\r
- .align(chartTitleOptions, false, spacingBox);\r
- }\r
- });\r
- \r
- }\r
- \r
- /**\r
- * Get chart width and height according to options and container size\r
- */\r
- function getChartSize() {\r
-\r
- containerWidth = (renderToClone || renderTo).offsetWidth;\r
- containerHeight = (renderToClone || renderTo).offsetHeight;\r
- chart.chartWidth = chartWidth = optionsChart.width || containerWidth || 600;\r
- chart.chartHeight = chartHeight = optionsChart.height || \r
- // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7:\r
- (containerHeight > 19 ? containerHeight : 400);\r
- }\r
-\r
- \r
- /**\r
- * Get the containing element, determine the size and create the inner container\r
- * div to hold the chart\r
- */\r
- function getContainer() {\r
- renderTo = optionsChart.renderTo;\r
- containerId = PREFIX + idCounter++;\r
- \r
- if (isString(renderTo)) {\r
- renderTo = doc.getElementById(renderTo);\r
- }\r
- \r
- // remove previous chart\r
- renderTo.innerHTML = '';\r
- \r
- // If the container doesn't have an offsetWidth, it has or is a child of a node\r
- // that has display:none. We need to temporarily move it out to a visible\r
- // state to determine the size, else the legend and tooltips won't render\r
- // properly \r
- if (!renderTo.offsetWidth) {\r
- renderToClone = renderTo.cloneNode(0);\r
- css(renderToClone, {\r
- position: ABSOLUTE,\r
- top: '-9999px',\r
- display: ''\r
- });\r
- doc.body.appendChild(renderToClone);\r
- }\r
- \r
- // get the width and height\r
- getChartSize();\r
- \r
- // create the inner container\r
- chart.container = container = createElement(DIV, {\r
- className: 'highcharts-container' + \r
- (optionsChart.className ? ' '+ optionsChart.className : ''),\r
- id: containerId\r
- }, extend({\r
- position: RELATIVE,\r
- overflow: HIDDEN, // needed for context menu (avoid scrollbars) and \r
- // content overflow in IE\r
- width: chartWidth + PX,\r
- height: chartHeight + PX,\r
- textAlign: 'left'\r
- }, optionsChart.style),\r
- renderToClone || renderTo\r
- );\r
- \r
- chart.renderer = renderer = \r
- optionsChart.forExport ? // force SVG, used for SVG export\r
- new SVGRenderer(container, chartWidth, chartHeight, true) : \r
- new Renderer(container, chartWidth, chartHeight);\r
- \r
- // Issue 110 workaround:\r
- // In Firefox, if a div is positioned by percentage, its pixel position may land\r
- // between pixels. The container itself doesn't display this, but an SVG element\r
- // inside this container will be drawn at subpixel precision. In order to draw\r
- // sharp lines, this must be compensated for. This doesn't seem to work inside\r
- // iframes though (like in jsFiddle).\r
- var subPixelFix, rect;\r
- if (isFirefox && container.getBoundingClientRect) {\r
- subPixelFix = function() {\r
- css(container, { left: 0, top: 0 });\r
- rect = container.getBoundingClientRect();\r
- css(container, {\r
- left: (-(rect.left - pInt(rect.left))) + PX,\r
- top: (-(rect.top - pInt(rect.top))) + PX\r
- });\r
- };\r
- \r
- // run the fix now\r
- subPixelFix();\r
- \r
- // run it on resize\r
- addEvent(win, 'resize', subPixelFix);\r
- \r
- // remove it on chart destroy\r
- addEvent(chart, 'destroy', function() {\r
- removeEvent(win, 'resize', subPixelFix);\r
- });\r
- }\r
- }\r
- \r
- /**\r
- * Calculate margins by rendering axis labels in a preliminary position. Title,\r
- * subtitle and legend have already been rendered at this stage, but will be \r
- * moved into their final positions\r
- */\r
- getMargins = function() {\r
- var legendOptions = options.legend,\r
- legendMargin = pick(legendOptions.margin, 10),\r
- legendX = legendOptions.x,\r
- legendY = legendOptions.y,\r
- align = legendOptions.align,\r
- verticalAlign = legendOptions.verticalAlign,\r
- titleOffset;\r
-\r
- resetMargins();\r
-\r
- // adjust for title and subtitle\r
- if ((chart.title || chart.subtitle) && !defined(optionsMarginTop)) {\r
- titleOffset = mathMax(\r
- (chart.title && !chartTitleOptions.floating && !chartTitleOptions.verticalAlign && chartTitleOptions.y) || 0, \r
- (chart.subtitle && !chartSubtitleOptions.floating && !chartSubtitleOptions.verticalAlign && chartSubtitleOptions.y) || 0 \r
- );\r
- if (titleOffset) {\r
- plotTop = mathMax(plotTop, titleOffset + pick(chartTitleOptions.margin, 15) + spacingTop);\r
- }\r
- }\r
- // adjust for legend\r
- if (legendOptions.enabled && !legendOptions.floating) {\r
- if (align === 'right') { // horizontal alignment handled first\r
- if (!defined(optionsMarginRight)) {\r
- marginRight = mathMax(\r
- marginRight,\r
- legendWidth - legendX + legendMargin + spacingRight\r
- );\r
- }\r
- } else if (align === 'left') {\r
- if (!defined(optionsMarginLeft)) {\r
- plotLeft = mathMax(\r
- plotLeft,\r
- legendWidth + legendX + legendMargin + spacingLeft\r
- );\r
- }\r
- \r
- } else if (verticalAlign === 'top') {\r
- if (!defined(optionsMarginTop)) {\r
- plotTop = mathMax(\r
- plotTop, \r
- legendHeight + legendY + legendMargin + spacingTop\r
- );\r
- }\r
- \r
- } else if (verticalAlign === 'bottom') {\r
- if (!defined(optionsMarginBottom)) {\r
- marginBottom = mathMax(\r
- marginBottom, \r
- legendHeight - legendY + legendMargin + spacingBottom\r
- );\r
- }\r
- }\r
- }\r
- \r
- // pre-render axes to get labels offset width\r
- if (hasCartesianSeries) {\r
- each(axes, function(axis) {\r
- axis.getOffset();\r
- });\r
- }\r
- \r
- if (!defined(optionsMarginLeft)) {\r
- plotLeft += axisOffset[3];\r
- }\r
- if (!defined(optionsMarginTop)) {\r
- plotTop += axisOffset[0];\r
- }\r
- if (!defined(optionsMarginBottom)) {\r
- marginBottom += axisOffset[2];\r
- }\r
- if (!defined(optionsMarginRight)) {\r
- marginRight += axisOffset[1];\r
- }\r
- \r
- setChartSize();\r
- \r
- };\r
- \r
- /**\r
- * Add the event handlers necessary for auto resizing\r
- * \r
- */\r
- function initReflow() {\r
- var reflowTimeout;\r
- function reflow() {\r
- var width = optionsChart.width || renderTo.offsetWidth,\r
- height = optionsChart.height || renderTo.offsetHeight;\r
- \r
- if (width && height) { // means container is display:none\r
- if (width !== containerWidth || height !== containerHeight) {\r
- clearTimeout(reflowTimeout);\r
- reflowTimeout = setTimeout(function() {\r
- resize(width, height, false);\r
- }, 100);\r
- }\r
- containerWidth = width;\r
- containerHeight = height;\r
- }\r
- }\r
- addEvent(win, 'resize', reflow);\r
- addEvent(chart, 'destroy', function() {\r
- removeEvent(win, 'resize', reflow);\r
- });\r
- }\r
- \r
- /**\r
- * Resize the chart to a given width and height\r
- * @param {Number} width\r
- * @param {Number} height\r
- * @param {Object|Boolean} animation\r
- */\r
- resize = function(width, height, animation) {\r
- var chartTitle = chart.title,\r
- chartSubtitle = chart.subtitle;\r
- \r
- isResizing += 1;\r
- \r
- // set the animation for the current process\r
- setAnimation(animation, chart);\r
- \r
- oldChartHeight = chartHeight;\r
- oldChartWidth = chartWidth;\r
- chart.chartWidth = chartWidth = mathRound(width);\r
- chart.chartHeight = chartHeight = mathRound(height);\r
- \r
- css(container, {\r
- width: chartWidth + PX,\r
- height: chartHeight + PX\r
- });\r
- renderer.setSize(chartWidth, chartHeight, animation);\r
- \r
- // update axis lengths for more correct tick intervals:\r
- plotWidth = chartWidth - plotLeft - marginRight; \r
- plotHeight = chartHeight - plotTop - marginBottom;\r
- \r
- // handle axes\r
- maxTicks = null;\r
- each(axes, function(axis) {\r
- axis.isDirty = true;\r
- axis.setScale();\r
- });\r
- \r
- // make sure non-cartesian series are also handled\r
- each(series, function(serie) {\r
- serie.isDirty = true;\r
- });\r
- \r
- chart.isDirtyLegend = true; // force legend redraw\r
- chart.isDirtyBox = true; // force redraw of plot and chart border\r
- \r
- getMargins();\r
- \r
- // move titles\r
- if (chartTitle) {\r
- chartTitle.align(null, null, spacingBox);\r
- }\r
- if (chartSubtitle) {\r
- chartSubtitle.align(null, null, spacingBox);\r
- }\r
- \r
- redraw(animation);\r
- \r
- \r
- oldChartHeight = null;\r
- fireEvent(chart, 'resize');\r
- \r
- // fire endResize and set isResizing back \r
- setTimeout(function() {\r
- fireEvent(chart, 'endResize', null, function() {\r
- isResizing -= 1;\r
- });\r
- }, (globalAnimation && globalAnimation.duration) || 500);\r
- };\r
- \r
- /**\r
- * Set the public chart properties. This is done before and after the pre-render\r
- * to determine margin sizes\r
- */\r
- setChartSize = function() {\r
- \r
- chart.plotLeft = plotLeft = mathRound(plotLeft);\r
- chart.plotTop = plotTop = mathRound(plotTop);\r
- chart.plotWidth = plotWidth = mathRound(chartWidth - plotLeft - marginRight);\r
- chart.plotHeight = plotHeight = mathRound(chartHeight - plotTop - marginBottom);\r
- \r
- chart.plotSizeX = inverted ? plotHeight : plotWidth;\r
- chart.plotSizeY = inverted ? plotWidth : plotHeight;\r
- \r
- spacingBox = {\r
- x: spacingLeft,\r
- y: spacingTop,\r
- width: chartWidth - spacingLeft - spacingRight,\r
- height: chartHeight - spacingTop - spacingBottom\r
- };\r
- };\r
- \r
- /**\r
- * Initial margins before auto size margins are applied\r
- */\r
- resetMargins = function() {\r
- plotTop = pick(optionsMarginTop, spacingTop);\r
- marginRight = pick(optionsMarginRight, spacingRight);\r
- marginBottom = pick(optionsMarginBottom, spacingBottom);\r
- plotLeft = pick(optionsMarginLeft, spacingLeft);\r
- axisOffset = [0, 0, 0, 0]; // top, right, bottom, left\r
- };\r
- \r
- /**\r
- * Draw the borders and backgrounds for chart and plot area\r
- */\r
- drawChartBox = function() {\r
- var chartBorderWidth = optionsChart.borderWidth || 0,\r
- chartBackgroundColor = optionsChart.backgroundColor,\r
- plotBackgroundColor = optionsChart.plotBackgroundColor,\r
- plotBackgroundImage = optionsChart.plotBackgroundImage,\r
- mgn,\r
- plotSize = {\r
- x: plotLeft,\r
- y: plotTop,\r
- width: plotWidth,\r
- height: plotHeight\r
- };\r
-\r
- // Chart area\r
- mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);\r
- \r
- if (chartBorderWidth || chartBackgroundColor) {\r
- if (!chartBackground) {\r
- chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, \r
- optionsChart.borderRadius, chartBorderWidth)\r
- .attr({ \r
- stroke: optionsChart.borderColor,\r
- 'stroke-width': chartBorderWidth,\r
- fill: chartBackgroundColor || NONE\r
- })\r
- .add()\r
- .shadow(optionsChart.shadow);\r
- } else { // resize\r
- chartBackground.animate(\r
- chartBackground.crisp(null, null, null, chartWidth - mgn, chartHeight - mgn)\r
- );\r
- }\r
- }\r
- \r
- \r
- // Plot background\r
- if (plotBackgroundColor) {\r
- if (!plotBackground) {\r
- plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0)\r
- .attr({\r
- fill: plotBackgroundColor\r
- })\r
- .add()\r
- .shadow(optionsChart.plotShadow);\r
- } else {\r
- plotBackground.animate(plotSize);\r
- }\r
- }\r
- if (plotBackgroundImage) {\r
- if (!plotBGImage) {\r
- plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)\r
- .add();\r
- } else {\r
- plotBGImage.animate(plotSize);\r
- }\r
- }\r
- \r
- // Plot area border\r
- if (optionsChart.plotBorderWidth) {\r
- if (!plotBorder) {\r
- plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, optionsChart.plotBorderWidth)\r
- .attr({\r
- stroke: optionsChart.plotBorderColor,\r
- 'stroke-width': optionsChart.plotBorderWidth,\r
- zIndex: 4\r
- })\r
- .add();\r
- } else {\r
- plotBorder.animate(\r
- plotBorder.crisp(null, plotLeft, plotTop, plotWidth, plotHeight)\r
- );\r
- }\r
- }\r
- \r
- // reset\r
- chart.isDirtyBox = false;\r
- };\r
- \r
- /**\r
- * Render all graphics for the chart\r
- */\r
- function render () {\r
- var labels = options.labels,\r
- credits = options.credits,\r
- creditsHref;\r
- \r
- // Title\r
- setTitle();\r
- \r
- \r
- // Legend\r
- legend = chart.legend = new Legend(chart);\r
- \r
- // Get margins by pre-rendering axes\r
- getMargins();\r
- each(axes, function(axis) {\r
- axis.setTickPositions(true); // update to reflect the new margins \r
- });\r
- adjustTickAmounts();\r
- getMargins(); // second pass to check for new labels\r
- \r
- \r
- // Draw the borders and backgrounds\r
- drawChartBox();\r
- \r
- // Axes\r
- if (hasCartesianSeries) {\r
- each(axes, function(axis) { \r
- axis.render();\r
- });\r
- }\r
- \r
- \r
- // The series\r
- if (!chart.seriesGroup) {\r
- chart.seriesGroup = renderer.g('series-group')\r
- .attr({ zIndex: 3 })\r
- .add();\r
- }\r
- each(series, function(serie) {\r
- serie.translate();\r
- serie.setTooltipPoints();\r
- serie.render();\r
- });\r
- \r
- \r
- // Labels\r
- if (labels.items) {\r
- each(labels.items, function() {\r
- var style = extend(labels.style, this.style),\r
- x = pInt(style.left) + plotLeft,\r
- y = pInt(style.top) + plotTop + 12;\r
- \r
- // delete to prevent rewriting in IE\r
- delete style.left;\r
- delete style.top;\r
- \r
- renderer.text(\r
- this.html,\r
- x,\r
- y\r
- )\r
- .attr({ zIndex: 2 })\r
- .css(style)\r
- .add();\r
- \r
- });\r
- }\r
- \r
- // Toolbar (don't redraw)\r
- if (!chart.toolbar) {\r
- chart.toolbar = Toolbar(chart);\r
- }\r
- \r
- // Credits\r
- if (credits.enabled && !chart.credits) {\r
- creditsHref = credits.href;\r
- renderer.text(\r
- credits.text,\r
- 0,\r
- 0\r
- )\r
- .on('click', function() {\r
- if (creditsHref) {\r
- location.href = creditsHref;\r
- }\r
- })\r
- .attr({\r
- align: credits.position.align, \r
- zIndex: 8\r
- })\r
- .css(credits.style)\r
- .add()\r
- .align(credits.position); \r
- }\r
- \r
- placeTrackerGroup();\r
-\r
- // Set flag\r
- chart.hasRendered = true;\r
- \r
- // If the chart was rendered outside the top container, put it back in\r
- if (renderToClone) {\r
- renderTo.appendChild(container);\r
- discardElement(renderToClone);\r
- //updatePosition(container);\r
- }\r
- }\r
- \r
- /**\r
- * Clean up memory usage\r
- */\r
- function destroy() {\r
- var i = series.length,\r
- parentNode = container && container.parentNode;\r
- \r
- // fire the chart.destoy event\r
- fireEvent(chart, 'destroy');\r
-\r
- // remove events\r
- removeEvent(win, 'unload', destroy);\r
- removeEvent(chart);\r
- \r
- each(axes, function(axis) {\r
- removeEvent(axis);\r
- });\r
-\r
- // destroy each series\r
- while (i--) {\r
- series[i].destroy();\r
- }\r
- \r
- // remove container and all SVG\r
- if (container) { // can break in IE when destroyed before finished loading\r
- container.innerHTML = '';\r
- removeEvent(container);\r
- if (parentNode) {\r
- parentNode.removeChild(container);\r
- }\r
- \r
- // IE6 leak \r
- container = null;\r
- }\r
- \r
- // IE7 leak\r
- if (renderer) { // can break in IE when destroyed before finished loading\r
- renderer.alignedObjects = null;\r
- }\r
- \r
- // memory and CPU leak\r
- clearInterval(tooltipInterval);\r
- \r
- // clean it all up\r
- for (i in chart) {\r
- delete chart[i];\r
- }\r
- \r
- }\r
- /**\r
- * Prepare for first rendering after all data are loaded\r
- */\r
- function firstRender() {\r
-\r
- // VML namespaces can't be added until after complete. Listening\r
- // for Perini's doScroll hack is not enough.\r
- var ONREADYSTATECHANGE = 'onreadystatechange',\r
- COMPLETE = 'complete';\r
- // Note: in spite of JSLint's complaints, win == win.top is required\r
- if (!hasSVG && win == win.top && doc.readyState !== COMPLETE) {\r
- doc.attachEvent(ONREADYSTATECHANGE, function() {\r
- doc.detachEvent(ONREADYSTATECHANGE, firstRender);\r
- if (doc.readyState === COMPLETE) {\r
- firstRender();\r
- }\r
- });\r
- return;\r
- }\r
-\r
- // create the container\r
- getContainer();\r
- \r
- resetMargins();\r
- setChartSize();\r
- \r
- // Initialize the series\r
- each(options.series || [], function(serieOptions) {\r
- initSeries(serieOptions);\r
- });\r
- \r
- // Set the common inversion and transformation for inverted series after initSeries\r
- chart.inverted = inverted = pick(inverted, options.chart.inverted); \r
- \r
- \r
- getAxes();\r
- \r
- \r
- chart.render = render;\r
- \r
- // depends on inverted and on margins being set \r
- chart.tracker = tracker = new MouseTracker(chart, options.tooltip);\r
- \r
- //globalAnimation = false;\r
- render();\r
- \r
- fireEvent(chart, 'load');\r
- \r
- //globalAnimation = true;\r
- \r
- // run callbacks\r
- if (callback) {\r
- callback.apply(chart, [chart]);\r
- }\r
- each(chart.callbacks, function(fn) {\r
- fn.apply(chart, [chart]);\r
- });\r
- }\r
- \r
- // Run chart\r
- \r
- \r
- // Destroy the chart and free up memory. \r
- addEvent(win, 'unload', destroy);\r
- \r
- // Set up auto resize\r
- if (optionsChart.reflow !== false) {\r
- addEvent(chart, 'load', initReflow);\r
- }\r
- \r
- // Chart event handlers\r
- if (chartEvents) {\r
- for (eventType in chartEvents) { \r
- addEvent(chart, eventType, chartEvents[eventType]);\r
- }\r
- }\r
- \r
- \r
- chart.options = options;\r
- chart.series = series;\r
-\r
- \r
- \r
- \r
- \r
- // Expose methods and variables\r
- chart.addSeries = addSeries;\r
- chart.animation = pick(optionsChart.animation, true);\r
- chart.destroy = destroy;\r
- chart.get = get;\r
- chart.getSelectedPoints = getSelectedPoints;\r
- chart.getSelectedSeries = getSelectedSeries;\r
- chart.hideLoading = hideLoading;\r
- chart.isInsidePlot = isInsidePlot;\r
- chart.redraw = redraw;\r
- chart.setSize = resize;\r
- chart.setTitle = setTitle;\r
- chart.showLoading = showLoading; \r
- chart.pointCount = 0;\r
- chart.counters = new ChartCounters();\r
- /*\r
- if ($) $(function() {\r
- $container = $('#container');\r
- var origChartWidth,\r
- origChartHeight;\r
- if ($container) {\r
- $('<button>+</button>')\r
- .insertBefore($container)\r
- .click(function() {\r
- if (origChartWidth === UNDEFINED) {\r
- origChartWidth = chartWidth;\r
- origChartHeight = chartHeight;\r
- } \r
- chart.resize(chartWidth *= 1.1, chartHeight *= 1.1);\r
- });\r
- $('<button>-</button>')\r
- .insertBefore($container)\r
- .click(function() {\r
- if (origChartWidth === UNDEFINED) {\r
- origChartWidth = chartWidth;\r
- origChartHeight = chartHeight;\r
- } \r
- chart.resize(chartWidth *= 0.9, chartHeight *= 0.9);\r
- });\r
- $('<button>1:1</button>')\r
- .insertBefore($container)\r
- .click(function() { \r
- if (origChartWidth === UNDEFINED) {\r
- origChartWidth = chartWidth;\r
- origChartHeight = chartHeight;\r
- } \r
- chart.resize(origChartWidth, origChartHeight);\r
- });\r
- }\r
- })\r
- */\r
- \r
- \r
- \r
- \r
- firstRender();\r
- \r
- \r
-} // end Chart\r
-\r
-// Hook for exporting module\r
-Chart.prototype.callbacks = [];\r
-/**\r
- * The Point object and prototype. Inheritable and used as base for PiePoint\r
- */\r
-var Point = function() {};\r
-Point.prototype = {\r
-\r
- /**\r
- * Initialize the point\r
- * @param {Object} series The series object containing this point\r
- * @param {Object} options The data in either number, array or object format\r
- */\r
- init: function(series, options) {\r
- var point = this,\r
- counters = series.chart.counters,\r
- defaultColors;\r
- point.series = series;\r
- point.applyOptions(options);\r
- point.pointAttr = {};\r
- \r
- if (series.options.colorByPoint) {\r
- defaultColors = series.chart.options.colors;\r
- if (!point.options) {\r
- point.options = {};\r
- }\r
- point.color = point.options.color = point.color || defaultColors[counters.color++];\r
- \r
- // loop back to zero\r
- counters.wrapColor(defaultColors.length);\r
- }\r
- \r
- series.chart.pointCount++;\r
- return point;\r
- },\r
- /**\r
- * Apply the options containing the x and y data and possible some extra properties.\r
- * This is called on point init or from point.update.\r
- * \r
- * @param {Object} options\r
- */\r
- applyOptions: function(options) {\r
- var point = this,\r
- series = point.series;\r
- \r
- point.config = options;\r
- \r
- // onedimensional array input\r
- if (isNumber(options) || options === null) {\r
- point.y = options; \r
- }\r
- \r
- // object input\r
- else if (isObject(options) && !isNumber(options.length)) {\r
- \r
- // copy options directly to point\r
- extend(point, options);\r
- point.options = options;\r
- }\r
- \r
- // categorized data with name in first position\r
- else if (isString(options[0])) {\r
- point.name = options[0];\r
- point.y = options[1];\r
- }\r
- \r
- // two-dimentional array\r
- else if (isNumber(options[0])) {\r
- point.x = options[0];\r
- point.y = options[1];\r
- }\r
- \r
- /* \r
- * If no x is set by now, get auto incremented value. All points must have an\r
- * x value, however the y value can be null to create a gap in the series\r
- */\r
- if (point.x === UNDEFINED) {\r
- point.x = series.autoIncrement();\r
- }\r
- \r
- },\r
- \r
- /**\r
- * Destroy a point to clear memory. Its reference still stays in series.data.\r
- */\r
- destroy: function() {\r
- var point = this,\r
- series = point.series,\r
- prop;\r
- \r
- series.chart.pointCount--;\r
- \r
- if (point === series.chart.hoverPoint) {\r
- point.onMouseOut();\r
- }\r
- series.chart.hoverPoints = null; // remove reference\r
- \r
- // remove all events\r
- removeEvent(point);\r
- \r
- each(['graphic', 'tracker', 'group', 'dataLabel', 'connector'], function(prop) {\r
- if (point[prop]) {\r
- point[prop].destroy();\r
- }\r
- }); \r
- \r
- if (point.legendItem) { // pies have legend items\r
- point.series.chart.legend.destroyItem(point);\r
- }\r
- \r
- for (prop in point) {\r
- point[prop] = null;\r
- }\r
- \r
- \r
- },\r
- \r
- /**\r
- * Return the configuration hash needed for the data label and tooltip formatters\r
- */\r
- getLabelConfig: function() {\r
- var point = this;\r
- return {\r
- x: point.category,\r
- y: point.y,\r
- series: point.series,\r
- point: point,\r
- percentage: point.percentage,\r
- total: point.total || point.stackTotal\r
- };\r
- },\r
- \r
- /**\r
- * Toggle the selection status of a point\r
- * @param {Boolean} selected Whether to select or unselect the point.\r
- * @param {Boolean} accumulate Whether to add to the previous selection. By default,\r
- * this happens if the control key (Cmd on Mac) was pressed during clicking.\r
- */\r
- select: function(selected, accumulate) {\r
- var point = this,\r
- series = point.series,\r
- chart = series.chart;\r
- \r
- point.selected = selected = pick(selected, !point.selected);\r
- \r
- //series.isDirty = true;\r
- point.firePointEvent(selected ? 'select' : 'unselect');\r
- point.setState(selected && SELECT_STATE);\r
- \r
- // unselect all other points unless Ctrl or Cmd + click\r
- if (!accumulate) {\r
- each(chart.getSelectedPoints(), function (loopPoint) {\r
- if (loopPoint.selected && loopPoint !== point) {\r
- loopPoint.selected = false;\r
- loopPoint.setState(NORMAL_STATE);\r
- loopPoint.firePointEvent('unselect');\r
- }\r
- });\r
- }\r
- \r
- },\r
- \r
- onMouseOver: function() {\r
- var point = this,\r
- chart = point.series.chart,\r
- tooltip = chart.tooltip,\r
- hoverPoint = chart.hoverPoint;\r
- \r
- // set normal state to previous series\r
- if (hoverPoint && hoverPoint !== point) {\r
- hoverPoint.onMouseOut();\r
- }\r
- \r
- // trigger the event\r
- point.firePointEvent('mouseOver');\r
- \r
- // update the tooltip\r
- if (tooltip && !tooltip.shared) {\r
- tooltip.refresh(point);\r
- }\r
- \r
- // hover this\r
- point.setState(HOVER_STATE);\r
- chart.hoverPoint = point;\r
- },\r
- \r
- onMouseOut: function() {\r
- var point = this;\r
- point.firePointEvent('mouseOut');\r
- \r
- point.setState();\r
- point.series.chart.hoverPoint = null;\r
- },\r
- \r
- /**\r
- * Extendable method for formatting each point's tooltip line \r
- * \r
- * @param {Boolean} useHeader Whether a common header is used for multiple series in the tooltip\r
- * \r
- * @return {String} A string to be concatenated in to the common tooltip text\r
- */\r
- tooltipFormatter: function(useHeader) {\r
- var point = this,\r
- series = point.series;\r
- \r
- return ['<span style="color:'+ series.color +'">', (point.name || series.name), '</span>: ',\r
- (!useHeader ? ('<b>x = '+ (point.name || point.x) + ',</b> ') : ''), \r
- '<b>', (!useHeader ? 'y = ' : '' ), point.y, '</b>'].join('');\r
- \r
- },\r
- \r
- /**\r
- * Update the point with new options (typically x/y data) and optionally redraw the series.\r
- * \r
- * @param {Object} options Point options as defined in the series.data array\r
- * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call\r
- * @param {Boolean|Object} animation Whether to apply animation, and optionally animation\r
- * configuration\r
- * \r
- */\r
- update: function(options, redraw, animation) {\r
- var point = this,\r
- series = point.series,\r
- dataLabel = point.dataLabel,\r
- graphic = point.graphic,\r
- chart = series.chart;\r
- \r
- redraw = pick(redraw, true);\r
- \r
- // fire the event with a default handler of doing the update\r
- point.firePointEvent('update', { options: options }, function() {\r
-\r
- point.applyOptions(options);\r
- \r
- // update visuals\r
- if (isObject(options)) {\r
- series.getAttribs();\r
- if (graphic) {\r
- graphic.attr(point.pointAttr[series.state]);\r
- }\r
- }\r
- \r
- // redraw\r
- series.isDirty = true;\r
- if (redraw) {\r
- chart.redraw(animation);\r
- }\r
- });\r
- },\r
- \r
- /**\r
- * Remove a point and optionally redraw the series and if necessary the axes\r
- * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call\r
- * @param {Boolean|Object} animation Whether to apply animation, and optionally animation\r
- * configuration\r
- */\r
- remove: function(redraw, animation) {\r
- var point = this,\r
- series = point.series,\r
- chart = series.chart,\r
- data = series.data;\r
- \r
- setAnimation(animation, chart);\r
- redraw = pick(redraw, true);\r
- \r
- // fire the event with a default handler of removing the point \r
- point.firePointEvent('remove', null, function() {\r
-\r
- erase(data, point);\r
- \r
- point.destroy();\r
- \r
- \r
- // redraw\r
- series.isDirty = true;\r
- if (redraw) {\r
- chart.redraw();\r
- }\r
- });\r
- \r
- \r
- },\r
- \r
- /**\r
- * Fire an event on the Point object. Must not be renamed to fireEvent, as this\r
- * causes a name clash in MooTools\r
- * @param {String} eventType\r
- * @param {Object} eventArgs Additional event arguments\r
- * @param {Function} defaultFunction Default event handler\r
- */\r
- firePointEvent: function(eventType, eventArgs, defaultFunction) {\r
- var point = this,\r
- series = this.series,\r
- seriesOptions = series.options;\r
- \r
- // load event handlers on demand to save time on mouseover/out\r
- if (seriesOptions.point.events[eventType] || (\r
- point.options && point.options.events && point.options.events[eventType])) {\r
- this.importEvents();\r
- }\r
- \r
- // add default handler if in selection mode\r
- if (eventType === 'click' && seriesOptions.allowPointSelect) {\r
- defaultFunction = function (event) {\r
- // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera\r
- point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);\r
- };\r
- }\r
- \r
- fireEvent(this, eventType, eventArgs, defaultFunction);\r
- },\r
- /**\r
- * Import events from the series' and point's options. Only do it on \r
- * demand, to save processing time on hovering.\r
- */\r
- importEvents: function() {\r
- if (!this.hasImportedEvents) {\r
- var point = this,\r
- options = merge(point.series.options.point, point.options),\r
- events = options.events,\r
- eventType;\r
- \r
- point.events = events;\r
- \r
- for (eventType in events) {\r
- addEvent(point, eventType, events[eventType]);\r
- }\r
- this.hasImportedEvents = true;\r
- \r
- }\r
- },\r
- \r
- /**\r
- * Set the point's state\r
- * @param {String} state\r
- */\r
- setState: function(state) {\r
- var point = this,\r
- series = point.series,\r
- stateOptions = series.options.states,\r
- markerOptions = defaultPlotOptions[series.type].marker && series.options.marker,\r
- normalDisabled = markerOptions && !markerOptions.enabled,\r
- markerStateOptions = markerOptions && markerOptions.states[state],\r
- stateDisabled = markerStateOptions && markerStateOptions.enabled === false,\r
- stateMarkerGraphic = series.stateMarkerGraphic,\r
- chart = series.chart,\r
- pointAttr = point.pointAttr;\r
- \r
- state = state || NORMAL_STATE; // empty string\r
- \r
- if (\r
- // already has this state\r
- state === point.state ||\r
- // selected points don't respond to hover\r
- (point.selected && state !== SELECT_STATE) ||\r
- // series' state options is disabled\r
- (stateOptions[state] && stateOptions[state].enabled === false) ||\r
- // point marker's state options is disabled\r
- (state && (stateDisabled || (normalDisabled && !markerStateOptions.enabled)))\r
-\r
- ) {\r
- return;\r
- }\r
- \r
- // apply hover styles to the existing point\r
- if (point.graphic) {\r
- point.graphic.attr(pointAttr[state]);\r
- }\r
- // if a graphic is not applied to each point in the normal state, create a shared\r
- // graphic for the hover state\r
- else {\r
- if (state) {\r
- if (!stateMarkerGraphic) {\r
- series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.circle(\r
- 0, 0, pointAttr[state].r\r
- )\r
- .attr(pointAttr[state])\r
- .add(series.group);\r
- }\r
- \r
- stateMarkerGraphic.translate(\r
- point.plotX, \r
- point.plotY\r
- );\r
- }\r
- \r
- if (stateMarkerGraphic) {\r
- stateMarkerGraphic[state ? 'show' : 'hide']();\r
- }\r
- }\r
- \r
- point.state = state;\r
- }\r
-};\r
-\r
-/**\r
- * The base function which all other series types inherit from\r
- * @param {Object} chart\r
- * @param {Object} options\r
- */\r
-var Series = function() {};\r
-\r
-Series.prototype = {\r
- \r
- isCartesian: true,\r
- type: 'line',\r
- pointClass: Point,\r
- pointAttrToOptions: { // mapping between SVG attributes and the corresponding options\r
- stroke: 'lineColor',\r
- 'stroke-width': 'lineWidth',\r
- fill: 'fillColor',\r
- r: 'radius'\r
- },\r
- init: function(chart, options) {\r
- var series = this,\r
- eventType,\r
- events,\r
- //pointEvent,\r
- index = chart.series.length;\r
- \r
- series.chart = chart;\r
- options = series.setOptions(options); // merge with plotOptions\r
- \r
- // set some variables\r
- extend(series, {\r
- index: index,\r
- options: options,\r
- name: options.name || 'Series '+ (index + 1),\r
- state: NORMAL_STATE,\r
- pointAttr: {},\r
- visible: options.visible !== false, // true by default\r
- selected: options.selected === true // false by default\r
- });\r
- \r
- // register event listeners\r
- events = options.events;\r
- for (eventType in events) {\r
- addEvent(series, eventType, events[eventType]);\r
- }\r
- if (\r
- (events && events.click) || \r
- (options.point && options.point.events && options.point.events.click) ||\r
- options.allowPointSelect \r
- ) {\r
- chart.runTrackerClick = true;\r
- }\r
- \r
- series.getColor();\r
- series.getSymbol();\r
- \r
- \r
- // set the data\r
- series.setData(options.data, false);\r
- \r
- },\r
- \r
- \r
- /**\r
- * Return an auto incremented x value based on the pointStart and pointInterval options. \r
- * This is only used if an x value is not given for the point that calls autoIncrement.\r
- */\r
- autoIncrement: function() {\r
- var series = this,\r
- options = series.options,\r
- xIncrement = series.xIncrement;\r
- \r
- xIncrement = pick(xIncrement, options.pointStart, 0);\r
- \r
- series.pointInterval = pick(series.pointInterval, options.pointInterval, 1);\r
- \r
- series.xIncrement = xIncrement + series.pointInterval;\r
- return xIncrement;\r
- },\r
- \r
- /**\r
- * Sort the data and remove duplicates \r
- */\r
- cleanData: function() {\r
- var series = this,\r
- chart = series.chart,\r
- data = series.data,\r
- closestPoints,\r
- smallestInterval,\r
- chartSmallestInterval = chart.smallestInterval,\r
- interval,\r
- i;\r
- \r
- // sort the data points\r
- data.sort(function(a, b){\r
- return (a.x - b.x);\r
- });\r
- \r
- // remove points with equal x values\r
- // record the closest distance for calculation of column widths\r
- /*for (i = data.length - 1; i >= 0; i--) {\r
- if (data[i - 1]) {\r
- if (data[i - 1].x == data[i].x) {\r
- data[i - 1].destroy();\r
- data.splice(i - 1, 1); // remove the duplicate\r
- }\r
- }\r
- }*/\r
- \r
- // connect nulls\r
- if (series.options.connectNulls) {\r
- for (i = data.length - 1; i >= 0; i--) {\r
- if (data[i].y === null && data[i - 1] && data[i + 1]) {\r
- data.splice(i, 1);\r
- }\r
- }\r
- }\r
- \r
- // find the closes pair of points\r
- for (i = data.length - 1; i >= 0; i--) {\r
- if (data[i - 1]) {\r
- interval = data[i].x - data[i - 1].x;\r
- if (interval > 0 && (smallestInterval === UNDEFINED || interval < smallestInterval)) {\r
- smallestInterval = interval;\r
- closestPoints = i; \r
- }\r
- }\r
- }\r
- \r
- if (chartSmallestInterval === UNDEFINED || smallestInterval < chartSmallestInterval) {\r
- chart.smallestInterval = smallestInterval;\r
- }\r
- series.closestPoints = closestPoints;\r
- }, \r
- \r
- /**\r
- * Divide the series data into segments divided by null values. Also sort\r
- * the data points and delete duplicate values.\r
- */\r
- getSegments: function() {\r
- var lastNull = -1,\r
- segments = [],\r
- data = this.data;\r
- \r
- // create the segments\r
- each(data, function(point, i) {\r
- if (point.y === null) {\r
- if (i > lastNull + 1) {\r
- segments.push(data.slice(lastNull + 1, i));\r
- }\r
- lastNull = i; \r
- } else if (i === data.length - 1) { // last value\r
- segments.push(data.slice(lastNull + 1, i + 1));\r
- }\r
- });\r
- this.segments = segments;\r
- \r
- \r
- },\r
- /**\r
- * Set the series options by merging from the options tree\r
- * @param {Object} itemOptions\r
- */\r
- setOptions: function(itemOptions) {\r
- var plotOptions = this.chart.options.plotOptions,\r
- options = merge(\r
- plotOptions[this.type],\r
- plotOptions.series,\r
- itemOptions\r
- );\r
- \r
- return options;\r
- \r
- },\r
- /**\r
- * Get the series' color\r
- */\r
- getColor: function(){\r
- var defaultColors = this.chart.options.colors,\r
- counters = this.chart.counters;\r
- this.color = this.options.color || defaultColors[counters.color++] || '#0000ff';\r
- counters.wrapColor(defaultColors.length);\r
- },\r
- /**\r
- * Get the series' symbol\r
- */\r
- getSymbol: function(){\r
- var defaultSymbols = this.chart.options.symbols,\r
- counters = this.chart.counters;\r
- this.symbol = this.options.marker.symbol || defaultSymbols[counters.symbol++];\r
- counters.wrapSymbol(defaultSymbols.length);\r
- },\r
- \r
- /**\r
- * Add a point dynamically after chart load time\r
- * @param {Object} options Point options as given in series.data\r
- * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call\r
- * @param {Boolean} shift If shift is true, a point is shifted off the start \r
- * of the series as one is appended to the end.\r
- * @param {Boolean|Object} animation Whether to apply animation, and optionally animation\r
- * configuration\r
- */\r
- addPoint: function(options, redraw, shift, animation) {\r
- var series = this,\r
- data = series.data,\r
- graph = series.graph,\r
- area = series.area,\r
- chart = series.chart,\r
- point = (new series.pointClass()).init(series, options);\r
- \r
- setAnimation(animation, chart);\r
- \r
- if (graph && shift) { // make graph animate sideways\r
- graph.shift = shift;\r
- }\r
- if (area) {\r
- area.shift = shift;\r
- area.isArea = true;\r
- }\r
- \r
- redraw = pick(redraw, true);\r
- \r
- data.push(point);\r
- if (shift) {\r
- data[0].remove(false);\r
- }\r
- series.getAttribs();\r
- \r
- \r
- // redraw\r
- series.isDirty = true;\r
- if (redraw) {\r
- chart.redraw();\r
- }\r
- },\r
- \r
- /**\r
- * Replace the series data with a new set of data\r
- * @param {Object} data\r
- * @param {Object} redraw\r
- */\r
- setData: function(data, redraw) {\r
- var series = this,\r
- oldData = series.data,\r
- initialColor = series.initialColor,\r
- chart = series.chart,\r
- i = (oldData && oldData.length) || 0;\r
- \r
- series.xIncrement = null; // reset for new data\r
- if (defined(initialColor)) { // reset colors for pie\r
- chart.counters.color = initialColor;\r
- }\r
- \r
- data = map(splat(data || []), function(pointOptions) {\r
- return (new series.pointClass()).init(series, pointOptions);\r
- });\r
- \r
- // destroy old points\r
- while (i--) {\r
- oldData[i].destroy();\r
- }\r
- \r
- // set the data\r
- series.data = data;\r
- \r
- series.cleanData(); \r
- series.getSegments();\r
- \r
- \r
- // cache attributes for shapes\r
- series.getAttribs();\r
- \r
- // redraw\r
- series.isDirty = true;\r
- chart.isDirtyBox = true;\r
- if (pick(redraw, true)) {\r
- chart.redraw(false);\r
- }\r
- },\r
- \r
- /**\r
- * Remove a series and optionally redraw the chart\r
- * \r
- * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call\r
- * @param {Boolean|Object} animation Whether to apply animation, and optionally animation\r
- * configuration\r
- */\r
- \r
- remove: function(redraw, animation) {\r
- var series = this,\r
- chart = series.chart;\r
- redraw = pick(redraw, true);\r
- \r
- if (!series.isRemoving) { /* prevent triggering native event in jQuery\r
- (calling the remove function from the remove event) */ \r
- series.isRemoving = true;\r
-\r
- // fire the event with a default handler of removing the point \r
- fireEvent(series, 'remove', null, function() {\r
- \r
- \r
- // destroy elements\r
- series.destroy();\r
- \r
- \r
- // redraw\r
- chart.isDirtyLegend = chart.isDirtyBox = true;\r
- if (redraw) {\r
- chart.redraw(animation);\r
- }\r
- });\r
- \r
- } \r
- series.isRemoving = false;\r
- },\r
- \r
- /**\r
- * Translate data points from raw data values to chart specific positioning data\r
- * needed later in drawPoints, drawGraph and drawTracker. \r
- */\r
- translate: function() {\r
- var series = this, \r
- chart = series.chart, \r
- stacking = series.options.stacking,\r
- categories = series.xAxis.categories,\r
- yAxis = series.yAxis,\r
- data = series.data, \r
- i = data.length;\r
- \r
- // do the translation\r
- while (i--) {\r
- var point = data[i],\r
- xValue = point.x, \r
- yValue = point.y, \r
- yBottom = point.low,\r
- stack = yAxis.stacks[(yValue < 0 ? '-' : '') + series.stackKey],\r
- pointStack,\r
- pointStackTotal;\r
- point.plotX = series.xAxis.translate(xValue);\r
- \r
- // calculate the bottom y value for stacked series\r
- if (stacking && series.visible && stack && stack[xValue]) {\r
- pointStack = stack[xValue];\r
- pointStackTotal = pointStack.total;\r
- pointStack.cum = yBottom = pointStack.cum - yValue; // start from top\r
- yValue = yBottom + yValue;\r
- \r
- if (stacking === 'percent') {\r
- yBottom = pointStackTotal ? yBottom * 100 / pointStackTotal : 0;\r
- yValue = pointStackTotal ? yValue * 100 / pointStackTotal : 0;\r
- }\r
-\r
- point.percentage = pointStackTotal ? point.y * 100 / pointStackTotal : 0;\r
- point.stackTotal = pointStackTotal;\r
- }\r
- \r
- if (defined(yBottom)) {\r
- point.yBottom = yAxis.translate(yBottom, 0, 1, 0, 1);\r
- }\r
- \r
- // set the y value\r
- if (yValue !== null) {\r
- point.plotY = yAxis.translate(yValue, 0, 1, 0, 1);\r
- }\r
- \r
- // set client related positions for mouse tracking\r
- point.clientX = chart.inverted ? \r
- chart.plotHeight - point.plotX : \r
- point.plotX; // for mouse tracking\r
- \r
- // some API data\r
- point.category = categories && categories[point.x] !== UNDEFINED ? \r
- categories[point.x] : point.x;\r
- \r
- }\r
- },\r
- /**\r
- * Memoize tooltip texts and positions\r
- */\r
- setTooltipPoints: function (renew) {\r
- var series = this,\r
- chart = series.chart,\r
- inverted = chart.inverted,\r
- data = [],\r
- plotSize = mathRound((inverted ? chart.plotTop : chart.plotLeft) + chart.plotSizeX),\r
- low,\r
- high,\r
- tooltipPoints = []; // a lookup array for each pixel in the x dimension\r
- \r
- // renew\r
- if (renew) {\r
- series.tooltipPoints = null;\r
- }\r
- \r
- // concat segments to overcome null values\r
- each(series.segments, function(segment){\r
- data = data.concat(segment);\r
- });\r
- \r
- // loop the concatenated data and apply each point to all the closest\r
- // pixel positions\r
- if (series.xAxis && series.xAxis.reversed) {\r
- data = data.reverse();//reverseArray(data);\r
- }\r
- \r
- each(data, function(point, i) {\r
- \r
- low = data[i - 1] ? data[i - 1]._high + 1 : 0;\r
- high = point._high = data[i + 1] ? (\r
- mathFloor((point.plotX + (data[i + 1] ? \r
- data[i + 1].plotX : plotSize)) / 2)) :\r
- plotSize;\r
- \r
- while (low <= high) {\r
- tooltipPoints[inverted ? plotSize - low++ : low++] = point;\r
- }\r
- });\r
- series.tooltipPoints = tooltipPoints;\r
- },\r
- \r
- \r
-\r
- \r
- /**\r
- * Series mouse over handler\r
- */\r
- onMouseOver: function() {\r
- var series = this,\r
- chart = series.chart,\r
- hoverSeries = chart.hoverSeries;\r
- \r
- if (!hasTouch && chart.mouseIsDown) {\r
- return;\r
- }\r
- \r
- // set normal state to previous series\r
- if (hoverSeries && hoverSeries !== series) {\r
- hoverSeries.onMouseOut();\r
- }\r
- \r
- // trigger the event, but to save processing time, \r
- // only if defined\r
- if (series.options.events.mouseOver) { \r
- fireEvent(series, 'mouseOver');\r
- }\r
- \r
- \r
- // bring to front\r
- // Todo: optimize. This is one of two operations slowing down the tooltip in Firefox.\r
- // Can the tracking be done otherwise?\r
- if (series.tracker) {\r
- series.tracker.toFront();\r
- }\r
- \r
- // hover this\r
- series.setState(HOVER_STATE);\r
- chart.hoverSeries = series;\r
- },\r
- \r
- /**\r
- * Series mouse out handler\r
- */\r
- onMouseOut: function() {\r
- // trigger the event only if listeners exist\r
- var series = this,\r
- options = series.options,\r
- chart = series.chart,\r
- tooltip = chart.tooltip,\r
- hoverPoint = chart.hoverPoint;\r
- \r
- // trigger mouse out on the point, which must be in this series\r
- if (hoverPoint) {\r
- hoverPoint.onMouseOut();\r
- } \r
- \r
- // fire the mouse out event\r
- if (series && options.events.mouseOut) { \r
- fireEvent(series, 'mouseOut');\r
- }\r
- \r
- \r
- // hide the tooltip\r
- if (tooltip && !options.stickyTracking) {\r
- tooltip.hide();\r
- }\r
- \r
- // set normal state\r
- series.setState();\r
- chart.hoverSeries = null; \r
- },\r
- \r
- /**\r
- * Animate in the series\r
- */\r
- animate: function(init) {\r
- var series = this,\r
- chart = series.chart,\r
- clipRect = series.clipRect,\r
- animation = series.options.animation;\r
- \r
- if (animation && !isObject(animation)) {\r
- animation = {};\r
- }\r
- \r
- if (init) { // initialize the animation\r
- if (!clipRect.isAnimating) { // apply it only for one of the series\r
- clipRect.attr( 'width', 0 );\r
- clipRect.isAnimating = true;\r
- }\r
- \r
- } else { // run the animation\r
- clipRect.animate({ \r
- width: chart.plotSizeX \r
- }, animation);\r
- \r
- // delete this function to allow it only once\r
- this.animate = null;\r
- }\r
- },\r
- \r
- \r
- /**\r
- * Draw the markers\r
- */\r
- drawPoints: function(){\r
- var series = this,\r
- pointAttr,\r
- data = series.data, \r
- chart = series.chart,\r
- plotX,\r
- plotY,\r
- i,\r
- point,\r
- radius,\r
- graphic;\r
- \r
- if (series.options.marker.enabled) {\r
- i = data.length;\r
- while (i--) {\r
- point = data[i];\r
- plotX = point.plotX;\r
- plotY = point.plotY;\r
- graphic = point.graphic;\r
- \r
- // only draw the point if y is defined\r
- if (plotY !== UNDEFINED && !isNaN(plotY)) {\r
- \r
- /* && removed this code because points stayed after zoom\r
- point.plotX >= 0 && point.plotX <= chart.plotSizeX &&\r
- point.plotY >= 0 && point.plotY <= chart.plotSizeY*/\r
- \r
- // shortcuts\r
- pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE];\r
- radius = pointAttr.r;\r
- \r
- if (graphic) { // update\r
- graphic.animate({\r
- x: plotX,\r
- y: plotY,\r
- r: radius\r
- });\r
- } else {\r
- point.graphic = chart.renderer.symbol(\r
- pick(point.marker && point.marker.symbol, series.symbol),\r
- plotX,\r
- plotY, \r
- radius\r
- )\r
- .attr(pointAttr)\r
- .add(series.group);\r
- }\r
- }\r
- }\r
- }\r
- \r
- },\r
- \r
- /**\r
- * Convert state properties from API naming conventions to SVG attributes\r
- * \r
- * @param {Object} options API options object\r
- * @param {Object} base1 SVG attribute object to inherit from\r
- * @param {Object} base2 Second level SVG attribute object to inherit from\r
- */\r
- convertAttribs: function(options, base1, base2, base3) {\r
- var conversion = this.pointAttrToOptions,\r
- attr,\r
- option,\r
- obj = {};\r
- \r
- options = options || {};\r
- base1 = base1 || {};\r
- base2 = base2 || {};\r
- base3 = base3 || {};\r
- \r
- for (attr in conversion) {\r
- option = conversion[attr];\r
- obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]); \r
- }\r
- return obj;\r
- },\r
- \r
- /**\r
- * Get the state attributes. Each series type has its own set of attributes\r
- * that are allowed to change on a point's state change. Series wide attributes are stored for\r
- * all series, and additionally point specific attributes are stored for all \r
- * points with individual marker options. If such options are not defined for the point,\r
- * a reference to the series wide attributes is stored in point.pointAttr.\r
- */\r
- getAttribs: function() {\r
- var series = this, \r
- normalOptions = defaultPlotOptions[series.type].marker ? series.options.marker : series.options,\r
- stateOptions = normalOptions.states,\r
- stateOptionsHover = stateOptions[HOVER_STATE],\r
- pointStateOptionsHover,\r
- seriesColor = series.color,\r
- normalDefaults = {\r
- stroke: seriesColor,\r
- fill: seriesColor\r
- },\r
- data = series.data,\r
- i,\r
- point,\r
- seriesPointAttr = [],\r
- pointAttr,\r
- pointAttrToOptions = series.pointAttrToOptions,\r
- hasPointSpecificOptions,\r
- key;\r
- \r
- // series type specific modifications\r
- if (series.options.marker) { // line, spline, area, areaspline, scatter\r
- \r
- // if no hover radius is given, default to normal radius + 2 \r
- stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + 2;\r
- stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + 1;\r
- \r
- } else { // column, bar, pie\r
- \r
- // if no hover color is given, brighten the normal color\r
- stateOptionsHover.color = stateOptionsHover.color || \r
- Color(stateOptionsHover.color || seriesColor)\r
- .brighten(stateOptionsHover.brightness).get();\r
- }\r
- \r
- // general point attributes for the series normal state\r
- seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults);\r
- \r
- // HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius\r
- each([HOVER_STATE, SELECT_STATE], function(state) {\r
- seriesPointAttr[state] = \r
- series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]);\r
- });\r
- \r
- // set it\r
- series.pointAttr = seriesPointAttr;\r
- \r
- \r
- // Generate the point-specific attribute collections if specific point\r
- // options are given. If not, create a referance to the series wide point \r
- // attributes\r
- i = data.length;\r
- while (i--) {\r
- point = data[i];\r
- normalOptions = (point.options && point.options.marker) || point.options;\r
- if (normalOptions && normalOptions.enabled === false) {\r
- normalOptions.radius = 0;\r
- }\r
- hasPointSpecificOptions = false;\r
- \r
- // check if the point has specific visual options\r
- if (point.options) {\r
- for (key in pointAttrToOptions) {\r
- if (defined(normalOptions[pointAttrToOptions[key]])) {\r
- hasPointSpecificOptions = true;\r
- }\r
- }\r
- }\r
- \r
- \r
- \r
- // a specific marker config object is defined for the individual point:\r
- // create it's own attribute collection\r
- if (hasPointSpecificOptions) {\r
-\r
- pointAttr = [];\r
- stateOptions = normalOptions.states || {}; // reassign for individual point\r
- pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {};\r
- \r
- // if no hover color is given, brighten the normal color\r
- if (!series.options.marker) { // column, bar, point\r
- pointStateOptionsHover.color = \r
- Color(pointStateOptionsHover.color || point.options.color)\r
- .brighten(pointStateOptionsHover.brightness || \r
- stateOptionsHover.brightness).get();\r
- \r
- }\r
- \r
- // normal point state inherits series wide normal state\r
- pointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, seriesPointAttr[NORMAL_STATE]);\r
- \r
- // inherit from point normal and series hover\r
- pointAttr[HOVER_STATE] = series.convertAttribs(\r
- stateOptions[HOVER_STATE],\r
- seriesPointAttr[HOVER_STATE],\r
- pointAttr[NORMAL_STATE]\r
- );\r
- // inherit from point normal and series hover\r
- pointAttr[SELECT_STATE] = series.convertAttribs(\r
- stateOptions[SELECT_STATE],\r
- seriesPointAttr[SELECT_STATE],\r
- pointAttr[NORMAL_STATE]\r
- );\r
- \r
- \r
- \r
- // no marker config object is created: copy a reference to the series-wide\r
- // attribute collection\r
- } else {\r
- pointAttr = seriesPointAttr;\r
- }\r
- \r
- point.pointAttr = pointAttr;\r
-\r
- }\r
-\r
- },\r
-\r
- \r
- /**\r
- * Clear DOM objects and free up memory\r
- */\r
- destroy: function() {\r
- var series = this,\r
- chart = series.chart,\r
- //chartSeries = series.chart.series,\r
- clipRect = series.clipRect,\r
- issue134 = /\/5[0-9\.]+ (Safari|Mobile)\//.test(userAgent), // todo: update when Safari bug is fixed\r
- destroy,\r
- prop;\r
- \r
- // add event hook\r
- fireEvent(series, 'destroy');\r
- \r
- // remove all events\r
- removeEvent(series);\r
- \r
- // remove legend items\r
- if (series.legendItem) {\r
- series.chart.legend.destroyItem(series);\r
- }\r
- \r
- // destroy all points with their elements\r
- each(series.data, function(point) {\r
- point.destroy();\r
- });\r
- // destroy all SVGElements associated to the series\r
- each(['area', 'graph', 'dataLabelsGroup', 'group', 'tracker'], function(prop) {\r
- if (series[prop]) {\r
- \r
- // issue 134 workaround\r
- destroy = issue134 && prop === 'group' ?\r
- 'hide' :\r
- 'destroy';\r
- \r
- series[prop][destroy]();\r
- }\r
- });\r
- \r
- // remove from hoverSeries\r
- if (chart.hoverSeries === series) {\r
- chart.hoverSeries = null;\r
- }\r
- erase(chart.series, series);\r
- \r
- // clear all members\r
- for (prop in series) {\r
- delete series[prop];\r
- } \r
- },\r
- \r
- /**\r
- * Draw the data labels\r
- */\r
- drawDataLabels: function() {\r
- if (this.options.dataLabels.enabled) {\r
- var series = this,\r
- x, \r
- y, \r
- data = series.data, \r
- options = series.options.dataLabels,\r
- str, \r
- dataLabelsGroup = series.dataLabelsGroup, \r
- chart = series.chart, \r
- inverted = chart.inverted,\r
- seriesType = series.type,\r
- color,\r
- stacking = series.options.stacking,\r
- isBarLike = seriesType === 'column' || seriesType === 'bar',\r
- vAlignIsNull = options.verticalAlign === null,\r
- yIsNull = options.y === null;\r
-\r
- if (isBarLike) {\r
- if (stacking) {\r
- // In stacked series the default label placement is inside the bars\r
- if (vAlignIsNull) {\r
- options = merge(options, {verticalAlign: 'middle'});\r
- }\r
-\r
- // If no y delta is specified, try to create a good default\r
- if (yIsNull) {\r
- options = merge(options, {y: {top: 14, middle: 4, bottom: -6}[options.verticalAlign]}); \r
- }\r
- } else {\r
- // In non stacked series the default label placement is on top of the bars\r
- if (vAlignIsNull) {\r
- options = merge(options, {verticalAlign: 'top'});\r
- }\r
- }\r
- }\r
-\r
- // create a separate group for the data labels to avoid rotation\r
- if (!dataLabelsGroup) {\r
- dataLabelsGroup = series.dataLabelsGroup = \r
- chart.renderer.g('data-labels')\r
- .attr({ \r
- visibility: series.visible ? VISIBLE : HIDDEN,\r
- zIndex: 6 \r
- })\r
- .translate(chart.plotLeft, chart.plotTop)\r
- .add();\r
- }\r
- \r
- // determine the color\r
- color = options.color;\r
- if (color === 'auto') { // 1.0 backwards compatibility\r
- color = null; \r
- }\r
- options.style.color = pick(color, series.color);\r
- \r
- // make the labels for each point\r
- each(data, function(point, i){\r
- var barX = point.barX,\r
- plotX = (barX && barX + point.barW / 2) || point.plotX || -999,\r
- plotY = pick(point.plotY, -999),\r
- dataLabel = point.dataLabel,\r
- align = options.align,\r
- individualYDelta = yIsNull ? (point.y > 0 ? -6 : 12) : options.y;\r
-\r
- // get the string\r
- str = options.formatter.call(point.getLabelConfig());\r
- x = (inverted ? chart.plotWidth - plotY : plotX) + options.x;\r
- y = (inverted ? chart.plotHeight - plotX : plotY) + individualYDelta;\r
- \r
- // in columns, align the string to the column\r
- if (seriesType === 'column') {\r
- x += { left: -1, right: 1 }[align] * point.barW / 2 || 0;\r
- }\r
- \r
- if (inverted && point.y < 0) {\r
- align = 'right';\r
- x -= 10;\r
- }\r
-\r
- // update existing label\r
- if (dataLabel) {\r
- // vertically centered\r
- if (inverted && !options.y) {\r
- y = y + pInt(dataLabel.styles.lineHeight) * 0.9 - dataLabel.getBBox().height / 2;\r
- }\r
- dataLabel\r
- .attr({\r
- text: str\r
- }).animate({\r
- x: x,\r
- y: y\r
- });\r
- // create new label\r
- } else if (defined(str)) {\r
- dataLabel = point.dataLabel = chart.renderer.text(\r
- str, \r
- x, \r
- y\r
- )\r
- .attr({\r
- align: align,\r
- rotation: options.rotation,\r
- zIndex: 1\r
- })\r
- .css(options.style)\r
- .add(dataLabelsGroup);\r
- // vertically centered\r
- if (inverted && !options.y) {\r
- dataLabel.attr({\r
- y: y + pInt(dataLabel.styles.lineHeight) * 0.9 - dataLabel.getBBox().height / 2\r
- });\r
- }\r
- }\r
- \r
- \r
- /*if (series.isCartesian) {\r
- dataLabel[chart.isInsidePlot(plotX, plotY) ? 'show' : 'hide']();\r
- }*/\r
-\r
- if (isBarLike && series.options.stacking) {\r
- var barY = point.barY,\r
- barW = point.barW,\r
- barH = point.barH;\r
-\r
- dataLabel.align(options, null, \r
- {\r
- x: inverted ? chart.plotWidth - barY - barH : barX,\r
- y: inverted ? chart.plotHeight - barX - barW : barY,\r
- width: inverted ? barH : barW,\r
- height: inverted ? barW : barH\r
- });\r
- }\r
- });\r
- }\r
- },\r
- \r
- /**\r
- * Draw the actual graph\r
- */\r
- drawGraph: function(state) {\r
- var series = this, \r
- options = series.options, \r
- chart = series.chart,\r
- graph = series.graph,\r
- graphPath = [],\r
- fillColor,\r
- area = series.area,\r
- group = series.group,\r
- color = options.lineColor || series.color, \r
- lineWidth = options.lineWidth,\r
- dashStyle = options.dashStyle,\r
- segmentPath,\r
- renderer = chart.renderer,\r
- translatedThreshold = series.yAxis.getThreshold(options.threshold || 0),\r
- useArea = /^area/.test(series.type),\r
- singlePoints = [], // used in drawTracker\r
- areaPath = [],\r
- attribs;\r
- \r
- \r
- // divide into segments and build graph and area paths\r
- each(series.segments, function(segment) {\r
- segmentPath = [];\r
- \r
- // build the segment line\r
- each(segment, function(point, i) {\r
-\r
- if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object\r
- segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i));\r
- \r
- } else {\r
- \r
- // moveTo or lineTo\r
- segmentPath.push(i ? L : M);\r
- \r
- // step line?\r
- if (i && options.step) {\r
- var lastPoint = segment[i - 1];\r
- segmentPath.push(\r
- point.plotX, \r
- lastPoint.plotY \r
- );\r
- }\r
- \r
- // normal line to next point\r
- segmentPath.push(\r
- point.plotX, \r
- point.plotY\r
- );\r
- }\r
- });\r
- \r
- // add the segment to the graph, or a single point for tracking\r
- if (segment.length > 1) {\r
- graphPath = graphPath.concat(segmentPath);\r
- } else {\r
- singlePoints.push(segment[0]);\r
- }\r
- \r
- // build the area\r
- if (useArea) {\r
- var areaSegmentPath = [],\r
- i,\r
- segLength = segmentPath.length;\r
- for (i = 0; i < segLength; i++) {\r
- areaSegmentPath.push(segmentPath[i]);\r
- }\r
- if (segLength === 3) { // for animation from 1 to two points\r
- areaSegmentPath.push(L, segmentPath[1], segmentPath[2]);\r
- }\r
- if (options.stacking && series.type !== 'areaspline') {\r
- // follow stack back. Todo: implement areaspline\r
- for (i = segment.length - 1; i >= 0; i--) {\r
- areaSegmentPath.push(segment[i].plotX, segment[i].yBottom);\r
- }\r
- \r
- } else { // follow zero line back\r
- areaSegmentPath.push(\r
- L,\r
- segment[segment.length - 1].plotX, \r
- translatedThreshold,\r
- L,\r
- segment[0].plotX, \r
- translatedThreshold\r
- );\r
- }\r
- areaPath = areaPath.concat(areaSegmentPath);\r
- }\r
- });\r
-\r
- // used in drawTracker:\r
- series.graphPath = graphPath;\r
- series.singlePoints = singlePoints;\r
-\r
- // draw the area if area series or areaspline\r
- if (useArea) {\r
- fillColor = pick(\r
- options.fillColor,\r
- Color(series.color).setOpacity(options.fillOpacity || 0.75).get()\r
- );\r
- if (area) {\r
- area.animate({ d: areaPath });\r
- \r
- } else {\r
- // draw the area\r
- series.area = series.chart.renderer.path(areaPath)\r
- .attr({\r
- fill: fillColor\r
- }).add(group);\r
- }\r
- }\r
- \r
- // draw the graph\r
- if (graph) {\r
- //graph.animate({ d: graphPath.join(' ') });\r
- graph.animate({ d: graphPath });\r
- \r
- } else {\r
- if (lineWidth) {\r
- attribs = {\r
- 'stroke': color,\r
- 'stroke-width': lineWidth\r
- };\r
- if (dashStyle) {\r
- attribs.dashstyle = dashStyle;\r
- }\r
- \r
- series.graph = renderer.path(graphPath)\r
- .attr(attribs).add(group).shadow(options.shadow);\r
- }\r
- }\r
- },\r
- \r
- \r
- /**\r
- * Render the graph and markers\r
- */\r
- render: function() {\r
- var series = this,\r
- chart = series.chart,\r
- group,\r
- setInvert,\r
- options = series.options,\r
- animation = options.animation,\r
- doAnimation = animation && series.animate,\r
- duration = doAnimation ? (animation && animation.duration) || 500 : 0,\r
- clipRect = series.clipRect,\r
- renderer = chart.renderer;\r
- \r
- \r
- // Add plot area clipping rectangle. If this is before chart.hasRendered,\r
- // create one shared clipRect. \r
- if (!clipRect) {\r
- clipRect = series.clipRect = !chart.hasRendered && chart.clipRect ?\r
- chart.clipRect : \r
- renderer.clipRect(0, 0, chart.plotSizeX, chart.plotSizeY);\r
- if (!chart.clipRect) {\r
- chart.clipRect = clipRect;\r
- }\r
- }\r
- \r
- \r
- // the group\r
- if (!series.group) {\r
- group = series.group = renderer.g('series');\r
- \r
- if (chart.inverted) {\r
- setInvert = function() {\r
- group.attr({\r
- width: chart.plotWidth,\r
- height: chart.plotHeight\r
- }).invert();\r
- };\r
- \r
- setInvert(); // do it now\r
- addEvent(chart, 'resize', setInvert); // do it on resize\r
- addEvent(series, 'destroy', function() {\r
- removeEvent(chart, 'resize', setInvert);\r
- });\r
- } \r
- group.clip(series.clipRect)\r
- .attr({ \r
- visibility: series.visible ? VISIBLE : HIDDEN,\r
- zIndex: options.zIndex\r
- })\r
- .translate(chart.plotLeft, chart.plotTop)\r
- .add(chart.seriesGroup);\r
- }\r
- \r
- series.drawDataLabels();\r
-\r
- // initiate the animation\r
- if (doAnimation) {\r
- series.animate(true);\r
- }\r
- \r
- // cache attributes for shapes\r
- //series.getAttribs();\r
- \r
- // draw the graph if any\r
- if (series.drawGraph) {\r
- series.drawGraph();\r
- }\r
- \r
- // draw the points\r
- series.drawPoints();\r
- \r
- // draw the mouse tracking area\r
- if (series.options.enableMouseTracking !== false) {\r
- series.drawTracker();\r
- }\r
- \r
- // run the animation\r
- if (doAnimation) {\r
- series.animate();\r
- }\r
- \r
- // finish the individual clipRect\r
- setTimeout(function() {\r
- clipRect.isAnimating = false;\r
- group = series.group; // can be destroyed during the timeout\r
- if (group && clipRect !== chart.clipRect && clipRect.renderer) {\r
- group.clip((series.clipRect = chart.clipRect));\r
- clipRect.destroy();\r
- }\r
- }, duration);\r
- \r
- \r
- series.isDirty = false; // means data is in accordance with what you see\r
- \r
- },\r
- \r
- /**\r
- * Redraw the series after an update in the axes.\r
- */\r
- redraw: function() {\r
- var series = this,\r
- chart = series.chart,\r
- clipRect = series.clipRect,\r
- group = series.group;\r
- \r
- /*if (clipRect) {\r
- stop(clipRect);\r
- clipRect.animate({ // for chart resize\r
- width: chart.plotSizeX,\r
- height: chart.plotSizeY\r
- });\r
- }*/\r
- \r
- // reposition on resize\r
- if (group) {\r
- if (chart.inverted) {\r
- group.attr({\r
- width: chart.plotWidth,\r
- height: chart.plotHeight\r
- });\r
- }\r
- \r
- group.animate({\r
- translateX: chart.plotLeft, \r
- translateY: chart.plotTop\r
- });\r
- }\r
- \r
- series.translate();\r
- series.setTooltipPoints(true);\r
- series.render();\r
- },\r
- \r
- /**\r
- * Set the state of the graph\r
- */\r
- setState: function(state) {\r
- var series = this,\r
- options = series.options,\r
- graph = series.graph,\r
- stateOptions = options.states,\r
- lineWidth = options.lineWidth;\r
-\r
- state = state || NORMAL_STATE;\r
- \r
- if (series.state !== state) {\r
- series.state = state;\r
- \r
- if (stateOptions[state] && stateOptions[state].enabled === false) {\r
- return;\r
- }\r
- \r
- if (state) {\r
- lineWidth = stateOptions[state].lineWidth || lineWidth + 1;\r
- }\r
- \r
- if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML\r
- graph.attr({ // use attr because animate will cause any other animation on the graph to stop\r
- 'stroke-width': lineWidth\r
- }, state ? 0 : 500);\r
- }\r
- }\r
- },\r
- \r
- /**\r
- * Set the visibility of the graph\r
- * \r
- * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED,\r
- * the visibility is toggled.\r
- */\r
- setVisible: function(vis, redraw) {\r
- var series = this,\r
- chart = series.chart,\r
- legendItem = series.legendItem,\r
- seriesGroup = series.group,\r
- seriesTracker = series.tracker,\r
- dataLabelsGroup = series.dataLabelsGroup,\r
- showOrHide,\r
- i,\r
- data = series.data,\r
- point,\r
- ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,\r
- oldVisibility = series.visible;\r
- \r
- // if called without an argument, toggle visibility\r
- series.visible = vis = vis === UNDEFINED ? !oldVisibility : vis;\r
- showOrHide = vis ? 'show' : 'hide';\r
- \r
- // show or hide series\r
- if (seriesGroup) { // pies don't have one\r
- seriesGroup[showOrHide]();\r
- }\r
- \r
- // show or hide trackers\r
- if (seriesTracker) {\r
- seriesTracker[showOrHide]();\r
- } else {\r
- i = data.length;\r
- while (i--) {\r
- point = data[i];\r
- if (point.tracker) {\r
- point.tracker[showOrHide]();\r
- }\r
- }\r
- }\r
- \r
- \r
- if (dataLabelsGroup) {\r
- dataLabelsGroup[showOrHide]();\r
- }\r
- \r
- if (legendItem) {\r
- chart.legend.colorizeItem(series, vis);\r
- }\r
- \r
- \r
- // rescale or adapt to resized chart\r
- series.isDirty = true;\r
- // in a stack, all other series are affected\r
- if (series.options.stacking) {\r
- each(chart.series, function(otherSeries) {\r
- if (otherSeries.options.stacking && otherSeries.visible) { \r
- otherSeries.isDirty = true;\r
- }\r
- });\r
- }\r
- \r
- if (ignoreHiddenSeries) {\r
- chart.isDirtyBox = true;\r
- }\r
- if (redraw !== false) {\r
- chart.redraw();\r
- }\r
- \r
- fireEvent(series, showOrHide);\r
- },\r
- \r
- /**\r
- * Show the graph\r
- */\r
- show: function() {\r
- this.setVisible(true);\r
- },\r
- \r
- /**\r
- * Hide the graph\r
- */\r
- hide: function() {\r
- this.setVisible(false);\r
- },\r
- \r
- \r
- /**\r
- * Set the selected state of the graph\r
- * \r
- * @param selected {Boolean} True to select the series, false to unselect. If\r
- * UNDEFINED, the selection state is toggled.\r
- */\r
- select: function(selected) {\r
- var series = this;\r
- // if called without an argument, toggle\r
- series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected;\r
-\r
- if (series.checkbox) {\r
- series.checkbox.checked = selected;\r
- }\r
- \r
- fireEvent(series, selected ? 'select' : 'unselect');\r
- },\r
- \r
- \r
- /**\r
- * Draw the tracker object that sits above all data labels and markers to\r
- * track mouse events on the graph or points. For the line type charts\r
- * the tracker uses the same graphPath, but with a greater stroke width\r
- * for better control.\r
- */\r
- drawTracker: function() {\r
- var series = this,\r
- options = series.options,\r
- trackerPath = [].concat(series.graphPath),\r
- trackerPathLength = trackerPath.length,\r
- chart = series.chart,\r
- snap = chart.options.tooltip.snap,\r
- tracker = series.tracker,\r
- cursor = options.cursor,\r
- css = cursor && { cursor: cursor },\r
- singlePoints = series.singlePoints,\r
- singlePoint,\r
- i;\r
- \r
- // Extend end points. A better way would be to use round linecaps,\r
- // but those are not clickable in VML.\r
- if (trackerPathLength) {\r
- i = trackerPathLength + 1;\r
- while (i--) {\r
- if (trackerPath[i] === M) { // extend left side\r
- trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L);\r
- }\r
- if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side\r
- trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]);\r
- }\r
- }\r
- }\r
- \r
- // handle single points\r
- for (i = 0; i < singlePoints.length; i++) {\r
- singlePoint = singlePoints[i];\r
- trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,\r
- L, singlePoint.plotX + snap, singlePoint.plotY);\r
- }\r
- \r
- // draw the tracker\r
- if (tracker) {\r
- tracker.attr({ d: trackerPath });\r
- \r
- } else { // create\r
- series.tracker = chart.renderer.path(trackerPath)\r
- .attr({\r
- isTracker: true,\r
- stroke: TRACKER_FILL,\r
- fill: NONE,\r
- 'stroke-width' : options.lineWidth + 2 * snap,\r
- visibility: series.visible ? VISIBLE : HIDDEN,\r
- zIndex: 1\r
- })\r
- .on(hasTouch ? 'touchstart' : 'mouseover', function() {\r
- if (chart.hoverSeries !== series) {\r
- series.onMouseOver();\r
- }\r
- })\r
- .on('mouseout', function() {\r
- if (!options.stickyTracking) {\r
- series.onMouseOut();\r
- }\r
- })\r
- .css(css)\r
- .add(chart.trackerGroup);\r
- }\r
- \r
- }\r
- \r
-}; // end Series prototype\r
-\r
-\r
-/**\r
- * LineSeries object\r
- */\r
-var LineSeries = extendClass(Series);\r
-seriesTypes.line = LineSeries;\r
-\r
-/**\r
- * AreaSeries object\r
- */\r
-var AreaSeries = extendClass(Series, {\r
- type: 'area'\r
-});\r
-seriesTypes.area = AreaSeries;\r
-\r
-\r
-\r
-\r
-/**\r
- * SplineSeries object\r
- */\r
-var SplineSeries = extendClass( Series, {\r
- type: 'spline',\r
- \r
- /**\r
- * Draw the actual graph\r
- */\r
- getPointSpline: function(segment, point, i) {\r
- var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc\r
- denom = smoothing + 1,\r
- plotX = point.plotX,\r
- plotY = point.plotY,\r
- lastPoint = segment[i - 1],\r
- nextPoint = segment[i + 1],\r
- leftContX,\r
- leftContY,\r
- rightContX,\r
- rightContY,\r
- ret;\r
- \r
- // find control points\r
- if (i && i < segment.length - 1) {\r
- var lastX = lastPoint.plotX,\r
- lastY = lastPoint.plotY,\r
- nextX = nextPoint.plotX,\r
- nextY = nextPoint.plotY,\r
- correction;\r
- \r
- leftContX = (smoothing * plotX + lastX) / denom;\r
- leftContY = (smoothing * plotY + lastY) / denom;\r
- rightContX = (smoothing * plotX + nextX) / denom;\r
- rightContY = (smoothing * plotY + nextY) / denom;\r
- \r
- // have the two control points make a straight line through main point\r
- correction = ((rightContY - leftContY) * (rightContX - plotX)) / \r
- (rightContX - leftContX) + plotY - rightContY;\r
- \r
- leftContY += correction;\r
- rightContY += correction;\r
- \r
- // to prevent false extremes, check that control points are between\r
- // neighbouring points' y values\r
- if (leftContY > lastY && leftContY > plotY) {\r
- leftContY = mathMax(lastY, plotY);\r
- rightContY = 2 * plotY - leftContY; // mirror of left control point\r
- } else if (leftContY < lastY && leftContY < plotY) {\r
- leftContY = mathMin(lastY, plotY);\r
- rightContY = 2 * plotY - leftContY;\r
- } \r
- if (rightContY > nextY && rightContY > plotY) {\r
- rightContY = mathMax(nextY, plotY);\r
- leftContY = 2 * plotY - rightContY;\r
- } else if (rightContY < nextY && rightContY < plotY) {\r
- rightContY = mathMin(nextY, plotY);\r
- leftContY = 2 * plotY - rightContY;\r
- }\r
- \r
- // record for drawing in next point\r
- point.rightContX = rightContX;\r
- point.rightContY = rightContY;\r
- \r
- }\r
- \r
- // moveTo or lineTo\r
- if (!i) {\r
- ret = [M, plotX, plotY];\r
- }\r
- \r
- // curve from last point to this\r
- else {\r
- ret = [\r
- 'C',\r
- lastPoint.rightContX || lastPoint.plotX, \r
- lastPoint.rightContY || lastPoint.plotY,\r
- leftContX || plotX, \r
- leftContY || plotY,\r
- plotX, \r
- plotY\r
- ];\r
- lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later\r
- }\r
- return ret;\r
- }\r
-});\r
-seriesTypes.spline = SplineSeries;\r
-\r
-\r
-\r
-/**\r
- * AreaSplineSeries object\r
- */\r
-var AreaSplineSeries = extendClass(SplineSeries, {\r
- type: 'areaspline'\r
-});\r
-seriesTypes.areaspline = AreaSplineSeries;\r
-\r
-/**\r
- * ColumnSeries object\r
- */\r
-var ColumnSeries = extendClass(Series, {\r
- type: 'column',\r
- pointAttrToOptions: { // mapping between SVG attributes and the corresponding options\r
- stroke: 'borderColor',\r
- 'stroke-width': 'borderWidth',\r
- fill: 'color',\r
- r: 'borderRadius'\r
- },\r
- init: function() {\r
- Series.prototype.init.apply(this, arguments);\r
- \r
- var series = this,\r
- chart = series.chart;\r
- \r
- // flag the chart in order to pad the x axis\r
- chart.hasColumn = true;\r
- \r
- // if the series is added dynamically, force redraw of other\r
- // series affected by a new column\r
- if (chart.hasRendered) {\r
- each(chart.series, function(otherSeries) {\r
- if (otherSeries.type === series.type) {\r
- otherSeries.isDirty = true;\r
- }\r
- });\r
- }\r
- },\r
- \r
- /**\r
- * Translate each point to the plot area coordinate system and find shape positions\r
- */\r
- translate: function() {\r
- var series = this,\r
- chart = series.chart,\r
- options = series.options,\r
- stacking = options.stacking,\r
- borderWidth = options.borderWidth,\r
- columnCount = 0,\r
- reversedXAxis = series.xAxis.reversed,\r
- categories = series.xAxis.categories,\r
- stackGroups = {},\r
- stackKey,\r
- columnIndex;\r
- \r
- Series.prototype.translate.apply(series);\r
- \r
- // Get the total number of column type series.\r
- // This is called on every series. Consider moving this logic to a \r
- // chart.orderStacks() function and call it on init, addSeries and removeSeries\r
- each(chart.series, function(otherSeries) {\r
- if (otherSeries.type === series.type && otherSeries.visible) {\r
- if (otherSeries.options.stacking) {\r
- stackKey = otherSeries.stackKey;\r
- if (stackGroups[stackKey] === UNDEFINED) {\r
- stackGroups[stackKey] = columnCount++; \r
- } \r
- columnIndex = stackGroups[stackKey];\r
- } else {\r
- columnIndex = columnCount++;\r
- }\r
- otherSeries.columnIndex = columnIndex;\r
- }\r
- });\r
- \r
- // calculate the width and position of each column based on \r
- // the number of column series in the plot, the groupPadding\r
- // and the pointPadding options\r
- var data = series.data,\r
- closestPoints = series.closestPoints,\r
- categoryWidth = mathAbs(\r
- data[1] ? data[closestPoints].plotX - data[closestPoints - 1].plotX : \r
- chart.plotSizeX / ((categories && categories.length) || 1)\r
- ),\r
- groupPadding = categoryWidth * options.groupPadding,\r
- groupWidth = categoryWidth - 2 * groupPadding,\r
- pointOffsetWidth = groupWidth / columnCount,\r
- optionPointWidth = options.pointWidth,\r
- pointPadding = defined(optionPointWidth) ? (pointOffsetWidth - optionPointWidth) / 2 : \r
- pointOffsetWidth * options.pointPadding,\r
- pointWidth = mathMax(pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), 1),\r
- colIndex = (reversedXAxis ? columnCount - \r
- series.columnIndex : series.columnIndex) || 0,\r
- pointXOffset = pointPadding + (groupPadding + colIndex *\r
- pointOffsetWidth -(categoryWidth / 2)) *\r
- (reversedXAxis ? -1 : 1),\r
- threshold = options.threshold || 0,\r
- translatedThreshold = series.yAxis.getThreshold(threshold),\r
- minPointLength = pick(options.minPointLength, 5);\r
- \r
- // record the new values\r
- each(data, function(point) {\r
- var plotY = point.plotY,\r
- yBottom = point.yBottom || translatedThreshold,\r
- barX = point.plotX + pointXOffset,\r
- barY = mathCeil(mathMin(plotY, yBottom)), \r
- barH = mathCeil(mathMax(plotY, yBottom) - barY),\r
- stack = series.yAxis.stacks[(point.y < 0 ? '-' : '') + series.stackKey],\r
- trackerY,\r
- shapeArgs;\r
- \r
- // Record the offset'ed position and width of the bar to be able to align the stacking total correctly\r
- if (stacking && series.visible && stack && stack[point.x]) {\r
- stack[point.x].setOffset(pointXOffset, pointWidth);\r
- }\r
- \r
- // handle options.minPointLength and tracker for small points\r
- if (mathAbs(barH) < minPointLength) { \r
- if (minPointLength) {\r
- barH = minPointLength;\r
- barY = \r
- mathAbs(barY - translatedThreshold) > minPointLength ? // stacked\r
- yBottom - minPointLength : // keep position\r
- translatedThreshold - (plotY <= translatedThreshold ? minPointLength : 0);\r
- }\r
- trackerY = barY - 3;\r
- }\r
- \r
- extend(point, {\r
- barX: barX,\r
- barY: barY, \r
- barW: pointWidth,\r
- barH: barH\r
- });\r
- \r
- // create shape type and shape args that are reused in drawPoints and drawTracker\r
- point.shapeType = 'rect';\r
- shapeArgs = extend(chart.renderer.Element.prototype.crisp.apply({}, [\r
- borderWidth,\r
- barX,\r
- barY,\r
- pointWidth,\r
- barH\r
- ]), {\r
- r: options.borderRadius\r
- });\r
- if (borderWidth % 2) { // correct for shorting in crisp method, visible in stacked columns with 1px border\r
- shapeArgs.y -= 1;\r
- shapeArgs.height += 1;\r
- }\r
- point.shapeArgs = shapeArgs;\r
- \r
- // make small columns responsive to mouse\r
- point.trackerArgs = defined(trackerY) && merge(point.shapeArgs, {\r
- height: mathMax(6, barH + 3),\r
- y: trackerY\r
- });\r
- });\r
- \r
- },\r
- \r
- getSymbol: function(){\r
- },\r
- \r
- /** \r
- * Columns have no graph\r
- */\r
- drawGraph: function() {},\r
- \r
- /**\r
- * Draw the columns. For bars, the series.group is rotated, so the same coordinates\r
- * apply for columns and bars. This method is inherited by scatter series.\r
- * \r
- */\r
- drawPoints: function() {\r
- var series = this,\r
- options = series.options,\r
- renderer = series.chart.renderer,\r
- graphic,\r
- shapeArgs; \r
- \r
- \r
- // draw the columns\r
- each(series.data, function(point) { \r
- var plotY = point.plotY;\r
- if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {\r
- graphic = point.graphic;\r
- shapeArgs = point.shapeArgs;\r
- if (graphic) { // update\r
- stop(graphic);\r
- graphic.animate(shapeArgs);\r
- \r
- } else {\r
- point.graphic = renderer[point.shapeType](shapeArgs)\r
- .attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE])\r
- .add(series.group)\r
- .shadow(options.shadow);\r
- }\r
- \r
- }\r
- });\r
- },\r
- /**\r
- * Draw the individual tracker elements.\r
- * This method is inherited by scatter and pie charts too.\r
- */\r
- drawTracker: function() {\r
- var series = this,\r
- chart = series.chart,\r
- renderer = chart.renderer,\r
- shapeArgs,\r
- tracker,\r
- trackerLabel = +new Date(),\r
- cursor = series.options.cursor,\r
- css = cursor && { cursor: cursor },\r
- rel;\r
- \r
- each(series.data, function(point) {\r
- tracker = point.tracker;\r
- shapeArgs = point.trackerArgs || point.shapeArgs;\r
- delete shapeArgs.strokeWidth;\r
- if (point.y !== null) {\r
- if (tracker) {// update\r
- tracker.attr(shapeArgs);\r
- \r
- } else {\r
- point.tracker = \r
- renderer[point.shapeType](shapeArgs)\r
- .attr({\r
- isTracker: trackerLabel,\r
- fill: TRACKER_FILL,\r
- visibility: series.visible ? VISIBLE : HIDDEN,\r
- zIndex: 1\r
- })\r
- .on(hasTouch ? 'touchstart' : 'mouseover', function(event) {\r
- rel = event.relatedTarget || event.fromElement;\r
- if (chart.hoverSeries !== series && attr(rel, 'isTracker') !== trackerLabel) {\r
- series.onMouseOver();\r
- }\r
- point.onMouseOver();\r
- \r
- })\r
- .on('mouseout', function(event) {\r
- if (!series.options.stickyTracking) {\r
- rel = event.relatedTarget || event.toElement;\r
- if (attr(rel, 'isTracker') !== trackerLabel) {\r
- series.onMouseOut();\r
- }\r
- }\r
- })\r
- .css(css)\r
- .add(point.group || chart.trackerGroup); // pies have point group - see issue #118\r
- }\r
- }\r
- });\r
- },\r
- \r
- \r
- /**\r
- * Animate the column heights one by one from zero\r
- * @param {Boolean} init Whether to initialize the animation or run it \r
- */\r
- animate: function(init) {\r
- var series = this,\r
- data = series.data;\r
- \r
- if (!init) { // run the animation\r
- /*\r
- * Note: Ideally the animation should be initialized by calling\r
- * series.group.hide(), and then calling series.group.show()\r
- * after the animation was started. But this rendered the shadows\r
- * invisible in IE8 standards mode. If the columns flicker on large\r
- * datasets, this is the cause.\r
- */\r
- \r
- each(data, function(point) {\r
- var graphic = point.graphic,\r
- shapeArgs = point.shapeArgs;\r
- \r
- if (graphic) {\r
- // start values\r
- graphic.attr({ \r
- height: 0,\r
- y: series.yAxis.translate(0, 0, 1)\r
- });\r
- \r
- // animate\r
- graphic.animate({ \r
- height: shapeArgs.height,\r
- y: shapeArgs.y\r
- }, series.options.animation);\r
- }\r
- });\r
- \r
- \r
- // delete this function to allow it only once\r
- series.animate = null;\r
- }\r
- \r
- },\r
- /**\r
- * Remove this series from the chart\r
- */\r
- remove: function() {\r
- var series = this,\r
- chart = series.chart;\r
- \r
- // column and bar series affects other series of the same type\r
- // as they are either stacked or grouped\r
- if (chart.hasRendered) {\r
- each(chart.series, function(otherSeries) {\r
- if (otherSeries.type === series.type) {\r
- otherSeries.isDirty = true;\r
- }\r
- });\r
- }\r
- \r
- Series.prototype.remove.apply(series, arguments);\r
- }\r
-});\r
-seriesTypes.column = ColumnSeries;\r
-\r
-var BarSeries = extendClass(ColumnSeries, {\r
- type: 'bar',\r
- init: function(chart) {\r
- chart.inverted = this.inverted = true;\r
- ColumnSeries.prototype.init.apply(this, arguments);\r
- }\r
-});\r
-seriesTypes.bar = BarSeries;\r
-\r
-/**\r
- * The scatter series class\r
- */\r
-var ScatterSeries = extendClass(Series, {\r
- type: 'scatter',\r
- \r
- /**\r
- * Extend the base Series' translate method by adding shape type and\r
- * arguments for the point trackers\r
- */\r
- translate: function() {\r
- var series = this;\r
-\r
- Series.prototype.translate.apply(series);\r
-\r
- each(series.data, function(point) {\r
- point.shapeType = 'circle';\r
- point.shapeArgs = {\r
- x: point.plotX,\r
- y: point.plotY,\r
- r: series.chart.options.tooltip.snap\r
- };\r
- });\r
- },\r
- \r
- \r
- /**\r
- * Create individual tracker elements for each point\r
- */\r
- //drawTracker: ColumnSeries.prototype.drawTracker,\r
- drawTracker: function() {\r
- var series = this,\r
- cursor = series.options.cursor,\r
- css = cursor && { cursor: cursor },\r
- graphic;\r
- \r
- each(series.data, function(point) {\r
- graphic = point.graphic;\r
- if (graphic) { // doesn't exist for null points\r
- graphic\r
- .attr({ isTracker: true })\r
- .on('mouseover', function(event) {\r
- series.onMouseOver();\r
- point.onMouseOver(); \r
- })\r
- .on('mouseout', function(event) {\r
- if (!series.options.stickyTracking) {\r
- series.onMouseOut();\r
- }\r
- })\r
- .css(css);\r
- }\r
- });\r
-\r
- },\r
- \r
- /**\r
- * Cleaning the data is not necessary in a scatter plot\r
- */\r
- cleanData: function() {}\r
-});\r
-seriesTypes.scatter = ScatterSeries;\r
-\r
-/**\r
- * Extended point object for pies\r
- */\r
-var PiePoint = extendClass(Point, {\r
- /**\r
- * Initiate the pie slice\r
- */\r
- init: function () {\r
- \r
- Point.prototype.init.apply(this, arguments);\r
- \r
- var point = this,\r
- toggleSlice;\r
- \r
- //visible: options.visible !== false,\r
- extend(point, {\r
- visible: point.visible !== false,\r
- name: pick(point.name, 'Slice')\r
- });\r
- \r
- // add event listener for select\r
- toggleSlice = function() {\r
- point.slice();\r
- };\r
- addEvent(point, 'select', toggleSlice);\r
- addEvent(point, 'unselect', toggleSlice);\r
- \r
- return point;\r
- },\r
- \r
- /**\r
- * Toggle the visibility of the pie slice\r
- * @param {Boolean} vis Whether to show the slice or not. If undefined, the\r
- * visibility is toggled\r
- */\r
- setVisible: function(vis) { \r
- var point = this, \r
- chart = point.series.chart,\r
- tracker = point.tracker,\r
- dataLabel = point.dataLabel,\r
- connector = point.connector,\r
- shadowGroup = point.shadowGroup,\r
- method;\r
- \r
- // if called without an argument, toggle visibility\r
- point.visible = vis = vis === UNDEFINED ? !point.visible : vis;\r
- \r
- method = vis ? 'show' : 'hide';\r
- \r
- point.group[method]();\r
- if (tracker) {\r
- tracker[method]();\r
- }\r
- if (dataLabel) {\r
- dataLabel[method]();\r
- }\r
- if (connector) {\r
- connector[method]();\r
- }\r
- if (shadowGroup) {\r
- shadowGroup[method]();\r
- }\r
- if (point.legendItem) {\r
- chart.legend.colorizeItem(point, vis);\r
- }\r
- },\r
- \r
- /**\r
- * Set or toggle whether the slice is cut out from the pie\r
- * @param {Boolean} sliced When undefined, the slice state is toggled \r
- * @param {Boolean} redraw Whether to redraw the chart. True by default.\r
- */\r
- slice: function(sliced, redraw, animation) {\r
- var point = this,\r
- series = point.series,\r
- chart = series.chart,\r
- slicedTranslation = point.slicedTranslation,\r
- translation;\r
- \r
- setAnimation(animation, chart);\r
- \r
- // redraw is true by default\r
- redraw = pick(redraw, true);\r
- \r
- // if called without an argument, toggle\r
- sliced = point.sliced = defined(sliced) ? sliced : !point.sliced;\r
- \r
- translation = {\r
- translateX: (sliced ? slicedTranslation[0] : chart.plotLeft),\r
- translateY: (sliced ? slicedTranslation[1] : chart.plotTop)\r
- };\r
- point.group.animate(translation);\r
- if (point.shadowGroup) {\r
- point.shadowGroup.animate(translation);\r
- }\r
- \r
- }\r
-});\r
-\r
-/**\r
- * The Pie series class\r
- */\r
-var PieSeries = extendClass(Series, {\r
- type: 'pie',\r
- isCartesian: false,\r
- pointClass: PiePoint,\r
- pointAttrToOptions: { // mapping between SVG attributes and the corresponding options\r
- stroke: 'borderColor',\r
- 'stroke-width': 'borderWidth',\r
- fill: 'color'\r
- },\r
- \r
- /**\r
- * Pies have one color each point\r
- */\r
- getColor: function() {\r
- // record first color for use in setData\r
- this.initialColor = this.chart.counters.color;\r
- },\r
- \r
- /**\r
- * Animate the column heights one by one from zero\r
- * @param {Boolean} init Whether to initialize the animation or run it \r
- */\r
- animate: function(init) {\r
- var series = this,\r
- data = series.data;\r
- \r
- each(data, function(point) {\r
- var graphic = point.graphic,\r
- args = point.shapeArgs,\r
- up = -mathPI / 2;\r
- \r
- if (graphic) {\r
- // start values\r
- graphic.attr({ \r
- r: 0,\r
- start: up,\r
- end: up\r
- });\r
- \r
- // animate\r
- graphic.animate({ \r
- r: args.r,\r
- start: args.start,\r
- end: args.end\r
- }, series.options.animation);\r
- }\r
- });\r
- \r
- // delete this function to allow it only once\r
- series.animate = null;\r
- \r
- },\r
- /**\r
- * Do translation for pie slices\r
- */\r
- translate: function() {\r
- var total = 0,\r
- series = this,\r
- cumulative = -0.25, // start at top\r
- precision = 1000, // issue #172\r
- options = series.options,\r
- slicedOffset = options.slicedOffset,\r
- connectorOffset = slicedOffset + options.borderWidth,\r
- positions = options.center.concat([options.size, options.innerSize || 0]),\r
- chart = series.chart,\r
- plotWidth = chart.plotWidth,\r
- plotHeight = chart.plotHeight,\r
- start,\r
- end,\r
- angle,\r
- data = series.data,\r
- circ = 2 * mathPI,\r
- fraction,\r
- smallestSize = mathMin(plotWidth, plotHeight),\r
- isPercent,\r
- radiusX, // the x component of the radius vector for a given point\r
- radiusY,\r
- labelDistance = options.dataLabels.distance;\r
- \r
- // get positions - either an integer or a percentage string must be given\r
- positions = map(positions, function(length, i) {\r
- \r
- isPercent = /%$/.test(length); \r
- return isPercent ? \r
- // i == 0: centerX, relative to width\r
- // i == 1: centerY, relative to height\r
- // i == 2: size, relative to smallestSize\r
- // i == 4: innerSize, relative to smallestSize\r
- [plotWidth, plotHeight, smallestSize, smallestSize][i] *\r
- pInt(length) / 100:\r
- length;\r
- });\r
- \r
- // utility for getting the x value from a given y, used for anticollision logic in data labels\r
- series.getX = function(y, left) {\r
- \r
- angle = math.asin((y - positions[1]) / (positions[2] / 2 + labelDistance));\r
- \r
- return positions[0] + \r
- (left ? -1 : 1) *\r
- (mathCos(angle) * (positions[2] / 2 + labelDistance));\r
- };\r
- \r
- // set center for later use\r
- series.center = positions;\r
- \r
- // get the total sum\r
- each(data, function(point) {\r
- total += point.y;\r
- });\r
- \r
- each(data, function(point) {\r
- // set start and end angle\r
- fraction = total ? point.y / total : 0;\r
- start = mathRound(cumulative * circ * precision) / precision;\r
- cumulative += fraction;\r
- end = mathRound(cumulative * circ * precision) / precision;\r
- \r
- // set the shape\r
- point.shapeType = 'arc';\r
- point.shapeArgs = {\r
- x: positions[0],\r
- y: positions[1],\r
- r: positions[2] / 2,\r
- innerR: positions[3] / 2,\r
- start: start,\r
- end: end\r
- };\r
- \r
- // center for the sliced out slice\r
- angle = (end + start) / 2;\r
- point.slicedTranslation = map([\r
- mathCos(angle) * slicedOffset + chart.plotLeft, \r
- mathSin(angle) * slicedOffset + chart.plotTop\r
- ], mathRound);\r
- \r
- // set the anchor point for tooltips\r
- radiusX = mathCos(angle) * positions[2] / 2;\r
- series.radiusY = radiusY = mathSin(angle) * positions[2] / 2;\r
- point.tooltipPos = [\r
- positions[0] + radiusX * 0.7,\r
- positions[1] + radiusY * 0.7\r
- ];\r
- \r
- // set the anchor point for data labels \r
- point.labelPos = [\r
- positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector\r
- positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a\r
- positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie\r
- positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a\r
- positions[0] + radiusX, // landing point for connector\r
- positions[1] + radiusY, // a/a\r
- labelDistance < 0 ? // alignment\r
- 'center' :\r
- angle < circ / 4 ? 'left' : 'right', // alignment\r
- angle // center angle\r
- ];\r
- \r
- // API properties\r
- point.percentage = fraction * 100;\r
- point.total = total;\r
- \r
- });\r
- \r
- \r
- this.setTooltipPoints();\r
- },\r
- \r
- /**\r
- * Render the slices\r
- */\r
- render: function() {\r
- var series = this;\r
- \r
- // cache attributes for shapes\r
- //series.getAttribs();\r
-\r
- this.drawPoints();\r
- \r
- // draw the mouse tracking area\r
- if (series.options.enableMouseTracking !== false) {\r
- series.drawTracker();\r
- }\r
- \r
- this.drawDataLabels();\r
- \r
- if (series.options.animation && series.animate) {\r
- series.animate();\r
- }\r
- \r
- series.isDirty = false; // means data is in accordance with what you see\r
- },\r
- \r
- /**\r
- * Draw the data points\r
- */\r
- drawPoints: function() {\r
- var series = this,\r
- chart = series.chart,\r
- renderer = chart.renderer,\r
- groupTranslation,\r
- //center,\r
- graphic,\r
- group,\r
- shadow = series.options.shadow,\r
- shadowGroup,\r
- shapeArgs;\r
- \r
- \r
- // draw the slices\r
- each(series.data, function(point) {\r
- graphic = point.graphic;\r
- shapeArgs = point.shapeArgs;\r
- group = point.group;\r
- shadowGroup = point.shadowGroup;\r
-\r
- // put the shadow behind all points\r
- if (shadow && !shadowGroup) {\r
- shadowGroup = point.shadowGroup = renderer.g('shadow')\r
- .attr({ zIndex: 4 })\r
- .add();\r
- }\r
- \r
- // create the group the first time\r
- if (!group) {\r
- group = point.group = renderer.g('point')\r
- .attr({ zIndex: 5 })\r
- .add();\r
- }\r
- \r
- // if the point is sliced, use special translation, else use plot area traslation\r
- groupTranslation = point.sliced ? point.slicedTranslation : [chart.plotLeft, chart.plotTop];\r
- group.translate(groupTranslation[0], groupTranslation[1]);\r
- if (shadowGroup) {\r
- shadowGroup.translate(groupTranslation[0], groupTranslation[1]);\r
- }\r
- \r
- \r
- // draw the slice\r
- if (graphic) {\r
- graphic.animate(shapeArgs);\r
- } else {\r
- point.graphic = \r
- renderer.arc(shapeArgs)\r
- .attr(extend(\r
- point.pointAttr[NORMAL_STATE],\r
- { 'stroke-linejoin': 'round' }\r
- ))\r
- .add(point.group)\r
- .shadow(shadow, shadowGroup);\r
- }\r
- \r
- // detect point specific visibility\r
- if (point.visible === false) {\r
- point.setVisible(false);\r
- }\r
- \r
- });\r
- \r
- },\r
- \r
- /**\r
- * Override the base drawDataLabels method by pie specific functionality\r
- */\r
- drawDataLabels: function() {\r
- var series = this,\r
- data = series.data,\r
- point,\r
- chart = series.chart,\r
- options = series.options.dataLabels,\r
- connectorPadding = pick(options.connectorPadding, 10),\r
- connectorWidth = pick(options.connectorWidth, 1),\r
- connector,\r
- connectorPath,\r
- distanceOption = options.distance,\r
- radiusY = series.radiusY,\r
- outside = distanceOption > 0,\r
- dataLabel,\r
- labelPos,\r
- labelHeight,\r
- lastY,\r
- centerY = series.center[1],\r
- halves = [// divide the points into right and left halves for anti collision\r
- [], // right\r
- [] // left\r
- ],\r
- x,\r
- y,\r
- visibility,\r
- overlapping,\r
- rankArr,\r
- secondPass,\r
- sign,\r
- lowerHalf,\r
- sort,\r
- i = 2,\r
- j;\r
- \r
- // get out if not enabled\r
- if (!options.enabled) {\r
- return;\r
- }\r
- \r
- // run parent method\r
- Series.prototype.drawDataLabels.apply(series);\r
- \r
- // arrange points for detection collision\r
- each(data, function(point) {\r
- halves[\r
- point.labelPos[7] < mathPI / 2 ? 0 : 1\r
- ].push(point);\r
- });\r
- halves[1].reverse();\r
- \r
- // define the sorting algorithm\r
- sort = function(a, b) {\r
- return b.y - a.y;\r
- };\r
- \r
- // assume equal label heights\r
- labelHeight = halves[0][0] && halves[0][0].dataLabel && pInt(halves[0][0].dataLabel.styles.lineHeight);\r
- \r
- /* Loop over the points in each quartile, starting from the top and bottom\r
- * of the pie to detect overlapping labels.\r
- */\r
- while (i--) {\r
- \r
- var slots = [],\r
- slotsLength,\r
- usedSlots = [],\r
- points = halves[i],\r
- pos,\r
- length = points.length,\r
- slotIndex;\r
- \r
- lowerHalf = i % 3;\r
- sign = lowerHalf ? 1 : -1;\r
- \r
- // build the slots\r
- for (pos = centerY + radiusY - distanceOption; pos <= centerY - radiusY + distanceOption; pos += labelHeight) {\r
- slots.push(pos);\r
- // visualize the slot \r
- /* \r
- var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0),\r
- slotY = pos + chart.plotTop;\r
- if (!isNaN(slotX)) {\r
- chart.renderer.rect(slotX, slotY - 7, 100, labelHeight)\r
- .attr({\r
- 'stroke-width': 1,\r
- stroke: 'silver'\r
- })\r
- .add(); \r
- chart.renderer.text('Slot '+ (slots.length - 1), slotX, slotY + 4)\r
- .attr({\r
- fill: 'silver'\r
- }).add();\r
- }\r
- // */\r
- }\r
- slotsLength = slots.length;\r
- \r
- // if there are more values than available slots, remove lowest values\r
- if (length > slotsLength) {\r
- // create an array for sorting and ranking the points within each quarter\r
- rankArr = [].concat(points);\r
- rankArr.sort(sort);\r
- j = length;\r
- while (j--) {\r
- rankArr[j].rank = j;\r
- }\r
- j = length;\r
- while (j--) {\r
- if (points[j].rank >= slotsLength) {\r
- points.splice(j, 1); \r
- }\r
- }\r
- length = points.length;\r
- }\r
- \r
- // The label goes to the nearest open slot, but not closer to the edge than\r
- // the label's index. \r
- for (j = 0; j < length; j++) {\r
- \r
- point = points[j];\r
- labelPos = point.labelPos; \r
- \r
- var closest = 9999,\r
- distance,\r
- slotI;\r
- \r
- // find the closest slot index\r
- for (slotI = 0; slotI < slotsLength; slotI++) {\r
- distance = mathAbs(slots[slotI] - labelPos[1]);\r
- if (distance < closest) {\r
- closest = distance;\r
- slotIndex = slotI;\r
- }\r
- }\r
- \r
- // if that slot index is closer to the edges of the slots, move it\r
- // to the closest appropriate slot\r
- if (slotIndex < j && slots[j] !== null) { // cluster at the top\r
- slotIndex = j;\r
- } else if (slotsLength < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom\r
- slotIndex = slotsLength - length + j;\r
- } else { \r
- // Slot is taken, find next free slot below. In the next run, the next slice will find the\r
- // slot above these, because it is the closest one \r
- while(slots[slotIndex] === null) {\r
- slotIndex++;\r
- }\r
- }\r
- \r
- usedSlots.push({ i: slotIndex, y: slots[slotIndex] });\r
- slots[slotIndex] = null; // mark as taken\r
- }\r
- // sort them in order to fill in from the top\r
- usedSlots.sort(sort);\r
- \r
- \r
- // now the used slots are sorted, fill them up sequentially\r
- for (j = 0; j < length; j++) {\r
- \r
- point = points[j];\r
- labelPos = point.labelPos;\r
- dataLabel = point.dataLabel;\r
- var slot = usedSlots.pop(),\r
- naturalY = labelPos[1];\r
-\r
- visibility = point.visible === false ? HIDDEN : VISIBLE;\r
- slotIndex = slot.i;\r
-\r
- // if the slot next to currrent slot is free, the y value is allowed \r
- // to fall back to the natural position\r
- y = slot.y;\r
- if ((naturalY > y && slots[slotIndex + 1] !== null) ||\r
- (naturalY < y && slots[slotIndex - 1] !== null)) {\r
- y = naturalY;\r
- }\r
- \r
- // get the x\r
- x = series.getX(y, i);\r
- \r
- // move or place the data label\r
- dataLabel\r
- .attr({\r
- visibility: visibility,\r
- align: labelPos[6]\r
- })[dataLabel.moved ? 'animate' : 'attr']({\r
- x: x + options.x + \r
- ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0),\r
- y: y + options.y\r
- });\r
- dataLabel.moved = true;\r
- \r
- // draw the connector\r
- if (outside && connectorWidth) {\r
- connector = point.connector;\r
- \r
- connectorPath = [\r
- M,\r
- x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label\r
- L,\r
- x, y, // first break, next to the label\r
- L,\r
- labelPos[2], labelPos[3], // second break\r
- L,\r
- labelPos[4], labelPos[5] // base\r
- ];\r
- \r
- if (connector) {\r
- connector.animate({ d: connectorPath });\r
- connector.attr('visibility', visibility);\r
- \r
- } else { \r
- point.connector = connector = series.chart.renderer.path(connectorPath).attr({\r
- 'stroke-width': connectorWidth,\r
- stroke: options.connectorColor || '#606060',\r
- visibility: visibility,\r
- zIndex: 3\r
- })\r
- .translate(chart.plotLeft, chart.plotTop)\r
- .add();\r
- }\r
- }\r
- }\r
- }\r
- },\r
- \r
- /**\r
- * Draw point specific tracker objects. Inherit directly from column series.\r
- */\r
- drawTracker: ColumnSeries.prototype.drawTracker,\r
- \r
- /**\r
- * Pies don't have point marker symbols\r
- */\r
- getSymbol: function() {}\r
- \r
-});\r
-seriesTypes.pie = PieSeries;\r
-\r
-\r
-// global variables\r
-win.Highcharts = {\r
- Chart: Chart,\r
- dateFormat: dateFormat,\r
- pathAnim: pathAnim,\r
- getOptions: getOptions,\r
- numberFormat: numberFormat,\r
- Point: Point,\r
- Color: Color,\r
- Renderer: Renderer,\r
- seriesTypes: seriesTypes,\r
- setOptions: setOptions,\r
- Series: Series,\r
- \r
- // Expose utility funcitons for modules\r
- addEvent: addEvent,\r
- createElement: createElement,\r
- discardElement: discardElement,\r
- css: css,\r
- each: each,\r
- extend: extend,\r
- map: map,\r
- merge: merge,\r
- pick: pick,\r
- extendClass: extendClass,\r
- product: 'Highcharts',\r
- version: '2.1.6'\r
-};\r
-}());\r