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

85

+
+
18,22500$1,822.50$1,822.48$0.02$100.00$0.00
+
+
+
+
+

Pacing:

+

85

+
+
+
Lifetime Budget:
+
$7000.00
+
Daily Budget:
+
$500.00
+
Advertiser Goal:
+
$6.50 CPC
+
+
+
+ +
+
+
+
+ +

85

+
+
18,22500$1,822.50$1,822.48$0.02$100.00$0.00
+
+
+
+
+

Pacing:

+

85

+
+
+
Lifetime Budget:
+
$7000.00
+
Daily Budget:
+
$500.00
+
Advertiser Goal:
+
$6.50 CPC
+
+
+
+ +
+
+
+
+ +

85

+
+
18,22500$1,822.50$1,822.48$0.02$100.00$0.00
+
+
+
+
+

Pacing:

+

85

+
+
+
Lifetime Budget:
+
$7000.00
+
Daily Budget:
+
$500.00
+
Advertiser Goal:
+
$6.50 CPC
+
+
+
+ +
+
+
+
+ +

85

+
+
18,22500$1,822.50$1,822.48$0.02$100.00$0.00
+
+
+
+
+

Pacing:

+

85

+
+
+
Lifetime Budget:
+
$7000.00
+
Daily Budget:
+
$500.00
+
Advertiser Goal:
+
$6.50 CPC
+
+
+
+ +
+
+
+
+ +

85

+
+
18,22500$1,822.50$1,822.48$0.02$100.00$0.00
+
+
+
+
+

Pacing:

+

85

+
+
+
Lifetime Budget:
+
$7000.00
+
Daily Budget:
+
$500.00
+
Advertiser Goal:
+
$6.50 CPC
+
+
+
+ +
+
+
+
+ +

85

+
+
18,22500$1,822.50$1,822.48$0.02$100.00$0.00
+
+
+
+
+

Pacing:

+

85

+
+
+
Lifetime Budget:
+
$7000.00
+
Daily Budget:
+
$500.00
+
Advertiser Goal:
+
$6.50 CPC
+
+
+
+ +
+
+
+
+ +

85

+
+
18,22500$1,822.50$1,822.48$0.02$100.00$0.00
+
+
+
+
+

Pacing:

+

85

+
+
+
Lifetime Budget:
+
$7000.00
+
Daily Budget:
+
$500.00
+
Advertiser Goal:
+
$6.50 CPC
+
+
+
+ +
+
+
+
+ + diff --git a/list.html b/list.html index 75c4c2d..3e1cbbb 100644 --- a/list.html +++ b/list.html @@ -8,7 +8,7 @@ untitled - + @@ -38,7 +38,7 @@
-

85

+

95

18,225 @@ -50,14 +50,14 @@ $100.00 $0.00 - +
+ + + + +
+
+ +
+ +

85

+
+ + 1,244 + 0 + 0 + $10,490.79 + $10,490.42 + $0.36 + $8,433.11 + $0.29 + + + +
+
+ +
+
+
+

Pacing:

+

95

+
+
+
Lifetime Budget:
+
$7000.00
+
Daily Budget:
+
$500.00
+
Advertiser Goal:
+
$6.50 CPC
+
Spend:
+
-$241
@@ -82,7 +135,7 @@
-

85

+

68

18,225 @@ -94,152 +147,144 @@ $100.00 $0.00 - - -
- -

85

+ + +
+
+

Target: 240,000 Impressions

+
+
+
+

Actual: 84,302 Impressions

+
+
+
+
+
+
+

Pacing:

+

95

+
+
+
Lifetime Budget:
+
$7000.00
+
Daily Budget:
+
$500.00
+
Advertiser Goal:
+
$6.50 CPC
+
Spend:
+
-$241
+
+
+
+ +
- 18,225 - 0 - 0 - $1,822.50 - $1,822.48 - $0.02 - $100.00 - $0.00
-

85

+

103

- 18,225 + 1,244 0 0 - $1,822.50 - $1,822.48 - $0.02 - $100.00 - $0.00 + $10,490.79 + $10,490.42 + $0.36 + $8,433.11 + $0.29 - - -
- -

85

+ + +
+
+
+
+

Pacing:

+

103

+
+
+
Lifetime Budget:
+
$7000.00
+
Daily Budget:
+
$500.00
+
Advertiser Goal:
+
$6.50 CPC
+
Spend:
+
-$241
+
+
+
+ + +
- 18,225 - 0 - 0 - $1,822.50 - $1,822.48 - $0.02 - $100.00 - $0.00 - - - -
- -

85

-
- - 18,225 - 0 - 0 - $1,822.50 - $1,822.48 - $0.02 - $100.00 - $0.00 - - - -
- -

85

-
- - 18,225 - 0 - 0 - $1,822.50 - $1,822.48 - $0.02 - $100.00 - $0.00 - - - -
- -

85

-
- - 18,225 - 0 - 0 - $1,822.50 - $1,822.48 - $0.02 - $100.00 - $0.00 - - - -
- -

85

-
- - 18,225 - 0 - 0 - $1,822.50 - $1,822.48 - $0.02 - $100.00 - $0.00 - - - -
- -

85

-
- - 18,225 - 0 - 0 - $1,822.50 - $1,822.48 - $0.02 - $100.00 - $0.00 - - - -
- -

85

-
- - 18,225 - 0 - 0 - $1,822.50 - $1,822.48 - $0.02 - $100.00 - $0.00
+ diff --git a/psd/pacing chart2.psd b/psd/pacing chart2.psd index 42a8cb4536726666bc21ce43e8c11d500eed2b26..2414e1ccf7fcefb7ca153dcabfee6fe3d0d72e48 100644 GIT binary patch delta 3936 zcmeHIe{dAl9slm$-sL1DSsa2WjXWk&P#`Qx_Bf#R7NpSAak`E$8I{PvvuN~m!$V5ej{sFDs>#6Vev+;G`l(ecE!!_kUft2F6vDregN<;$>Q&_s~%WV zp{>8Es+q$o(t#_oa=i&69+q8ehjt)Jjy|O{e zPkq}SzRg@qW(Q+0nCI{QFmCpmN44%I1akU*glz}b|9fZ6v4O^4l*~IO-b0Y%vzk~W z5$5CnIQiS{+ixA%5Y@VjkzcKAyVE{>=JZU0bq5UZcM1<_p4R_6uO9}EE^+g~c0`RdSb6N2|mjO?+WxmEI znx9;c5T@zvD&<;NU(MvtQUbu=UeS90V*oS90~b4&nHQgw`P%MSX$V$8GpvO*(3(l^ zevw<6;r7(taCt48+|(c7BH3vjOT(O;ebjj4mJB;oHbEpK!7(aXdui9y%p-^P1dj~I z9$J?zJIubXjfV+P1~Z`s=E7`hv#8Zjn*&u)0yjW-$OP#;@RBK=1y|#2bTweKprAqv zz?t~*=O+aaSW-)}0+fge8VTk>Sk*`bkdy+TR3y0&N+X&YItdRdB?m3XNuvUyg|>x` z1ZiX*@FJ#*a1U4lb!oI1ipmTC`JFylt}og48uFEkIxKHT}4y7T||Va)X_LCGyyd$=$~b zC-~f)(}CwpxJa&RydZ|TNPF)@VG#{wmBP1Z_)b3XG!2I7qa_CpxB zV{BHyz|625sfpCfwRNw~5t`^fCAWhU-kP3km?4Y{Oq&E#pb}~!3JXA{Ru6R$p(ew0 zsG#R7w%_+0H#gULhwyj)%Yu97d^zx@y@AQ?T~oN9=Z5zQ&-1w|7Hs8mO|me*S-Xfs z3Rvfnw-oD7xCMVreG6VhH^nhXr?ZTR76xr znWicdPh^H^NFZ5seu~K6yV96TC5Q?Mr_eU%SN$@UCK45O5K{@?8XbCSjC2}#+jR1D zho+sUt09(2z7ohTf@YKYE!a&trM`RF(%&$kQV5@4;8n#qnBj)b_$hqX6W7!BdfI;F V()P2;tNq}5@?KBguTt`^{4d%sp!EO% delta 5654 zcmeH}e{dA#8ONW!+dJ}u9}5k?!cdk`ev}!)<&rZAAlacs3l_Lm1ZkvhCR54~gcXsg zQo0AVV{I!-Ii|68rYnNO6s0G<;#nxH9R|k^GKXO(lfiPL7$t~XD}nsFefoXx!cU>~ zkM_SL+4r6|_dcKJ$NRj`yWX~8|9EBCX0_;(J7IL00x8<+_=J%#~~B z&pUUqq0ZdqG&k6(bx&Ow_1dTJzIy)r66x^XQ$JZZ=JAcY8i#yT{p%C^79ZHO^6g)g zy~%W}uD1DpXZP$WA7|Q});rfXA6{40d?2-DVAt!9{^7uLV}JSk-H{nbp5D=Uv2Jhb zgH!QO8~(NY2YX)YvfsPA(Y)vRCqvuEyDi@-Z~XIz=Eiet+a5e}@aWQ$7na_0vi8K~ z#She;ThadVlGStOpD-7BjXSf`vZgn+)jWFe8Ky_??MyTM?IotWx+Y`~kNK#UY5bp> z?`wbf1k>}0bD8r@pPj5+|Io*~4dcxr%>|dI{VKa-RmzU2L7 z^1Q}Y>-0f$`Nj1gw4U)2`;KSh#}S!V2K>10J9aW{>si9IWzmqi-m~7zUc>K02$(uT~lBb8e>UpoKJAunLQn`}69iD}XIGJ`v7 ze|ve-baTp1>*-VPGfCV+xmp;D9ZBwQl&)@m^3aM=gMzz5vkq$uL(#3n&VOZ_#X;L-Qcbhdj)u zJBcpxYmO6YCCSl0Ct0(dgx*7eknK2z(oH_a33cU0zsVf!Ccn+BVKtiAn$gLR@zMj^^cQPTl6OM$!~n{2R>C0S>1 zJd#zzG@~AIsE|}sC5mF2r68M%VHyg|={aq-Da1yW3f%;e_F_0br49A;C78xdjE9VPOxz@(2+`I|U-g>7YV2!8Sb4lWz2?RFLcp zAzbl2lFAv!5>hWoyGH6HzwTzRExtU-$@^rAYJk275W5ovEr&unJPHVKFG@#%nu!3- zoMWgDoBS59JbA7rb6Oj~W5-t>kJ0?Thx&l+!!PJ2zHuE&prOKu#WKn*QtuN4q z8l1~NAO>5ShH^Jh6j>@Z+150RaoBr(LuFXptre*vA*L2hyXqHku;Cy;a{+jY<8jc5 zGHW6!0n=r6P4DU+bQ3vUEF%=f_E^f`l=+aDeBS;^K}Ey%sG-Zm+8IQkg+6I=%B3-* zUZUInez^teZS_GWiHe%eM5HlKpN>d)e5unB|G*!WQm(^A>({Iy{D_9Q;D>9%M2|P5 zuVZ5Kpv3N+WhPLp|_1@-*?;QzFk2pWp8CKP5i* z4V4e3ws!k%pO}ppDR6q!@2fR`nh> zWmi#p)!|Bpq8^u6Xn3ruDD9k-h-t8pE5JyF$HfUqngF?^BUnptO-R_eOWe;! zXYK{ZZVMg&Fe{?F>G`zitQAnkNs-wvPB-}-N zR5e3Ly;!8}3bvE|wxVKBeR-^QTVF*Iu*(U6b5x|yDCWc$dOWaSbgQ)a?{uxj``}5mGW&*|$Q&xP~kUr(L`a1s7^~ zy>Ubm12NnTN(`6ARjhKvwh9T$v>qkgM8KFdKDFF$zWxy z@6+eG|6JyoIcM#?_TFo+wf5S-y;lgnpZNUSiJPa6EzP?tXWB@$Ui*@6KN~xDX3mdu?wotqZFkrzlhSI9h9}3_zI)Ac zmzU>iZEW{kb+qhU{Rn$5W=EmIloRucbGSlmCQJWDB0B!=g;Hpl%l6Lk_^$JnO+ zdeRMg@%BW1xp8TJi7|KnBe|dx=wT`+Bb5==Zd>r7uJ-Xo>r?Rd~xD`{66qn;@q?5vAI8<^N86|eIn!j^vv%+@XYoHU$Q*+QR*zuybRm)4BO)8 zo7d3oZyeeA=_FchTy*A{EtZ=|zvH)Aw#>XdMzx#r#$Gb|AM;-tV-RX1-7jqSth!UL zr-`?(K8H=90``^S^mpu(s`1{A`H#^UN?P;+6Ug`PCX=SwKrAO(? zr@@x@zr^_VQLfL?zu=008f+6xLcT}Ot)|ZO_rCTt*lsq9UiMnA|D}O8KJX)^d*slS zPvL~)w+kI6yjx;kW5J7XVMVKWCJ9<2@J?L)hgT+Ev0Nrv#fzvkXfNON6ikBipZm4` zvpf6;`@{JEiowB$Jo~Y*4cB6|KBUX|JeWk#{oqwQ2((z@G=44Sfm-3 zn_srdXjyKwSc*$mm7`pQAzEUw%=`HhPZo^wja`~=DSq0x%vzCOh(Zy!D91E)s>xis z3a>&ay1%USGWwyy%jru<9+6`l6;G?w4y|m+&5YRD&fJU_uno= z9a~&#F_xL~3yg)Lt*}3(l$w|1TP((TW|O&0l#0SBma_cP@-_Ko#?pdy_ZC}L8_VXJ zR+-E2R-z6u{)%GJY^k}_h)3GrXkB8iD7B~)BqezNS@=ICK3`I96w&*V8V#itCLx6Z z-^mMdmn_c7FL-KI89FG`RC}2^HU32KUwesHnNW_Y!WgI4iI-Jd&1LtO8I7fJ45^nf zn9CkA7RJ?H-rQWa+yafO^}-Nwqc1GUUuAqS-?Cc8QZWyi_;9&VshL}jfvoDRG_9wI z$$sk;Cu5LFb4}0WuPdKhSUA^YnQO6>6+cm7F>1bW^=QdFfH_q*;bAHBOy+XqT$3p` zf0g1nBL2~s`K1NsLU^mn2}Vj4&a*uKX_S7eFk`_xZk~C~x=T9KW^(<#6>Chz1<;P@ zOF+sT<7)J1E{oIFWa|P;iAiIaXD(R-_bIiM|H8Cbter%aRi{4F{?1eACRI`H@`@*l z%%(zPSsa#4qGBqcd|CxOD|#ooW~mi&iD9uCpC~pJTh{$ew-U;`TJ#iSU6Nm*&2rqO z(ubEWd_*(BHJ6H46qgp7pHbBf|M+=@Wle=;NrlN$yvAg-in)(gro~kLjrvIbJM;g0 z`qAozo~~OC>%_*_g#DYkCw0tg)#rDQc12+;wX-7Z+{DFDxxERTPTe5Tz`m z@iA2`Uui5utSehptcFe{{wypl1h42_1%Fp9E-p3BFBOZTTpd)nz*RBZJtEeqHnG%Y z6=sWAQ=^M+bj)j(uR)kCE1zGo#JuDDmRqhZYK6F|n(ZDE^=s?z&E$ub9?2O{~jmgqg;# zz$Rr}Qe0YGl5bK*iS3b-m2wp1nwJ+B8VmBv?kk=dxB8NxZQ+`uN>%o#9YO{gQ&N7% zGkEHhvYoXzKcQ#rRokrv8Dj#k5(U=sk=jQK?$*`*`02;T8O}Vsq19GdGTHXlhM(FN zTh`eM{^JG1mZqBqY}1OqYx69+#`XitjkYnym)X^}|1|!);Sck!ZLOVFG+D>S*(Mau zvAHdUD4RF~eN3(8tH08-@uTGp-W$0n|M4Ag)(}Km5A5-O#QB9Nd8Uw6_n81A_{Boe zeI^7h1u)?BFQ_v-V%uDBt8Gl_zu6k@ouZuMpDrnX3XFNE724BXaU4|+UpVMp`BV;P zIHKX8_N@oG*g@~g1E5AL2SxO#5YT| z`O}Kc$JizqZ<(01IDeh7Z0c>IiDpaN_R_om=StzStA)5L1z2~jY5JW)p-HqDh+v&^ z!!?y|BL$b5O3M|)nex|_#SB?2K)MLb*N(2kz@y2|%a(KRhIQ9mqp+732sE35FSDJq ztT$9mc=83oT07yn+g4W9{EFRDH|b}ra*A!-b4j&RLa*mMbv2En@%T(oKi4SF*soSL znXbnY{tivS=LWiwrqfOIL%Nx==oaN8oVs#Sj>J=|_~9Jz<{jT-r1MA=bJjdAq8 z2&Z(X+Ilz9%rK|5adaK%BTll$Mt+TGR*183YMa^u1qXthR%-O?L~~?LyW;4BGG}gm zyV{@zM<+k0*>Q9|=#3iv2GM+p)7d!sm=8zqtPZtD^;^nk{M5#6$=8bv!&*@!ase5`jWw~%s%Vu^!2y0EUr0~#* zDAzen$qiITY<@>KG~uE@6U&nZIXjl-k1=(Gxz4>l^*Fr`2{v_c){)gsJBjIS{hT?o z0x>Qr$aUTq(@xR>h}_sA5=@^FW^+B98iCD@$JEt<0eQOujb(E#F(@{vg zwE?a&e}q1VHhSTJ2lW{NVi^~jIMu8CyE}wc#&-$JoFzLBMU4?GR6TqoJ4^iUTiB`)3Vj4fB~yOB+vh6;Bx=+r*yBi({m+(H2>y_izvz@Jb)C>g$y7)dH?M*xE5k0lO}pLDlwaJodtVPd(ScevgK#8h=6gI#YuFr|}d!3D&P0Sb~X zv$Cn3Cxllbj&FM#I?AInSRB&ZoTX!UP6?A{7i~S*0T1`A>>_&X0(#9EqzLIUo;};b z_2HE>9dDsC(w~D{O8YpgKs!d$IXxg!MX+{%T%X{IeXnhfesly_!JckGar+00p*&pGqLzp9}8jsyextWsV==DXxZt)z#*r71`OzRF|}bCFyM2HzwBYy_wyv zvyB_s{7sYqBFaD8;zQ3&xrcs=edaFgKR3~L=^FJjp01{py3BNznKWI@bcI}`%geM4 zRMWG8BHF4<^|4Dcb$Y5y)1mtd2o?tiIaQpxY+}!VuMr`2q8d;B+wo4;E#p3+1YTYm4SX+O|hM4U~&rP+az=~JL zp%D#%T>cz4idIDVM4*ys^gVoHzc=DJoqj->Dn|LC^86vt%I)x*JLp!_-2p@*u*vNT z-)%rFnP@*bMS(&B{J@=Rr#`1=*9nO^A^em5SoeKEd0)fBhOaeZ-yYVP&OS?iF0U00^71fPu?i7)ZGfMim~4E zH+=uWe`Uv$s>bE9-&p^PQ`pWc`4GOlIScvY2+5}ruW^0Q=LSL*gM{92FSy zQMgUzu0^c};>FQ^(2_>)$8-iDUc1T<+7CL|uFd})$m@oD8UtvlAM$nZS8#srhINi2 z+#clbLhe3@*}?xW*UPX#k5Z)%`Q+#!j$I`xp<3?X+wkg-U;@1gg8|l)!iT{@axVny z;M+NiNFkq<`J?>`3&{~53q(KW??Dya5Vn*5S|;{sh~SXH5$xnU@aBUe z24W1bA-edKzYj58m`*2uRmcHF1_xstepoG3&;KflJ263P7vCi$fi?r47z+%^{E^T2 z2iUBn4zPFezd(=yltzbD7K|A@#t&ljHZ<(w@M5_i!bQ9agIDzM1;_p#ky!+H2|!RiwN}qfFy<3f^YONb)gwk<^b1xi>=0=2>7-z^ zH8${3unhB)f*Vc|#8#y!G8%{p;cr%$+qer@I}j2&{Y0o^gahP8q(O*{jVE{>qG&C? zg7L7D0PDj6*dZiz>;G8j#mB9Uq7{5gieh^Tzzlx1nREeNb#ga`k%g8qTS#&Ug%Uq4 z8uj}O0eF@F3^XJw!kri0ba9W;1PTmc^OirrB>WukJRmoAb77C|eozOs#pF^8>AWaZ$ zhPWXT4x$ZY3Il~iASV1FE&XU23I_bcLT$pm(bC_| zak4_oF0>2_Mbx5UB+LOt!?mav4I^qp__Y-3MFnPx^}&^f@Z}JW;PP8O7K}vsMO5#D zsMZh~>g0d~-||OrIt(>J!?0?U1^o`6E)W@hjK&#gfpd-E^AKW8|WrbC#5ArZJ zoJyZ7CJYoA-U*JgLTPXS_)Mj*4L!>{Pt))~DHBRd=mpL!;&lAZc5AqEz4prGwi0lBRs_ISv^ zLJkxRX($1hqStGuE{z6;2oBp>GaNLAdQjjRvXf7v0VDh#JN3rV9iUq^8n9tx09w&d z!hZZ=N*f9WwF1C~A-A16H2Ph{lh%HyMZpoC7>;0)PXQ8xeb7$_{}VQ{9=p9yMG_u+ zcSJ!F5eMqK6(kXAkJwpPqz{YaC^xw6_7<-KCHkzrP@#e){4F6nYYVARq7VG8kbn{_ z*xUoH3M|2fnq@m{38-KK_GFG>1x$2zL(!dlCv4CwK|x*>PQaq9GpN9c0T)!Sz=?1N zbUF}&6IOyznZu(1icn`KtfAnEKeKMey`L^^vRhEK&65e(iN zRA9w`1LcweD}t?^fE9sOOhf|oi9T%Z50W7R6*otK$Azt=lVjU{4)CY4(r}?L1GUtRQ`o*RlY=z2OfYn%}3scn4 zg0~0i@hQ+E;5;LAqCksa3)WL7poNU-(x8eLJT5SdD9~cyl-&yLDA+=7!D7M*myI}D zA&1I5&}nDRVFg=+I!_1}i(Tg!z~KPMCg40O zeAz1yM!?>lJayi_3sW&j*T%05<%NUc1gM zIZmTMv^j==k5Ri7v?2Aj45J0mM!HNOM-RL~Ldy1UyEAG^Z<9g=WFpbwyqfV&mzzzfvjb<~RGBv1#M?icZ2 zw2ktFNMNYn<%HWih5vkXeBqy`8P? zB0K+6={H z0fa{wc>}V*@8m#|G&I!L>uN{TgadU3fkVW)Y>(n@Ju=)if`)K5MKG)iZ}aPRaz@bC zc}#f#M-gg2Za4HtysqY9u)6G4MKE9o?@n;R+HC;!)cU2J5L_$mSkjKpo*k$=_5AmYU!x(;duUqTH5?$^jK?5t1ceZQ{hd4Ws% zo(C?O_X2Rqqc7lAV>fQ87TIz6G=wjRf+HI78m^f;VM%N%oAVbP5huR)#on6Qjt<~r^a0?y|V_!aUsf6lq{~Jyfi7O!vZu)Ydij{Hn z3DCPV8Y!EV0g-+Y*(%ZCE#xwqpjRck6)PYUjImK;K*lD;jRGaVB^a!Gi8jEvN`gxi z?8Bw37X`&4!xbx_t%$(;H5z%H8NyklWRbxV9B&itfiEu7=vt!L9!^{1=p&$=8oeKX zR)Ju2nPfI~gAFN#`HK!B6+~+h zcvp5Hm!#&cdX-weneR$HB$7a>9x&|c5XmGpf7PdO7%I7y_vRi34{gSw&#mti=_ECc zC2sDI;=ef{;k9SV$MhbtX}Cn8*_|Spq~^2SF&-F}dGd~chjt;~w6RlUlGMD`K#Zjh z0qwxFPmxH{>DPzB)YwU{;;uB7-5QKB9TRKGTrV<6>4$NIOYahyOeMWllROmTfqhy1 zjL+x;Vk_!C5h>nGS4q$I{${`{fxf z*y5nxgB(|r6?wWcmE8o?)v)Y5L=}_7M(oTQpayWcaMr3EBDT!i3Q<(<^_@!I>?+CD?X8xEcFSaXrGrN#>g$L*&Z1RZPaO;(cGnIdvQ%RgT#P}&Ya4}@l z4-+ow)x_Co-b`iR*P`UjT7>{J9Y;kH&nvFtZxy+ON*PHPq1yC7Oqe6hN+zw1x@onN zO6yl<3=r6uJ|YrR*=})#krN`eHjHz7ncD7blagKQKzb}byOw?!w_UE8K4b`3wg+IQ zH!X^EUkxr9>eZI6i&f0VuKU$tcv-F_(hjpc{oHU+PtzLDKrlHbn2hb(nc9VP$7Sia z9U`;l7a6Nr9%L&wTzTf#A_Lh$;@kiD{UHxlS<*Q6eV?K4$>`U9J6traUJOh^djNc$m>;Ef`o zkzKTD-vxB($?HWj9<$llgCtyhmTt#ho*+LvCz(W=OeE?kJ%q+OBL|Guizt(4HMM>T6v);S zdjRF)@C}W*R)p}!yYH(7Cv!O4KP`eprB-r+CwMFH$7VCMf-f;*HqFw=FF4!0QX@VW M$ffqr8>@8x4`#n=o&W#< delta 16927 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 LUXr Prototype - Triage Screen - +