diff --git a/app/assets/javascripts/highcharts.src.js b/app/assets/javascripts/highcharts.src.js new file mode 100644 index 0000000..f477baf --- /dev/null +++ b/app/assets/javascripts/highcharts.src.js @@ -0,0 +1,14681 @@ +// ==ClosureCompiler== +// @compilation_level SIMPLE_OPTIMIZATIONS + +/** + * @license Highcharts JS v2.2.5 (2012-06-08) + * + * (c) 2009-2011 Torstein Hønsi + * + * License: www.highcharts.com/license + */ + +// JSLint options: +/*global Highcharts, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console */ + +(function () { +// encapsulated variables +var UNDEFINED, + 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), + SVG_NS = 'http://www.w3.org/2000/svg', + hasSVG = !!doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect, + hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4, // issue #38 + useCanVG = !hasSVG && !isIE && !!doc.createElement('canvas').getContext, + Renderer, + hasTouch = doc.documentElement.ontouchstart !== UNDEFINED, + symbolSizes = {}, + idCounter = 0, + garbageBin, + defaultOptions, + dateFormat, // function + globalAnimation, + pathAnim, + timeUnits, + noop = function () {}, + + // some constants for frequently used strings + 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 + //TRACKER_FILL = 'rgba(192,192,192,0.5)', + NORMAL_STATE = '', + HOVER_STATE = 'hover', + SELECT_STATE = 'select', + MILLISECOND = 'millisecond', + SECOND = 'second', + MINUTE = 'minute', + HOUR = 'hour', + DAY = 'day', + WEEK = 'week', + MONTH = 'month', + YEAR = 'year', + + // constants for attributes + FILL = 'fill', + LINEAR_GRADIENT = 'linearGradient', + STOPS = 'stops', + STROKE = 'stroke', + STROKE_WIDTH = 'stroke-width', + + // time methods, changed based on whether or not UTC is used + makeTime, + getMinutes, + getHours, + getDay, + getDate, + getMonth, + getFullYear, + setMinutes, + setHours, + setDate, + setMonth, + setFullYear, + + + // lookup over the types and the associated classes + seriesTypes = {}; + +// The Highcharts namespace +win.Highcharts = {}; + +/** + * 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) { + var n; + if (!a) { + a = {}; + } + for (n in b) { + a[n] = b[n]; + } + return a; +} + +/** + * Take an array and turn into a hash with even number arguments as keys and odd numbers as + * values. Allows creating constants for commonly used style properties, attributes etc. + * Avoid it in performance critical situations like looping + */ +function hash() { + var i = 0, + args = arguments, + length = args.length, + obj = {}; + for (; i < length; i++) { + obj[args[i++]] = args[i]; + } + return obj; +} + +/** + * Shortcut for parseInt + * @param {Object} s + * @param {Number} mag Magnitude + */ +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 array + * @param {Object} obj + */ +function isArray(obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; +} + +/** + * Check for number + * @param {Object} n + */ +function isNumber(n) { + return typeof n === 'number'; +} + +function log2lin(num) { + return math.log(num) / math.LN10; +} +function lin2log(num) { + return math.pow(10, num); +} + +/** + * 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) { + return isArray(obj) ? obj : [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; + } + } +} + +/** + * Set CSS on a given element + * @param {Object} el + * @param {Object} styles Style object with camel case property names + */ +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; +} + +/** + * 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; +} + +/** + * How many decimals are there in a number + */ +function getDecimals(number) { + + number = (number || 0).toString(); + + return number.indexOf('.') > -1 ? + number.split('.')[1].length : + 0; +} + +/** + * 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 = decimals === -1 ? + getDecimals(number) : + (isNaN(decimals = mathAbs(decimals)) ? 2 : decimals), + d = decPoint === undefined ? lang.decimalPoint : decPoint, + t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep, + s = n < 0 ? "-" : "", + i = String(pInt(n = mathAbs(+n || 0).toFixed(c))), + j = i.length > 3 ? i.length % 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) : ""); +} + +/** + * Pad a string to a given length by adding 0 to the beginning + * @param {Number} number + * @param {Number} length + */ +function pad(number, length) { + // Create an array of the remaining length +1 and join it with 0's + return new Array((length || 2) + 1 - String(number).length).join(0) + number; +} + +/** + * 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) { + if (!defined(timestamp) || isNaN(timestamp)) { + return 'Invalid date'; + } + format = pick(format, '%Y-%m-%d %H:%M:%S'); + + var date = new Date(timestamp), + key, // used in for constuct below + // 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, + /* // uncomment this and the 'W' format key below to enable week numbers + weekNumber = function () { + var clone = new Date(date.valueOf()), + day = clone[getDay]() == 0 ? 7 : clone[getDay](), + dayNumber; + clone.setDate(clone[getDate]() + 4 - day); + dayNumber = mathFloor((clone.getTime() - new Date(clone[getFullYear](), 0, 1, -6)) / 86400000); + return 1 + mathFloor(dayNumber / 7); + }, + */ + + // 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) + //'W': weekNumber(), + + // Month + 'b': lang.shortMonths[month], // Short month, like 'Jan' + 'B': lang.months[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 + 'L': pad(mathRound(timestamp % 1000), 3) // Milliseconds (naming from Ruby) + }; + + + // do the replaces + for (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; +}; + +/** + * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 + * @param {Number} interval + * @param {Array} multiples + * @param {Number} magnitude + * @param {Object} options + */ +function normalizeTickInterval(interval, multiples, magnitude, options) { + var normalized, i; + + // round to a tenfold of 1, 2, 2.5 or 5 + magnitude = pick(magnitude, 1); + normalized = interval / magnitude; + + // multiples for a linear scale + if (!multiples) { + multiples = [1, 2, 2.5, 5, 10]; + + // the allowDecimals option + if (options && 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 (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; +} + +/** + * Get a normalized tick interval for dates. Returns a configuration object with + * unit range (interval), count and name. Used to prepare data for getTimeTicks. + * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs + * of segments in stock charts, the normalizing logic was extracted in order to + * prevent it for running over again for each segment having the same interval. + * #662, #697. + */ +function normalizeTimeTickInterval(tickInterval, unitsOption) { + var units = unitsOption || [[ + MILLISECOND, // unit name + [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples + ], [ + SECOND, + [1, 2, 5, 10, 15, 30] + ], [ + MINUTE, + [1, 2, 5, 10, 15, 30] + ], [ + HOUR, + [1, 2, 3, 4, 6, 8, 12] + ], [ + DAY, + [1, 2] + ], [ + WEEK, + [1, 2] + ], [ + MONTH, + [1, 2, 3, 4, 6] + ], [ + YEAR, + null + ]], + unit = units[units.length - 1], // default unit is years + interval = timeUnits[unit[0]], + multiples = unit[1], + count, + i; + + // loop through the units to find the one that best fits the tickInterval + for (i = 0; i < units.length; i++) { + unit = units[i]; + interval = timeUnits[unit[0]]; + multiples = unit[1]; + + + if (units[i + 1]) { + // lessThan is in the middle between the highest multiple and the next unit. + var lessThan = (interval * multiples[multiples.length - 1] + + timeUnits[units[i + 1][0]]) / 2; + + // break and keep the current unit + if (tickInterval <= lessThan) { + break; + } + } + } + + // prevent 2.5 years intervals, though 25, 250 etc. are allowed + if (interval === timeUnits[YEAR] && tickInterval < 5 * interval) { + multiples = [1, 2, 5]; + } + + // prevent 2.5 years intervals, though 25, 250 etc. are allowed + if (interval === timeUnits[YEAR] && tickInterval < 5 * interval) { + multiples = [1, 2, 5]; + } + + // get the count + count = normalizeTickInterval(tickInterval / interval, multiples); + + return { + unitRange: interval, + count: count, + unitName: unit[0] + }; +} + +/** + * Set the tick positions to a time unit that makes sense, for example + * on the first of each month or on every Monday. Return an array + * with the time positions. Used in datetime axes as well as for grouping + * data on a datetime axis. + * + * @param {Object} normalizedInterval The interval in axis values (ms) and the count + * @param {Number} min The minimum in axis values + * @param {Number} max The maximum in axis values + * @param {Number} startOfWeek + */ +function getTimeTicks(normalizedInterval, min, max, startOfWeek) { + var tickPositions = [], + i, + higherRanks = {}, + useUTC = defaultOptions.global.useUTC, + minYear, // used in months and years as a basis for Date.UTC() + minDate = new Date(min), + interval = normalizedInterval.unitRange, + count = normalizedInterval.count; + + + + if (interval >= timeUnits[SECOND]) { // second + minDate.setMilliseconds(0); + minDate.setSeconds(interval >= timeUnits[MINUTE] ? 0 : + count * mathFloor(minDate.getSeconds() / count)); + } + + if (interval >= timeUnits[MINUTE]) { // minute + minDate[setMinutes](interval >= timeUnits[HOUR] ? 0 : + count * mathFloor(minDate[getMinutes]() / count)); + } + + if (interval >= timeUnits[HOUR]) { // hour + minDate[setHours](interval >= timeUnits[DAY] ? 0 : + count * mathFloor(minDate[getHours]() / count)); + } + + if (interval >= timeUnits[DAY]) { // day + minDate[setDate](interval >= timeUnits[MONTH] ? 1 : + count * mathFloor(minDate[getDate]() / count)); + } + + if (interval >= timeUnits[MONTH]) { // month + minDate[setMonth](interval >= timeUnits[YEAR] ? 0 : + count * mathFloor(minDate[getMonth]() / count)); + minYear = minDate[getFullYear](); + } + + if (interval >= timeUnits[YEAR]) { // year + minYear -= minYear % count; + minDate[setFullYear](minYear); + } + + // week is a special case that runs outside the hierarchy + if (interval === timeUnits[WEEK]) { + // get start of current week, independent of count + minDate[setDate](minDate[getDate]() - minDate[getDay]() + + pick(startOfWeek, 1)); + } + + + // get tick positions + i = 1; + minYear = minDate[getFullYear](); + var time = minDate.getTime(), + minMonth = minDate[getMonth](), + minDateDate = minDate[getDate](), + timezoneOffset = useUTC ? + 0 : + (24 * 3600 * 1000 + minDate.getTimezoneOffset() * 60 * 1000) % (24 * 3600 * 1000); // #950 + + // iterate and add tick positions at appropriate values + while (time < max) { + tickPositions.push(time); + + // if the interval is years, use Date.UTC to increase years + if (interval === timeUnits[YEAR]) { + time = makeTime(minYear + i * count, 0); + + // if the interval is months, use Date.UTC to increase months + } else if (interval === timeUnits[MONTH]) { + time = makeTime(minYear, minMonth + i * count); + + // if we're using global time, the interval is not fixed as it jumps + // one hour at the DST crossover + } else if (!useUTC && (interval === timeUnits[DAY] || interval === timeUnits[WEEK])) { + time = makeTime(minYear, minMonth, minDateDate + + i * count * (interval === timeUnits[DAY] ? 1 : 7)); + + // else, the interval is fixed and we use simple addition + } else { + time += interval * count; + + // mark new days if the time is dividable by day + if (interval <= timeUnits[HOUR] && time % timeUnits[DAY] === timezoneOffset) { + higherRanks[time] = DAY; + } + } + + i++; + } + + // push the last time + tickPositions.push(time); + + // record information on the chosen unit - for dynamic label formatter + tickPositions.info = extend(normalizedInterval, { + higherRanks: higherRanks, + totalRange: interval * count + }); + + return tickPositions; +} + +/** + * Helper class that contains variuos counters that are local to the chart. + */ +function ChartCounters() { + this.color = 0; + this.symbol = 0; +} + +ChartCounters.prototype = { + /** + * Wraps the color counter if it reaches the specified length. + */ + wrapColor: function (length) { + if (this.color >= length) { + this.color = 0; + } + }, + + /** + * Wraps the symbol counter if it reaches the specified length. + */ + wrapSymbol: function (length) { + if (this.symbol >= length) { + this.symbol = 0; + } + } +}; + + +/** + * Utility method that sorts an object array and keeping the order of equal items. + * ECMA script standard does not specify the behaviour when items are equal. + */ +function stableSort(arr, sortFunction) { + var length = arr.length, + sortValue, + i; + + // Add index to each item + for (i = 0; i < length; i++) { + arr[i].ss_i = i; // stable sort index + } + + arr.sort(function (a, b) { + sortValue = sortFunction(a, b); + return sortValue === 0 ? a.ss_i - b.ss_i : sortValue; + }); + + // Remove index from items + for (i = 0; i < length; i++) { + delete arr[i].ss_i; // stable sort index + } +} + +/** + * Non-recursive method to find the lowest member of an array. Math.min raises a maximum + * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This + * method is slightly slower, but safe. + */ +function arrayMin(data) { + var i = data.length, + min = data[0]; + + while (i--) { + if (data[i] < min) { + min = data[i]; + } + } + return min; +} + +/** + * Non-recursive method to find the lowest member of an array. Math.min raises a maximum + * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This + * method is slightly slower, but safe. + */ +function arrayMax(data) { + var i = data.length, + max = data[0]; + + while (i--) { + if (data[i] > max) { + max = data[i]; + } + } + return max; +} + +/** + * Utility method that destroys any SVGElement or VMLElement that are properties on the given object. + * It loops all properties and invokes destroy if there is a destroy method. The property is + * then delete'ed. + * @param {Object} The object to destroy properties on + * @param {Object} Exception, do not destroy this property, only delete it. + */ +function destroyObjectProperties(obj, except) { + var n; + for (n in obj) { + // If the object is non-null and destroy is defined + if (obj[n] && obj[n] !== except && obj[n].destroy) { + // Invoke the destroy + obj[n].destroy(); + } + + // Delete the property from the object. + delete obj[n]; + } +} + + +/** + * 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 = ''; +} + +/** + * Provide error messages for debugging, with links to online explanation + */ +function error(code, stop) { + var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code; + if (stop) { + throw msg; + } else if (win.console) { + console.log(msg); + } +} + +/** + * Fix JS round off float errors + * @param {Number} num + */ +function correctFloat(num) { + return parseFloat( + num.toPrecision(14) + ); +} + +/** + * The time unit lookup + */ +/*jslint white: true*/ +timeUnits = hash( + MILLISECOND, 1, + SECOND, 1000, + MINUTE, 60000, + HOUR, 3600000, + DAY, 24 * 3600000, + WEEK, 7 * 24 * 3600000, + MONTH, 30 * 24 * 3600000, + YEAR, 31556952000 +); +/*jslint white: false*/ +/** + * 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.length / numParams) { + while (shift--) { + end = [].concat(end).splice(0, numParams).concat(end); + } + } + elem.shift = 0; // 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 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); +} + + + +// check for a custom HighchartsAdapter defined prior to this file +var 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. + adapterRun = adapter.adapterRun, + getScript = adapter.getScript, + each = adapter.each, + grep = adapter.grep, + offset = adapter.offset, + map = adapter.map, + merge = adapter.merge, + addEvent = adapter.addEvent, + removeEvent = adapter.removeEvent, + fireEvent = adapter.fireEvent, + washMouseEvent = adapter.washMouseEvent, + animate = adapter.animate, + stop = adapter.stop; + +/* + * Define the adapter for frameworks. If an external adapter is not defined, + * Highcharts reverts to the built-in jQuery adapter. + */ +if (globalAdapter && globalAdapter.init) { + // Initialize the adapter with the pathAnim object that takes care + // of path animations. + globalAdapter.init(pathAnim); +} +if (!globalAdapter && win.jQuery) { + var jQ = jQuery; + + /** + * Downloads a script and executes a callback when done. + * @param {String} scriptLocation + * @param {Function} callback + */ + getScript = jQ.getScript; + + /** + * A direct link to jQuery methods. MooTools and Prototype adapters must be implemented for each case of method. + * @param {Object} elem The HTML element + * @param {String} method Which method to run on the wrapped element + */ + adapterRun = function (elem, method) { + return jQ(elem)[method](); + }; + + /** + * Utility for iterating over an array. Parameters are reversed compared to jQuery. + * @param {Array} arr + * @param {Function} fn + */ + each = function (arr, fn) { + var i = 0, + len = arr.length; + for (; 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 = [], + i = 0, + len = arr.length; + for (; 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]); + }; + + /** + * Get the position of an element relative to the top left of the page + */ + offset = function (el) { + return jQ(el).offset(); + }; + + /** + * 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, + defaultPrevented; + + // Remove warnings in Chrome when accessing layerX and layerY. Although Highcharts + // never uses these properties, Chrome includes them in the default click event and + // raises the warning when they are copied over in the extend statement below. + // + // To avoid problems in IE (see #1010) where we cannot delete the properties and avoid + // testing if they are there (warning in chrome) the only option is to test if running IE. + if (!isIE && eventArguments) { + delete eventArguments.layerX; + delete eventArguments.layerY; + } + + 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; + } + + // Wrap preventDefault and stopPropagation in try/catch blocks in + // order to prevent JS errors when cancelling events on non-DOM + // objects. #615. + each(['preventDefault', 'stopPropagation'], function (fn) { + var base = event[fn]; + event[fn] = function () { + try { + base.call(event); + } catch (e) { + if (fn === 'preventDefault') { + defaultPrevented = true; + } + } + }; + }); + + // trigger it + jQ(el).trigger(event); + + // attach the method + if (el[detachedType]) { + el[type] = el[detachedType]; + el[detachedType] = null; + } + + if (defaultFunction && !event.isDefaultPrevented() && !defaultPrevented) { + defaultFunction(event); + } + }; + + /** + * Extension method needed for MooTools + */ + washMouseEvent = function (e) { + return e; + }; + + /** + * 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 on init + + /*jslint unparam: true*//* allow unused param x in this function */ + jQ.extend(jQ.easing, { + easeOutQuad: function (x, t, b, c, d) { + return -c * (t /= d) * (t - 2) + b; + } + }); + /*jslint unparam: false*/ + + // extend the animate function to allow SVG animations + var jFx = jQ.fx, + jStep = jFx.step; + + // extend some methods to check for elem.attr, which means it is a Highcharts SVG object + each(['cur', '_default', 'width', 'height'], function (fn, i) { + var obj = jStep, + base, + elem; + + // Handle different parent objects + if (fn === 'cur') { + obj = jFx.prototype; // 'cur', the getter, relates to jFx.prototype + + } else if (fn === '_default' && jQ.Tween) { // jQuery 1.8 model + obj = jQ.Tween.propHooks[fn]; + fn = 'set'; + } + + // Overwrite the method + base = obj[fn]; + if (base) { // step.width and step.height don't exist in jQuery < 1.7 + + // create the extended function replacement + obj[fn] = function (fx) { + + // jFx.prototype.cur does not use fx argument + fx = i ? fx : this; + + // shortcut + elem = fx.elem; + + // jFX.prototype.cur returns the current value. The other ones are setters + // and returning a value has no effect. + return elem.attr ? // is SVG element wrapper + elem.attr(fx.prop, fn === 'cur' ? UNDEFINED : fx.now) : // apply the SVG wrapper's method + base.apply(this, arguments); // use jQuery's built-in method + }; + } + }); + + // animate paths + jStep.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)); + + }; +} + +/* **************************************************************************** + * 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'], + shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + decimalPoint: '.', + resetZoom: 'Reset zoom', + resetZoomTitle: 'Reset zoom level 1:1', + thousandsSep: ',' + }, + global: { + useUTC: true, + canvasToolsURL: 'http://code.highcharts.com/2.2.5/modules/canvas-tools.js' + }, + 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: '' + resetZoomButton: { + theme: { + zIndex: 20 + }, + position: { + align: 'right', + x: -10, + //verticalAlign: 'top', + y: 10 + } + // relativeTo: 'plot' + } + }, + title: { + text: 'Chart title', + align: 'center', + // floating: false, + // margin: 15, + // x: 0, + // verticalAlign: 'top', + y: 15, + style: { + color: '#3E576F', + fontSize: '16px' + } + + }, + subtitle: { + text: '', + align: 'center', + // floating: false + // x: 0, + // verticalAlign: 'top', + y: 30, + style: { + color: '#6D869F' + } + }, + + plotOptions: { + line: { // base series options + allowPointSelect: false, + showCheckbox: false, + animation: { + duration: 1000 + }, + //connectNulls: false, + //cursor: 'default', + //clip: true, + //dashStyle: null, + //enableMouseTracking: true, + events: {}, + //legendIndex: 0, + 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; + } + // backgroundColor: undefined, + // borderColor: undefined, + // borderRadius: undefined, + // borderWidth: undefined, + // padding: 3, + // shadow: false + }), + cropThreshold: 300, // draw points outside the plot area when the number of points is less than this + pointRange: 0, + //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 + //tooltip: { + //pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b>' + //valueDecimals: null, + //xDateFormat: '%A, %b %e, %Y', + //valuePrefix: '', + //ySuffix: '' + //} + // turboThreshold: 1000 + // 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; + }, + borderWidth: 1, + borderColor: '#909090', + borderRadius: 5, + navigation: { + // animation: true, + activeColor: '#3E576F', + // arrowSize: 12 + inactiveColor: '#CCC' + // style: {} // text styles + }, + // margin: 10, + // reversed: false, + shadow: false, + // backgroundColor: null, + /*style: { + padding: '5px' + },*/ + itemStyle: { + cursor: 'pointer', + color: '#3E576F', + fontSize: '12px' + }, + itemHoverStyle: { + //cursor: 'pointer', removed as of #601 + color: '#000' + }, + itemHiddenStyle: { + color: '#CCC' + }, + itemCheckboxStyle: { + position: ABSOLUTE, + width: '13px', // for IE precision + height: '13px' + }, + // itemWidth: undefined, + symbolWidth: 16, + symbolPadding: 5, + verticalAlign: 'bottom', + // width: undefined, + x: 0, + y: 0 + }, + + loading: { + // hideDuration: 100, + labelStyle: { + fontWeight: 'bold', + position: RELATIVE, + top: '1em' + }, + // showDuration: 0, + 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, + dateTimeLabelFormats: { + millisecond: '%A, %b %e, %H:%M:%S.%L', + second: '%A, %b %e, %H:%M:%S', + minute: '%A, %b %e, %H:%M', + hour: '%A, %b %e, %H:%M', + day: '%A, %b %e, %Y', + week: 'Week from %A, %b %e, %Y', + month: '%B %Y', + year: '%Y' + }, + //formatter: defaultFormatter, + headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>', + pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b><br/>', + shadow: true, + shared: useCanVG, + snap: hasTouch ? 25 : 10, + style: { + color: '#333333', + fontSize: '12px', + padding: '5px', + whiteSpace: 'nowrap' + } + //xDateFormat: '%A, %b %e, %Y', + //valueDecimals: null, + //valuePrefix: '', + //valueSuffix: '' + }, + + 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' + } + } +}; + + + + +// Series defaults +var defaultPlotOptions = defaultOptions.plotOptions, + defaultSeriesOptions = defaultPlotOptions.line; + +// set the default time methods +setTimeMethods(); + + + +/** + * 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, + GET = useUTC ? 'getUTC' : 'get', + SET = useUTC ? 'setUTC' : 'set'; + + 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 = GET + 'Minutes'; + getHours = GET + 'Hours'; + getDay = GET + 'Day'; + getDate = GET + 'Date'; + getMonth = GET + 'Month'; + getFullYear = GET + 'FullYear'; + setMinutes = SET + 'Minutes'; + setHours = SET + 'Hours'; + setDate = SET + 'Date'; + setMonth = SET + 'Month'; + setFullYear = SET + 'FullYear'; + +} + +/** + * Merge the default options with custom options and return the new options structure + * @param {Object} options The new custom options + */ +function setOptions(options) { + + // Pull out axis options and apply them to the respective default axis options + /*defaultXAxisOptions = merge(defaultXAxisOptions, options.xAxis); + defaultYAxisOptions = merge(defaultYAxisOptions, options.yAxis); + options.xAxis = options.yAxis = UNDEFINED;*/ + + // Merge in the default 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; +} + + + +/** + * 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 + 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); + if (result) { + rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)]; + } else { // hex + result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input); + if (result) { + 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 + }; +}; + + +/** + * A wrapper object for SVG elements + */ +function SVGElement() {} + +SVGElement.prototype = { + /** + * Initialize the SVG renderer + * @param {Object} renderer + * @param {String} nodeName + */ + init: function (renderer, nodeName) { + var wrapper = this; + wrapper.element = nodeName === 'span' ? + createElement(nodeName) : + doc.createElementNS(SVG_NS, nodeName); + wrapper.renderer = renderer; + /** + * A collection of attribute setters. These methods, if defined, are called right before a certain + * attribute is set on an element wrapper. Returning false prevents the default attribute + * setter to run. Returning a value causes the default setter to set that value. Used in + * Renderer.label. + */ + wrapper.attrSetters = {}; + }, + /** + * 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); + stop(this); // stop regardless of animation actually running, or reverting to .attr (#607) + 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 wrapper = this, + key, + value, + result, + i, + child, + element = wrapper.element, + nodeName = element.nodeName, + renderer = wrapper.renderer, + skipAttr, + titleNode, + attrSetters = wrapper.attrSetters, + shadows = wrapper.shadows, + hasSetSymbolSize, + doTransform, + ret = wrapper; + + // 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) || wrapper[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]; + + // check for a specific attribute setter + result = attrSetters[key] && attrSetters[key](value, key); + + if (result !== false) { + if (result !== UNDEFINED) { + value = result; // the attribute setter has returned a new value to set + } + + // paths + if (key === 'd') { + if (value && value.join) { // join path + value = value.join(' '); + } + if (/(NaN| {2}|^$)/.test(value)) { + value = 'M 0 0'; + } + //wrapper.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 (wrapper.rotation) { + attr(element, 'transform', 'rotate(' + wrapper.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; + + // rectangle border radius + } else if (nodeName === 'rect' && key === 'r') { + attr(element, { + rx: value, + ry: value + }); + skipAttr = true; + + // translation and text rotation + } else if (key === 'translateX' || key === 'translateY' || key === 'rotation' || key === 'verticalAlign') { + doTransform = true; + 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'; + value = value && value.toLowerCase(); + if (value === 'solid') { + value = NONE; + } else if (value) { + value = value + .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') { + wrapper[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]; + + // Title requires a subnode, #431 + } else if (key === 'title') { + titleNode = element.getElementsByTagName('title')[0]; + if (!titleNode) { + titleNode = doc.createElementNS(SVG_NS, 'title'); + element.appendChild(titleNode); + } + titleNode.textContent = 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 (wrapper.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) { + + + if (!hasSetSymbolSize) { + wrapper.symbolAttr(hash); + hasSetSymbolSize = true; + } + skipAttr = true; + } + + // let the shadow follow the main element + if (shadows && /^(width|height|visibility|x|y|d|transform)$/.test(key)) { + i = shadows.length; + while (i--) { + attr( + shadows[i], + key, + key === 'height' ? + mathMax(value - (shadows[i].cutHeight || 0), 0) : + value + ); + } + } + + // validate heights + if ((key === 'width' || key === 'height') && nodeName === 'rect' && value < 0) { + value = 0; + } + + // Record for animation and quick access without polling the DOM + wrapper[key] = value; + + // Update transform + if (doTransform) { + wrapper.updateTransform(); + } + + + if (key === 'text') { + // only one node allowed + wrapper.textStr = value; + if (wrapper.added) { + renderer.buildText(wrapper); + } + } else if (!skipAttr) { + attr(element, key, value); + } + + } + + } + + } + + // Workaround for our #732, WebKit's issue https://bugs.webkit.org/show_bug.cgi?id=78385 + // TODO: If the WebKit team fix this bug before the final release of Chrome 18, remove the workaround. + if (isWebKit && /Chrome\/(18|19)/.test(userAgent)) { + if (nodeName === 'text' && (hash.x !== UNDEFINED || hash.y !== UNDEFINED)) { + var parent = element.parentNode, + next = element.nextSibling; + + if (parent) { + parent.removeChild(element); + if (next) { + parent.insertBefore(element, next); + } else { + parent.appendChild(element); + } + } + } + } + // End of workaround for #732 + + 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', 'anchorX', 'anchorY'], function (key) { + wrapper[key] = pick(hash[key], wrapper[key]); + }); + + wrapper.attr({ + d: wrapper.renderer.symbols[wrapper.symbolName](wrapper.x, wrapper.y, wrapper.width, wrapper.height, wrapper) + }); + }, + + /** + * 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, + attribs = {}, + values = {}, + normalizer; + + strokeWidth = strokeWidth || wrapper.strokeWidth || (wrapper.attr && wrapper.attr('stroke-width')) || 0; + normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors + + // 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] = attribs[key] = values[key]; + } + } + + return attribs; + }, + + /** + * Set styles for the element + * @param {Object} styles + */ + css: function (styles) { + /*jslint unparam: true*//* allow unused param a in the regexp function below */ + var elemWrapper = this, + elem = elemWrapper.element, + textWidth = styles && styles.width && elem.nodeName === 'text', + n, + serializedCss = '', + hyphenate = function (a, b) { return '-' + b.toLowerCase(); }; + /*jslint unparam: false*/ + + // convert legacy + if (styles && styles.color) { + styles.fill = styles.color; + } + + // Merge the new styles with the old ones + 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 { + for (n in styles) { + serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';'; + } + elemWrapper.attr({ + style: serializedCss + }); + } + + + // 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; + }, + + /** + * Set the coordinates needed to draw a consistent radial gradient across + * pie slices regardless of positioning inside the chart. The format is + * [centerX, centerY, diameter] in pixels. + */ + setRadialReference: function (coordinates) { + this.element.radialReference = coordinates; + 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; + }, + + /** + * Apply CSS to HTML elements. This is used in text within SVG rendering and + * by the VML renderer + */ + htmlCss: function (styles) { + var wrapper = this, + element = wrapper.element, + textWidth = styles && element.tagName === 'SPAN' && styles.width; + + if (textWidth) { + delete styles.width; + wrapper.textWidth = textWidth; + wrapper.updateTransform(); + } + + wrapper.styles = extend(wrapper.styles, styles); + css(wrapper.element, styles); + + return wrapper; + }, + + + + /** + * VML and useHTML method for calculating the bounding box based on offsets + * @param {Boolean} refresh Whether to force a fresh value from the DOM or to + * use the cached value + * + * @return {Object} A hash containing values for x, y, width and height + */ + + htmlGetBBox: function (refresh) { + var wrapper = this, + element = wrapper.element, + bBox = wrapper.bBox; + + // faking getBBox in exported SVG in legacy IE + if (!bBox || refresh) { + // faking getBBox in exported SVG in legacy IE + if (element.nodeName === 'text') { + element.style.position = ABSOLUTE; + } + + bBox = wrapper.bBox = { + x: element.offsetLeft, + y: element.offsetTop, + width: element.offsetWidth, + height: element.offsetHeight + }; + } + + return bBox; + }, + + /** + * VML override private method to update elements based on internal + * properties based on SVG transform + */ + htmlUpdateTransform: function () { + // aligning non added elements is expensive + if (!this.added) { + this.alignOnAdd = true; + return; + } + + var wrapper = this, + renderer = wrapper.renderer, + 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', + shadows = wrapper.shadows; + + // apply translate + if (translateX || translateY) { + css(elem, { + marginLeft: translateX, + marginTop: translateY + }); + if (shadows) { // used in labels/tooltip + each(shadows, function (shadow) { + css(shadow, { + marginLeft: translateX + 1, + marginTop: translateY + 1 + }); + }); + } + } + + // apply inversion + if (wrapper.inverted) { // wrapper is a group + each(elem.childNodes, function (child) { + renderer.invertChild(child, elem); + }); + } + + if (elem.tagName === 'SPAN') { + + var width, height, + rotation = wrapper.rotation, + baseline, + 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. Rotation of useHTML content is not yet implemented + // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+ + // has support for CSS3 transform. The getBBox method also needs to be updated + // to compensate for the rotation, like it currently does for SVG. + // 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 = pick(wrapper.elemWidth, elem.offsetWidth); + height = pick(wrapper.elemHeight, elem.offsetHeight); + + // update textWidth + if (width > textWidth && /[ \-]/.test(elem.innerText)) { // #983 + css(elem, { + width: textWidth + PX, + display: 'block', + whiteSpace: 'normal' + }); + width = textWidth; + } + + // correct x and y + baseline = renderer.fontMetrics(elem.style.fontSize).b; + xCorr = costheta < 0 && -width; + yCorr = sintheta < 0 && -height; + + // correct for baseline and corners spilling out after rotation + quad = costheta * sintheta < 0; + xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection); + yCorr -= costheta * baseline * (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) + PX, + top: (y + yCorr) + PX + }); + + // record current text transform + wrapper.cTT = currentTextTransform; + } + }, + + /** + * 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 || 0) + ' ' + (wrapper.y || 0) + ')'); + } + + 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) { + var elemWrapper = this; + + if (!alignOptions) { // called on resize + alignOptions = elemWrapper.alignOptions; + alignByTranslate = elemWrapper.alignByTranslate; + } else { // first call on instanciate + elemWrapper.alignOptions = alignOptions; + elemWrapper.alignByTranslate = alignByTranslate; + if (!box) { // boxes other than renderer handle this internally + elemWrapper.renderer.alignedObjects.push(elemWrapper); + } + } + + box = pick(box, elemWrapper.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 + elemWrapper[elemWrapper.placed ? 'animate' : 'attr'](attribs); + elemWrapper.placed = true; + elemWrapper.alignAttr = attribs; + + return elemWrapper; + }, + + /** + * Get the bounding box (width, height, x and y) for the element + */ + getBBox: function (refresh) { + var wrapper = this, + bBox, + width, + height, + rotation = wrapper.rotation, + element = wrapper.element, + rad = rotation * deg2rad; + + // SVG elements + if (element.namespaceURI === SVG_NS || wrapper.renderer.forExport) { + try { // Fails in Firefox if the container has display: none. + + bBox = element.getBBox ? + // SVG: use extend because IE9 is not allowed to change width and height in case + // of rotation (below) + extend({}, element.getBBox()) : + // Canvas renderer and legacy IE in export mode + { + width: element.offsetWidth, + height: element.offsetHeight + }; + } catch (e) {} + + // If the bBox is not set, the try-catch block above failed. The other condition + // is for Opera that returns a width of -Infinity on hidden elements. + if (!bBox || bBox.width < 0) { + 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)); + } + + // VML Renderer or useHTML within SVG + } else { + bBox = wrapper.htmlGetBBox(refresh); + } + + return bBox; + }, + + /** + * 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, + inserted; + + // 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); + inserted = true; + break; + } + } + } + + // default: append at the end + if (!inserted) { + parentNode.appendChild(element); + } + + // mark as added + this.added = true; + + // fire an event for internal hooks + fireEvent(this, 'add'); + + return this; + }, + + /** + * Removes a child either by removeChild or move to garbageBin. + * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not. + */ + safeRemoveChild: function (element) { + var parentNode = element.parentNode; + if (parentNode) { + parentNode.removeChild(element); + } + }, + + /** + * Destroy the element and element wrapper + */ + destroy: function () { + var wrapper = this, + element = wrapper.element || {}, + shadows = wrapper.shadows, + box = wrapper.box, + key, + i; + + // remove events + element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = null; + stop(wrapper); // stop running animations + + if (wrapper.clipPath) { + wrapper.clipPath = wrapper.clipPath.destroy(); + } + + // Destroy stops in case this is a gradient object + if (wrapper.stops) { + for (i = 0; i < wrapper.stops.length; i++) { + wrapper.stops[i] = wrapper.stops[i].destroy(); + } + wrapper.stops = null; + } + + // remove element + wrapper.safeRemoveChild(element); + + // destroy shadows + if (shadows) { + each(shadows, function (shadow) { + wrapper.safeRemoveChild(shadow); + }); + } + + // destroy label box + if (box) { + box.destroy(); + } + + // 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, group, cutOff) { + var shadows = [], + i, + shadow, + element = this.element, + strokeWidth, + + // compensate for inverted plot area + transform = this.parentInverted ? '(-1,-1)' : '(1,1)'; + + + if (apply) { + for (i = 1; i <= 3; i++) { + shadow = element.cloneNode(0); + strokeWidth = 7 - 2 * i; + attr(shadow, { + 'isShadow': 'true', + 'stroke': 'rgb(0, 0, 0)', + 'stroke-opacity': 0.05 * i, + 'stroke-width': strokeWidth, + 'transform': 'translate' + transform, + 'fill': NONE + }); + if (cutOff) { + attr(shadow, 'height', mathMax(attr(shadow, 'height') - strokeWidth, 0)); + shadow.cutHeight = strokeWidth; + } + + if (group) { + group.element.appendChild(shadow); + } else { + 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 = { + Element: SVGElement, + + /** + * 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; + + boxWrapper = renderer.createElement('svg') + .attr({ + xmlns: SVG_NS, + version: '1.1' + }); + container.appendChild(boxWrapper.element); + + // object properties + renderer.isSVG = true; + renderer.box = boxWrapper.element; + renderer.boxWrapper = boxWrapper; + renderer.alignedObjects = []; + renderer.url = isIE ? '' : loc.href.replace(/#.*?$/, '') + .replace(/([\('\)])/g, '\\$1'); // Page url used for internal references. #24, #672. + renderer.defs = this.createElement('defs').add(); + renderer.forExport = forExport; + renderer.gradients = {}; // Object where gradient SvgElements are stored + + renderer.setSize(width, height, false); + + + + // 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) { + renderer.subPixelFix = subPixelFix = function () { + css(container, { left: 0, top: 0 }); + rect = container.getBoundingClientRect(); + css(container, { + left: (mathCeil(rect.left) - rect.left) + PX, + top: (mathCeil(rect.top) - rect.top) + PX + }); + }; + + // run the fix now + subPixelFix(); + + // run it on resize + addEvent(win, 'resize', subPixelFix); + } + }, + + /** + * Detect whether the renderer is hidden. This happens when one of the parent elements + * has display: none. #608. + */ + isHidden: function () { + return !this.boxWrapper.getBBox().width; + }, + + /** + * Destroys the renderer and its allocated members. + */ + destroy: function () { + var renderer = this, + rendererDefs = renderer.defs; + renderer.box = null; + renderer.boxWrapper = renderer.boxWrapper.destroy(); + + // Call destroy on all gradient elements + destroyObjectProperties(renderer.gradients || {}); + renderer.gradients = null; + + // Defs are null in VMLRenderer + // Otherwise, destroy them here. + if (rendererDefs) { + renderer.defs = rendererDefs.destroy(); + } + + // Remove sub pixel fix handler + // We need to check that there is a handler, otherwise all functions that are registered for event 'resize' are removed + // See issue #982 + if (renderer.subPixelFix) { + removeEvent(win, 'resize', renderer.subPixelFix); + } + + renderer.alignedObjects = null; + + return null; + }, + + /** + * Create a wrapper for an SVG element + * @param {Object} nodeName + */ + createElement: function (nodeName) { + var wrapper = new this.Element(); + wrapper.init(this, nodeName); + return wrapper; + }, + + /** + * Dummy function for use in canvas renderer + */ + draw: function () {}, + + /** + * Parse a simple HTML string into SVG tspans + * + * @param {Object} textNode The parent text SVG node + */ + buildText: function (wrapper) { + var textNode = wrapper.element, + lines = pick(wrapper.textStr, '').toString() + .replace(/<(b|strong)>/g, '<span style="font-weight:bold">') + .replace(/<(i|em)>/g, '<span style="font-style:italic">') + .replace(/<a/g, '<span') + .replace(/<\/(b|strong|i|em|a)>/g, '</span>') + .split(/<br.*?>/g), + childNodes = textNode.childNodes, + styleRegex = /style="([^"]+)"/, + hrefRegex = /href="([^"]+)"/, + parentX = attr(textNode, 'x'), + textStyles = wrapper.styles, + width = textStyles && pInt(textStyles.width), + textLineHeight = textStyles && textStyles.lineHeight, + lastLine, + GET_COMPUTED_STYLE = 'getComputedStyle', + i = childNodes.length, + linePositions = []; + + // Needed in IE9 because it doesn't report tspan's offsetHeight (#893) + function getLineHeightByBBox(lineNo) { + linePositions[lineNo] = textNode.getBBox().height; + return mathRound(linePositions[lineNo] - (linePositions[lineNo - 1] || 0)); + } + + // 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 + } + + // remove empty line at end + if (lines[lines.length - 1] === '') { + lines.pop(); + } + + // build the lines + each(lines, function (line, lineNo) { + var spans, spanNo = 0, lineHeight; + + line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||'); + spans = line.split('|||'); + + each(spans, function (span) { + if (span !== '' || spans.length === 1) { + var attributes = {}, + tspan = doc.createElementNS(SVG_NS, 'tspan'); + if (styleRegex.test(span)) { + attr( + tspan, + 'style', + span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2') + ); + } + if (hrefRegex.test(span)) { + attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"'); + css(tspan, { cursor: 'pointer' }); + } + + span = (span.replace(/<(.|\n)*?>/g, '') || ' ') + .replace(/</g, '<') + .replace(/>/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) { + + // allow getting the right offset height in exporting in IE + if (!hasSVG && wrapper.renderer.forExport) { + css(tspan, { display: 'block' }); + } + + // Webkit and opera sometimes return 'normal' as the line height. In that + // case, webkit uses offsetHeight, while Opera falls back to 18 + lineHeight = win[GET_COMPUTED_STYLE] && + pInt(win[GET_COMPUTED_STYLE](lastLine, null).getPropertyValue('line-height')); + + if (!lineHeight || isNaN(lineHeight)) { + lineHeight = textLineHeight || lastLine.offsetHeight || getLineHeightByBBox(lineNo) || 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 = wrapper.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, { + dy: textLineHeight || 16, + x: parentX + }); + 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()); + } + if (words.length) { + tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); + } + } + } + } + }); + }); + }, + + /** + * Create a button with preset states + * @param {String} text + * @param {Number} x + * @param {Number} y + * @param {Function} callback + * @param {Object} normalState + * @param {Object} hoverState + * @param {Object} pressedState + */ + button: function (text, x, y, callback, normalState, hoverState, pressedState) { + var label = this.label(text, x, y), + curState = 0, + stateOptions, + stateStyle, + normalStyle, + hoverStyle, + pressedStyle, + STYLE = 'style', + verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 }; + + // prepare the attributes + /*jslint white: true*/ + normalState = merge(hash( + STROKE_WIDTH, 1, + STROKE, '#999', + FILL, hash( + LINEAR_GRADIENT, verticalGradient, + STOPS, [ + [0, '#FFF'], + [1, '#DDD'] + ] + ), + 'r', 3, + 'padding', 3, + STYLE, hash( + 'color', 'black' + ) + ), normalState); + /*jslint white: false*/ + normalStyle = normalState[STYLE]; + delete normalState[STYLE]; + + /*jslint white: true*/ + hoverState = merge(normalState, hash( + STROKE, '#68A', + FILL, hash( + LINEAR_GRADIENT, verticalGradient, + STOPS, [ + [0, '#FFF'], + [1, '#ACF'] + ] + ) + ), hoverState); + /*jslint white: false*/ + hoverStyle = hoverState[STYLE]; + delete hoverState[STYLE]; + + /*jslint white: true*/ + pressedState = merge(normalState, hash( + STROKE, '#68A', + FILL, hash( + LINEAR_GRADIENT, verticalGradient, + STOPS, [ + [0, '#9BD'], + [1, '#CDF'] + ] + ) + ), pressedState); + /*jslint white: false*/ + pressedStyle = pressedState[STYLE]; + delete pressedState[STYLE]; + + // add the events + addEvent(label.element, 'mouseenter', function () { + label.attr(hoverState) + .css(hoverStyle); + }); + addEvent(label.element, 'mouseleave', function () { + stateOptions = [normalState, hoverState, pressedState][curState]; + stateStyle = [normalStyle, hoverStyle, pressedStyle][curState]; + label.attr(stateOptions) + .css(stateStyle); + }); + + label.setState = function (state) { + curState = state; + if (!state) { + label.attr(normalState) + .css(normalStyle); + } else if (state === 2) { + label.attr(pressedState) + .css(pressedStyle); + } + }; + + return label + .on('click', function () { + callback.call(label); + }) + .attr(normalState) + .css(extend({ cursor: 'default' }, normalStyle)); + }, + + /** + * 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) { + var attr = { + fill: NONE + }; + if (isArray(path)) { + attr.d = path; + } else if (isObject(path)) { // attributes + extend(attr, path); + } + return this.createElement('path').attr(attr); + }, + + /** + * 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, 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) { + + r = isObject(x) ? x.r : r; + + var wrapper = this.createElement('rect').attr({ + rx: r, + ry: r, + fill: NONE + }); + return wrapper.attr( + isObject(x) ? + x : + // do not crispify when an object is passed in (as in column charts) + 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) { + var elem = this.createElement('g'); + return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem; + }, + + /** + * 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 + if (elemWrapper.element.setAttributeNS) { + elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink', + 'href', src); + } else { + // could be exporting in IE + // using href throws "not supported" in ie7 and under, requries regex shim to fix later + elemWrapper.element.setAttribute('hc-svg-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, width, height, 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( + mathRound(x), + mathRound(y), + width, + height, + options + ), + + imageRegex = /^url\((.*?)\)$/, + imageSrc, + imageSize, + centerImage; + + if (path) { + + obj = this.path(path); + // expando properties for use in animate and attr + extend(obj, { + symbolName: symbol, + x: x, + y: y, + width: width, + height: height + }); + if (options) { + extend(obj, options); + } + + + // image symbols + } else if (imageRegex.test(symbol)) { + + // On image load, set the size and position + centerImage = function (img, size) { + img.attr({ + width: size[0], + height: size[1] + }); + + if (!img.alignByTranslate) { // #185 + img.translate( + -mathRound(size[0] / 2), + -mathRound(size[1] / 2) + ); + } + }; + + imageSrc = symbol.match(imageRegex)[1]; + imageSize = symbolSizes[imageSrc]; + + // create the image synchronously, add attribs async + obj = this.image(imageSrc) + .attr({ + x: x, + y: y + }); + + if (imageSize) { + centerImage(obj, imageSize); + } else { + // initialize image to be 0 size so export will still function if there's no cached sizes + obj.attr({ width: 0, height: 0 }); + + // create a dummy JavaScript image to get the width and height + createElement('img', { + onload: function () { + var img = this; + + centerImage(obj, symbolSizes[imageSrc] = [img.width, img.height]); + }, + src: imageSrc + }); + } + } + + return obj; + }, + + /** + * An extendable collection of functions for defining symbol paths. + */ + symbols: { + 'circle': function (x, y, w, h) { + var cpw = 0.166 * w; + return [ + M, x + w / 2, y, + 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h, + 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y, + 'Z' + ]; + }, + + 'square': function (x, y, w, h) { + return [ + M, x, y, + L, x + w, y, + x + w, y + h, + x, y + h, + 'Z' + ]; + }, + + 'triangle': function (x, y, w, h) { + return [ + M, x + w / 2, y, + L, x + w, y + h, + x, y + h, + 'Z' + ]; + }, + + 'triangle-down': function (x, y, w, h) { + return [ + M, x, y, + L, x + w, y, + x + w / 2, y + h, + 'Z' + ]; + }, + 'diamond': function (x, y, w, h) { + return [ + M, x + w / 2, y, + L, x + w, y + h / 2, + x + w / 2, y + h, + x, y + h / 2, + 'Z' + ]; + }, + 'arc': function (x, y, w, h, options) { + var start = options.start, + radius = options.r || w || h, + end = options.end - 0.000001, // to prevent cos and sin of start and end from becoming equal on 360 arcs + innerRadius = options.innerR, + open = options.open, + 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, + open ? M : 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, + + open ? '' : '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; + wrapper.clipPath = clipPath; + + return wrapper; + }, + + + /** + * Take a color and return it if it's a string, make it a gradient if it's a + * gradient configuration object. Prior to Highstock, an array was used to define + * a linear gradient with pixel positions relative to the SVG. In newer versions + * we change the coordinates to apply relative to the shape, using coordinates + * 0-1 within the shape. To preserve backwards compatibility, linearGradient + * in this definition is an object of x1, y1, x2 and y2. + * + * @param {Object} color The color or config object + */ + color: function (color, elem, prop) { + var renderer = this, + colorObject, + regexRgba = /^rgba/, + gradName; + + // Apply linear or radial gradients + if (color && color.linearGradient) { + gradName = 'linearGradient'; + } else if (color && color.radialGradient) { + gradName = 'radialGradient'; + } + + if (gradName) { + var gradAttr = color[gradName], + gradients = renderer.gradients, + gradientObject, + stopColor, + stopOpacity, + radialReference = elem.radialReference; + + // Check if a gradient object with the same config object is created within this renderer + if (!gradAttr.id || !gradients[gradAttr.id]) { + + // Keep < 2.2 kompatibility + if (isArray(gradAttr)) { + color[gradName] = gradAttr = { + x1: gradAttr[0], + y1: gradAttr[1], + x2: gradAttr[2], + y2: gradAttr[3], + gradientUnits: 'userSpaceOnUse' + }; + } + + // Correct the radial gradient for the radial reference system + if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) { + extend(gradAttr, { + cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2], + cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2], + r: gradAttr.r * radialReference[2], + gradientUnits: 'userSpaceOnUse' + }); + } + + // Set the id and create the element + gradAttr.id = PREFIX + idCounter++; + gradients[gradAttr.id] = gradientObject = renderer.createElement(gradName) + .attr(gradAttr) + .add(renderer.defs); + + + // The gradient needs to keep a list of stops to be able to destroy them + gradientObject.stops = []; + each(color.stops, function (stop) { + var stopObject; + if (regexRgba.test(stop[1])) { + colorObject = Color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + stopObject = renderer.createElement('stop').attr({ + offset: stop[0], + 'stop-color': stopColor, + 'stop-opacity': stopOpacity + }).add(gradientObject); + + // Add the stop element to the gradient + gradientObject.stops.push(stopObject); + }); + } + + // Return the reference to the gradient object + return 'url(' + renderer.url + '#' + gradAttr.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 { + // Remove the opacity attribute added above. Does not throw if the attribute is not there. + elem.removeAttribute(prop + '-opacity'); + + return color; + } + + }, + + + /** + * Add text to the SVG object + * @param {String} str + * @param {Number} x Left position + * @param {Number} y Top position + * @param {Boolean} useHTML Use HTML to render the text + */ + text: function (str, x, y, useHTML) { + + // declare variables + var renderer = this, + defaultChartStyle = defaultOptions.chart.style, + wrapper; + + if (useHTML && !renderer.forExport) { + return renderer.html(str, x, y); + } + + x = mathRound(pick(x, 0)); + y = mathRound(pick(y, 0)); + + wrapper = renderer.createElement('text') + .attr({ + x: x, + y: y, + text: str + }) + .css({ + fontFamily: defaultChartStyle.fontFamily, + fontSize: defaultChartStyle.fontSize + }); + + wrapper.x = x; + wrapper.y = y; + return wrapper; + }, + + + /** + * Create HTML text node. This is used by the VML renderer as well as the SVG + * renderer through the useHTML option. + * + * @param {String} str + * @param {Number} x + * @param {Number} y + */ + html: function (str, x, y) { + var defaultChartStyle = defaultOptions.chart.style, + wrapper = this.createElement('span'), + attrSetters = wrapper.attrSetters, + element = wrapper.element, + renderer = wrapper.renderer; + + // Text setter + attrSetters.text = function (value) { + element.innerHTML = value; + return false; + }; + + // Various setters which rely on update transform + attrSetters.x = attrSetters.y = attrSetters.align = function (value, key) { + if (key === 'align') { + key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML. + } + wrapper[key] = value; + wrapper.htmlUpdateTransform(); + return false; + }; + + // Set the default attributes + wrapper.attr({ + text: str, + x: mathRound(x), + y: mathRound(y) + }) + .css({ + position: ABSOLUTE, + whiteSpace: 'nowrap', + fontFamily: defaultChartStyle.fontFamily, + fontSize: defaultChartStyle.fontSize + }); + + // Use the HTML specific .css method + wrapper.css = wrapper.htmlCss; + + // This is specific for HTML within SVG + if (renderer.isSVG) { + wrapper.add = function (svgGroupWrapper) { + + var htmlGroup, + htmlGroupStyle, + container = renderer.box.parentNode; + + // Create a mock group to hold the HTML elements + if (svgGroupWrapper) { + htmlGroup = svgGroupWrapper.div; + if (!htmlGroup) { + htmlGroup = svgGroupWrapper.div = createElement(DIV, { + className: attr(svgGroupWrapper.element, 'class') + }, { + position: ABSOLUTE, + left: svgGroupWrapper.attr('translateX') + PX, + top: svgGroupWrapper.attr('translateY') + PX + }, container); + + // Ensure dynamic updating position + htmlGroupStyle = htmlGroup.style; + extend(svgGroupWrapper.attrSetters, { + translateX: function (value) { + htmlGroupStyle.left = value + PX; + }, + translateY: function (value) { + htmlGroupStyle.top = value + PX; + }, + visibility: function (value, key) { + htmlGroupStyle[key] = value; + } + }); + + } + } else { + htmlGroup = container; + } + + htmlGroup.appendChild(element); + + // Shared with VML: + wrapper.added = true; + if (wrapper.alignOnAdd) { + wrapper.htmlUpdateTransform(); + } + + return wrapper; + }; + } + return wrapper; + }, + + /** + * Utility to return the baseline offset and total line height from the font size + */ + fontMetrics: function (fontSize) { + fontSize = pInt(fontSize || 11); + + // Empirical values found by comparing font size and bounding box height. + // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/ + var lineHeight = fontSize < 24 ? fontSize + 4 : mathRound(fontSize * 1.2), + baseline = mathRound(lineHeight * 0.8); + + return { + h: lineHeight, + b: baseline + }; + }, + + /** + * Add a label, a text item that can hold a colored or gradient background + * as well as a border and shadow. + * @param {string} str + * @param {Number} x + * @param {Number} y + * @param {String} shape + * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the + * coordinates it should be pinned to + * @param {Number} anchorY + * @param {Boolean} baseline Whether to position the label relative to the text baseline, + * like renderer.text, or to the upper border of the rectangle. + * @param {String} className Class name for the group + */ + label: function (str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) { + + var renderer = this, + wrapper = renderer.g(className), + text = renderer.text('', 0, 0, useHTML) + .attr({ + zIndex: 1 + }) + .add(wrapper), + box, + bBox, + alignFactor = 0, + padding = 3, + width, + height, + wrapperX, + wrapperY, + crispAdjust = 0, + deferredAttr = {}, + baselineOffset, + attrSetters = wrapper.attrSetters; + + /** + * This function runs after the label is added to the DOM (when the bounding box is + * available), and after the text of the label is updated to detect the new bounding + * box and reflect it in the border box. + */ + function updateBoxSize() { + var boxY, + style = text.element.style; + + bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && + text.getBBox(true); + wrapper.width = (width || bBox.width || 0) + 2 * padding; + wrapper.height = (height || bBox.height || 0) + 2 * padding; + + // update the label-scoped y offset + baselineOffset = padding + renderer.fontMetrics(style && style.fontSize).b; + + + // create the border box if it is not already present + if (!box) { + boxY = baseline ? -baselineOffset : 0; + + wrapper.box = box = shape ? + renderer.symbol(shape, -alignFactor * padding, boxY, wrapper.width, wrapper.height) : + renderer.rect(-alignFactor * padding, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]); + box.add(wrapper); + } + + // apply the box attributes + box.attr(merge({ + width: wrapper.width, + height: wrapper.height + }, deferredAttr)); + deferredAttr = null; + } + + /** + * This function runs after setting text or padding, but only if padding is changed + */ + function updateTextPadding() { + var styles = wrapper.styles, + textAlign = styles && styles.textAlign, + x = padding * (1 - alignFactor), + y; + + // determin y based on the baseline + y = baseline ? 0 : baselineOffset; + + // compensate for alignment + if (defined(width) && (textAlign === 'center' || textAlign === 'right')) { + x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width); + } + + // update if anything changed + if (x !== text.x || y !== text.y) { + text.attr({ + x: x, + y: y + }); + } + + // record current values + text.x = x; + text.y = y; + } + + /** + * Set a box attribute, or defer it if the box is not yet created + * @param {Object} key + * @param {Object} value + */ + function boxAttr(key, value) { + if (box) { + box.attr(key, value); + } else { + deferredAttr[key] = value; + } + } + + function getSizeAfterAdd() { + wrapper.attr({ + text: str, // alignment is available now + x: x, + y: y + }); + + if (defined(anchorX)) { + wrapper.attr({ + anchorX: anchorX, + anchorY: anchorY + }); + } + } + + /** + * After the text element is added, get the desired size of the border box + * and add it before the text in the DOM. + */ + addEvent(wrapper, 'add', getSizeAfterAdd); + + /* + * Add specific attribute setters. + */ + + // only change local variables + attrSetters.width = function (value) { + width = value; + return false; + }; + attrSetters.height = function (value) { + height = value; + return false; + }; + attrSetters.padding = function (value) { + if (defined(value) && value !== padding) { + padding = value; + updateTextPadding(); + } + + return false; + }; + + // change local variable and set attribue as well + attrSetters.align = function (value) { + alignFactor = { left: 0, center: 0.5, right: 1 }[value]; + return false; // prevent setting text-anchor on the group + }; + + // apply these to the box and the text alike + attrSetters.text = function (value, key) { + text.attr(key, value); + updateBoxSize(); + updateTextPadding(); + return false; + }; + + // apply these to the box but not to the text + attrSetters[STROKE_WIDTH] = function (value, key) { + crispAdjust = value % 2 / 2; + boxAttr(key, value); + return false; + }; + attrSetters.stroke = attrSetters.fill = attrSetters.r = function (value, key) { + boxAttr(key, value); + return false; + }; + attrSetters.anchorX = function (value, key) { + anchorX = value; + boxAttr(key, value + crispAdjust - wrapperX); + return false; + }; + attrSetters.anchorY = function (value, key) { + anchorY = value; + boxAttr(key, value - wrapperY); + return false; + }; + + // rename attributes + attrSetters.x = function (value) { + wrapper.x = value; // for animation getter + value -= alignFactor * ((width || bBox.width) + padding); + wrapperX = mathRound(value); + + wrapper.attr('translateX', wrapperX); + return false; + }; + attrSetters.y = function (value) { + wrapperY = wrapper.y = mathRound(value); + wrapper.attr('translateY', value); + return false; + }; + + // Redirect certain methods to either the box or the text + var baseCss = wrapper.css; + return extend(wrapper, { + /** + * Pick up some properties and apply them to the text instead of the wrapper + */ + css: function (styles) { + if (styles) { + var textStyles = {}; + styles = merge({}, styles); // create a copy to avoid altering the original object (#537) + each(['fontSize', 'fontWeight', 'fontFamily', 'color', 'lineHeight', 'width'], function (prop) { + if (styles[prop] !== UNDEFINED) { + textStyles[prop] = styles[prop]; + delete styles[prop]; + } + }); + text.css(textStyles); + } + return baseCss.call(wrapper, styles); + }, + /** + * Return the bounding box of the box, not the group + */ + getBBox: function () { + return box.getBBox(); + }, + /** + * Apply the shadow to the box + */ + shadow: function (b) { + box.shadow(b); + return wrapper; + }, + /** + * Destroy and release memory. + */ + destroy: function () { + removeEvent(wrapper, 'add', getSizeAfterAdd); + + // Added by button implementation + removeEvent(wrapper.element, 'mouseenter'); + removeEvent(wrapper.element, 'mouseleave'); + + if (text) { + // Destroy the text element + text = text.destroy(); + } + // Call base implementation to destroy the rest + SVGElement.prototype.destroy.call(wrapper); + } + }); + } +}; // end SVGRenderer + + +// general renderer +Renderer = 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. * + * * + *****************************************************************************/ + +/** + * @constructor + */ +var VMLRenderer; +if (!hasSVG && !useCanVG) { + +/** + * The VML element wrapper. + */ +var VMLElement = { + + /** + * 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 wrapper = this, + 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:1px;height:1px;'); + } + 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); + wrapper.element = createElement(markup); + } + + wrapper.renderer = renderer; + wrapper.attrSetters = {}; + }, + + /** + * 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.deferUpdateTransform) { + wrapper.updateTransform(); + } + + // fire an event for internal hooks + fireEvent(wrapper, 'add'); + + return wrapper; + }, + + /** + * In IE8 documentMode 8, we need to recursively set the visibility down in the DOM + * tree for nested groups. Related to #61, #586. + */ + toggleChildren: function (element, visibility) { + var childNodes = element.childNodes, + i = childNodes.length; + + while (i--) { + + // apply the visibility + css(childNodes[i], { visibility: visibility }); + + // we have a nested group, apply it to its children again + if (childNodes[i].nodeName === 'DIV') { + this.toggleChildren(childNodes[i], visibility); + } + } + }, + + /** + * VML always uses htmlUpdateTransform + */ + updateTransform: SVGElement.prototype.htmlUpdateTransform, + + /** + * Get or set attributes + */ + attr: function (hash, val) { + var wrapper = this, + key, + value, + i, + result, + element = wrapper.element || {}, + elemStyle = element.style, + nodeName = element.nodeName, + renderer = wrapper.renderer, + symbolName = wrapper.symbolName, + hasSetSymbolSize, + shadows = wrapper.shadows, + skipAttr, + attrSetters = wrapper.attrSetters, + ret = wrapper; + + // 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 = wrapper.strokeweight; + } else { + ret = wrapper[key]; + } + + // setter + } else { + for (key in hash) { + value = hash[key]; + skipAttr = false; + + // check for a specific attribute setter + result = attrSetters[key] && attrSetters[key](value, key); + + if (result !== false && value !== null) { // #620 + + if (result !== UNDEFINED) { + value = result; // the attribute setter has returned a new value to set + } + + + // prepare paths + // symbols + if (symbolName && /^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.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) { + wrapper.symbolAttr(hash); + + hasSetSymbolSize = true; + } + skipAttr = true; + + } else if (key === 'd') { + value = value || []; + wrapper.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; + } else if (value[i] === 'Z') { // close the path + 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 = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value; + } + } + skipAttr = true; + + // directly mapped to css + } else if (key === 'zIndex' || key === 'visibility') { + + // workaround for #61 and #586 + if (docMode8 && key === 'visibility' && nodeName === 'DIV') { + element.gVis = value; + wrapper.toggleChildren(element, value); + if (value === VISIBLE) { // #74 + value = null; + } + } + + if (value) { + elemStyle[key] = value; + } + + + + skipAttr = true; + + // width and height + } else if (key === 'width' || key === 'height') { + + value = mathMax(0, value); // don't set width or height below zero (#311) + + this[key] = value; // used in getter + + // clipping rectangle special + if (wrapper.updateClipping) { + wrapper[key] = value; + wrapper.updateClipping(); + } else { + // normal + elemStyle[key] = value; + } + + skipAttr = true; + + // x and y + } else if (key === 'x' || key === 'y') { + + wrapper[key] = value; // used in getter + 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'; + wrapper[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(['<stroke/>']), null, null, element); + strokeElem[key] = value || 'solid'; + wrapper.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'; + } + + // rotation on VML elements + } else if (nodeName === 'shape' && key === 'rotation') { + wrapper[key] = value; + + // translation for animation + } else if (key === 'translateX' || key === 'translateY' || key === 'rotation') { + wrapper[key] = value; + wrapper.updateTransform(); + + skipAttr = true; + + // text for rotated and non-rotated elements + } else if (key === 'text') { + this.bBox = null; + 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, + element = wrapper.element, + parentNode = element.parentNode; + + clipMembers.push(wrapper); + wrapper.destroyClip = function () { + erase(clipMembers, wrapper); + }; + + // Issue #863 workaround - related to #140, #61, #74 + if (parentNode && parentNode.className === 'highcharts-tracker' && !docMode8) { + css(element, { visibility: HIDDEN }); + } + + return wrapper.css(clipRect.getCSS(wrapper)); + }, + + /** + * Set styles for the element + * @param {Object} styles + */ + css: SVGElement.prototype.htmlCss, + + /** + * Removes a child either by removeChild or move to garbageBin. + * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not. + */ + safeRemoveChild: function (element) { + // discardElement will detach the node from its parent before attaching it + // to the garbage bin. Therefore it is important that the node is attached and have parent. + var parentNode = element.parentNode; + if (parentNode) { + discardElement(element); + } + }, + + /** + * Extend element.destroy by removing it from the clip members array + */ + destroy: function () { + var wrapper = this; + + if (wrapper.destroyClip) { + wrapper.destroyClip(); + } + + return 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); + } + }, + + /** + * 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; + }, + + /** + * In stacked columns, cut off the shadows so that they don't overlap + */ + cutOffPath: function (path, length) { + + var len; + + path = path.split(/[ ,]/); + len = path.length; + + if (len === 9 || len === 11) { + path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length; + } + return path.join(' '); + }, + + /** + * Apply a drop shadow by copying elements and giving them different strokes + * @param {Boolean} apply + */ + shadow: function (apply, group, cutOff) { + var shadows = [], + i, + element = this.element, + renderer = this.renderer, + shadow, + elemStyle = element.style, + markup, + path = element.path, + strokeWidth, + modifiedPath; + + // some times empty paths are not strings + if (path && typeof path.value !== 'string') { + path = 'x'; + } + modifiedPath = path; + + if (apply) { + for (i = 1; i <= 3; i++) { + + strokeWidth = 7 - 2 * i; + + // Cut off shadows for stacked column items + if (cutOff) { + modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5); + } + + markup = ['<shape isShadow="true" strokeweight="', (7 - 2 * i), + '" filled="false" path="', modifiedPath, + '" coordsize="10 10" style="', element.style.cssText, '" />']; + + shadow = createElement(renderer.prepVML(markup), + null, { + left: pInt(elemStyle.left) + 1, + top: pInt(elemStyle.top) + 1 + } + ); + if (cutOff) { + shadow.cutOff = strokeWidth + 1; + } + + // apply the opacity + markup = ['<stroke color="black" opacity="', (0.05 * i), '"/>']; + createElement(renderer.prepVML(markup), null, null, shadow); + + + // insert it + if (group) { + group.element.appendChild(shadow); + } else { + element.parentNode.insertBefore(shadow, element); + } + + // record it + shadows.push(shadow); + + } + + this.shadows = shadows; + } + return this; + + } +}; +VMLElement = extendClass(SVGElement, VMLElement); + +/** + * The VML renderer + */ +var VMLRendererExtension = { // inherit SVGRenderer + + Element: VMLElement, + 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, + box; + + renderer.alignedObjects = []; + + boxWrapper = renderer.createElement(DIV); + box = boxWrapper.element; + box.style.position = RELATIVE; // for freeform drawing using renderer directly + container.appendChild(boxWrapper.element); + + + // generate the containing box + renderer.box = box; + 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; } '; + + } + }, + + + /** + * Detect whether the renderer is hidden. This happens when one of the parent elements + * has display: none + */ + isHidden: function () { + return !this.box.offsetWidth; + }, + + /** + * 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 (wrapper) { + var inverted = wrapper.inverted, + rect = this, + 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 && wrapper.element.nodeName !== 'IMG') { + 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)); + }); + } + }); + + }, + + + /** + * 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, + fillType, + ret = NONE; + + // Check for linear or radial gradient + if (color && color.linearGradient) { + fillType = 'gradient'; + } else if (color && color.radialGradient) { + fillType = 'pattern'; + } + + + if (fillType) { + + var stopColor, + stopOpacity, + gradient = color.linearGradient || color.radialGradient, + x1, + y1, + x2, + y2, + angle, + opacity1, + opacity2, + color1, + color2, + fillAttr = '', + stops = color.stops, + firstStop, + lastStop, + colors = []; + + // Extend from 0 to 1 + firstStop = stops[0]; + lastStop = stops[stops.length - 1]; + if (firstStop[0] > 0) { + stops.unshift([ + 0, + firstStop[1] + ]); + } + if (lastStop[0] < 1) { + stops.push([ + 1, + lastStop[1] + ]); + } + + // Compute the stops + each(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; + } + + // Build the color attribute + colors.push((stop[0] * 100) + '% ' + stopColor); + + // Only start and end opacities are allowed, so we use the first and the last + if (!i) { + opacity1 = stopOpacity; + color2 = stopColor; + } else { + opacity2 = stopOpacity; + color1 = stopColor; + } + }); + + // Handle linear gradient angle + if (fillType === 'gradient') { + x1 = gradient.x1 || gradient[0] || 0; + y1 = gradient.y1 || gradient[1] || 0; + x2 = gradient.x2 || gradient[2] || 0; + y2 = gradient.y2 || gradient[3] || 0; + angle = 90 - math.atan( + (y2 - y1) / // y vector + (x2 - x1) // x vector + ) * 180 / mathPI; + + // Radial (circular) gradient + } else { + // pie: http://jsfiddle.net/highcharts/66g8H/ + // reference: http://jsfiddle.net/highcharts/etznJ/ + // http://jsfiddle.net/highcharts/XRbCc/ + // http://jsfiddle.net/highcharts/F3fwR/ + // TODO: + // - correct for radialRefeence + // - check whether gradient stops are supported + // - add global option for gradient image (must relate to version) + var r = gradient.r, + size = r * 2, + cx = gradient.cx, + cy = gradient.cy; + //radialReference = elem.radialReference; + + //if (radialReference) { + // Try setting pixel size, or other way to adjust the gradient size to the bounding box + //} + fillAttr = 'src="http://code.highcharts.com/gfx/radial-gradient.png" ' + + 'size="' + size + ',' + size + '" ' + + 'origin="0.5,0.5" ' + + 'position="' + cx + ',' + cy + '" ' + + 'color2="' + color2 + '" '; + + // The fill element's color attribute is broken in IE8 standards mode, so we + // need to set the parent shape's fillcolor attribute instead. + ret = color1; + } + + + + // Apply the gradient to fills only. + if (prop === 'fill') { + + // when colors attribute is used, the meanings of opacity and o:opacity2 + // are reversed. + markup = ['<fill colors="' + colors.join(',') + '" angle="', angle, + '" opacity="', opacity2, '" o:opacity2="', opacity1, + '" type="', fillType, '" ', fillAttr, 'focus="100%" method="any" />']; + createElement(this.prepVML(markup), null, null, elem); + + // Gradients are not supported for VML stroke, return the first color. #722. + } else { + ret = stopColor; + } + + + // 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); + + ret = colorObject.get('rgb'); + + + } else { + var strokeNodes = elem.getElementsByTagName(prop); + if (strokeNodes.length) { + strokeNodes[0].opacity = 1; + } + ret = color; + } + + return ret; + }, + + /** + * 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('<', '<hcv:'); + } + + return markup; + }, + + /** + * Create rotated and aligned text + * @param {String} str + * @param {Number} x + * @param {Number} y + */ + text: SVGRenderer.prototype.html, + + /** + * Create and return a path element + * @param {Array} path + */ + path: function (path) { + var attr = { + // subpixel precision down to 0.1 (width and height = 1px) + coordsize: '10 10' + }; + if (isArray(path)) { + attr.d = path; + } else if (isObject(path)) { // attributes + extend(attr, path); + } + // create the shape + return this.createElement('shape').attr(attr); + }, + + /** + * Create and return a circle element. In VML circles are implemented as + * shapes, which is faster than v:oval + * @param {Number} x + * @param {Number} y + * @param {Number} r + */ + circle: function (x, y, r) { + return this.symbol('circle').attr({ x: x - r, y: y - r, width: 2 * r, height: 2 * r }); + }, + + /** + * Create a group using an outer div and an inner v:group to allow rotating + * and flipping. A simple v:group would have problems with positioning + * child HTML elements and CSS clip. + * + * @param {String} name The name of the group + */ + g: function (name) { + var wrapper, + attribs; + + // set the class name + if (name) { + attribs = { 'className': PREFIX + name, 'class': PREFIX + name }; + } + + // the div to hold HTML and clipping + wrapper = this.createElement(DIV).attr(attribs); + + return wrapper; + }, + + /** + * VML override to create a regular HTML image + * @param {String} src + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + image: function (src, x, y, width, height) { + var obj = this.createElement('img') + .attr({ src: src }); + + if (arguments.length > 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; + strokeWidth = x.strokeWidth; + 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) - 1, + top: pInt(parentStyle.height) - 1, + rotation: -90 + }); + }, + + /** + * Symbol definitions that override the parent SVG renderer's symbols + * + */ + symbols: { + // VML specific arc function + arc: function (x, y, w, h, options) { + var start = options.start, + end = options.end, + radius = options.r || w || h, + cosStart = mathCos(start), + sinStart = mathSin(start), + cosEnd = mathCos(end), + sinEnd = mathSin(end), + innerRadius = options.innerR, + circleCorrection = 0.08 / radius, // #760 + innerCorrection = (innerRadius && 0.1 / innerRadius) || 0, + ret; + + if (end - start === 0) { // no angle, don't show it. + return ['x']; + + } 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); + } + + ret = [ + '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 + ]; + + if (options.open) { + ret.push( + M, + x - innerRadius, + y - innerRadius + ); + } + + ret.push( + '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 + ); + + return ret; + + }, + // Add circle symbol path. This performs significantly faster than v:oval. + circle: function (x, y, w, h) { + + return [ + 'wa', // clockwisearcto + x, // left + y, // top + x + w, // right + y + h, // bottom + x + w, // start x + y + h / 2, // start y + x + w, // end x + y + h / 2, // 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, width, height, options) { + + var right = left + width, + bottom = top + height, + ret, + r; + + // No radius, return the more lightweight square + if (!defined(options) || !options.r) { + ret = SVGRenderer.prototype.symbols.square.apply(0, arguments); + + // Has radius add arcs for the corners + } else { + + r = mathMin(options.r, width, height); + ret = [ + 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' + ]; + } + return ret; + } + } +}; +VMLRenderer = function () { + this.init.apply(this, arguments); +}; +VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension); + + // general renderer + Renderer = VMLRenderer; +} + +/* **************************************************************************** + * * + * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE * + * * + *****************************************************************************/ +/* **************************************************************************** + * * + * START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT * + * TARGETING THAT SYSTEM. * + * * + *****************************************************************************/ +var CanVGRenderer, + CanVGController; + +if (useCanVG) { + /** + * The CanVGRenderer is empty from start to keep the source footprint small. + * When requested, the CanVGController downloads the rest of the source packaged + * together with the canvg library. + */ + CanVGRenderer = function () { + // Empty constructor + }; + + /** + * Start with an empty symbols object. This is needed when exporting is used (exporting.src.js will add a few symbols), but + * the implementation from SvgRenderer will not be merged in until first render. + */ + CanVGRenderer.prototype.symbols = {}; + + /** + * Handles on demand download of canvg rendering support. + */ + CanVGController = (function () { + // List of renderering calls + var deferredRenderCalls = []; + + /** + * When downloaded, we are ready to draw deferred charts. + */ + function drawDeferred() { + var callLength = deferredRenderCalls.length, + callIndex; + + // Draw all pending render calls + for (callIndex = 0; callIndex < callLength; callIndex++) { + deferredRenderCalls[callIndex](); + } + // Clear the list + deferredRenderCalls = []; + } + + return { + push: function (func, scriptLocation) { + // Only get the script once + if (deferredRenderCalls.length === 0) { + getScript(scriptLocation, drawDeferred); + } + // Register render call + deferredRenderCalls.push(func); + } + }; + }()); +} // end CanVGRenderer + +/* **************************************************************************** + * * + * END OF ANDROID < 3 SPECIFIC CODE * + * * + *****************************************************************************/ + +/** + * General renderer + */ +Renderer = VMLRenderer || CanVGRenderer || SVGRenderer; +/** + * The Tick class + */ +function Tick(axis, pos, type) { + this.axis = axis; + this.pos = pos; + this.type = type || ''; + this.isNew = true; + + if (!type) { + this.addLabel(); + } +} + +Tick.prototype = { + /** + * Write the tick label + */ + addLabel: function () { + var tick = this, + axis = tick.axis, + options = axis.options, + chart = axis.chart, + horiz = axis.horiz, + categories = axis.categories, + pos = tick.pos, + labelOptions = options.labels, + str, + tickPositions = axis.tickPositions, + width = (categories && horiz && categories.length && + !labelOptions.step && !labelOptions.staggerLines && + !labelOptions.rotation && + chart.plotWidth / tickPositions.length) || + (!horiz && chart.plotWidth / 2), + isFirst = pos === tickPositions[0], + isLast = pos === tickPositions[tickPositions.length - 1], + css, + attr, + value = categories && defined(categories[pos]) ? categories[pos] : pos, + label = tick.label, + tickPositionInfo = tickPositions.info, + dateTimeLabelFormat; + + // Set the datetime label format. If a higher rank is set for this position, use that. If not, + // use the general format. + if (axis.isDatetimeAxis && tickPositionInfo) { + dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName]; + } + + // set properties for access in render method + tick.isFirst = isFirst; + tick.isLast = isLast; + + // get the string + str = axis.labelFormatter.call({ + axis: axis, + chart: chart, + isFirst: isFirst, + isLast: isLast, + dateTimeLabelFormat: dateTimeLabelFormat, + value: axis.isLog ? correctFloat(lin2log(value)) : value + }); + + // prepare CSS + css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX }; + css = extend(css, labelOptions.style); + + // first call + if (!defined(label)) { + attr = { + align: labelOptions.align + }; + if (isNumber(labelOptions.rotation)) { + attr.rotation = labelOptions.rotation; + } + tick.label = + defined(str) && labelOptions.enabled ? + chart.renderer.text( + str, + 0, + 0, + labelOptions.useHTML + ) + .attr(attr) + // without position absolute, IE export sometimes is wrong + .css(css) + .add(axis.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, + axis = this.axis; + return label ? + ((this.labelBBox = label.getBBox(true)))[axis.horiz ? 'height' : 'width'] : + 0; + }, + + /** + * Find how far the labels extend to the right and left of the tick's x position. Used for anti-collision + * detection with overflow logic. + */ + getLabelSides: function () { + var bBox = this.labelBBox, // assume getLabelSize has run at this point + axis = this.axis, + options = axis.options, + labelOptions = options.labels, + width = bBox.width, + leftSide = width * { left: 0, center: 0.5, right: 1 }[labelOptions.align] - labelOptions.x; + + return [-leftSide, width - leftSide]; + }, + + /** + * Handle the label overflow by adjusting the labels to the left and right edge, or + * hide them if they collide into the neighbour label. + */ + handleOverflow: function (index, xy) { + var show = true, + axis = this.axis, + chart = axis.chart, + isFirst = this.isFirst, + isLast = this.isLast, + x = xy.x, + reversed = axis.reversed, + tickPositions = axis.tickPositions; + + if (isFirst || isLast) { + + var sides = this.getLabelSides(), + leftSide = sides[0], + rightSide = sides[1], + plotLeft = chart.plotLeft, + plotRight = plotLeft + axis.len, + neighbour = axis.ticks[tickPositions[index + (isFirst ? 1 : -1)]], + neighbourEdge = neighbour && neighbour.label.xy.x + neighbour.getLabelSides()[isFirst ? 0 : 1]; + + if ((isFirst && !reversed) || (isLast && reversed)) { + // Is the label spilling out to the left of the plot area? + if (x + leftSide < plotLeft) { + + // Align it to plot left + x = plotLeft - leftSide; + + // Hide it if it now overlaps the neighbour label + if (neighbour && x + rightSide > neighbourEdge) { + show = false; + } + } + + } else { + // Is the label spilling out to the right of the plot area? + if (x + rightSide > plotRight) { + + // Align it to plot right + x = plotRight - rightSide; + + // Hide it if it now overlaps the neighbour label + if (neighbour && x + leftSide < neighbourEdge) { + show = false; + } + + } + } + + // Set the modified x position of the label + xy.x = x; + } + return show; + }, + + /** + * Get the x and y position for ticks and labels + */ + getPosition: function (horiz, pos, tickmarkOffset, old) { + var axis = this.axis, + chart = axis.chart, + cHeight = (old && chart.oldChartHeight) || chart.chartHeight; + + return { + x: horiz ? + axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB : + axis.left + axis.offset + (axis.opposite ? ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left : 0), + + y: horiz ? + cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : + cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB + }; + + }, + + /** + * Get the x, y position of the tick label + */ + getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { + var axis = this.axis, + transA = axis.transA, + reversed = axis.reversed, + staggerLines = axis.staggerLines; + + 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 += pInt(label.styles.lineHeight) * 0.9 - label.getBBox().height / 2; + } + + // Correct for staggered labels + if (staggerLines) { + y += (index / (step || 1) % staggerLines) * 16; + } + + return { + x: x, + y: y + }; + }, + + /** + * Extendible method to return the path of the marker + */ + getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) { + return renderer.crispLine([ + M, + x, + y, + L, + x + (horiz ? 0 : -tickLength), + y + (horiz ? tickLength : 0) + ], tickWidth); + }, + + /** + * 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, + axis = tick.axis, + options = axis.options, + chart = axis.chart, + renderer = chart.renderer, + horiz = axis.horiz, + type = tick.type, + label = tick.label, + pos = tick.pos, + labelOptions = options.labels, + gridLine = tick.gridLine, + gridPrefix = type ? type + 'Grid' : 'grid', + tickPrefix = type ? type + 'Tick' : 'tick', + gridLineWidth = options[gridPrefix + 'LineWidth'], + gridLineColor = options[gridPrefix + 'LineColor'], + dashStyle = options[gridPrefix + 'LineDashStyle'], + tickLength = options[tickPrefix + 'Length'], + tickWidth = options[tickPrefix + 'Width'] || 0, + tickColor = options[tickPrefix + 'Color'], + tickPosition = options[tickPrefix + 'Position'], + gridLinePath, + mark = tick.mark, + markPath, + step = labelOptions.step, + attribs, + show = true, + tickmarkOffset = (options.categories && options.tickmarkPlacement === 'between') ? 0.5 : 0, + xy = tick.getPosition(horiz, pos, tickmarkOffset, old), + x = xy.x, + y = xy.y, + staggerLines = axis.staggerLines; + + // create the grid line + if (gridLineWidth) { + gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth, old); + + if (gridLine === UNDEFINED) { + attribs = { + stroke: gridLineColor, + 'stroke-width': gridLineWidth + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + if (!type) { + attribs.zIndex = 1; + } + tick.gridLine = gridLine = + gridLineWidth ? + renderer.path(gridLinePath) + .attr(attribs).add(axis.gridGroup) : + null; + } + + // If the parameter 'old' is set, the current call will be followed + // by another call, therefore do not do any animations this time + if (!old && gridLine && gridLinePath) { + gridLine[tick.isNew ? 'attr' : 'animate']({ + d: gridLinePath + }); + } + } + + // create the tick mark + if (tickWidth) { + + // negate the length + if (tickPosition === 'inside') { + tickLength = -tickLength; + } + if (axis.opposite) { + tickLength = -tickLength; + } + + markPath = tick.getMarkPath(x, y, tickLength, tickWidth, horiz, renderer); + + if (mark) { // updating + mark.animate({ + d: markPath + }); + } else { // first time + tick.mark = renderer.path( + markPath + ).attr({ + stroke: tickColor, + 'stroke-width': tickWidth + }).add(axis.axisGroup); + } + } + + // the label is created on init - now move it into place + if (label && !isNaN(x)) { + label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step); + + // apply show first and show last + if ((tick.isFirst && !pick(options.showFirstLabel, 1)) || + (tick.isLast && !pick(options.showLastLabel, 1))) { + show = false; + + // Handle label overflow and show or hide accordingly + } else if (!staggerLines && horiz && labelOptions.overflow === 'justify' && !tick.handleOverflow(index, xy)) { + show = false; + } + + // apply step + if (step && index % step) { + // show those indices dividable by step + show = false; + } + + // Set the new position, and show or hide + if (show) { + label[tick.isNew ? 'attr' : 'animate'](xy); + label.show(); + tick.isNew = false; + } else { + label.hide(); + } + } + }, + + /** + * Destructor for the tick prototype + */ + destroy: function () { + destroyObjectProperties(this, this.axis); + } +}; + +/** + * The object wrapper for plot lines and plot bands + * @param {Object} options + */ +function PlotLineOrBand(axis, options) { + this.axis = axis; + + if (options) { + this.options = options; + this.id = options.id; + } + + //plotLine.render() + return this; +} + +PlotLineOrBand.prototype = { + + /** + * Render the plot line or plot band. If it is already existing, + * move it. + */ + render: function () { + var plotLine = this, + axis = plotLine.axis, + horiz = axis.horiz, + halfPointRange = (axis.pointRange || 0) / 2, + options = plotLine.options, + optionsLabel = options.label, + label = plotLine.label, + width = options.width, + to = options.to, + from = options.from, + isBand = defined(from) && defined(to), + value = options.value, + dashStyle = options.dashStyle, + svgElem = plotLine.svgElem, + path = [], + addEvent, + eventType, + xs, + ys, + x, + y, + color = options.color, + zIndex = options.zIndex, + events = options.events, + attribs, + renderer = axis.chart.renderer; + + // logarithmic conversion + if (axis.isLog) { + from = log2lin(from); + to = log2lin(to); + value = log2lin(value); + } + + // plot line + if (width) { + path = axis.getPlotLinePath(value, width); + attribs = { + stroke: color, + 'stroke-width': width + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + } else if (isBand) { // plot band + + // keep within plot area + from = mathMax(from, axis.min - halfPointRange); + to = mathMin(to, axis.max + halfPointRange); + + path = axis.getPlotBandPath(from, to, options); + attribs = { + fill: color + }; + if (options.borderWidth) { + attribs.stroke = options.borderColor; + attribs['stroke-width'] = options.borderWidth; + } + } 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 && axis.width > 0 && axis.height > 0) { + // apply defaults + optionsLabel = merge({ + align: horiz && isBand && 'center', + x: horiz ? !isBand && 4 : 10, + verticalAlign : !horiz && isBand && 'middle', + y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4, + rotation: horiz && !isBand && 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], pick(path[6], path[1])]; + ys = [path[2], path[5], pick(path[7], path[2])]; + x = arrayMin(xs); + y = arrayMin(ys); + + label.align(optionsLabel, false, { + x: x, + y: y, + width: arrayMax(xs) - x, + height: arrayMax(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 plotLine = this, + axis = plotLine.axis; + + // remove it from the lookup + erase(axis.plotLinesAndBands, plotLine); + + destroyObjectProperties(plotLine, this.axis); + } +}; +/** + * The class for stack items + */ +function StackItem(axis, options, isNegative, x, stackOption) { + var inverted = axis.chart.inverted; + + this.axis = axis; + + // Tells if the stack is negative + this.isNegative = isNegative; + + // Save the options to be able to style the label + this.options = options; + + // Save the x value to be able to position the label later + this.x = x; + + // Save the stack option on the series configuration object + this.stack = stackOption; + + // The align options and text align varies on whether the stack is negative and + // if the chart is inverted or not. + // First test the user supplied value, then use the dynamic. + this.alignOptions = { + align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'), + verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')), + y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)), + x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0) + }; + + this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center'); +} + +StackItem.prototype = { + destroy: function () { + destroyObjectProperties(this, this.axis); + }, + + /** + * Sets the total of this stack. Should be called when a serie is hidden or shown + * since that will affect the total of other stacks. + */ + setTotal: function (total) { + this.total = total; + this.cum = total; + }, + + /** + * Renders the stack total label and adds it to the stack label group. + */ + render: function (group) { + var str = this.options.formatter.call(this); // format the text in the label + + // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden + if (this.label) { + this.label.attr({text: str, visibility: HIDDEN}); + // Create new label + } else { + this.label = + this.axis.chart.renderer.text(str, 0, 0) // dummy positions, actual position updated with setOffset method in columnseries + .css(this.options.style) // apply style + .attr({align: this.textAlign, // fix the text-anchor + rotation: this.options.rotation, // rotation + visibility: HIDDEN }) // hidden until setOffset is called + .add(group); // add to the labels-group + } + }, + + /** + * Sets the offset that the stack has from the x value and repositions the label. + */ + setOffset: function (xOffset, xWidth) { + var stackItem = this, + axis = stackItem.axis, + chart = axis.chart, + inverted = chart.inverted, + neg = this.isNegative, // special treatment is needed for negative stacks + y = axis.translate(this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates + yZero = axis.translate(0), // stack origin + h = mathAbs(y - yZero), // stack height + x = chart.xAxis[0].translate(this.x) + xOffset, // stack x position + plotHeight = chart.plotHeight, + stackBox = { // this is the box for the complete stack + x: inverted ? (neg ? y : y - h) : x, + y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y), + width: inverted ? h : xWidth, + height: inverted ? xWidth : h + }; + + if (this.label) { + this.label + .align(this.alignOptions, null, stackBox) // align the label to the box + .attr({visibility: VISIBLE}); // set visibility + } + + } +}; +/** + * Create a new axis object + * @param {Object} chart + * @param {Object} options + */ +function Axis() { + this.init.apply(this, arguments); +} + +Axis.prototype = { + + /** + * Default options for the X axis - the Y axis has extended defaults + */ + defaultOptions: { + // allowDecimals: null, + // alternateGridColor: null, + // categories: [], + dateTimeLabelFormats: { + millisecond: '%H:%M:%S.%L', + 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', + // gridLineWidth: 0, + // reversed: false, + + labels: defaultLabelOptions, + // { step: null }, + lineColor: '#C0D0E0', + lineWidth: 1, + //linkedTo: null, + //max: undefined, + //min: undefined, + minPadding: 0.01, + maxPadding: 0.01, + //minRange: 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: true, + 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, logarithmic or datetime + }, + + /** + * This options set extends the defaultOptions for Y axes + */ + defaultYAxisOptions: { + 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' + }, + stackLabels: { + enabled: false, + //align: dynamic, + //y: dynamic, + //x: dynamic, + //verticalAlign: dynamic, + //textAlign: dynamic, + //rotation: 0, + formatter: function () { + return this.total; + }, + style: defaultLabelOptions.style + } + }, + + /** + * These options extend the defaultOptions for left axes + */ + defaultLeftAxisOptions: { + labels: { + align: 'right', + x: -8, + y: null + }, + title: { + rotation: 270 + } + }, + + /** + * These options extend the defaultOptions for right axes + */ + defaultRightAxisOptions: { + labels: { + align: 'left', + x: 8, + y: null + }, + title: { + rotation: 90 + } + }, + + /** + * These options extend the defaultOptions for bottom axes + */ + defaultBottomAxisOptions: { + labels: { + align: 'center', + x: 0, + y: 14 + // overflow: undefined, + // staggerLines: null + }, + title: { + rotation: 0 + } + }, + /** + * These options extend the defaultOptions for left axes + */ + defaultTopAxisOptions: { + labels: { + align: 'center', + x: 0, + y: -5 + // overflow: undefined + // staggerLines: null + }, + title: { + rotation: 0 + } + }, + + /** + * Initialize the axis + */ + init: function (chart, userOptions) { + + + var isXAxis = userOptions.isX, + axis = this; + + // Flag, is the axis horizontal + axis.horiz = chart.inverted ? !isXAxis : isXAxis; + + // Flag, isXAxis + axis.isXAxis = isXAxis; + axis.xOrY = isXAxis ? 'x' : 'y'; + + + axis.opposite = userOptions.opposite; // needed in setOptions + axis.side = axis.horiz ? + (axis.opposite ? 0 : 2) : // top : bottom + (axis.opposite ? 1 : 3); // right : left + + axis.setOptions(userOptions); + + + var options = this.options, + type = options.type, + isDatetimeAxis = type === 'datetime'; + + axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format + + + // Flag, stagger lines or not + axis.staggerLines = axis.horiz && options.labels.staggerLines; + axis.userOptions = userOptions; + + //axis.axisTitleMargin = UNDEFINED,// = options.title.margin, + axis.minPixelPadding = 0; + //axis.ignoreMinPadding = UNDEFINED; // can be set to true by a column or bar series + //axis.ignoreMaxPadding = UNDEFINED; + + axis.chart = chart; + axis.reversed = options.reversed; + + // Initial categories + axis.categories = options.categories; + + // Elements + //axis.axisGroup = UNDEFINED; + //axis.gridGroup = UNDEFINED; + //axis.axisTitle = UNDEFINED; + //axis.axisLine = UNDEFINED; + + // Flag if type === logarithmic + axis.isLog = type === 'logarithmic'; + + // Flag, if axis is linked to another axis + axis.isLinked = defined(options.linkedTo); + // Linked axis. + //axis.linkedParent = UNDEFINED; + + // Flag if type === datetime + axis.isDatetimeAxis = isDatetimeAxis; + + // Flag if percentage mode + //axis.usePercentage = UNDEFINED; + + + // Tick positions + //axis.tickPositions = UNDEFINED; // array containing predefined positions + // Tick intervals + //axis.tickInterval = UNDEFINED; + //axis.minorTickInterval = UNDEFINED; + + // Major ticks + axis.ticks = {}; + // Minor ticks + axis.minorTicks = {}; + //axis.tickAmount = UNDEFINED; + + // List of plotLines/Bands + axis.plotLinesAndBands = []; + + // Alternate bands + axis.alternateBands = {}; + + // Axis metrics + //axis.left = UNDEFINED; + //axis.top = UNDEFINED; + //axis.width = UNDEFINED; + //axis.height = UNDEFINED; + //axis.bottom = UNDEFINED; + //axis.right = UNDEFINED; + //axis.transA = UNDEFINED; + //axis.transB = UNDEFINED; + //axis.oldTransA = UNDEFINED; + axis.len = 0; + //axis.oldMin = UNDEFINED; + //axis.oldMax = UNDEFINED; + //axis.oldUserMin = UNDEFINED; + //axis.oldUserMax = UNDEFINED; + //axis.oldAxisLength = UNDEFINED; + axis.minRange = axis.userMinRange = options.minRange || options.maxZoom; + axis.range = options.range; + axis.offset = options.offset || 0; + + + // Dictionary for stacks + axis.stacks = {}; + + // Min and max in the data + //axis.dataMin = UNDEFINED, + //axis.dataMax = UNDEFINED, + + // The axis range + axis.max = null; + axis.min = null; + + // User set min and max + //axis.userMin = UNDEFINED, + //axis.userMax = UNDEFINED, + + // Run Axis + + var eventType, + events = axis.options.events; + + // Register + chart.axes.push(axis); + chart[isXAxis ? 'xAxis' : 'yAxis'].push(axis); + + axis.series = []; // populated by Series + + // inverted charts have reversed xAxes as default + if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) { + axis.reversed = true; + } + + axis.removePlotBand = axis.removePlotBandOrLine; + axis.removePlotLine = axis.removePlotBandOrLine; + axis.addPlotBand = axis.addPlotBandOrLine; + axis.addPlotLine = axis.addPlotBandOrLine; + + + // register event listeners + for (eventType in events) { + addEvent(axis, eventType, events[eventType]); + } + + // extend logarithmic axis + if (axis.isLog) { + axis.val2lin = log2lin; + axis.lin2val = lin2log; + } + }, + + /** + * Merge and set options + */ + setOptions: function (userOptions) { + this.options = merge( + this.defaultOptions, + this.isXAxis ? {} : this.defaultYAxisOptions, + [this.defaultTopAxisOptions, this.defaultRightAxisOptions, + this.defaultBottomAxisOptions, this.defaultLeftAxisOptions][this.side], + userOptions + ); + }, + + + /** + * The default label formatter. The context is a special config object for the label. + */ + defaultLabelFormatter: function () { + var axis = this.axis, + value = this.value, + categories = axis.categories, + tickInterval = axis.tickInterval, + dateTimeLabelFormat = this.dateTimeLabelFormat, + ret; + + if (categories) { + ret = value; + + } else 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 (value >= 1000) { // add thousands separators + ret = numberFormat(value, 0); + + } else { // small numbers + ret = numberFormat(value, -1); + } + return ret; + }, + + /** + * Get the minimum and maximum for the series of each axis + */ + getSeriesExtremes: function () { + var axis = this, + chart = axis.chart, + stacks = axis.stacks, + posStack = [], + negStack = [], + i; + + // reset dataMin and dataMax in case we're redrawing + axis.dataMin = axis.dataMax = null; + + // loop through this axis' series + each(axis.series, function (series) { + + if (series.visible || !chart.options.chart.ignoreHiddenSeries) { + + var seriesOptions = series.options, + stacking, + posPointStack, + negPointStack, + stackKey, + stackOption, + negKey, + xData, + yData, + x, + y, + threshold = seriesOptions.threshold, + yDataLength, + activeYData = [], + activeCounter = 0; + + // Validate threshold in logarithmic axes + if (axis.isLog && threshold <= 0) { + threshold = seriesOptions.threshold = null; + } + + // Get dataMin and dataMax for X axes + if (axis.isXAxis) { + xData = series.xData; + if (xData.length) { + axis.dataMin = mathMin(pick(axis.dataMin, xData[0]), arrayMin(xData)); + axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData)); + } + + // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data + } else { + var isNegative, + pointStack, + key, + cropped = series.cropped, + xExtremes = series.xAxis.getExtremes(), + //findPointRange, + //pointRange, + j, + hasModifyValue = !!series.modifyValue; + + + // Handle stacking + stacking = seriesOptions.stacking; + axis.usePercentage = stacking === 'percent'; + + // create a stack for this particular series type + if (stacking) { + stackOption = seriesOptions.stack; + stackKey = series.type + pick(stackOption, ''); + negKey = '-' + stackKey; + series.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 (axis.usePercentage) { + axis.dataMin = 0; + axis.dataMax = 99; + } + + // processData can alter series.pointRange, so this goes after + //findPointRange = series.pointRange === null; + + xData = series.processedXData; + yData = series.processedYData; + yDataLength = yData.length; + + // loop over the non-null y values and read them into a local array + for (i = 0; i < yDataLength; i++) { + x = xData[i]; + y = yData[i]; + if (y !== null && y !== UNDEFINED) { + + // read stacked values into a stack based on the x value, + // the sign of y and the stack key + if (stacking) { + isNegative = y < threshold; + pointStack = isNegative ? negPointStack : posPointStack; + key = isNegative ? negKey : stackKey; + + y = pointStack[x] = + defined(pointStack[x]) ? + pointStack[x] + y : y; + + + // add the series + if (!stacks[key]) { + stacks[key] = {}; + } + + // If the StackItem is there, just update the values, + // if not, create one first + if (!stacks[key][x]) { + stacks[key][x] = new StackItem(axis, axis.options.stackLabels, isNegative, x, stackOption); + } + stacks[key][x].setTotal(y); + + + // general hook, used for Highstock compare values feature + } else if (hasModifyValue) { + y = series.modifyValue(y); + } + + // get the smallest distance between points + /*if (i) { + distance = mathAbs(xData[i] - xData[i - 1]); + pointRange = pointRange === UNDEFINED ? distance : mathMin(distance, pointRange); + }*/ + + // for points within the visible range, including the first point outside the + // visible range, consider y extremes + if (cropped || ((xData[i + 1] || x) >= xExtremes.min && (xData[i - 1] || x) <= xExtremes.max)) { + + j = y.length; + if (j) { // array, like ohlc or range data + while (j--) { + if (y[j] !== null) { + activeYData[activeCounter++] = y[j]; + } + } + } else { + activeYData[activeCounter++] = y; + } + } + } + } + + // record the least unit distance + /*if (findPointRange) { + series.pointRange = pointRange || 1; + } + series.closestPointRange = pointRange;*/ + + // Get the dataMin and dataMax so far. If percentage is used, the min and max are + // always 0 and 100. If the length of activeYData is 0, continue with null values. + if (!axis.usePercentage && activeYData.length) { + axis.dataMin = mathMin(pick(axis.dataMin, activeYData[0]), arrayMin(activeYData)); + axis.dataMax = mathMax(pick(axis.dataMax, activeYData[0]), arrayMax(activeYData)); + } + + // Adjust to threshold + if (defined(threshold)) { + if (axis.dataMin >= threshold) { + axis.dataMin = threshold; + axis.ignoreMinPadding = true; + } else if (axis.dataMax < threshold) { + axis.dataMax = threshold; + axis.ignoreMaxPadding = true; + } + } + } + } + }); + }, + + /** + * Translate from axis value to pixel position on the chart, or back + * + */ + translate: function (val, backwards, cvsCoord, old, handleLog) { + var axis = this, + axisLength = axis.len, + sign = 1, + cvsOffset = 0, + localA = old ? axis.oldTransA : axis.transA, + localMin = old ? axis.oldMin : axis.min, + returnValue, + postTranslate = axis.options.ordinal || (axis.isLog && handleLog); + + if (!localA) { + localA = axis.transA; + } + + if (cvsCoord) { + sign *= -1; // canvas coordinates inverts the value + cvsOffset = axisLength; + } + if (axis.reversed) { // reversed axis + sign *= -1; + cvsOffset -= sign * axisLength; + } + + if (backwards) { // reverse translation + if (axis.reversed) { + val = axisLength - val; + } + returnValue = val / localA + localMin; // from chart pixel to value + if (postTranslate) { // log and ordinal axes + returnValue = axis.lin2val(returnValue); + } + + } else { // normal translation, from axis value to pixel, relative to plot + if (postTranslate) { // log and ordinal axes + val = axis.val2lin(val); + } + + returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * axis.minPixelPadding); + } + + 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 axis = this, + chart = axis.chart, + axisLeft = axis.left, + axisTop = axis.top, + x1, + y1, + x2, + y2, + translatedValue = axis.translate(value, null, null, old), + cHeight = (old && chart.oldChartHeight) || chart.chartHeight, + cWidth = (old && chart.oldChartWidth) || chart.chartWidth, + skip, + transB = axis.transB; + + x1 = x2 = mathRound(translatedValue + transB); + y1 = y2 = mathRound(cHeight - translatedValue - transB); + + if (isNaN(translatedValue)) { // no min or max + skip = true; + + } else if (axis.horiz) { + y1 = axisTop; + y2 = cHeight - axis.bottom; + if (x1 < axisLeft || x1 > axisLeft + axis.width) { + skip = true; + } + } else { + x1 = axisLeft; + x2 = cWidth - axis.right; + + if (y1 < axisTop || y1 > axisTop + axis.height) { + skip = true; + } + } + return skip ? + null : + chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 0); + }, + + /** + * Create the path for a plot band + */ + getPlotBandPath: function (from, to) { + + var toPath = this.getPlotLinePath(to), + path = this.getPlotLinePath(from); + + if (path && toPath) { + path.push( + toPath[4], + toPath[5], + toPath[1], + toPath[2] + ); + } else { // outside the axis area + path = null; + } + + return path; + }, + + /** + * Set the tick positions of a linear axis to round values like whole tens or every five. + */ + getLinearTickPositions: function (tickInterval, min, max) { + var pos, + lastPos, + roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval), + roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval), + tickPositions = []; + + // Populate the intermediate values + pos = roundedMin; + while (pos <= roundedMax) { + + // Place the tick on the rounded value + tickPositions.push(pos); + + // Always add the raw tickInterval, not the corrected one. + pos = correctFloat(pos + tickInterval); + + // If the interval is not big enough in the current min - max range to actually increase + // the loop variable, we need to break out to prevent endless loop. Issue #619 + if (pos === lastPos) { + break; + } + + // Record the last value + lastPos = pos; + } + return tickPositions; + }, + + /** + * Set the tick positions of a logarithmic axis + */ + getLogTickPositions: function (interval, min, max, minor) { + var axis = this, + options = axis.options, + axisLength = axis.len; + + // Since we use this method for both major and minor ticks, + // use a local variable and return the result + var positions = []; + + // Reset + if (!minor) { + axis._minorAutoInterval = null; + } + + // First case: All ticks fall on whole logarithms: 1, 10, 100 etc. + if (interval >= 0.5) { + interval = mathRound(interval); + positions = axis.getLinearTickPositions(interval, min, max); + + // Second case: We need intermediary ticks. For example + // 1, 2, 4, 6, 8, 10, 20, 40 etc. + } else if (interval >= 0.08) { + var roundedMin = mathFloor(min), + intermediate, + i, + j, + len, + pos, + lastPos, + break2; + + if (interval > 0.3) { + intermediate = [1, 2, 4]; + } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 4, 6, 8]; + } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + } + + for (i = roundedMin; i < max + 1 && !break2; i++) { + len = intermediate.length; + for (j = 0; j < len && !break2; j++) { + pos = log2lin(lin2log(i) * intermediate[j]); + + if (pos > min) { + positions.push(lastPos); + } + + if (lastPos > max) { + break2 = true; + } + lastPos = pos; + } + } + + // Third case: We are so deep in between whole logarithmic values that + // we might as well handle the tick positions like a linear axis. For + // example 1.01, 1.02, 1.03, 1.04. + } else { + var realMin = lin2log(min), + realMax = lin2log(max), + tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'], + filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption, + tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1), + totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength; + + interval = pick( + filteredTickIntervalOption, + axis._minorAutoInterval, + (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1) + ); + + interval = normalizeTickInterval( + interval, + null, + math.pow(10, mathFloor(math.log(interval) / math.LN10)) + ); + + positions = map(axis.getLinearTickPositions( + interval, + realMin, + realMax + ), log2lin); + + if (!minor) { + axis._minorAutoInterval = interval / 5; + } + } + + // Set the axis-level tickInterval variable + if (!minor) { + axis.tickInterval = interval; + } + return positions; + }, + + /** + * Return the minor tick positions. For logarithmic axes, reuse the same logic + * as for major ticks. + */ + getMinorTickPositions: function () { + var axis = this, + tickPositions = axis.tickPositions, + minorTickInterval = axis.minorTickInterval; + + var minorTickPositions = [], + pos, + i, + len; + + if (axis.isLog) { + len = tickPositions.length; + for (i = 1; i < len; i++) { + minorTickPositions = minorTickPositions.concat( + axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true) + ); + } + + } else { + for (pos = axis.min + (tickPositions[0] - axis.min) % minorTickInterval; pos <= axis.max; pos += minorTickInterval) { + minorTickPositions.push(pos); + } + } + + return minorTickPositions; + }, + + /** + * Adjust the min and max for the minimum range. Keep in mind that the series data is + * not yet processed, so we don't have information on data cropping and grouping, or + * updated axis.pointRange or series.pointRange. The data can't be processed until + * we have finally established min and max. + */ + adjustForMinRange: function () { + var axis = this, + options = axis.options, + min = axis.min, + max = axis.max, + zoomOffset, + spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange, + closestDataRange, + i, + distance, + xData, + loopLength, + minArgs, + maxArgs; + + // Set the automatic minimum range based on the closest point distance + if (axis.isXAxis && axis.minRange === UNDEFINED && !axis.isLog) { + + if (defined(options.min) || defined(options.max)) { + axis.minRange = null; // don't do this again + + } else { + + // Find the closest distance between raw data points, as opposed to + // closestPointRange that applies to processed points (cropped and grouped) + each(axis.series, function (series) { + xData = series.xData; + loopLength = series.xIncrement ? 1 : xData.length - 1; + for (i = loopLength; i > 0; i--) { + distance = xData[i] - xData[i - 1]; + if (closestDataRange === UNDEFINED || distance < closestDataRange) { + closestDataRange = distance; + } + } + }); + axis.minRange = mathMin(closestDataRange * 5, axis.dataMax - axis.dataMin); + } + } + + // if minRange is exceeded, adjust + if (max - min < axis.minRange) { + var minRange = axis.minRange; + zoomOffset = (minRange - max + min) / 2; + + // if min and max options have been set, don't go beyond it + minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)]; + if (spaceAvailable) { // if space is available, stay within the data range + minArgs[2] = axis.dataMin; + } + min = arrayMax(minArgs); + + maxArgs = [min + minRange, pick(options.max, min + minRange)]; + if (spaceAvailable) { // if space is availabe, stay within the data range + maxArgs[2] = axis.dataMax; + } + + max = arrayMin(maxArgs); + + // now if the max is adjusted, adjust the min back + if (max - min < minRange) { + minArgs[0] = max - minRange; + minArgs[1] = pick(options.min, max - minRange); + min = arrayMax(minArgs); + } + } + + // Record modified extremes + axis.min = min; + axis.max = max; + }, + + /** + * Update translation information + */ + setAxisTranslation: function () { + var axis = this, + range = axis.max - axis.min, + pointRange = 0, + closestPointRange, + seriesClosestPointRange, + transA = axis.transA; + + // adjust translation for padding + if (axis.isXAxis) { + if (axis.isLinked) { + pointRange = axis.linkedParent.pointRange; + } else { + each(axis.series, function (series) { + pointRange = mathMax(pointRange, series.pointRange); + seriesClosestPointRange = series.closestPointRange; + if (!series.noSharedTooltip && defined(seriesClosestPointRange)) { + closestPointRange = defined(closestPointRange) ? + mathMin(closestPointRange, seriesClosestPointRange) : + seriesClosestPointRange; + } + }); + } + + // pointRange means the width reserved for each point, like in a column chart + axis.pointRange = pointRange; + + // closestPointRange means the closest distance between points. In columns + // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange + // is some other value + axis.closestPointRange = closestPointRange; + } + + // secondary values + axis.oldTransA = transA; + axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRange) || 1); + axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend + axis.minPixelPadding = transA * (pointRange / 2); + }, + + /** + * Set the tick positions to round values and optionally extend the extremes + * to the nearest tick + */ + setTickPositions: function (secondPass) { + var axis = this, + chart = axis.chart, + options = axis.options, + isLog = axis.isLog, + isDatetimeAxis = axis.isDatetimeAxis, + isXAxis = axis.isXAxis, + isLinked = axis.isLinked, + tickPositioner = axis.options.tickPositioner, + magnitude, + maxPadding = options.maxPadding, + minPadding = options.minPadding, + length, + linkedParentExtremes, + tickIntervalOption = options.tickInterval, + tickPixelIntervalOption = options.tickPixelInterval, + tickPositions, + categories = axis.categories; + + // linked axis gets the extremes from the parent axis + if (isLinked) { + axis.linkedParent = chart[isXAxis ? 'xAxis' : 'yAxis'][options.linkedTo]; + linkedParentExtremes = axis.linkedParent.getExtremes(); + axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin); + axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax); + if (options.type !== axis.linkedParent.options.type) { + error(11, 1); // Can't link axes of different type + } + } else { // initial min and max from the extreme data values + axis.min = pick(axis.userMin, options.min, axis.dataMin); + axis.max = pick(axis.userMax, options.max, axis.dataMax); + } + + if (isLog) { + if (!secondPass && mathMin(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978 + error(10, 1); // Can't plot negative values on log axis + } + axis.min = correctFloat(log2lin(axis.min)); // correctFloat cures #934 + axis.max = correctFloat(log2lin(axis.max)); + } + + // handle zoomed range + if (axis.range) { + axis.userMin = axis.min = mathMax(axis.min, axis.max - axis.range); // #618 + axis.userMax = axis.max; + if (secondPass) { + axis.range = null; // don't use it when running setExtremes + } + } + + // adjust min and max for the minimum range + axis.adjustForMinRange(); + + // pad the values to get clear of the chart's edges + if (!categories && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) { + length = (axis.max - axis.min) || 1; + if (!defined(options.min) && !defined(axis.userMin) && minPadding && (axis.dataMin < 0 || !axis.ignoreMinPadding)) { + axis.min -= length * minPadding; + } + if (!defined(options.max) && !defined(axis.userMax) && maxPadding && (axis.dataMax > 0 || !axis.ignoreMaxPadding)) { + axis.max += length * maxPadding; + } + } + + // get tickInterval + if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) { + axis.tickInterval = 1; + } else if (isLinked && !tickIntervalOption && + tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) { + axis.tickInterval = axis.linkedParent.tickInterval; + } else { + axis.tickInterval = pick( + tickIntervalOption, + categories ? // for categoried axis, 1 is default, for linear axis use tickPix + 1 : + (axis.max - axis.min) * tickPixelIntervalOption / (axis.len || 1) + ); + } + + // Now we're finished detecting min and max, crop and group series data. This + // is in turn needed in order to find tick positions in ordinal axes. + if (isXAxis && !secondPass) { + each(axis.series, function (series) { + series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax); + }); + } + + // set the translation factor used in translate function + axis.setAxisTranslation(); + + // hook for ordinal axes. To do: merge with below + if (axis.beforeSetTickPositions) { + axis.beforeSetTickPositions(); + } + + // hook for extensions, used in Highstock ordinal axes + if (axis.postProcessTickInterval) { + axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval); + } + + // for linear axes, get magnitude and normalize the interval + if (!isDatetimeAxis && !isLog) { // linear + magnitude = math.pow(10, mathFloor(math.log(axis.tickInterval) / math.LN10)); + if (!defined(options.tickInterval)) { + axis.tickInterval = normalizeTickInterval(axis.tickInterval, null, magnitude, options); + } + } + + // get minorTickInterval + axis.minorTickInterval = options.minorTickInterval === 'auto' && axis.tickInterval ? + axis.tickInterval / 5 : options.minorTickInterval; + + // find the tick positions + axis.tickPositions = tickPositions = options.tickPositions || (tickPositioner && tickPositioner.apply(axis, [axis.min, axis.max])); + if (!tickPositions) { + if (isDatetimeAxis) { + tickPositions = (axis.getNonLinearTimeTicks || getTimeTicks)( + normalizeTimeTickInterval(axis.tickInterval, options.units), + axis.min, + axis.max, + options.startOfWeek, + axis.ordinalPositions, + axis.closestPointRange, + true + ); + } else if (isLog) { + tickPositions = axis.getLogTickPositions(axis.tickInterval, axis.min, axis.max); + } else { + tickPositions = axis.getLinearTickPositions(axis.tickInterval, axis.min, axis.max); + } + axis.tickPositions = tickPositions; + } + + if (!isLinked) { + + // reset min/max or remove extremes based on start/end on tick + var roundedMin = tickPositions[0], + roundedMax = tickPositions[tickPositions.length - 1]; + + if (options.startOnTick) { + axis.min = roundedMin; + } else if (axis.min > roundedMin) { + tickPositions.shift(); + } + + if (options.endOnTick) { + axis.max = roundedMax; + } else if (axis.max < roundedMax) { + tickPositions.pop(); + } + + } + }, + + /** + * Set the max ticks of either the x and y axis collection + */ + setMaxTicks: function () { + + var chart = this.chart, + maxTicks = chart.maxTicks, + tickPositions = this.tickPositions, + xOrY = this.xOrY; + + if (!maxTicks) { // first call, or maxTicks have been reset after a zoom operation + maxTicks = { + x: 0, + y: 0 + }; + } + + if (!this.isLinked && !this.isDatetimeAxis && tickPositions.length > maxTicks[xOrY] && this.options.alignTicks !== false) { + maxTicks[xOrY] = tickPositions.length; + } + chart.maxTicks = maxTicks; + }, + + /** + * When using multiple axes, adjust the number of ticks to match the highest + * number of ticks in that group + */ + adjustTickAmount: function () { + var axis = this, + chart = axis.chart, + xOrY = axis.xOrY, + tickPositions = axis.tickPositions, + maxTicks = chart.maxTicks; + + if (maxTicks && maxTicks[xOrY] && !axis.isDatetimeAxis && !axis.categories && !axis.isLinked && axis.options.alignTicks !== false) { // only apply to linear scale + var oldTickAmount = axis.tickAmount, + calculatedTickAmount = tickPositions.length, + tickAmount; + + // set the axis-level tickAmount to use below + axis.tickAmount = tickAmount = maxTicks[xOrY]; + + if (calculatedTickAmount < tickAmount) { + while (tickPositions.length < tickAmount) { + tickPositions.push(correctFloat( + tickPositions[tickPositions.length - 1] + axis.tickInterval + )); + } + axis.transA *= (calculatedTickAmount - 1) / (tickAmount - 1); + axis.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 + * + */ + setScale: function () { + var axis = this, + stacks = axis.stacks, + type, + i, + isDirtyData, + isDirtyAxisLength; + + axis.oldMin = axis.min; + axis.oldMax = axis.max; + axis.oldAxisLength = axis.len; + + // set the new axisLength + axis.setAxisSize(); + //axisLength = horiz ? axisWidth : axisHeight; + isDirtyAxisLength = axis.len !== axis.oldAxisLength; + + // is there new data? + each(axis.series, function (series) { + if (series.isDirtyData || series.isDirty || + series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well + isDirtyData = true; + } + }); + + // do we really need to go through all this? + if (isDirtyAxisLength || isDirtyData || axis.isLinked || + axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax) { + + // get data extremes if needed + axis.getSeriesExtremes(); + + // get fixed positions based on tickInterval + axis.setTickPositions(); + + // record old values to decide whether a rescale is necessary later on (#540) + axis.oldUserMin = axis.userMin; + axis.oldUserMax = axis.userMax; + + // Mark as dirty if it is not already set to dirty and extremes have changed. #595. + if (!axis.isDirty) { + axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax; + } + } + + + // reset stacks + if (!axis.isXAxis) { + for (type in stacks) { + for (i in stacks[type]) { + stacks[type][i].cum = stacks[type][i].total; + } + } + } + + // Set the maximum tick amount + axis.setMaxTicks(); + }, + + /** + * 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 + * @param {Object} eventArguments + * + */ + setExtremes: function (newMin, newMax, redraw, animation, eventArguments) { + var axis = this, + chart = axis.chart; + + redraw = pick(redraw, true); // defaults to true + + // Extend the arguments with min and max + eventArguments = extend(eventArguments, { + min: newMin, + max: newMax + }); + + // Fire the event + fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler + + axis.userMin = newMin; + axis.userMax = newMax; + + // Mark for running afterSetExtremes + axis.isDirtyExtremes = true; + + // redraw + if (redraw) { + chart.redraw(animation); + } + }); + }, + + /** + * Update the axis metrics + */ + setAxisSize: function () { + var axis = this, + chart = axis.chart, + options = axis.options; + + var offsetLeft = options.offsetLeft || 0, + offsetRight = options.offsetRight || 0; + + // basic values + // expose to use in Series object and navigator + axis.left = pick(options.left, chart.plotLeft + offsetLeft); + axis.top = pick(options.top, chart.plotTop); + axis.width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight); + axis.height = pick(options.height, chart.plotHeight); + axis.bottom = chart.chartHeight - axis.height - axis.top; + axis.right = chart.chartWidth - axis.width - axis.left; + axis.len = mathMax(axis.horiz ? axis.width : axis.height, 0); // mathMax fixes #905 + }, + + /** + * Get the actual axis extremes + */ + getExtremes: function () { + var axis = this, + isLog = axis.isLog; + + return { + min: isLog ? correctFloat(lin2log(axis.min)) : axis.min, + max: isLog ? correctFloat(lin2log(axis.max)) : axis.max, + dataMin: axis.dataMin, + dataMax: axis.dataMax, + userMin: axis.userMin, + userMax: axis.userMax + }; + }, + + /** + * Get the zero plane either based on zero or on the min or max value. + * Used in bar and area plots + */ + getThreshold: function (threshold) { + var axis = this, + isLog = axis.isLog; + + var realMin = isLog ? lin2log(axis.min) : axis.min, + realMax = isLog ? lin2log(axis.max) : axis.max; + + if (realMin > threshold || threshold === null) { + threshold = realMin; + } else if (realMax < threshold) { + threshold = realMax; + } + + return axis.translate(threshold, 0, 1, 0, 1); + }, + + /** + * Add a plot band or plot line after render time + * + * @param options {Object} The plotBand or plotLine configuration object + */ + addPlotBandOrLine: function (options) { + var obj = new PlotLineOrBand(this, options).render(); + this.plotLinesAndBands.push(obj); + return obj; + }, + + /** + * Render the tick labels to a preliminary position to get their sizes + */ + getOffset: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + tickPositions = axis.tickPositions, + ticks = axis.ticks, + horiz = axis.horiz, + side = axis.side, + hasData, + showAxis, + titleOffset = 0, + titleOffsetOption, + titleMargin = 0, + axisTitleOptions = options.title, + labelOptions = options.labels, + labelOffset = 0, // reset + axisOffset = chart.axisOffset, + directionFactor = [-1, 1, 1, -1][side], + n; + + + // For reuse in Axis.render + axis.hasData = hasData = axis.series.length && defined(axis.min) && defined(axis.max); + axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); + + // Create the axisGroup and gridGroup elements on first iteration + if (!axis.axisGroup) { + axis.axisGroup = renderer.g('axis') + .attr({ zIndex: options.zIndex || 7 }) + .add(); + axis.gridGroup = renderer.g('grid') + .attr({ zIndex: options.gridZIndex || 1 }) + .add(); + } + + if (hasData || axis.isLinked) { + each(tickPositions, function (pos) { + if (!ticks[pos]) { + ticks[pos] = new Tick(axis, pos); + } else { + ticks[pos].addLabel(); // update labels depending on tick interval + } + + }); + + each(tickPositions, function (pos) { + // 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 (axis.staggerLines) { + labelOffset += (axis.staggerLines - 1) * 16; + } + + } else { // doesn't have data + for (n in ticks) { + ticks[n].destroy(); + delete ticks[n]; + } + } + + if (axisTitleOptions && axisTitleOptions.text) { + if (!axis.axisTitle) { + axis.axisTitle = renderer.text( + axisTitleOptions.text, + 0, + 0, + axisTitleOptions.useHTML + ) + .attr({ + zIndex: 7, + rotation: axisTitleOptions.rotation || 0, + align: + axisTitleOptions.textAlign || + { low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align] + }) + .css(axisTitleOptions.style) + .add(axis.axisGroup); + axis.axisTitle.isNew = true; + } + + if (showAxis) { + titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; + titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10); + titleOffsetOption = axisTitleOptions.offset; + } + + // hide or show the title depending on whether showEmpty is set + axis.axisTitle[showAxis ? 'show' : 'hide'](); + } + + // handle automatic or user set offset + axis.offset = directionFactor * pick(options.offset, axisOffset[side]); + + + axis.axisTitleMargin = + pick(titleOffsetOption, + labelOffset + titleMargin + + (side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x']) + ); + + axisOffset[side] = mathMax( + axisOffset[side], + axis.axisTitleMargin + titleOffset + directionFactor * axis.offset + ); + + }, + + /** + * Get the path for the axis line + */ + getLinePath: function (lineWidth) { + var chart = this.chart, + opposite = this.opposite, + offset = this.offset, + horiz = this.horiz, + lineLeft = this.left + (opposite ? this.width : 0) + offset, + lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset; + + return chart.renderer.crispLine([ + M, + horiz ? + this.left : + lineLeft, + horiz ? + lineTop : + this.top, + L, + horiz ? + chart.chartWidth - this.right : + lineLeft, + horiz ? + lineTop : + chart.chartHeight - this.bottom + ], lineWidth); + }, + + /** + * Position the title + */ + getTitlePosition: function () { + // compute anchor points for each of the title align options + var horiz = this.horiz, + axisLeft = this.left, + axisTop = this.top, + axisLength = this.len, + axisTitleOptions = this.options.title, + margin = horiz ? axisLeft : axisTop, + opposite = this.opposite, + offset = this.offset, + 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 ? axisTop + this.height : axisLeft) + + (horiz ? 1 : -1) * // horizontal axis reverses the margin + (opposite ? -1 : 1) * // so does opposite axes + this.axisTitleMargin + + (this.side === 2 ? fontSize : 0); + + return { + x: horiz ? + alongAxis : + offAxis + (opposite ? this.width : 0) + offset + + (axisTitleOptions.x || 0), // x + y: horiz ? + offAxis - (opposite ? this.height : 0) + offset : + alongAxis + (axisTitleOptions.y || 0) // y + }; + }, + + /** + * Render the axis + */ + render: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + isLog = axis.isLog, + isLinked = axis.isLinked, + tickPositions = axis.tickPositions, + axisTitle = axis.axisTitle, + stacks = axis.stacks, + ticks = axis.ticks, + minorTicks = axis.minorTicks, + alternateBands = axis.alternateBands, + stackLabelOptions = options.stackLabels, + alternateGridColor = options.alternateGridColor, + lineWidth = options.lineWidth, + linePath, + hasRendered = chart.hasRendered, + slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin), + hasData = axis.hasData, + showAxis = axis.showAxis, + from, + to; + + // If the series has data draw the ticks. Else only the line and title + if (hasData || isLinked) { + + // minor ticks + if (axis.minorTickInterval && !axis.categories) { + each(axis.getMinorTickPositions(), function (pos) { + if (!minorTicks[pos]) { + minorTicks[pos] = new Tick(axis, pos, 'minor'); + } + + // 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. Pull out the first item and render it last so that + // we can get the position of the neighbour label. #808. + each(tickPositions.slice(1).concat([tickPositions[0]]), function (pos, i) { + + // Reorganize the indices + i = (i === tickPositions.length - 1) ? 0 : i + 1; + + // linked axes need an extra check to find out if + if (!isLinked || (pos >= axis.min && pos <= axis.max)) { + + if (!ticks[pos]) { + ticks[pos] = new Tick(axis, pos); + } + + // 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 < axis.max) { + if (!alternateBands[pos]) { + alternateBands[pos] = new PlotLineOrBand(axis); + } + from = pos; + to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : axis.max; + alternateBands[pos].options = { + from: isLog ? lin2log(from) : from, + to: isLog ? lin2log(to) : to, + color: alternateGridColor + }; + alternateBands[pos].render(); + alternateBands[pos].isActive = true; + } + }); + } + + // custom plot lines and bands + if (!axis._addedPlotLB) { // only first time + each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) { + //plotLinesAndBands.push(new PlotLineOrBand(plotLineOptions).render()); + axis.addPlotBandOrLine(plotLineOptions); + }); + axis._addedPlotLB = true; + } + + } // end if hasData + + // remove inactive ticks + each([ticks, minorTicks, alternateBands], function (coll) { + var pos; + for (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) { + linePath = axis.getLinePath(lineWidth); + if (!axis.axisLine) { + axis.axisLine = renderer.path(linePath) + .attr({ + stroke: options.lineColor, + 'stroke-width': lineWidth, + zIndex: 7 + }) + .add(); + } else { + axis.axisLine.animate({ d: linePath }); + } + + // show or hide the line depending on options.showEmpty + axis.axisLine[showAxis ? 'show' : 'hide'](); + + } + + if (axisTitle && showAxis) { + + axisTitle[axisTitle.isNew ? 'attr' : 'animate']( + axis.getTitlePosition() + ); + axisTitle.isNew = false; + } + + // Stacked totals: + if (stackLabelOptions && stackLabelOptions.enabled) { + var stackKey, oneStack, stackCategory, + stackTotalGroup = axis.stackTotalGroup; + + // Create a separate group for the stack total labels + if (!stackTotalGroup) { + axis.stackTotalGroup = stackTotalGroup = + renderer.g('stack-labels') + .attr({ + visibility: VISIBLE, + zIndex: 6 + }) + .add(); + } + + // plotLeft/Top will change when y axis gets wider so we need to translate the + // stackTotalGroup at every render call. See bug #506 and #516 + stackTotalGroup.translate(chart.plotLeft, chart.plotTop); + + // Render each stack total + for (stackKey in stacks) { + oneStack = stacks[stackKey]; + for (stackCategory in oneStack) { + oneStack[stackCategory].render(stackTotalGroup); + } + } + } + // End stacked totals + + axis.isDirty = false; + }, + + /** + * Remove a plot band or plot line from the chart by id + * @param {Object} id + */ + removePlotBandOrLine: function (id) { + var plotLinesAndBands = this.plotLinesAndBands, + i = plotLinesAndBands.length; + while (i--) { + if (plotLinesAndBands[i].id === id) { + plotLinesAndBands[i].destroy(); + } + } + }, + + /** + * Update the axis title by options + */ + setTitle: function (newTitleOptions, redraw) { + var axis = this, + chart = axis.chart, + options = axis.options, + axisTitle; + + options.title = merge(options.title, newTitleOptions); + + axis.axisTitle = axisTitle && axisTitle.destroy(); // #922 + axis.isDirty = true; + + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * Redraw the axis to reflect changes in the data or axis extremes + */ + redraw: function () { + var axis = this, + chart = axis.chart; + + // hide tooltip and hover states + if (chart.tracker.resetTracker) { + chart.tracker.resetTracker(true); + } + + // render the axis + axis.render(); + + // move plot lines and bands + each(axis.plotLinesAndBands, function (plotLine) { + plotLine.render(); + }); + + // mark associated series as dirty and ready for redraw + each(axis.series, function (series) { + series.isDirty = true; + }); + + }, + + /** + * Set new axis categories and optionally redraw + * @param {Array} newCategories + * @param {Boolean} doRedraw + */ + setCategories: function (newCategories, doRedraw) { + var axis = this, + chart = axis.chart; + + // set the categories + axis.categories = axis.userOptions.categories = newCategories; + + // force reindexing tooltips + each(axis.series, function (series) { + series.translate(); + series.setTooltipPoints(true); + }); + + + // optionally redraw + axis.isDirty = true; + + if (pick(doRedraw, true)) { + chart.redraw(); + } + }, + + /** + * Destroys an Axis instance. + */ + destroy: function () { + var axis = this, + stacks = axis.stacks, + stackKey; + + // Remove the events + removeEvent(axis); + + // Destroy each stack total + for (stackKey in stacks) { + destroyObjectProperties(stacks[stackKey]); + + stacks[stackKey] = null; + } + + // Destroy collections + each([axis.ticks, axis.minorTicks, axis.alternateBands, axis.plotLinesAndBands], function (coll) { + destroyObjectProperties(coll); + }); + + // Destroy local variables + each(['stackTotalGroup', 'axisLine', 'axisGroup', 'gridGroup', 'axisTitle'], function (prop) { + if (axis[prop]) { + axis[prop] = axis[prop].destroy(); + } + }); + } + + +}; // end Axis + +/** + * The tooltip object + * @param {Object} chart The chart instance + * @param {Object} options Tooltip options + */ +function Tooltip(chart, options) { + var borderWidth = options.borderWidth, + style = options.style, + shared = options.shared, + padding = pInt(style.padding); + + // Save the chart and options + this.chart = chart; + this.options = options; + + // remove padding CSS and apply padding on box instead + style.padding = 0; + + // Keep track of the current series + //this.currentSeries = UNDEFINED; + + // List of crosshairs + this.crosshairs = []; + + // Current values of x and y when animating + this.currentX = 0; + this.currentY = 0; + + // The tooltipTick function, initialized to nothing + //this.tooltipTick = UNDEFINED; + + // The tooltip is initially hidden + this.tooltipIsHidden = true; + + // create the label + this.label = chart.renderer.label('', 0, 0, null, null, null, options.useHTML, null, 'tooltip') + .attr({ + padding: padding, + fill: options.backgroundColor, + 'stroke-width': borderWidth, + r: options.borderRadius, + zIndex: 8 + }) + .css(style) + .hide() + .add(); + + // When using canVG the shadow shows up as a gray circle + // even if the tooltip is hidden. + if (!useCanVG) { + this.label.shadow(options.shadow); + } + + // Public property for getting the shared state. + this.shared = shared; +} + +Tooltip.prototype = { + /** + * Destroy the tooltip and its elements. + */ + destroy: function () { + each(this.crosshairs, function (crosshair) { + if (crosshair) { + crosshair.destroy(); + } + }); + + // Destroy and clear local variables + if (this.label) { + this.label = this.label.destroy(); + } + }, + + /** + * Provide a soft movement for the tooltip + * + * @param {Number} finalX + * @param {Number} finalY + * @private + */ + move: function (finalX, finalY) { + var tooltip = this; + + // get intermediate values for animation + tooltip.currentX = tooltip.tooltipIsHidden ? finalX : (2 * tooltip.currentX + finalX) / 3; + tooltip.currentY = tooltip.tooltipIsHidden ? finalY : (tooltip.currentY + finalY) / 2; + + // move to the intermediate value + tooltip.label.attr({ x: tooltip.currentX, y: tooltip.currentY }); + + // run on next tick of the mouse tracker + if (mathAbs(finalX - tooltip.currentX) > 1 || mathAbs(finalY - tooltip.currentY) > 1) { + tooltip.tooltipTick = function () { + tooltip.move(finalX, finalY); + }; + } else { + tooltip.tooltipTick = null; + } + }, + + /** + * Hide the tooltip + */ + hide: function () { + if (!this.tooltipIsHidden) { + var hoverPoints = this.chart.hoverPoints; + + this.label.hide(); + + // hide previous hoverPoints and set new + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + this.chart.hoverPoints = null; + this.tooltipIsHidden = true; + } + }, + + /** + * Hide the crosshairs + */ + hideCrosshairs: function () { + each(this.crosshairs, function (crosshair) { + if (crosshair) { + crosshair.hide(); + } + }); + }, + + /** + * Extendable method to get the anchor position of the tooltip + * from a point or set of points + */ + getAnchor: function (points, mouseEvent) { + var ret, + chart = this.chart, + inverted = chart.inverted, + plotX = 0, + plotY = 0; + + points = splat(points); + + // Pie uses a special tooltipPos + ret = points[0].tooltipPos; + + // When shared, use the average position + if (!ret) { + each(points, function (point) { + plotX += point.plotX; + plotY += point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY; + }); + + plotX /= points.length; + plotY /= points.length; + + ret = [ + inverted ? chart.plotWidth - plotY : plotX, + this.shared && !inverted && points.length > 1 && mouseEvent ? + mouseEvent.chartY - chart.plotTop : // place shared tooltip next to the mouse (#424) + inverted ? chart.plotHeight - plotX : plotY + ]; + } + + return map(ret, mathRound); + }, + + /** + * Place the tooltip in a chart without spilling over + * and not covering the point it self. + */ + getPosition: function (boxWidth, boxHeight, point) { + + // Set up the variables + var chart = this.chart, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + distance = pick(this.options.distance, 12), + pointX = point.plotX, + pointY = point.plotY, + x = pointX + plotLeft + (chart.inverted ? distance : -boxWidth - distance), + y = pointY - boxHeight + plotTop + 15, // 15 means the point is 15 pixels up from the bottom of the tooltip + alignedRight; + + // It is too far to the left, adjust it + if (x < 7) { + x = plotLeft + pointX + distance; + } + + // Test to see if the tooltip is too far to the right, + // if it is, move it back to be inside and then up to not cover the point. + if ((x + boxWidth) > (plotLeft + plotWidth)) { + x -= (x + boxWidth) - (plotLeft + plotWidth); + y = pointY - boxHeight + plotTop - distance; + alignedRight = true; + } + + // If it is now above the plot area, align it to the top of the plot area + if (y < plotTop + 5) { + y = plotTop + 5; + + // If the tooltip is still covering the point, move it below instead + if (alignedRight && pointY >= y && pointY <= (y + boxHeight)) { + y = pointY + plotTop + distance; // below + } + } + + // Now if the tooltip is below the chart, move it up. It's better to cover the + // point than to disappear outside the chart. #834. + if (y + boxHeight > plotTop + plotHeight) { + y = mathMax(plotTop, plotTop + plotHeight - boxHeight - distance); // below + } + + + return {x: x, y: y}; + }, + + /** + * Refresh the tooltip's text and position. + * @param {Object} point + */ + refresh: function (point, mouseEvent) { + var tooltip = this, + chart = tooltip.chart, + label = tooltip.label, + options = tooltip.options; + + /** + * In case no user defined formatter is given, this will be used + */ + function defaultFormatter() { + var pThis = this, + items = pThis.points || splat(pThis), + series = items[0].series, + s; + + // build the header + s = [series.tooltipHeaderFormatter(items[0].key)]; + + // build the values + each(items, function (item) { + series = item.series; + s.push((series.tooltipFormatter && series.tooltipFormatter(item)) || + item.point.tooltipFormatter(series.tooltipOptions.pointFormat)); + }); + + // footer + s.push(options.footerFormat || ''); + + return s.join(''); + } + + var x, + y, + show, + anchor, + textConfig = {}, + text, + pointConfig = [], + formatter = options.formatter || defaultFormatter, + hoverPoints = chart.hoverPoints, + placedTooltipPoint, + borderColor, + crosshairsOptions = options.crosshairs, + shared = tooltip.shared, + currentSeries; + + // get the reference point coordinates (pie charts use tooltipPos) + anchor = tooltip.getAnchor(point, mouseEvent); + x = anchor[0]; + y = anchor[1]; + + // shared tooltip, array is sent over + if (shared && !(point.series && point.series.noSharedTooltip)) { + + // hide previous hoverPoints and set new + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + chart.hoverPoints = point; + + each(point, function (item) { + item.setState(HOVER_STATE); + + pointConfig.push(item.getLabelConfig()); + }); + + textConfig = { + x: point[0].category, + y: point[0].y + }; + textConfig.points = pointConfig; + point = point[0]; + + // single point tooltip + } else { + textConfig = point.getLabelConfig(); + } + text = formatter.call(textConfig); + + // register the current series + currentSeries = point.series; + + + // For line type series, hide tooltip if the point falls outside the plot + show = shared || !currentSeries.isCartesian || currentSeries.tooltipOutsidePlot || chart.isInsidePlot(x, y); + + // update the inner HTML + if (text === false || !show) { + this.hide(); + } else { + + // show it + if (tooltip.tooltipIsHidden) { + label.show(); + } + + // update text + label.attr({ + text: text + }); + + // set the stroke color of the box + borderColor = options.borderColor || point.color || currentSeries.color || '#606060'; + label.attr({ + stroke: borderColor + }); + + placedTooltipPoint = (options.positioner || tooltip.getPosition).call( + tooltip, + label.width, + label.height, + { plotX: x, plotY: y } + ); + + // do the move + tooltip.move(mathRound(placedTooltipPoint.x), mathRound(placedTooltipPoint.y)); + + + tooltip.tooltipIsHidden = false; + } + + // crosshairs + if (crosshairsOptions) { + crosshairsOptions = splat(crosshairsOptions); // [x, y] + + var path, + i = crosshairsOptions.length, + attribs, + axis; + + while (i--) { + axis = point.series[i ? 'yAxis' : 'xAxis']; + if (crosshairsOptions[i] && axis) { + + path = axis.getPlotLinePath( + i ? pick(point.stackY, point.y) : point.x, // #814 + 1 + ); + + if (tooltip.crosshairs[i]) { + tooltip.crosshairs[i].attr({ d: path, visibility: VISIBLE }); + } else { + attribs = { + 'stroke-width': crosshairsOptions[i].width || 1, + stroke: crosshairsOptions[i].color || '#C0C0C0', + zIndex: crosshairsOptions[i].zIndex || 2 + }; + if (crosshairsOptions[i].dashStyle) { + attribs.dashstyle = crosshairsOptions[i].dashStyle; + } + tooltip.crosshairs[i] = chart.renderer.path(path) + .attr(attribs) + .add(); + } + } + } + } + fireEvent(chart, 'tooltipRefresh', { + text: text, + x: x + chart.plotLeft, + y: y + chart.plotTop, + borderColor: borderColor + }); + }, + + /** + * Runs the tooltip animation one tick. + */ + tick: function () { + if (this.tooltipTick) { + this.tooltipTick(); + } + } +}; +/** + * The mouse tracker object + * @param {Object} chart The Chart instance + * @param {Object} options The root options object + */ +function MouseTracker(chart, options) { + var zoomType = useCanVG ? '' : options.chart.zoomType; + + // Zoom status + this.zoomX = /x/.test(zoomType); + this.zoomY = /y/.test(zoomType); + + // Store reference to options + this.options = options; + + // Reference to the chart + this.chart = chart; + + // The interval id + //this.tooltipInterval = UNDEFINED; + + // The cached x hover position + //this.hoverX = UNDEFINED; + + // The chart position + //this.chartPosition = UNDEFINED; + + // The selection marker element + //this.selectionMarker = UNDEFINED; + + // False or a value > 0 if a dragging operation + //this.mouseDownX = UNDEFINED; + //this.mouseDownY = UNDEFINED; + this.init(chart, options.tooltip); +} + +MouseTracker.prototype = { + /** + * Add crossbrowser support for chartX and chartY + * @param {Object} e The event object in standard browsers + */ + normalizeMouseEvent: function (e) { + var chartPosition, + chartX, + chartY, + ePos; + + // common IE normalizing + e = e || win.event; + if (!e.target) { + e.target = e.srcElement; + } + + // jQuery only copies over some properties. IE needs e.x and iOS needs touches. + if (e.originalEvent) { + e = e.originalEvent; + } + + // The same for MooTools. It renames e.pageX to e.page.x. #445. + if (e.event) { + e = e.event; + } + + // iOS + ePos = e.touches ? e.touches.item(0) : e; + + // get mouse position + this.chartPosition = chartPosition = offset(this.chart.container); + + // chartX and chartY + if (ePos.pageX === UNDEFINED) { // IE < 9. #886. + chartX = e.x; + chartY = e.y; + } else { + chartX = ePos.pageX - chartPosition.left; + chartY = ePos.pageY - chartPosition.top; + } + + return extend(e, { + chartX: mathRound(chartX), + chartY: mathRound(chartY) + + }); + }, + + /** + * Get the click position in terms of axis values. + * + * @param {Object} e A mouse event + */ + getMouseCoordinates: function (e) { + var coordinates = { + xAxis: [], + yAxis: [] + }, + chart = this.chart; + + each(chart.axes, function (axis) { + var isXAxis = axis.isXAxis, + isHorizontal = chart.inverted ? !isXAxis : isXAxis; + + coordinates[isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + value: axis.translate( + isHorizontal ? + e.chartX - chart.plotLeft : + chart.plotHeight - e.chartY + chart.plotTop, + true + ) + }); + }); + return coordinates; + }, + + /** + * With line type charts with a single tracker, get the point closest to the mouse + */ + onmousemove: function (e) { + var mouseTracker = this, + chart = mouseTracker.chart, + series = chart.series, + point, + points, + hoverPoint = chart.hoverPoint, + hoverSeries = chart.hoverSeries, + i, + j, + distance = chart.chartWidth, + // the index in the tooltipPoints array, corresponding to pixel position in plot area + index = chart.inverted ? chart.plotHeight + chart.plotTop - e.chartY : e.chartX - chart.plotLeft; + + // shared tooltip + if (chart.tooltip && mouseTracker.options.tooltip.shared && !(hoverSeries && hoverSeries.noSharedTooltip)) { + 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].options.enableMouseTracking !== false && + !series[j].noSharedTooltip && 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 !== mouseTracker.hoverX)) { + chart.tooltip.refresh(points, e); + mouseTracker.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 + */ + resetTracker: function (allowMove) { + var mouseTracker = this, + chart = mouseTracker.chart, + hoverSeries = chart.hoverSeries, + hoverPoint = chart.hoverPoint, + tooltipPoints = chart.hoverPoints || hoverPoint, + tooltip = chart.tooltip; + + // Narrow in allowMove + allowMove = allowMove && tooltip && tooltipPoints; + + // Check if the points have moved outside the plot area, #1003 + if (allowMove && splat(tooltipPoints)[0].plotX === UNDEFINED) { + allowMove = false; + } + + // Just move the tooltip, #349 + if (allowMove) { + tooltip.refresh(tooltipPoints); + + // Full reset + } else { + + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + if (hoverSeries) { + hoverSeries.onMouseOut(); + } + + if (tooltip) { + tooltip.hide(); + tooltip.hideCrosshairs(); + } + + mouseTracker.hoverX = null; + + } + }, + + /** + * Set the JS events on the container element + */ + setDOMEvents: function () { + var lastWasOutsidePlot = true, + mouseTracker = this, + chart = mouseTracker.chart, + container = chart.container, + hasDragged, + zoomHor = (mouseTracker.zoomX && !chart.inverted) || (mouseTracker.zoomY && chart.inverted), + zoomVert = (mouseTracker.zoomY && !chart.inverted) || (mouseTracker.zoomX && chart.inverted); + + /** + * Mouse up or outside the plot area + */ + function drop() { + if (mouseTracker.selectionMarker) { + var selectionData = { + xAxis: [], + yAxis: [] + }, + selectionBox = mouseTracker.selectionMarker.getBBox(), + selectionLeft = selectionBox.x - chart.plotLeft, + selectionTop = selectionBox.y - chart.plotTop, + runZoom; + + // a selection has been made + if (hasDragged) { + + // record each axis' min and max + each(chart.axes, function (axis) { + if (axis.options.zoomEnabled !== false) { + var isXAxis = axis.isXAxis, + isHorizontal = chart.inverted ? !isXAxis : isXAxis, + selectionMin = axis.translate( + isHorizontal ? + selectionLeft : + chart.plotHeight - selectionTop - selectionBox.height, + true, + 0, + 0, + 1 + ), + selectionMax = axis.translate( + isHorizontal ? + selectionLeft + selectionBox.width : + chart.plotHeight - selectionTop, + true, + 0, + 0, + 1 + ); + + if (!isNaN(selectionMin) && !isNaN(selectionMax)) { // #859 + selectionData[isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + min: mathMin(selectionMin, selectionMax), // for reversed axes, + max: mathMax(selectionMin, selectionMax) + }); + runZoom = true; + } + } + }); + if (runZoom) { + fireEvent(chart, 'selection', selectionData, function (args) { chart.zoom(args); }); + } + + } + mouseTracker.selectionMarker = mouseTracker.selectionMarker.destroy(); + } + + if (chart) { // it may be destroyed on mouse up - #877 + css(container, { cursor: 'auto' }); + chart.cancelClick = hasDragged; // #370 + chart.mouseIsDown = hasDragged = false; + } + + removeEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop); + } + + /** + * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea. + */ + mouseTracker.hideTooltipOnMouseMove = function (e) { + + // Get e.pageX and e.pageY back in MooTools + washMouseEvent(e); + + // If we're outside, hide the tooltip + if (mouseTracker.chartPosition && chart.hoverSeries && chart.hoverSeries.isCartesian && + !chart.isInsidePlot(e.pageX - mouseTracker.chartPosition.left - chart.plotLeft, + e.pageY - mouseTracker.chartPosition.top - chart.plotTop)) { + mouseTracker.resetTracker(); + } + }; + + /** + * When mouse leaves the container, hide the tooltip. + */ + mouseTracker.hideTooltipOnMouseLeave = function () { + mouseTracker.resetTracker(); + mouseTracker.chartPosition = null; // also reset the chart position, used in #149 fix + }; + + + /* + * Record the starting position of a dragoperation + */ + container.onmousedown = function (e) { + e = mouseTracker.normalizeMouseEvent(e); + + // issue #295, dragging not always working in Firefox + if (!hasTouch && e.preventDefault) { + e.preventDefault(); + } + + // record the start position + chart.mouseIsDown = true; + chart.cancelClick = false; + chart.mouseDownX = mouseTracker.mouseDownX = e.chartX; + mouseTracker.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 = mouseTracker.normalizeMouseEvent(e); + if (!hasTouch) { // not for touch devices + e.returnValue = false; + } + + var chartX = e.chartX, + chartY = e.chartY, + isOutsidePlot = !chart.isInsidePlot(chartX - chart.plotLeft, chartY - chart.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 (!chart.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 < chart.plotLeft) { + chartX = chart.plotLeft; + } else if (chartX > chart.plotLeft + chart.plotWidth) { + chartX = chart.plotLeft + chart.plotWidth; + } + + if (chartY < chart.plotTop) { + chartY = chart.plotTop; + } else if (chartY > chart.plotTop + chart.plotHeight) { + chartY = chart.plotTop + chart.plotHeight; + } + } + + if (chart.mouseIsDown && e.type !== 'touchstart') { // make selection + + // determine if the mouse has moved more than 10px + hasDragged = Math.sqrt( + Math.pow(mouseTracker.mouseDownX - chartX, 2) + + Math.pow(mouseTracker.mouseDownY - chartY, 2) + ); + if (hasDragged > 10) { + var clickedInside = chart.isInsidePlot(mouseTracker.mouseDownX - chart.plotLeft, mouseTracker.mouseDownY - chart.plotTop); + + // make a selection + if (chart.hasCartesianSeries && (mouseTracker.zoomX || mouseTracker.zoomY) && clickedInside) { + if (!mouseTracker.selectionMarker) { + mouseTracker.selectionMarker = chart.renderer.rect( + chart.plotLeft, + chart.plotTop, + zoomHor ? 1 : chart.plotWidth, + zoomVert ? 1 : chart.plotHeight, + 0 + ) + .attr({ + fill: mouseTracker.options.chart.selectionMarkerFill || 'rgba(69,114,167,0.25)', + zIndex: 7 + }) + .add(); + } + } + + // adjust the width of the selection marker + if (mouseTracker.selectionMarker && zoomHor) { + var xSize = chartX - mouseTracker.mouseDownX; + mouseTracker.selectionMarker.attr({ + width: mathAbs(xSize), + x: (xSize > 0 ? 0 : xSize) + mouseTracker.mouseDownX + }); + } + // adjust the height of the selection marker + if (mouseTracker.selectionMarker && zoomVert) { + var ySize = chartY - mouseTracker.mouseDownY; + mouseTracker.selectionMarker.attr({ + height: mathAbs(ySize), + y: (ySize > 0 ? 0 : ySize) + mouseTracker.mouseDownY + }); + } + + // panning + if (clickedInside && !mouseTracker.selectionMarker && mouseTracker.options.chart.panning) { + chart.pan(chartX); + } + } + + } else if (!isOutsidePlot) { + // show the tooltip + mouseTracker.onmousemove(e); + } + + lastWasOutsidePlot = isOutsidePlot; + + // when outside plot, allow touch-drag by returning true + return isOutsidePlot || !chart.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', mouseTracker.hideTooltipOnMouseLeave); + + // issue #149 workaround + // The mouseleave event above does not always fire. Whenever the mouse is moving + // outside the plotarea, hide the tooltip + addEvent(doc, 'mousemove', mouseTracker.hideTooltipOnMouseMove); + + container.ontouchstart = function (e) { + // For touch devices, use touchmove to zoom + if (mouseTracker.zoomX || mouseTracker.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) { + mouseTracker.resetTracker(); + } + }; + + + // MooTools 1.2.3 doesn't fire this in IE when using addEvent + container.onclick = function (e) { + var hoverPoint = chart.hoverPoint, + plotX, + plotY; + e = mouseTracker.normalizeMouseEvent(e); + + e.cancelBubble = true; // IE specific + + + if (!chart.cancelClick) { + // Detect clicks on trackers or tracker groups, #783 + if (hoverPoint && (attr(e.target, 'isTracker') || attr(e.target.parentNode, 'isTracker'))) { + plotX = hoverPoint.plotX; + plotY = hoverPoint.plotY; + + // add page position info + extend(hoverPoint, { + pageX: mouseTracker.chartPosition.left + chart.plotLeft + + (chart.inverted ? chart.plotWidth - plotY : plotX), + pageY: mouseTracker.chartPosition.top + chart.plotTop + + (chart.inverted ? chart.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, mouseTracker.getMouseCoordinates(e)); + + // fire a click event in the chart + if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + fireEvent(chart, 'click', e); + } + } + + + } + }; + + }, + + /** + * Destroys the MouseTracker object and disconnects DOM events. + */ + destroy: function () { + var mouseTracker = this, + chart = mouseTracker.chart, + container = chart.container; + + // Destroy the tracker group element + if (chart.trackerGroup) { + chart.trackerGroup = chart.trackerGroup.destroy(); + } + + removeEvent(container, 'mouseleave', mouseTracker.hideTooltipOnMouseLeave); + removeEvent(doc, 'mousemove', mouseTracker.hideTooltipOnMouseMove); + container.onclick = container.onmousedown = container.onmousemove = container.ontouchstart = container.ontouchend = container.ontouchmove = null; + + // memory and CPU leak + clearInterval(this.tooltipInterval); + }, + + // Run MouseTracker + init: function (chart, options) { + if (!chart.trackerGroup) { + chart.trackerGroup = chart.renderer.g('tracker') + .attr({ zIndex: 9 }) + .add(); + } + + if (options.enabled) { + chart.tooltip = new Tooltip(chart, options); + + // set the fixed interval ticking for the smooth tooltip + this.tooltipInterval = setInterval(function () { chart.tooltip.tick(); }, 32); + } + + this.setDOMEvents(); + } +}; +/** + * The overview of the chart's series + */ +function Legend(chart) { + + this.init(chart); +} + +Legend.prototype = { + + /** + * Initialize the legend + */ + init: function (chart) { + var legend = this, + options = legend.options = chart.options.legend; + + if (!options.enabled) { + return; + } + + var //style = options.style || {}, // deprecated + itemStyle = options.itemStyle, + padding = pick(options.padding, 8), + itemMarginTop = options.itemMarginTop || 0; + + legend.baseline = pInt(itemStyle.fontSize) + 3 + itemMarginTop; // used in Series prototype + legend.itemStyle = itemStyle; + legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle); + legend.itemMarginTop = itemMarginTop; + legend.padding = padding; + legend.initialItemX = padding; + legend.initialItemY = padding - 5; // 5 is the number of pixels above the text + legend.maxItemWidth = 0; + legend.chart = chart; + //legend.allItems = UNDEFINED; + //legend.legendWidth = UNDEFINED; + //legend.legendHeight = UNDEFINED; + //legend.offsetWidth = UNDEFINED; + legend.itemHeight = 0; + legend.lastLineHeight = 0; + //legend.itemX = UNDEFINED; + //legend.itemY = UNDEFINED; + //legend.lastItemY = UNDEFINED; + + // Elements + //legend.group = UNDEFINED; + //legend.box = UNDEFINED; + + // run legend + legend.render(); + + // move checkboxes + addEvent(legend.chart, 'endResize', function () { legend.positionCheckboxes(); }); + +/* // expose + return { + colorizeItem: colorizeItem, + destroyItem: destroyItem, + render: render, + destroy: destroy, + getLegendWidth: getLegendWidth, + getLegendHeight: getLegendHeight + };*/ + }, + + /** + * Set the colors for the legend item + * @param {Object} item A Series or Point instance + * @param {Object} visible Dimmed or colored + */ + colorizeItem: function (item, visible) { + var legend = this, + options = legend.options, + legendItem = item.legendItem, + legendLine = item.legendLine, + legendSymbol = item.legendSymbol, + hiddenColor = legend.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 + */ + positionItem: function (item) { + var legend = this, + options = legend.options, + symbolPadding = options.symbolPadding, + ltr = !options.rtl, + legendItemPos = item._legendItemPos, + itemX = legendItemPos[0], + itemY = legendItemPos[1], + checkbox = item.checkbox; + + if (item.legendGroup) { + item.legendGroup.translate( + ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4, + itemY + ); + } + + if (checkbox) { + checkbox.x = itemX; + checkbox.y = itemY; + } + }, + + /** + * Destroy a single legend item + * @param {Object} item The series or point + */ + destroyItem: function (item) { + var checkbox = item.checkbox; + + // destroy SVG elements + each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) { + if (item[key]) { + item[key].destroy(); + } + }); + + if (checkbox) { + discardElement(item.checkbox); + } + }, + + /** + * Destroys the legend. + */ + destroy: function () { + var legend = this, + legendGroup = legend.group, + box = legend.box; + + if (box) { + legend.box = box.destroy(); + } + + if (legendGroup) { + legend.group = legendGroup.destroy(); + } + }, + + /** + * Position the checkboxes after the width is determined + */ + positionCheckboxes: function () { + var legend = this; + + each(legend.allItems, function (item) { + var checkbox = item.checkbox, + alignAttr = legend.group.alignAttr; + if (checkbox) { + css(checkbox, { + left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 20) + PX, + top: (alignAttr.translateY + checkbox.y + 3) + PX + }); + } + }); + }, + + /** + * Render a single specific legend item + * @param {Object} item A series or point + */ + renderItem: function (item) { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + options = legend.options, + horizontal = options.layout === 'horizontal', + symbolWidth = options.symbolWidth, + symbolPadding = options.symbolPadding, + itemStyle = legend.itemStyle, + itemHiddenStyle = legend.itemHiddenStyle, + padding = legend.padding, + ltr = !options.rtl, + itemHeight, + widthOption = options.width, + itemMarginBottom = options.itemMarginBottom || 0, + itemMarginTop = legend.itemMarginTop, + initialItemX = legend.initialItemX, + bBox, + itemWidth, + li = item.legendItem, + series = item.series || item, + itemOptions = series.options, + showCheckbox = itemOptions.showCheckbox; + + if (!li) { // generate it once, later move it + + // Generate the group box + // A group to hold the symbol and text. Text is to be appended in Legend class. + item.legendGroup = renderer.g('legend-item') + .attr({ zIndex: 1 }) + .add(legend.scrollGroup); + + // Draw the legend symbol inside the group box + series.drawLegendSymbol(legend, item); + + // Generate the list item text and add it to the group + item.legendItem = li = renderer.text( + options.labelFormatter.call(item), + ltr ? symbolWidth + symbolPadding : -symbolPadding, + legend.baseline, + options.useHTML + ) + .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021) + .attr({ + align: ltr ? 'left' : 'right', + zIndex: 2 + }) + .add(item.legendGroup); + + // Set the events on the item group + item.legendGroup.on('mouseover', function () { + item.setState(HOVER_STATE); + li.css(legend.options.itemHoverStyle); + }) + .on('mouseout', function () { + li.css(item.visible ? itemStyle : itemHiddenStyle); + item.setState(); + }) + .on('click', function (event) { + var strLegendItemClick = 'legendItemClick', + fnLegendItemClick = function () { + item.setVisible(); + }; + + // Pass over the click/touch event. #4. + event = { + browserEvent: event + }; + + // click the name or symbol + if (item.firePointEvent) { // point + item.firePointEvent(strLegendItemClick, event, fnLegendItemClick); + } else { + fireEvent(item, strLegendItemClick, event, fnLegendItemClick); + } + }); + + // Colorize the items + legend.colorizeItem(item, item.visible); + + // add the HTML checkbox on top + if (itemOptions && showCheckbox) { + item.checkbox = createElement('input', { + type: 'checkbox', + checked: item.selected, + defaultChecked: item.selected // required by IE7 + }, options.itemCheckboxStyle, chart.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 + padding + + (showCheckbox ? 20 : 0); + legend.itemHeight = itemHeight = bBox.height; + + // if the item exceeds the width, start a new line + if (horizontal && legend.itemX - initialItemX + itemWidth > + (widthOption || (chart.chartWidth - 2 * padding - initialItemX))) { + legend.itemX = initialItemX; + legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom; + legend.lastLineHeight = 0; // reset for next line + } + + // If the item exceeds the height, start a new column + /*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) { + legend.itemY = legend.initialItemY; + legend.itemX += legend.maxItemWidth; + legend.maxItemWidth = 0; + }*/ + + // Set the edge positions + legend.maxItemWidth = mathMax(legend.maxItemWidth, itemWidth); + legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom; + legend.lastLineHeight = mathMax(itemHeight, legend.lastLineHeight); // #915 + + // cache the position of the newly generated or reordered items + item._legendItemPos = [legend.itemX, legend.itemY]; + + // advance + if (horizontal) { + legend.itemX += itemWidth; + + } else { + legend.itemY += itemMarginTop + itemHeight + itemMarginBottom; + legend.lastLineHeight = itemHeight; + } + + // the width of the widest item + legend.offsetWidth = widthOption || mathMax( + horizontal ? legend.itemX - initialItemX : itemWidth, + legend.offsetWidth + ); + }, + + /** + * 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. + */ + render: function () { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + legendGroup = legend.group, + allItems, + display, + legendWidth, + legendHeight, + box = legend.box, + options = legend.options, + padding = legend.padding, + legendBorderWidth = options.borderWidth, + legendBackgroundColor = options.backgroundColor; + + legend.itemX = legend.initialItemX; + legend.itemY = legend.initialItemY; + legend.offsetWidth = 0; + legend.lastItemY = 0; + + if (!legendGroup) { + legend.group = legendGroup = renderer.g('legend') + // #414, #759. Trackers will be drawn above the legend, but we have + // to sacrifice that because tooltips need to be above the legend + // and trackers above tooltips + .attr({ zIndex: 7 }) + .add(); + legend.contentGroup = renderer.g() + .attr({ zIndex: 1 }) // above background + .add(legendGroup); + legend.scrollGroup = renderer.g() + .add(legend.contentGroup); + legend.clipRect = renderer.clipRect(0, 0, 9999, chart.chartHeight); + legend.contentGroup.clip(legend.clipRect); + } + + // add each series or point + allItems = []; + each(chart.series, function (serie) { + var seriesOptions = serie.options; + + if (!seriesOptions.showInLegend) { + return; + } + + // use points or series for the legend item depending on legendType + allItems = allItems.concat( + serie.legendItems || + (seriesOptions.legendType === 'point' ? + serie.data : + serie) + ); + }); + + // sort by legendIndex + stableSort(allItems, function (a, b) { + return (a.options.legendIndex || 0) - (b.options.legendIndex || 0); + }); + + // reversed legend + if (options.reversed) { + allItems.reverse(); + } + + legend.allItems = allItems; + legend.display = display = !!allItems.length; + + // render the items + each(allItems, function (item) { + legend.renderItem(item); + }); + + // Draw the border + legendWidth = options.width || legend.offsetWidth; + legendHeight = legend.lastItemY + legend.lastLineHeight; + + + legendHeight = legend.handleOverflow(legendHeight); + + if (legendBorderWidth || legendBackgroundColor) { + legendWidth += padding; + legendHeight += padding; + + if (!box) { + legend.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); + box.isNew = true; + + } else if (legendWidth > 0 && legendHeight > 0) { + box[box.isNew ? 'attr' : 'animate']( + box.crisp(null, null, null, legendWidth, legendHeight) + ); + box.isNew = false; + } + + // hide the border if no items + box[display ? 'show' : 'hide'](); + } + + legend.legendWidth = legendWidth; + legend.legendHeight = legendHeight; + + // Now that the legend width and height are established, put the items in the + // final position + each(allItems, function (item) { + legend.positionItem(item); + }); + + // 1.x compatibility: positioning based on style + /*var props = ['left', 'right', 'top', 'bottom'], + prop, + i = 4; + while (i--) { + prop = props[i]; + if (options.style[prop] && options.style[prop] !== 'auto') { + options[i < 2 ? 'align' : 'verticalAlign'] = prop; + options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1); + } + }*/ + + if (display) { + legendGroup.align(extend({ + width: legendWidth, + height: legendHeight + }, options), true, chart.spacingBox); + } + + if (!chart.isResizing) { + this.positionCheckboxes(); + } + }, + + /** + * Set up the overflow handling by adding navigation with up and down arrows below the + * legend. + */ + handleOverflow: function (legendHeight) { + var legend = this, + chart = this.chart, + renderer = chart.renderer, + pageCount, + options = this.options, + optionsY = options.y, + alignTop = options.verticalAlign === 'top', + spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding, + maxHeight = options.maxHeight, // docs + clipHeight, + clipRect = this.clipRect, + navOptions = options.navigation, + animation = pick(navOptions.animation, true), + arrowSize = navOptions.arrowSize || 12, + nav = this.nav; + + // Adjust the height + if (options.layout === 'horizontal') { + spaceHeight /= 2; + } + if (maxHeight) { + spaceHeight = mathMin(spaceHeight, maxHeight); + } + + // Reset the legend height and adjust the clipping rectangle + if (legendHeight > spaceHeight) { + + this.clipHeight = clipHeight = spaceHeight - 20; + this.pageCount = pageCount = mathCeil(legendHeight / clipHeight); + this.currentPage = pick(this.currentPage, 1); + this.fullHeight = legendHeight; + + clipRect.attr({ + height: clipHeight + }); + + // Add navigation elements + if (!nav) { + this.nav = nav = renderer.g().attr({ zIndex: 1 }).add(this.group); + this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize) + .on('click', function () { + legend.scroll(-1, animation); + }) + .add(nav); + this.pager = renderer.text('', 15, 10) + .css(navOptions.style) + .add(nav); + this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize) + .on('click', function () { + legend.scroll(1, animation); + }) + .add(nav); + } + + // Set initial position + legend.scroll(0); + + legendHeight = spaceHeight; + + } else if (nav) { + clipRect.attr({ + height: chart.chartHeight + }); + nav.hide(); + this.scrollGroup.attr({ + translateY: 1 + }); + } + + return legendHeight; + }, + + /** + * Scroll the legend by a number of pages + * @param {Object} scrollBy + * @param {Object} animation + */ + scroll: function (scrollBy, animation) { + var pageCount = this.pageCount, + currentPage = this.currentPage + scrollBy, + clipHeight = this.clipHeight, + navOptions = this.options.navigation, + activeColor = navOptions.activeColor, + inactiveColor = navOptions.inactiveColor, + pager = this.pager, + padding = this.padding; + + // When resizing while looking at the last page + if (currentPage > pageCount) { + currentPage = pageCount; + } + + if (currentPage > 0) { + + if (animation !== UNDEFINED) { + setAnimation(animation, this.chart); + } + + this.nav.attr({ + translateX: padding, + translateY: clipHeight + 7, + visibility: VISIBLE + }); + this.up.attr({ + fill: currentPage === 1 ? inactiveColor : activeColor + }) + .css({ + cursor: currentPage === 1 ? 'default' : 'pointer' + }); + pager.attr({ + text: currentPage + '/' + this.pageCount + }); + this.down.attr({ + x: 18 + this.pager.getBBox().width, // adjust to text width + fill: currentPage === pageCount ? inactiveColor : activeColor + }) + .css({ + cursor: currentPage === pageCount ? 'default' : 'pointer' + }); + + this.scrollGroup.animate({ + translateY: -mathMin(clipHeight * (currentPage - 1), this.fullHeight - clipHeight + padding) + 1 + }); + pager.attr({ + text: currentPage + '/' + pageCount + }); + + + this.currentPage = currentPage; + } + + } + +}; + + +/** + * The chart class + * @param {Object} options + * @param {Function} callback Function to run when the chart has loaded + */ +function Chart(userOptions, callback) { + // Handle regular options + var options, + seriesOptions = userOptions.series; // skip merging data points to increase performance + userOptions.series = null; + options = merge(defaultOptions, userOptions); // do the merge + options.series = userOptions.series = seriesOptions; // set back the series data + + var optionsChart = options.chart, + optionsMargin = optionsChart.margin, + margin = isObject(optionsMargin) ? + optionsMargin : + [optionsMargin, optionsMargin, optionsMargin, optionsMargin]; + + this.optionsMarginTop = pick(optionsChart.marginTop, margin[0]); + this.optionsMarginRight = pick(optionsChart.marginRight, margin[1]); + this.optionsMarginBottom = pick(optionsChart.marginBottom, margin[2]); + this.optionsMarginLeft = pick(optionsChart.marginLeft, margin[3]); + + var chartEvents = optionsChart.events; + + this.runChartClick = chartEvents && !!chartEvents.click; + this.callback = callback; + this.isResizing = 0; + this.options = options; + //chartTitleOptions = UNDEFINED; + //chartSubtitleOptions = UNDEFINED; + + this.axes = []; + this.series = []; + this.hasCartesianSeries = optionsChart.showAxes; + //this.axisOffset = UNDEFINED; + //this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes + //this.inverted = UNDEFINED; + //this.loadingShown = UNDEFINED; + //this.container = UNDEFINED; + //this.chartWidth = UNDEFINED; + //this.chartHeight = UNDEFINED; + //this.marginRight = UNDEFINED; + //this.marginBottom = UNDEFINED; + //this.containerWidth = UNDEFINED; + //this.containerHeight = UNDEFINED; + //this.oldChartWidth = UNDEFINED; + //this.oldChartHeight = UNDEFINED; + + //this.renderTo = UNDEFINED; + //this.renderToClone = UNDEFINED; + //this.tracker = UNDEFINED; + + //this.spacingBox = UNDEFINED + + //this.legend = UNDEFINED; + + // Elements + //this.chartBackground = UNDEFINED; + //this.plotBackground = UNDEFINED; + //this.plotBGImage = UNDEFINED; + //this.plotBorder = UNDEFINED; + //this.loadingDiv = UNDEFINED; + //this.loadingSpan = UNDEFINED; + + this.init(chartEvents); +} + +Chart.prototype = { + + /** + * Initialize an individual series, called internally before render time + */ + initSeries: function (options) { + var chart = this, + optionsChart = chart.options.chart, + type = options.type || optionsChart.type || optionsChart.defaultSeriesType, + series = new seriesTypes[type](); + + series.init(this, options); + return series; + }, + + /** + * 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 + */ + addSeries: function (options, redraw, animation) { + var series, + chart = this; + + if (options) { + setAnimation(animation, chart); + redraw = pick(redraw, true); // defaults to true + + fireEvent(chart, 'addSeries', { options: options }, function () { + chart.initSeries(options); + //series = chart.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 plot area + * @param {Number} y Pixel y relative to the plot area + */ + isInsidePlot: function (x, y) { + return x >= 0 && + x <= this.plotWidth && + y >= 0 && + y <= this.plotHeight; + }, + + /** + * Adjust all axes tick amounts + */ + adjustTickAmounts: function () { + if (this.options.chart.alignTicks !== false) { + each(this.axes, function (axis) { + axis.adjustTickAmount(); + }); + } + this.maxTicks = null; + }, + + /** + * Redraw legend, axes or series based on updated data + * + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + redraw: function (animation) { + var chart = this, + axes = chart.axes, + series = chart.series, + tracker = chart.tracker, + legend = chart.legend, + redrawLegend = chart.isDirtyLegend, + hasStackedSeries, + isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed? + seriesLength = series.length, + i = seriesLength, + clipRect = chart.clipRect, + serie, + renderer = chart.renderer, + isHiddenChart = renderer.isHidden(); + + setAnimation(animation, chart); + + if (isHiddenChart) { + chart.cloneRenderTo(); + } + + // 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 + if (serie.options.legendType === 'point') { + redrawLegend = true; + } + } + }); + + // handle added or removed series + if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed + // draw legend graphics + legend.render(); + + chart.isDirtyLegend = false; + } + + + if (chart.hasCartesianSeries) { + if (!chart.isResizing) { + + // reset maxTicks + chart.maxTicks = null; + + // set axes scales + each(axes, function (axis) { + axis.setScale(); + }); + } + chart.adjustTickAmounts(); + chart.getMargins(); + + // redraw axes + each(axes, function (axis) { + + // Fire 'afterSetExtremes' only if extremes are set + if (axis.isDirtyExtremes) { // #821 + axis.isDirtyExtremes = false; + fireEvent(axis, 'afterSetExtremes', axis.getExtremes()); // #747, #751 + } + + if (axis.isDirty || isDirtyBox || hasStackedSeries) { + axis.redraw(); + isDirtyBox = true; // #792 + } + }); + + + } + + // the plot areas size has changed + if (isDirtyBox) { + chart.drawChartBox(); + + // move clip rect + if (clipRect) { + stop(clipRect); + clipRect.animate({ // for chart resize + width: chart.plotSizeX, + height: chart.plotSizeY + 1 + }); + } + + } + + + // redraw affected series + each(series, function (serie) { + if (serie.isDirty && serie.visible && + (!serie.isCartesian || serie.xAxis)) { // issue #153 + serie.redraw(); + } + }); + + + // move tooltip or reset + if (tracker && tracker.resetTracker) { + tracker.resetTracker(true); + } + + // redraw if canvas + renderer.draw(); + + // fire the event + fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw + + if (isHiddenChart) { + chart.cloneRenderTo(true); + } + }, + + + + /** + * 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 + */ + showLoading: function (str) { + var chart = this, + options = chart.options, + loadingDiv = chart.loadingDiv; + + var loadingOptions = options.loading; + + // create the layer at the first call + if (!loadingDiv) { + chart.loadingDiv = loadingDiv = createElement(DIV, { + className: PREFIX + 'loading' + }, extend(loadingOptions.style, { + left: chart.plotLeft + PX, + top: chart.plotTop + PX, + width: chart.plotWidth + PX, + height: chart.plotHeight + PX, + zIndex: 10, + display: NONE + }), chart.container); + + chart.loadingSpan = createElement( + 'span', + null, + loadingOptions.labelStyle, + loadingDiv + ); + + } + + // update text + chart.loadingSpan.innerHTML = str || options.lang.loading; + + // show it + if (!chart.loadingShown) { + css(loadingDiv, { opacity: 0, display: '' }); + animate(loadingDiv, { + opacity: loadingOptions.style.opacity + }, { + duration: loadingOptions.showDuration || 0 + }); + chart.loadingShown = true; + } + }, + + /** + * Hide the loading layer + */ + hideLoading: function () { + var options = this.options, + loadingDiv = this.loadingDiv; + + if (loadingDiv) { + animate(loadingDiv, { + opacity: 0 + }, { + duration: options.loading.hideDuration || 100, + complete: function () { + css(loadingDiv, { display: NONE }); + } + }); + } + this.loadingShown = false; + }, + + /** + * Get an axis, series or point object by id. + * @param id {String} The id as given in the configuration options + */ + get: function (id) { + var chart = this, + axes = chart.axes, + series = chart.series; + + var i, + j, + points; + + // 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++) { + points = series[i].points || []; + for (j = 0; j < points.length; j++) { + if (points[j].id === id) { + return points[j]; + } + } + } + return null; + }, + + /** + * Create the Axis instances based on the config options + */ + getAxes: function () { + var chart = this, + options = this.options; + + var xAxisOptions = options.xAxis || {}, + yAxisOptions = options.yAxis || {}, + optionsArray, + 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 + optionsArray = xAxisOptions.concat(yAxisOptions); + + each(optionsArray, function (axisOptions) { + axis = new Axis(chart, axisOptions); + }); + + chart.adjustTickAmounts(); + }, + + + /** + * Get the currently selected points from all series + */ + getSelectedPoints: function () { + var points = []; + each(this.series, function (serie) { + points = points.concat(grep(serie.points, function (point) { + return point.selected; + })); + }); + return points; + }, + + /** + * Get the currently selected series + */ + getSelectedSeries: function () { + return grep(this.series, function (serie) { + return serie.selected; + }); + }, + + /** + * Display the zoom button + */ + showResetZoom: function () { + var chart = this, + lang = defaultOptions.lang, + btnOptions = chart.options.chart.resetZoomButton, + theme = btnOptions.theme, + states = theme.states, + box = btnOptions.relativeTo === 'chart' ? null : { + x: chart.plotLeft, + y: chart.plotTop, + width: chart.plotWidth, + height: chart.plotHeight + }; + this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, function () { chart.zoomOut(); }, theme, states && states.hover) + .attr({ + align: btnOptions.position.align, + title: lang.resetZoomTitle + }) + .add() + .align(btnOptions.position, false, box); + }, + + /** + * Zoom out to 1:1 + */ + zoomOut: function () { + var chart = this, + resetZoomButton = chart.resetZoomButton; + + fireEvent(chart, 'selection', { resetSelection: true }, function () { chart.zoom(); }); + if (resetZoomButton) { + chart.resetZoomButton = resetZoomButton.destroy(); + } + }, + + /** + * Zoom into a given portion of the chart given by axis coordinates + * @param {Object} event + */ + zoom: function (event) { + var chart = this, + optionsChart = chart.options.chart; + + // add button to reset selection + var hasZoomed; + + if (chart.resetZoomEnabled !== false && !chart.resetZoomButton) { // hook for Stock charts etc. + chart.showResetZoom(); + } + + // if zoom is called with no arguments, reset the axes + if (!event || event.resetSelection) { + each(chart.axes, function (axis) { + if (axis.options.zoomEnabled !== false) { + axis.setExtremes(null, null, false); + hasZoomed = true; + } + }); + } else { // else, zoom in on all axes + each(event.xAxis.concat(event.yAxis), function (axisData) { + var axis = axisData.axis; + + // don't zoom more than minRange + if (chart.tracker[axis.isXAxis ? 'zoomX' : 'zoomY']) { + axis.setExtremes(axisData.min, axisData.max, false); + hasZoomed = true; + } + }); + } + + // Redraw + if (hasZoomed) { + chart.redraw( + pick(optionsChart.animation, chart.pointCount < 100) // animation + ); + } + }, + + /** + * Pan the chart by dragging the mouse across the pane. This function is called + * on mouse move, and the distance to pan is computed from chartX compared to + * the first chartX position in the dragging operation. + */ + pan: function (chartX) { + var chart = this; + + var xAxis = chart.xAxis[0], + mouseDownX = chart.mouseDownX, + halfPointRange = xAxis.pointRange / 2, + extremes = xAxis.getExtremes(), + newMin = xAxis.translate(mouseDownX - chartX, true) + halfPointRange, + newMax = xAxis.translate(mouseDownX + chart.plotWidth - chartX, true) - halfPointRange, + hoverPoints = chart.hoverPoints; + + // remove active points for shared tooltip + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + if (xAxis.series.length && newMin > mathMin(extremes.dataMin, extremes.min) && newMax < mathMax(extremes.dataMax, extremes.max)) { + xAxis.setExtremes(newMin, newMax, true, false); + } + + chart.mouseDownX = chartX; // set new reference for next run + css(chart.container, { cursor: 'move' }); + }, + + /** + * Show the title and subtitle of the chart + * + * @param titleOptions {Object} New title options + * @param subtitleOptions {Object} New subtitle options + * + */ + setTitle: function (titleOptions, subtitleOptions) { + var chart = this, + options = chart.options, + chartTitleOptions, + chartSubtitleOptions; + + chart.chartTitleOptions = chartTitleOptions = merge(options.title, titleOptions); + chart.chartSubtitleOptions = 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 = title.destroy(); // remove old + } + if (chartTitleOptions && chartTitleOptions.text && !title) { + chart[name] = chart.renderer.text( + chartTitleOptions.text, + 0, + 0, + chartTitleOptions.useHTML + ) + .attr({ + align: chartTitleOptions.align, + 'class': PREFIX + name, + zIndex: chartTitleOptions.zIndex || 4 + }) + .css(chartTitleOptions.style) + .add() + .align(chartTitleOptions, false, chart.spacingBox); + } + }); + + }, + + /** + * Get chart width and height according to options and container size + */ + getChartSize: function () { + var chart = this, + optionsChart = chart.options.chart, + renderTo = chart.renderToClone || chart.renderTo; + + // get inner width and height from jQuery (#824) + chart.containerWidth = adapterRun(renderTo, 'width'); + chart.containerHeight = adapterRun(renderTo, 'height'); + + chart.chartWidth = optionsChart.width || chart.containerWidth || 600; + chart.chartHeight = optionsChart.height || + // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: + (chart.containerHeight > 19 ? chart.containerHeight : 400); + }, + + /** + * Create a clone of the chart's renderTo div and place it outside the viewport to allow + * size computation on chart.render and chart.redraw + */ + cloneRenderTo: function (revert) { + var clone = this.renderToClone, + container = this.container; + + // Destroy the clone and bring the container back to the real renderTo div + if (revert) { + if (clone) { + this.renderTo.appendChild(container); + discardElement(clone); + delete this.renderToClone; + } + + // Set up the clone + } else { + if (container) { + this.renderTo.removeChild(container); // do not clone this + } + this.renderToClone = clone = this.renderTo.cloneNode(0); + css(clone, { + position: ABSOLUTE, + top: '-9999px', + display: 'block' // #833 + }); + doc.body.appendChild(clone); + if (container) { + clone.appendChild(container); + } + } + }, + + /** + * Get the containing element, determine the size and create the inner container + * div to hold the chart + */ + getContainer: function () { + var chart = this, + container, + optionsChart = chart.options.chart, + chartWidth, + chartHeight, + renderTo, + containerId; + + chart.renderTo = renderTo = optionsChart.renderTo; + containerId = PREFIX + idCounter++; + + if (isString(renderTo)) { + chart.renderTo = renderTo = doc.getElementById(renderTo); + } + + // Display an error if the renderTo is wrong + if (!renderTo) { + error(13, true); + } + + // 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) { + chart.cloneRenderTo(); + } + + // get the width and height + chart.getChartSize(); + chartWidth = chart.chartWidth; + chartHeight = chart.chartHeight; + + // create the inner container + chart.container = container = createElement(DIV, { + className: PREFIX + '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', + lineHeight: 'normal' // #427 + }, optionsChart.style), + chart.renderToClone || renderTo + ); + + chart.renderer = + optionsChart.forExport ? // force SVG, used for SVG export + new SVGRenderer(container, chartWidth, chartHeight, true) : + new Renderer(container, chartWidth, chartHeight); + + if (useCanVG) { + // If we need canvg library, extend and configure the renderer + // to get the tracker for translating mouse events + chart.renderer.create(chart, container, chartWidth, chartHeight); + } + }, + + /** + * 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 chart = this, + optionsChart = chart.options.chart, + spacingTop = optionsChart.spacingTop, + spacingRight = optionsChart.spacingRight, + spacingBottom = optionsChart.spacingBottom, + spacingLeft = optionsChart.spacingLeft, + axisOffset, + legend = chart.legend, + optionsMarginTop = chart.optionsMarginTop, + optionsMarginLeft = chart.optionsMarginLeft, + optionsMarginRight = chart.optionsMarginRight, + optionsMarginBottom = chart.optionsMarginBottom, + chartTitleOptions = chart.chartTitleOptions, + chartSubtitleOptions = chart.chartSubtitleOptions, + legendOptions = chart.options.legend, + legendMargin = pick(legendOptions.margin, 10), + legendX = legendOptions.x, + legendY = legendOptions.y, + align = legendOptions.align, + verticalAlign = legendOptions.verticalAlign, + titleOffset; + + chart.resetMargins(); + axisOffset = chart.axisOffset; + + // adjust for title and subtitle + if ((chart.title || chart.subtitle) && !defined(chart.optionsMarginTop)) { + titleOffset = mathMax( + (chart.title && !chartTitleOptions.floating && !chartTitleOptions.verticalAlign && chartTitleOptions.y) || 0, + (chart.subtitle && !chartSubtitleOptions.floating && !chartSubtitleOptions.verticalAlign && chartSubtitleOptions.y) || 0 + ); + if (titleOffset) { + chart.plotTop = mathMax(chart.plotTop, titleOffset + pick(chartTitleOptions.margin, 15) + spacingTop); + } + } + // adjust for legend + if (legend.display && !legendOptions.floating) { + if (align === 'right') { // horizontal alignment handled first + if (!defined(optionsMarginRight)) { + chart.marginRight = mathMax( + chart.marginRight, + legend.legendWidth - legendX + legendMargin + spacingRight + ); + } + } else if (align === 'left') { + if (!defined(optionsMarginLeft)) { + chart.plotLeft = mathMax( + chart.plotLeft, + legend.legendWidth + legendX + legendMargin + spacingLeft + ); + } + + } else if (verticalAlign === 'top') { + if (!defined(optionsMarginTop)) { + chart.plotTop = mathMax( + chart.plotTop, + legend.legendHeight + legendY + legendMargin + spacingTop + ); + } + + } else if (verticalAlign === 'bottom') { + if (!defined(optionsMarginBottom)) { + chart.marginBottom = mathMax( + chart.marginBottom, + legend.legendHeight - legendY + legendMargin + spacingBottom + ); + } + } + } + + // adjust for scroller + if (chart.extraBottomMargin) { + chart.marginBottom += chart.extraBottomMargin; + } + if (chart.extraTopMargin) { + chart.plotTop += chart.extraTopMargin; + } + + // pre-render axes to get labels offset width + if (chart.hasCartesianSeries) { + each(chart.axes, function (axis) { + axis.getOffset(); + }); + } + + if (!defined(optionsMarginLeft)) { + chart.plotLeft += axisOffset[3]; + } + if (!defined(optionsMarginTop)) { + chart.plotTop += axisOffset[0]; + } + if (!defined(optionsMarginBottom)) { + chart.marginBottom += axisOffset[2]; + } + if (!defined(optionsMarginRight)) { + chart.marginRight += axisOffset[1]; + } + + chart.setChartSize(); + + }, + + /** + * Add the event handlers necessary for auto resizing + * + */ + initReflow: function () { + var chart = this, + optionsChart = chart.options.chart, + renderTo = chart.renderTo; + + var reflowTimeout; + function reflow(e) { + var width = optionsChart.width || adapterRun(renderTo, 'width'), + height = optionsChart.height || adapterRun(renderTo, 'height'), + target = e ? e.target : win; // #805 - MooTools doesn't supply e + + // Width and height checks for display:none. Target is doc in IE8 and Opera, + // win in Firefox, Chrome and IE9. + if (width && height && (target === win || target === doc)) { + + if (width !== chart.containerWidth || height !== chart.containerHeight) { + clearTimeout(reflowTimeout); + reflowTimeout = setTimeout(function () { + chart.resize(width, height, false); + }, 100); + } + chart.containerWidth = width; + chart.containerHeight = height; + } + } + addEvent(win, 'resize', reflow); + addEvent(chart, 'destroy', function () { + removeEvent(win, 'resize', reflow); + }); + }, + + /** + * Fires endResize event on chart instance. + */ + fireEndResize: function () { + var chart = this; + + if (chart) { + fireEvent(chart, 'endResize', null, function () { + chart.isResizing -= 1; + }); + } + }, + + /** + * Resize the chart to a given width and height + * @param {Number} width + * @param {Number} height + * @param {Object|Boolean} animation + */ + // TODO: This method is called setSize in the api + resize: function (width, height, animation) { + var chart = this, + chartWidth, + chartHeight, + spacingBox, + chartTitle = chart.title, + chartSubtitle = chart.subtitle; + + chart.isResizing += 1; + + // set the animation for the current process + setAnimation(animation, chart); + + chart.oldChartHeight = chart.chartHeight; + chart.oldChartWidth = chart.chartWidth; + if (defined(width)) { + chart.chartWidth = chartWidth = mathRound(width); + } + if (defined(height)) { + chart.chartHeight = chartHeight = mathRound(height); + } + + css(chart.container, { + width: chartWidth + PX, + height: chartHeight + PX + }); + chart.renderer.setSize(chartWidth, chartHeight, animation); + + // update axis lengths for more correct tick intervals: + chart.plotWidth = chartWidth - chart.plotLeft - chart.marginRight; + chart.plotHeight = chartHeight - chart.plotTop - chart.marginBottom; + + // handle axes + chart.maxTicks = null; + each(chart.axes, function (axis) { + axis.isDirty = true; + axis.setScale(); + }); + + // make sure non-cartesian series are also handled + each(chart.series, function (serie) { + serie.isDirty = true; + }); + + chart.isDirtyLegend = true; // force legend redraw + chart.isDirtyBox = true; // force redraw of plot and chart border + + chart.getMargins(); + + // move titles + spacingBox = chart.spacingBox; + if (chartTitle) { + chartTitle.align(null, null, spacingBox); + } + if (chartSubtitle) { + chartSubtitle.align(null, null, spacingBox); + } + + chart.redraw(animation); + + + chart.oldChartHeight = null; + fireEvent(chart, 'resize'); + + // fire endResize and set isResizing back + // If animation is disabled, fire without delay + if (globalAnimation === false) { + chart.fireEndResize(); + } else { // else set a timeout with the animation duration + setTimeout(chart.fireEndResize, (globalAnimation && globalAnimation.duration) || 500); + } + }, + + /** + * Set the public chart properties. This is done before and after the pre-render + * to determine margin sizes + */ + setChartSize: function () { + var chart = this, + inverted = chart.inverted, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + optionsChart = chart.options.chart, + spacingTop = optionsChart.spacingTop, + spacingRight = optionsChart.spacingRight, + spacingBottom = optionsChart.spacingBottom, + spacingLeft = optionsChart.spacingLeft; + + chart.plotLeft = mathRound(chart.plotLeft); + chart.plotTop = mathRound(chart.plotTop); + chart.plotWidth = mathRound(chartWidth - chart.plotLeft - chart.marginRight); + chart.plotHeight = mathRound(chartHeight - chart.plotTop - chart.marginBottom); + + chart.plotSizeX = inverted ? chart.plotHeight : chart.plotWidth; + chart.plotSizeY = inverted ? chart.plotWidth : chart.plotHeight; + + chart.spacingBox = { + x: spacingLeft, + y: spacingTop, + width: chartWidth - spacingLeft - spacingRight, + height: chartHeight - spacingTop - spacingBottom + }; + + each(chart.axes, function (axis) { + axis.setAxisSize(); + axis.setAxisTranslation(); + }); + }, + + /** + * Initial margins before auto size margins are applied + */ + resetMargins: function () { + var chart = this, + optionsChart = chart.options.chart, + spacingTop = optionsChart.spacingTop, + spacingRight = optionsChart.spacingRight, + spacingBottom = optionsChart.spacingBottom, + spacingLeft = optionsChart.spacingLeft; + + chart.plotTop = pick(chart.optionsMarginTop, spacingTop); + chart.marginRight = pick(chart.optionsMarginRight, spacingRight); + chart.marginBottom = pick(chart.optionsMarginBottom, spacingBottom); + chart.plotLeft = pick(chart.optionsMarginLeft, spacingLeft); + chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left + }, + + /** + * Draw the borders and backgrounds for chart and plot area + */ + drawChartBox: function () { + var chart = this, + optionsChart = chart.options.chart, + renderer = chart.renderer, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + chartBackground = chart.chartBackground, + plotBackground = chart.plotBackground, + plotBorder = chart.plotBorder, + plotBGImage = chart.plotBGImage, + chartBorderWidth = optionsChart.borderWidth || 0, + chartBackgroundColor = optionsChart.backgroundColor, + plotBackgroundColor = optionsChart.plotBackgroundColor, + plotBackgroundImage = optionsChart.plotBackgroundImage, + mgn, + bgAttr, + plotSize = { + x: chart.plotLeft, + y: chart.plotTop, + width: chart.plotWidth, + height: chart.plotHeight + }; + + // Chart area + mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); + + if (chartBorderWidth || chartBackgroundColor) { + if (!chartBackground) { + + bgAttr = { + fill: chartBackgroundColor || NONE + }; + if (chartBorderWidth) { // #980 + bgAttr.stroke = optionsChart.borderColor; + bgAttr['stroke-width'] = chartBorderWidth; + } + chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, + optionsChart.borderRadius, chartBorderWidth) + .attr(bgAttr) + .add() + .shadow(optionsChart.shadow); + } else { // resize + chartBackground.animate( + chartBackground.crisp(null, null, null, chartWidth - mgn, chartHeight - mgn) + ); + } + } + + + // Plot background + if (plotBackgroundColor) { + if (!plotBackground) { + chart.plotBackground = renderer.rect(chart.plotLeft, chart.plotTop, chart.plotWidth, chart.plotHeight, 0) + .attr({ + fill: plotBackgroundColor + }) + .add() + .shadow(optionsChart.plotShadow); + } else { + plotBackground.animate(plotSize); + } + } + if (plotBackgroundImage) { + if (!plotBGImage) { + chart.plotBGImage = renderer.image(plotBackgroundImage, chart.plotLeft, chart.plotTop, chart.plotWidth, chart.plotHeight) + .add(); + } else { + plotBGImage.animate(plotSize); + } + } + + // Plot area border + if (optionsChart.plotBorderWidth) { + if (!plotBorder) { + chart.plotBorder = renderer.rect(chart.plotLeft, chart.plotTop, chart.plotWidth, chart.plotHeight, 0, optionsChart.plotBorderWidth) + .attr({ + stroke: optionsChart.plotBorderColor, + 'stroke-width': optionsChart.plotBorderWidth, + zIndex: 4 + }) + .add(); + } else { + plotBorder.animate( + plotBorder.crisp(null, chart.plotLeft, chart.plotTop, chart.plotWidth, chart.plotHeight) + ); + } + } + + // reset + chart.isDirtyBox = false; + }, + + /** + * Detect whether a certain chart property is needed based on inspecting its options + * and series. This mainly applies to the chart.invert property, and in extensions to + * the chart.angular and chart.polar properties. + */ + propFromSeries: function () { + var chart = this, + optionsChart = chart.options.chart, + klass, + seriesOptions = chart.options.series, + i, + value; + + + each(['inverted', 'angular', 'polar'], function (key) { + + // The default series type's class + klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType]; + + // Get the value from available chart-wide properties + value = ( + chart[key] || // 1. it is set before + optionsChart[key] || // 2. it is set in the options + (klass && klass.prototype[key]) // 3. it's default series class requires it + ); + + // 4. Check if any the chart's series require it + i = seriesOptions && seriesOptions.length; + while (!value && i--) { + klass = seriesTypes[seriesOptions[i].type]; + if (klass && klass.prototype[key]) { + value = true; + } + } + + // Set the chart property + chart[key] = value; + }); + + }, + + /** + * Render all graphics for the chart + */ + render: function () { + var chart = this, + axes = chart.axes, + renderer = chart.renderer, + options = chart.options; + + var labels = options.labels, + credits = options.credits, + creditsHref; + + // Title + chart.setTitle(); + + + // Legend + chart.legend = new Legend(chart); + + // Get margins by pre-rendering axes + // set axes scales + each(axes, function (axis) { + axis.setScale(); + }); + chart.getMargins(); + + chart.maxTicks = null; // reset for second pass + each(axes, function (axis) { + axis.setTickPositions(true); // update to reflect the new margins + axis.setMaxTicks(); + }); + chart.adjustTickAmounts(); + chart.getMargins(); // second pass to check for new labels + + + // Draw the borders and backgrounds + chart.drawChartBox(); + + + // Axes + if (chart.hasCartesianSeries) { + each(axes, function (axis) { + axis.render(); + }); + } + + // The series + if (!chart.seriesGroup) { + chart.seriesGroup = renderer.g('series-group') + .attr({ zIndex: 3 }) + .add(); + } + each(chart.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) + chart.plotLeft, + y = pInt(style.top) + chart.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(); + + }); + } + + // Credits + if (credits.enabled && !chart.credits) { + creditsHref = credits.href; + chart.credits = 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); + } + + // Set flag + chart.hasRendered = true; + + }, + + /** + * Clean up memory usage + */ + destroy: function () { + var chart = this, + axes = chart.axes, + series = chart.series, + container = chart.container; + + var i, + parentNode = container && container.parentNode; + + // If the chart is destroyed already, do nothing. + // This will happen if if a script invokes chart.destroy and + // then it will be called again on win.unload + if (chart === null) { + return; + } + + // fire the chart.destoy event + fireEvent(chart, 'destroy'); + + // remove events + removeEvent(chart); + + // ==== Destroy collections: + // Destroy axes + i = axes.length; + while (i--) { + axes[i] = axes[i].destroy(); + } + + // Destroy each series + i = series.length; + while (i--) { + series[i] = series[i].destroy(); + } + + // ==== Destroy chart properties: + each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage', 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'tracker', 'scroller', 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) { + var prop = chart[name]; + + if (prop) { + chart[name] = prop.destroy(); + } + }); + + // remove container and all SVG + if (container) { // can break in IE when destroyed before finished loading + container.innerHTML = ''; + removeEvent(container); + if (parentNode) { + discardElement(container); + } + + // IE6 leak + container = null; + } + + // clean it all up + for (i in chart) { + delete chart[i]; + } + + chart.options = null; + chart = null; + }, + + /** + * Prepare for first rendering after all data are loaded + */ + firstRender: function () { + var chart = this, + options = chart.options, + callback = chart.callback; + + // VML namespaces can't be added until after complete. Listening + // for Perini's doScroll hack is not enough. + var ONREADYSTATECHANGE = 'onreadystatechange', + COMPLETE = 'complete'; + // Note: in spite of JSLint's complaints, win == win.top is required + /*jslint eqeq: true*/ + if ((!hasSVG && (win == win.top && doc.readyState !== COMPLETE)) || (useCanVG && !win.canvg)) { + /*jslint eqeq: false*/ + if (useCanVG) { + // Delay rendering until canvg library is downloaded and ready + CanVGController.push(function () { chart.firstRender(); }, options.global.canvasToolsURL); + } else { + doc.attachEvent(ONREADYSTATECHANGE, function () { + doc.detachEvent(ONREADYSTATECHANGE, chart.firstRender); + if (doc.readyState === COMPLETE) { + chart.firstRender(); + } + }); + } + return; + } + + // create the container + chart.getContainer(); + + // Run an early event after the container and renderer are established + fireEvent(chart, 'init'); + + // Initialize range selector for stock charts + if (Highcharts.RangeSelector && options.rangeSelector.enabled) { + chart.rangeSelector = new Highcharts.RangeSelector(chart); + } + + chart.resetMargins(); + chart.setChartSize(); + + // Set the common chart properties (mainly invert) from the given series + chart.propFromSeries(); + + // get axes + chart.getAxes(); + + // Initialize the series + each(options.series || [], function (serieOptions) { + chart.initSeries(serieOptions); + }); + + // Run an event where series and axes can be added + //fireEvent(chart, 'beforeRender'); + + // Initialize scroller for stock charts + if (Highcharts.Scroller && (options.navigator.enabled || options.scrollbar.enabled)) { + chart.scroller = new Highcharts.Scroller(chart); + } + + // depends on inverted and on margins being set + chart.tracker = new MouseTracker(chart, options); + + chart.render(); + + // add canvas + chart.renderer.draw(); + // run callbacks + if (callback) { + callback.apply(chart, [chart]); + } + each(chart.callbacks, function (fn) { + fn.apply(chart, [chart]); + }); + + + // If the chart was rendered outside the top container, put it back in + chart.cloneRenderTo(true); + + fireEvent(chart, 'load'); + + }, + + init: function (chartEvents) { + var chart = this, + optionsChart = chart.options.chart, + eventType; + + // Run chart + + // Set up auto resize + if (optionsChart.reflow !== false) { + addEvent(chart, 'load', chart.initReflow); + } + + // Chart event handlers + if (chartEvents) { + for (eventType in chartEvents) { + addEvent(chart, eventType, chartEvents[eventType]); + } + } + + chart.xAxis = []; + chart.yAxis = []; + + // Expose methods and variables + chart.animation = useCanVG ? false : pick(optionsChart.animation, true); + chart.setSize = chart.resize; + chart.pointCount = 0; + chart.counters = new ChartCounters(); + /* + if ($) $(function () { + $container = $('#container'); + var origChartWidth, + origChartHeight; + if ($container) { + $('<button>+</button>') + .insertBefore($container) + .click(function () { + if (origChartWidth === UNDEFINED) { + origChartWidth = chartWidth; + origChartHeight = chartHeight; + } + chart.resize(chartWidth *= 1.1, chartHeight *= 1.1); + }); + $('<button>-</button>') + .insertBefore($container) + .click(function () { + if (origChartWidth === UNDEFINED) { + origChartWidth = chartWidth; + origChartHeight = chartHeight; + } + chart.resize(chartWidth *= 0.9, chartHeight *= 0.9); + }); + $('<button>1:1</button>') + .insertBefore($container) + .click(function () { + if (origChartWidth === UNDEFINED) { + origChartWidth = chartWidth; + origChartHeight = chartHeight; + } + chart.resize(origChartWidth, origChartHeight); + }); + } + }) + */ + + chart.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, x) { + var point = this, + counters = series.chart.counters, + defaultColors; + point.series = series; + point.applyOptions(options, x); + point.pointAttr = {}; + + if (series.options.colorByPoint) { + defaultColors = series.chart.options.colors; + if (!point.options) { + point.options = {}; + } + point.color = point.options.color = point.color || defaultColors[counters.color++]; + + // loop back to zero + counters.wrapColor(defaultColors.length); + } + + 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, x) { + var point = this, + series = point.series, + optionsType = typeof options; + + point.config = options; + + // onedimensional array input + if (optionsType === 'number' || options === null) { + point.y = options; + } else if (typeof options[0] === 'number') { // two-dimentional array + point.x = options[0]; + point.y = options[1]; + } else if (optionsType === 'object' && typeof options.length !== 'number') { // object input + // copy options directly to point + extend(point, options); + point.options = options; + + // This is the fastest way to detect if there are individual point dataLabels that need + // to be considered in drawDataLabels. These can only occur in object configs. + if (options.dataLabels) { + series._hasPointLabels = true; + } + } else if (typeof options[0] === 'string') { // categorized data with name in first position + point.name = 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 + */ + // todo: skip this? It is only used in applyOptions, in translate it should not be used + if (point.x === UNDEFINED) { + point.x = x === UNDEFINED ? series.autoIncrement() : x; + } + + + + }, + + /** + * Destroy a point to clear memory. Its reference still stays in series.data. + */ + destroy: function () { + var point = this, + series = point.series, + chart = series.chart, + hoverPoints = chart.hoverPoints, + prop; + + chart.pointCount--; + + if (hoverPoints) { + point.setState(); + erase(hoverPoints, point); + if (!hoverPoints.length) { + chart.hoverPoints = null; + } + + } + if (point === chart.hoverPoint) { + point.onMouseOut(); + } + + // remove all events + if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive + removeEvent(point); + point.destroyElements(); + } + + if (point.legendItem) { // pies have legend items + chart.legend.destroyItem(point); + } + + for (prop in point) { + point[prop] = null; + } + + + }, + + /** + * Destroy SVG elements associated with the point + */ + destroyElements: function () { + var point = this, + props = ['graphic', 'tracker', 'dataLabel', 'group', 'connector', 'shadowGroup'], + prop, + i = 6; + while (i--) { + prop = props[i]; + if (point[prop]) { + point[prop] = point[prop].destroy(); + } + } + }, + + /** + * Return the configuration hash needed for the data label and tooltip formatters + */ + getLabelConfig: function () { + var point = this; + return { + x: point.category, + y: point.y, + key: point.name || point.category, + series: point.series, + point: point, + percentage: point.percentage, + total: point.total || point.stackTotal + }; + }, + + /** + * 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; + + selected = pick(selected, !point.selected); + + // fire the event with the defalut handler + point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () { + point.selected = selected; + 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, + series = point.series, + chart = 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 || series.noSharedTooltip)) { + 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 + * + * @return {String} A string to be concatenated in to the common tooltip text + */ + tooltipFormatter: function (pointFormat) { + var point = this, + series = point.series, + seriesTooltipOptions = series.tooltipOptions, + match = pointFormat.match(/\{(series|point)\.[a-zA-Z]+\}/g), + splitter = /[{\.}]/, + obj, + key, + replacement, + repOptionKey, + parts, + prop, + i, + cfg = { // docs: percentageDecimals, percentagePrefix, percentageSuffix, totalDecimals, totalPrefix, totalSuffix + y: 0, // 0: use 'value' for repOptionKey + open: 0, + high: 0, + low: 0, + close: 0, + percentage: 1, // 1: use the self name for repOptionKey + total: 1 + }; + + // Backwards compatibility to y naming in early Highstock + seriesTooltipOptions.valuePrefix = seriesTooltipOptions.valuePrefix || seriesTooltipOptions.yPrefix; + seriesTooltipOptions.valueDecimals = seriesTooltipOptions.valueDecimals || seriesTooltipOptions.yDecimals; + seriesTooltipOptions.valueSuffix = seriesTooltipOptions.valueSuffix || seriesTooltipOptions.ySuffix; + + // loop over the variables defined on the form {series.name}, {point.y} etc + for (i in match) { + key = match[i]; + if (isString(key) && key !== pointFormat) { // IE matches more than just the variables + + // Split it further into parts + parts = (' ' + key).split(splitter); // add empty string because IE and the rest handles it differently + obj = { 'point': point, 'series': series }[parts[1]]; + prop = parts[2]; + + // Add some preformatting + if (obj === point && cfg.hasOwnProperty(prop)) { + repOptionKey = cfg[prop] ? prop : 'value'; + replacement = (seriesTooltipOptions[repOptionKey + 'Prefix'] || '') + + numberFormat(point[prop], pick(seriesTooltipOptions[repOptionKey + 'Decimals'], -1)) + + (seriesTooltipOptions[repOptionKey + 'Suffix'] || ''); + + // Automatic replacement + } else { + replacement = obj[prop]; + } + + pointFormat = pointFormat.replace(key, replacement); + } + } + + return pointFormat; + }, + + /** + * 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, + graphic = point.graphic, + i, + data = series.data, + dataLength = data.length, + 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); + + // update visuals + if (isObject(options)) { + series.getAttribs(); + if (graphic) { + graphic.attr(point.pointAttr[series.state]); + } + } + + // record changes in the parallel arrays + for (i = 0; i < dataLength; i++) { + if (data[i] === point) { + series.xData[i] = point.x; + series.yData[i] = point.y; + series.options.data[i] = options; + break; + } + } + + // redraw + series.isDirty = true; + series.isDirtyData = 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, + i, + data = series.data, + dataLength = data.length; + + setAnimation(animation, chart); + redraw = pick(redraw, true); + + // fire the event with a default handler of removing the point + point.firePointEvent('remove', null, function () { + + //erase(series.data, point); + + for (i = 0; i < dataLength; i++) { + if (data[i] === point) { + + // splice all the parallel arrays + data.splice(i, 1); + series.options.data.splice(i, 1); + series.xData.splice(i, 1); + series.yData.splice(i, 1); + break; + } + } + + point.destroy(); + + + // redraw + series.isDirty = true; + series.isDirtyData = 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, + plotX = point.plotX, + plotY = point.plotY, + 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, + radius, + pointAttr = point.pointAttr; + + 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) { + radius = markerOptions && point.graphic.symbolName && pointAttr[state].r; + point.graphic.attr(merge( + pointAttr[state], + radius ? { // new symbol attributes (#507, #612) + x: plotX - radius, + y: plotY - radius, + width: 2 * radius, + height: 2 * radius + } : {} + )); + } else { + // if a graphic is not applied to each point in the normal state, create a shared + // graphic for the hover state + if (state && markerStateOptions) { + if (!stateMarkerGraphic) { + radius = markerStateOptions.radius; + series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol( + series.symbol, + -radius, + -radius, + 2 * radius, + 2 * radius + ) + .attr(pointAttr[state]) + .add(series.group); + } + + stateMarkerGraphic.translate( + plotX, + plotY + ); + } + + if (stateMarkerGraphic) { + stateMarkerGraphic[state ? 'show' : 'hide'](); + } + } + + point.state = state; + } +}; + +/** + * @classDescription The base function which all other series types inherit from. The data in the series is stored + * in various arrays. + * + * - First, series.options.data contains all the original config options for + * each point whether added by options or methods like series.addPoint. + * - Next, series.data contains those values converted to points, but in case the series data length + * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It + * only contains the points that have been created on demand. + * - Then there's series.points that contains all currently visible point objects. In case of cropping, + * the cropped-away points are not part of this array. The series.points array starts at series.cropStart + * compared to series.data and series.options.data. If however the series data is grouped, these can't + * be correlated one to one. + * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points. + * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points. + * + * @param {Object} chart + * @param {Object} options + */ +var Series = function () {}; + +Series.prototype = { + + isCartesian: true, + type: 'line', + pointClass: Point, + sorted: true, // requires the data to be sorted + 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; + series.options = options = series.setOptions(options); // merge with plotOptions + + // bind the axes + series.bindAxes(); + + // set some variables + extend(series, { + index: index, + name: options.name || 'Series ' + (index + 1), + state: NORMAL_STATE, + pointAttr: {}, + visible: options.visible !== false, // true by default + selected: options.selected === true // false by default + }); + + // special + if (useCanVG) { + options.animation = false; + } + + // 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); + + // Mark cartesian + if (series.isCartesian) { + chart.hasCartesianSeries = true; + } + + // Register it in the chart + chart.series.push(series); + }, + + /** + * Set the xAxis and yAxis properties of cartesian series, and register the series + * in the axis.series array + */ + bindAxes: function () { + var series = this, + seriesOptions = series.options, + chart = series.chart, + axisOptions; + + if (series.isCartesian) { + + each(['xAxis', 'yAxis'], function (AXIS) { // repeat for xAxis and yAxis + + each(chart[AXIS], function (axis) { // loop through the chart's axis objects + + axisOptions = axis.options; + + // apply if the series xAxis or yAxis option mathches the number of the + // axis, or if undefined, use the first axis + if ((seriesOptions[AXIS] === axisOptions.index) || + (seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) { + + // register this series in the axis.series lookup + axis.series.push(series); + + // set this series.xAxis or series.yAxis reference + series[AXIS] = axis; + + // mark dirty for redraw + axis.isDirty = true; + } + }); + + }); + } + }, + + + /** + * 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; + }, + + /** + * Divide the series data into segments divided by null values. + */ + getSegments: function () { + var series = this, + lastNull = -1, + segments = [], + i, + points = series.points, + pointsLength = points.length; + + if (pointsLength) { // no action required for [] + + // if connect nulls, just remove null points + if (series.options.connectNulls) { + i = pointsLength; + while (i--) { + if (points[i].y === null) { + points.splice(i, 1); + } + } + if (points.length) { + segments = [points]; + } + + // else, split on null points + } else { + each(points, function (point, i) { + if (point.y === null) { + if (i > lastNull + 1) { + segments.push(points.slice(lastNull + 1, i)); + } + lastNull = i; + } else if (i === pointsLength - 1) { // last value + segments.push(points.slice(lastNull + 1, i + 1)); + } + }); + } + } + + // register it + series.segments = segments; + }, + /** + * Set the series options by merging from the options tree + * @param {Object} itemOptions + */ + setOptions: function (itemOptions) { + var series = this, + chart = series.chart, + chartOptions = chart.options, + plotOptions = chartOptions.plotOptions, + data = itemOptions.data, + options; + + itemOptions.data = null; // remove from merge to prevent looping over the data set + + options = merge( + plotOptions[this.type], + plotOptions.series, + itemOptions + ); + + // Re-insert the data array to the options and the original config (#717) + options.data = itemOptions.data = data; + + // the tooltip options are merged between global and series specific options + series.tooltipOptions = merge(chartOptions.tooltip, options.tooltip); + + return options; + + }, + /** + * Get the series' color + */ + getColor: function () { + var options = this.options, + defaultColors = this.chart.options.colors, + counters = this.chart.counters; + this.color = options.color || + (!options.colorByPoint && defaultColors[counters.color++]) || 'gray'; + counters.wrapColor(defaultColors.length); + }, + /** + * Get the series' symbol + */ + getSymbol: function () { + var series = this, + seriesMarkerOption = series.options.marker, + chart = series.chart, + defaultSymbols = chart.options.symbols, + counters = chart.counters; + series.symbol = seriesMarkerOption.symbol || defaultSymbols[counters.symbol++]; + + // don't substract radius in image symbols (#604) + if (/^url/.test(series.symbol)) { + seriesMarkerOption.radius = 0; + } + counters.wrapSymbol(defaultSymbols.length); + }, + + /** + * Get the series' symbol in the legend. This method should be overridable to create custom + * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols. + * + * @param {Object} legend The legend object + */ + drawLegendSymbol: function (legend) { + + var options = this.options, + markerOptions = options.marker, + radius, + legendOptions = legend.options, + legendSymbol, + symbolWidth = legendOptions.symbolWidth, + renderer = this.chart.renderer, + legendItemGroup = this.legendGroup, + baseline = legend.baseline, + attr; + + // Draw the line + if (options.lineWidth) { + attr = { + 'stroke-width': options.lineWidth + }; + if (options.dashStyle) { + attr.dashstyle = options.dashStyle; + } + this.legendLine = renderer.path([ + M, + 0, + baseline - 4, + L, + symbolWidth, + baseline - 4 + ]) + .attr(attr) + .add(legendItemGroup); + } + + // Draw the marker + if (markerOptions && markerOptions.enabled) { + radius = markerOptions.radius; + this.legendSymbol = legendSymbol = renderer.symbol( + this.symbol, + (symbolWidth / 2) - radius, + baseline - 4 - radius, + 2 * radius, + 2 * radius + ) + .attr(this.pointAttr[NORMAL_STATE]) + .add(legendItemGroup); + } + }, + + /** + * 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, + xData = series.xData, + yData = series.yData, + currentShift = (graph && graph.shift) || 0, + dataOptions = series.options.data, + point; + //point = (new series.pointClass()).init(series, options); + + setAnimation(animation, chart); + + // Make graph animate sideways + if (graph && shift) { + graph.shift = currentShift + 1; + } + if (area) { + if (shift) { // #780 + area.shift = currentShift + 1; + } + area.isArea = true; // needed in animation, both with and without shift + } + + // Optional redraw, defaults to true + redraw = pick(redraw, true); + + // Get options and push the point to xData, yData and series.options. In series.generatePoints + // the Point instance will be created on demand and pushed to the series.data array. + point = { series: series }; + series.pointClass.prototype.applyOptions.apply(point, [options]); + xData.push(point.x); + yData.push(series.valueCount === 4 ? [point.open, point.high, point.low, point.close] : point.y); + dataOptions.push(options); + + + // Shift the first point off the parallel arrays + // todo: consider series.removePoint(i) method + if (shift) { + if (data[0] && data[0].remove) { + data[0].remove(false); + } else { + data.shift(); + xData.shift(); + yData.shift(); + dataOptions.shift(); + } + } + series.getAttribs(); + + // redraw + series.isDirty = true; + series.isDirtyData = 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.points, + options = series.options, + initialColor = series.initialColor, + chart = series.chart, + firstPoint = null, + xAxis = series.xAxis, + i, + pointProto = series.pointClass.prototype; + + // reset properties + series.xIncrement = null; + series.pointRange = (xAxis && xAxis.categories && 1) || options.pointRange; + + if (defined(initialColor)) { // reset colors for pie + chart.counters.color = initialColor; + } + + // parallel arrays + var xData = [], + yData = [], + dataLength = data ? data.length : [], + turboThreshold = options.turboThreshold || 1000, + pt, + valueCount = series.valueCount; + + // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The + // first value is tested, and we assume that all the rest are defined the same + // way. Although the 'for' loops are similar, they are repeated inside each + // if-else conditional for max performance. + if (dataLength > turboThreshold) { + + // find the first non-null point + i = 0; + while (firstPoint === null && i < dataLength) { + firstPoint = data[i]; + i++; + } + + + if (isNumber(firstPoint)) { // assume all points are numbers + var x = pick(options.pointStart, 0), + pointInterval = pick(options.pointInterval, 1); + + for (i = 0; i < dataLength; i++) { + xData[i] = x; + yData[i] = data[i]; + x += pointInterval; + } + series.xIncrement = x; + } else if (isArray(firstPoint)) { // assume all points are arrays + if (valueCount) { // [x, low, high] or [x, o, h, l, c] + for (i = 0; i < dataLength; i++) { + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt.slice(1, valueCount + 1); + } + } else { // [x, y] + for (i = 0; i < dataLength; i++) { + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt[1]; + } + } + } /* else { + error(12); // Highcharts expects configs to be numbers or arrays in turbo mode + }*/ + } else { + for (i = 0; i < dataLength; i++) { + pt = { series: series }; + pointProto.applyOptions.apply(pt, [data[i]]); + xData[i] = pt.x; + yData[i] = pointProto.toYData ? pointProto.toYData.apply(pt) : pt.y; + } + } + + series.data = []; + series.options.data = data; + series.xData = xData; + series.yData = yData; + + // destroy old points + i = (oldData && oldData.length) || 0; + while (i--) { + if (oldData[i] && oldData[i].destroy) { + oldData[i].destroy(); + } + } + + // reset minRange (#878) + if (xAxis) { + xAxis.minRange = xAxis.userMinRange; + } + + // redraw + series.isDirty = series.isDirtyData = 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; + }, + + /** + * Process the data by cropping away unused data points if the series is longer + * than the crop threshold. This saves computing time for lage series. + */ + processData: function (force) { + var series = this, + processedXData = series.xData, // copied during slice operation below + processedYData = series.yData, + dataLength = processedXData.length, + cropStart = 0, + cropEnd = dataLength, + cropped, + distance, + closestPointRange, + xAxis = series.xAxis, + i, // loop variable + options = series.options, + cropThreshold = options.cropThreshold, + isCartesian = series.isCartesian; + + // If the series data or axes haven't changed, don't go through this. Return false to pass + // the message on to override methods like in data grouping. + if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) { + return false; + } + + // optionally filter out points outside the plot area + if (isCartesian && series.sorted && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) { + var extremes = xAxis.getExtremes(), + min = extremes.min, + max = extremes.max; + + // it's outside current extremes + if (processedXData[dataLength - 1] < min || processedXData[0] > max) { + processedXData = []; + processedYData = []; + + // only crop if it's actually spilling out + } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) { + + // iterate up to find slice start + for (i = 0; i < dataLength; i++) { + if (processedXData[i] >= min) { + cropStart = mathMax(0, i - 1); + break; + } + } + // proceed to find slice end + for (; i < dataLength; i++) { + if (processedXData[i] > max) { + cropEnd = i + 1; + break; + } + + } + processedXData = processedXData.slice(cropStart, cropEnd); + processedYData = processedYData.slice(cropStart, cropEnd); + cropped = true; + } + } + + + // Find the closest distance between processed points + for (i = processedXData.length - 1; i > 0; i--) { + distance = processedXData[i] - processedXData[i - 1]; + if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) { + closestPointRange = distance; + } + } + + // Record the properties + series.cropped = cropped; // undefined or true + series.cropStart = cropStart; + series.processedXData = processedXData; + series.processedYData = processedYData; + + if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC + series.pointRange = closestPointRange || 1; + } + series.closestPointRange = closestPointRange; + + }, + + /** + * Generate the data point after the data has been processed by cropping away + * unused points and optionally grouped in Highcharts Stock. + */ + generatePoints: function () { + var series = this, + options = series.options, + dataOptions = options.data, + data = series.data, + dataLength, + processedXData = series.processedXData, + processedYData = series.processedYData, + pointClass = series.pointClass, + processedDataLength = processedXData.length, + cropStart = series.cropStart || 0, + cursor, + hasGroupedData = series.hasGroupedData, + point, + points = [], + i; + + if (!data && !hasGroupedData) { + var arr = []; + arr.length = dataOptions.length; + data = series.data = arr; + } + + for (i = 0; i < processedDataLength; i++) { + cursor = cropStart + i; + if (!hasGroupedData) { + if (data[cursor]) { + point = data[cursor]; + } else if (dataOptions[cursor] !== UNDEFINED) { // #970 + data[cursor] = point = (new pointClass()).init(series, dataOptions[cursor], processedXData[i]); + } + points[i] = point; + } else { + // splat the y data in case of ohlc data array + points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i]))); + } + } + + // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when + // swithching view from non-grouped data to grouped data (#637) + if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) { + for (i = 0; i < dataLength; i++) { + if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points + i += processedDataLength; + } + if (data[i]) { + data[i].destroyElements(); + data[i].plotX = UNDEFINED; // #1003 + } + } + } + + series.data = data; + series.points = points; + }, + + /** + * Translate data points from raw data values to chart specific positioning data + * needed later in drawPoints, drawGraph and drawTracker. + */ + translate: function () { + if (!this.processedXData) { // hidden series + this.processData(); + } + this.generatePoints(); + var series = this, + chart = series.chart, + options = series.options, + stacking = options.stacking, + xAxis = series.xAxis, + categories = xAxis.categories, + yAxis = series.yAxis, + points = series.points, + dataLength = points.length, + hasModifyValue = !!series.modifyValue, + isLastSeries, + allStackSeries = yAxis.series, + i = allStackSeries.length; + + // Is it the last visible series? + while (i--) { + if (allStackSeries[i].visible) { + if (i === series.index) { + isLastSeries = true; + } + break; + } + } + + // Translate each point + for (i = 0; i < dataLength; i++) { + var point = points[i], + xValue = point.x, + yValue = point.y, + yBottom = point.low, + stack = yAxis.stacks[(yValue < options.threshold ? '-' : '') + series.stackKey], + pointStack, + pointStackTotal; + + // get the plotX translation + //point.plotX = mathRound(xAxis.translate(xValue, 0, 0, 0, 1) * 10) / 10; // Math.round fixes #591 + point.plotX = xAxis.translate(xValue, 0, 0, 0, 1); // Math.round fixes #591 + + // 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 (isLastSeries) { + yBottom = options.threshold; + } + + 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; + point.stackY = yValue; + } + + // Set translated yBottom or remove it + point.yBottom = defined(yBottom) ? + yAxis.translate(yBottom, 0, 1, 0, 1) : + null; + + // general hook, used for Highstock compare mode + if (hasModifyValue) { + yValue = series.modifyValue(yValue, point); + } + + // Set the the plotY value, reset it for redraws + point.plotY = (typeof yValue === 'number') ? + mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10 : // Math.round fixes #591 + UNDEFINED; + + // 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; + + + } + + // now that we have the cropped data, build the segments + series.getSegments(); + }, + /** + * Memoize tooltip texts and positions + */ + setTooltipPoints: function (renew) { + var series = this, + chart = series.chart, + points = [], + pointsLength, + plotSize = chart.plotSizeX, + low, + high, + xAxis = series.xAxis, + point, + i, + tooltipPoints = []; // a lookup array for each pixel in the x dimension + + // don't waste resources if tracker is disabled + if (series.options.enableMouseTracking === false) { + return; + } + + // renew + if (renew) { + series.tooltipPoints = null; + } + + // concat segments to overcome null values + each(series.segments || series.points, function (segment) { + points = points.concat(segment); + }); + + // loop the concatenated points and apply each point to all the closest + // pixel positions + if (xAxis && xAxis.reversed) { + points = points.reverse(); + } + + // Assign each pixel position to the nearest point + pointsLength = points.length; + for (i = 0; i < pointsLength; i++) { + point = points[i]; + low = points[i - 1] ? points[i - 1]._high + 1 : 0; + point._high = high = points[i + 1] ? + mathMax(0, mathFloor((point.plotX + (points[i + 1] ? points[i + 1].plotX : plotSize)) / 2)) : + plotSize; + + while (low >= 0 && low <= high) { + tooltipPoints[low++] = point; + } + } + series.tooltipPoints = tooltipPoints; + }, + + /** + * Format the header of the tooltip + */ + tooltipHeaderFormatter: function (key) { + var series = this, + tooltipOptions = series.tooltipOptions, + xDateFormat = tooltipOptions.xDateFormat, + xAxis = series.xAxis, + isDateTime = xAxis && xAxis.options.type === 'datetime', + n; + + // Guess the best date format based on the closest point distance (#568) + if (isDateTime && !xDateFormat) { + for (n in timeUnits) { + if (timeUnits[n] >= xAxis.closestPointRange) { + xDateFormat = tooltipOptions.dateTimeLabelFormats[n]; + break; + } + } + } + + return tooltipOptions.headerFormat + .replace('{point.key}', isDateTime ? dateFormat(xDateFormat, key) : key) + .replace('{series.name}', series.name) + .replace('{series.color}', series.color); + }, + + /** + * 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'); + } + + // 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.shared) { + 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, + points = series.points, + chart = series.chart, + plotX, + plotY, + i, + point, + radius, + symbol, + isImage, + graphic; + + if (series.options.marker.enabled) { + i = points.length; + while (i--) { + point = points[i]; + plotX = point.plotX; + plotY = point.plotY; + graphic = point.graphic; + + // only draw the point if y is defined + if (plotY !== UNDEFINED && !isNaN(plotY)) { + + // shortcuts + pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]; + radius = pointAttr.r; + symbol = pick(point.marker && point.marker.symbol, series.symbol); + isImage = symbol.indexOf('url') === 0; + + if (graphic) { // update + graphic.animate(extend({ + x: plotX - radius, + y: plotY - radius + }, graphic.symbolName ? { // don't apply to image symbols #507 + width: 2 * radius, + height: 2 * radius + } : {})); + } else if (radius > 0 || isImage) { + point.graphic = chart.renderer.symbol( + symbol, + plotX - radius, + plotY - radius, + 2 * radius, + 2 * 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 + }, + points = series.points || [], // #927 + i, + point, + seriesPointAttr = [], + pointAttr, + pointAttrToOptions = series.pointAttrToOptions, + hasPointSpecificOptions, + key; + + // 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 = points.length; + while (i--) { + point = points[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 (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, + seriesClipRect = series.clipRect, + issue134 = /AppleWebKit\/533/.test(userAgent), + destroy, + i, + data = series.data || [], + point, + prop, + axis; + + // add event hook + fireEvent(series, 'destroy'); + + // remove all events + removeEvent(series); + + // erase from axes + each(['xAxis', 'yAxis'], function (AXIS) { + axis = series[AXIS]; + if (axis) { + erase(axis.series, series); + axis.isDirty = true; + } + }); + + // remove legend items + if (series.legendItem) { + series.chart.legend.destroyItem(series); + } + + // destroy all points with their elements + i = data.length; + while (i--) { + point = data[i]; + if (point && point.destroy) { + point.destroy(); + } + } + series.points = null; + + // If this series clipRect is not the global one (which is removed on chart.destroy) we + // destroy it here. + if (seriesClipRect && seriesClipRect !== chart.clipRect) { + series.clipRect = seriesClipRect.destroy(); + } + + // destroy all SVGElements associated to the series + each(['area', 'graph', 'dataLabelsGroup', 'group', 'tracker', 'trackerGroup'], 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 () { + + var series = this, + seriesOptions = series.options, + options = seriesOptions.dataLabels; + + if (options.enabled || series._hasPointLabels) { + var x, + y, + points = series.points, + pointOptions, + generalOptions, + str, + dataLabelsGroup = series.dataLabelsGroup, + chart = series.chart, + xAxis = series.xAxis, + groupLeft = xAxis ? xAxis.left : chart.plotLeft, + yAxis = series.yAxis, + groupTop = yAxis ? yAxis.top : chart.plotTop, + renderer = chart.renderer, + inverted = chart.inverted, + seriesType = series.type, + stacking = seriesOptions.stacking, + isBarLike = seriesType === 'column' || seriesType === 'bar', + vAlignIsNull = options.verticalAlign === null, + yIsNull = options.y === null, + fontMetrics = renderer.fontMetrics(options.style.fontSize), // height and baseline + fontLineHeight = fontMetrics.h, + fontBaseline = fontMetrics.b, + dataLabel, + enabled; + + if (isBarLike) { + var defaultYs = { + top: fontBaseline, + middle: fontBaseline - fontLineHeight / 2, + bottom: -fontLineHeight + fontBaseline + }; + if (stacking) { + // In stacked series the default label placement is inside the bars + if (vAlignIsNull) { + options = merge(options, {verticalAlign: 'middle'}); + } + + // If no y delta is specified, try to create a good default + if (yIsNull) { + options = merge(options, { y: defaultYs[options.verticalAlign]}); + } + } else { + // In non stacked series the default label placement is on top of the bars + if (vAlignIsNull) { + options = merge(options, {verticalAlign: 'top'}); + + // If no y delta is specified, try to create a good default (like default bar) + } else if (yIsNull) { + options = merge(options, { y: defaultYs[options.verticalAlign]}); + } + + } + } + + + // create a separate group for the data labels to avoid rotation + if (!dataLabelsGroup) { + dataLabelsGroup = series.dataLabelsGroup = + renderer.g('data-labels') + .attr({ + visibility: series.visible ? VISIBLE : HIDDEN, + zIndex: 6 + }) + .translate(groupLeft, groupTop) + .add(); + } else { + dataLabelsGroup.translate(groupLeft, groupTop); + } + + // make the labels for each point + generalOptions = options; + each(points, function (point) { + + dataLabel = point.dataLabel; + + // Merge in individual options from point + options = generalOptions; // reset changes from previous points + pointOptions = point.options; + if (pointOptions && pointOptions.dataLabels) { + options = merge(options, pointOptions.dataLabels); + } + enabled = options.enabled; + + // Get the positions + if (enabled) { + var plotX = (point.barX && point.barX + point.barW / 2) || pick(point.plotX, -999), + plotY = pick(point.plotY, -999), + + // if options.y is null, which happens by default on column charts, set the position + // above or below the column depending on the threshold + individualYDelta = options.y === null ? + (point.y >= seriesOptions.threshold ? + -fontLineHeight + fontBaseline : // below the threshold + fontBaseline) : // above the threshold + options.y; + + x = (inverted ? chart.plotWidth - plotY : plotX) + options.x; + y = mathRound((inverted ? chart.plotHeight - plotX : plotY) + individualYDelta); + + } + + // If the point is outside the plot area, destroy it. #678, #820 + if (dataLabel && series.isCartesian && (!chart.isInsidePlot(x, y) || !enabled)) { + point.dataLabel = dataLabel.destroy(); + + // Individual labels are disabled if the are explicitly disabled + // in the point options, or if they fall outside the plot area. + } else if (enabled) { + + var align = options.align, + attr, + name; + + // Get the string + str = options.formatter.call(point.getLabelConfig(), options); + + // in columns, align the string to the column + if (seriesType === 'column') { + x += { left: -1, right: 1 }[align] * point.barW / 2 || 0; + } + + if (!stacking && inverted && point.y < 0) { + align = 'right'; + x -= 10; + } + + // Determine the color + options.style.color = pick(options.color, options.style.color, series.color, 'black'); + + + // update existing label + if (dataLabel) { + // vertically centered + dataLabel + .attr({ + text: str + }).animate({ + x: x, + y: y + }); + // create new label + } else if (defined(str)) { + attr = { + align: align, + fill: options.backgroundColor, + stroke: options.borderColor, + 'stroke-width': options.borderWidth, + r: options.borderRadius || 0, + rotation: options.rotation, + padding: options.padding, + zIndex: 1 + }; + // Remove unused attributes (#947) + for (name in attr) { + if (attr[name] === UNDEFINED) { + delete attr[name]; + } + } + + dataLabel = point.dataLabel = renderer[options.rotation ? 'text' : 'label']( // labels don't support rotation + str, + x, + y, + null, + null, + null, + options.useHTML, + true // baseline for backwards compat + ) + .attr(attr) + .css(options.style) + .add(dataLabelsGroup) + .shadow(options.shadow); + } + + if (isBarLike && seriesOptions.stacking && dataLabel) { + var barX = point.barX, + barY = point.barY, + barW = point.barW, + barH = point.barH; + + dataLabel.align(options, null, + { + x: inverted ? chart.plotWidth - barY - barH : barX, + y: inverted ? chart.plotHeight - barX - barW : barY, + width: inverted ? barH : barW, + height: inverted ? barW : barH + }); + } + + + } + }); + } + }, + + /** + * Return the graph path of a segment + */ + getSegmentPath: function (segment) { + var series = this, + 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 && series.options.step) { + var lastPoint = segment[i - 1]; + segmentPath.push( + point.plotX, + lastPoint.plotY + ); + } + + // normal line to next point + segmentPath.push( + point.plotX, + point.plotY + ); + } + }); + + return segmentPath; + }, + + /** + * Draw the actual graph + */ + drawGraph: function () { + var series = this, + options = series.options, + chart = series.chart, + graph = series.graph, + graphPath = [], + group = series.group, + color = options.lineColor || series.color, + lineWidth = options.lineWidth, + dashStyle = options.dashStyle, + segmentPath, + renderer = chart.renderer, + singlePoints = [], // used in drawTracker + attribs; + + // divide into segments and build graph and area paths + each(series.segments, function (segment) { + + segmentPath = series.getSegmentPath(segment); + + // 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]); + } + }); + + // used in drawTracker: + series.graphPath = graphPath; + series.singlePoints = singlePoints; + + // draw the graph + if (graph) { + stop(graph); // cancel running animations, #459 + 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); + } + } + }, + + /** + * Initialize and perform group inversion on series.group and series.trackerGroup + */ + invertGroups: function () { + var series = this, + group = series.group, + trackerGroup = series.trackerGroup, + chart = series.chart; + + // A fixed size is needed for inversion to work + function setInvert() { + var size = { + width: series.yAxis.len, + height: series.xAxis.len + }; + + // Set the series.group size + group.attr(size).invert(); + + // Set the tracker group size + if (trackerGroup) { + trackerGroup.attr(size).invert(); + } + } + + addEvent(chart, 'resize', setInvert); // do it on resize + addEvent(series, 'destroy', function () { + removeEvent(chart, 'resize', setInvert); + }); + + // Do it now + setInvert(); // do it now + + // On subsequent render and redraw, just do setInvert without setting up events again + series.invertGroups = setInvert; + }, + + /** + * Create the series group + */ + createGroup: function () { + + var chart = this.chart, + group = this.group = chart.renderer.g('series'); + + group.attr({ + visibility: this.visible ? VISIBLE : HIDDEN, + zIndex: this.options.zIndex + }) + .translate(this.xAxis.left, this.yAxis.top) + .add(chart.seriesGroup); + + // Only run this once + this.createGroup = noop; + }, + + /** + * Render the graph and markers + */ + render: function () { + var series = this, + chart = series.chart, + group, + options = series.options, + doClip = options.clip !== false, + 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. + + // Todo: since creating the clip property, the clipRect is created but + // never used when clip is false. A better way would be that the animation + // would run, then the clipRect destroyed. + if (!clipRect) { + clipRect = series.clipRect = !chart.hasRendered && chart.clipRect ? + chart.clipRect : + renderer.clipRect(0, 0, chart.plotSizeX, chart.plotSizeY + 1); + if (!chart.clipRect) { + chart.clipRect = clipRect; + } + } + + + // the group + series.createGroup(); + group = series.group; + + + 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(); + } + + // Handle inverted series and tracker groups + if (chart.inverted) { + series.invertGroups(); + } + + // Do the initial clipping. This must be done after inverting for VML. + if (doClip && !series.hasRendered) { + group.clip(clipRect); + if (series.trackerGroup) { + series.trackerGroup.clip(chart.clipRect); + } + } + + + // 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) { + if (doClip) { + group.clip((series.clipRect = chart.clipRect)); + } + clipRect.destroy(); + } + }, duration); + + series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see + // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see + series.hasRendered = true; + }, + + /** + * Redraw the series after an update in the axes. + */ + redraw: function () { + var series = this, + chart = series.chart, + wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after + group = series.group; + + // reposition on resize + if (group) { + if (chart.inverted) { + group.attr({ + width: chart.plotWidth, + height: chart.plotHeight + }); + } + + group.animate({ + translateX: series.xAxis.left, + translateY: series.yAxis.top + }); + } + + series.translate(); + series.setTooltipPoints(true); + + series.render(); + if (wasDirtyData) { + fireEvent(series, 'updatedData'); + } + }, + + /** + * 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, + points = series.points, + 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 if (points) { + i = points.length; + while (i--) { + point = points[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'); + }, + + /** + * Create a group that holds the tracking object or objects. This allows for + * individual clipping and placement of each series tracker. + */ + drawTrackerGroup: function () { + var trackerGroup = this.trackerGroup, + chart = this.chart; + + if (this.isCartesian) { + + // Generate it on first call + if (!trackerGroup) { + this.trackerGroup = trackerGroup = chart.renderer.g() + .attr({ + zIndex: this.options.zIndex || 1 + }) + .add(chart.trackerGroup); + + } + // Place it on first and subsequent (redraw) calls + trackerGroup.translate(this.xAxis.left, this.yAxis.top); + + } + + return trackerGroup; + }, + + /** + * 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, + trackByArea = options.trackByArea, + trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath), + trackerPathLength = trackerPath.length, + chart = series.chart, + renderer = chart.renderer, + snap = chart.options.tooltip.snap, + tracker = series.tracker, + cursor = options.cursor, + css = cursor && { cursor: cursor }, + singlePoints = series.singlePoints, + trackerGroup = series.drawTrackerGroup(), + singlePoint, + i; + + // Extend end points. A better way would be to use round linecaps, + // but those are not clickable in VML. + if (trackerPathLength && !trackByArea) { + 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 = renderer.path(trackerPath) + .attr({ + isTracker: true, + 'stroke-linejoin': 'bevel', + visibility: series.visible ? VISIBLE : HIDDEN, + stroke: TRACKER_FILL, + fill: trackByArea ? TRACKER_FILL : NONE, + 'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap) + }) + .on(hasTouch ? 'touchstart' : 'mouseover', function () { + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } + }) + .on('mouseout', function () { + if (!options.stickyTracking) { + series.onMouseOut(); + } + }) + .css(css) + .add(trackerGroup); + } + + } + +}; // end Series prototype + + +/** + * LineSeries object + */ +var LineSeries = extendClass(Series); +seriesTypes.line = LineSeries; + +/** + * Set the default options for area + */ +defaultPlotOptions.area = merge(defaultSeriesOptions, { + threshold: 0 + // trackByArea: false, + // lineColor: null, // overrides color, but lets fillColor be unaltered + // fillOpacity: 0.75, + // fillColor: null +}); + +/** + * AreaSeries object + */ +var AreaSeries = extendClass(Series, { + type: 'area', + + /** + * Extend the base Series getSegmentPath method by adding the path for the area. + * This path is pushed to the series.areaPath property. + */ + getSegmentPath: function (segment) { + + var segmentPath = Series.prototype.getSegmentPath.call(this, segment), // call base method + areaSegmentPath = [].concat(segmentPath), // work on a copy for the area path + i, + options = this.options, + segLength = segmentPath.length, + translatedThreshold = this.yAxis.getThreshold(options.threshold); + + if (segLength === 3) { // for animation from 1 to two points + areaSegmentPath.push(L, segmentPath[1], segmentPath[2]); + } + if (options.stacking && this.type !== 'areaspline') { + + // Follow stack back. Todo: implement areaspline. A general solution could be to + // reverse the entire graphPath of the previous series, though may be hard with + // splines and with series with different extremes + for (i = segment.length - 1; i >= 0; i--) { + + // step line? + if (i < segment.length - 1 && options.step) { + areaSegmentPath.push(segment[i + 1].plotX, segment[i].yBottom); + } + + 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 + ); + } + this.areaPath = this.areaPath.concat(areaSegmentPath); + + return segmentPath; + }, + + /** + * Draw the graph and the underlying area. This method calls the Series base + * function and adds the area. The areaPath is calculated in the getSegmentPath + * method called from Series.prototype.drawGraph. + */ + drawGraph: function () { + + // Define or reset areaPath + this.areaPath = []; + + // Call the base method + Series.prototype.drawGraph.apply(this); + + // Define local variables + var areaPath = this.areaPath, + options = this.options, + area = this.area; + + // Create or update the area + if (area) { // update + area.animate({ d: areaPath }); + + } else { // create + this.area = this.chart.renderer.path(areaPath) + .attr({ + fill: pick( + options.fillColor, + Color(this.color).setOpacity(options.fillOpacity || 0.75).get() + ) + }).add(this.group); + } + }, + + /** + * Get the series' symbol in the legend + * + * @param {Object} legend The legend object + * @param {Object} item The series (this) or point + */ + drawLegendSymbol: function (legend, item) { + + item.legendSymbol = this.chart.renderer.rect( + 0, + legend.baseline - 11, + legend.options.symbolWidth, + 12, + 2 + ).attr({ + zIndex: 3 + }).add(item.legendGroup); + + } +}); + +seriesTypes.area = AreaSeries;/** + * Set the default options for spline + */ +defaultPlotOptions.spline = merge(defaultSeriesOptions); + +/** + * 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]; + } else { // curve from last point to this + 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; + +/** + * Set the default options for areaspline + */ +defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); + +/** + * AreaSplineSeries object + */ +var areaProto = AreaSeries.prototype, + AreaSplineSeries = extendClass(SplineSeries, { + type: 'areaspline', + + // Mix in methods from the area series + getSegmentPath: areaProto.getSegmentPath, + drawGraph: areaProto.drawGraph + }); +seriesTypes.areaspline = AreaSplineSeries; + +/** + * Set the default options for column + */ +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, + cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes + pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories + states: { + hover: { + brightness: 0.1, + shadow: false + }, + select: { + color: '#C0C0C0', + borderColor: '#000000', + shadow: false + } + }, + dataLabels: { + y: null, + verticalAlign: null + }, + threshold: 0 +}); + +/** + * ColumnSeries object + */ +var ColumnSeries = extendClass(Series, { + type: 'column', + tooltipOutsidePlot: true, + 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; + + // 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, + options = series.options, + stacking = options.stacking, + borderWidth = options.borderWidth, + columnCount = 0, + xAxis = series.xAxis, + reversedXAxis = xAxis.reversed, + 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 && otherSeries.visible && + series.options.group === otherSeries.options.group) { // used in Stock charts navigator series + if (otherSeries.options.stacking) { + stackKey = otherSeries.stackKey; + if (stackGroups[stackKey] === UNDEFINED) { + stackGroups[stackKey] = columnCount++; + } + columnIndex = stackGroups[stackKey]; + } else { + 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 points = series.points, + categoryWidth = mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || 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 = pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), // exact point width, used in polar charts + barW = mathCeil(mathMax(pointWidth, 1 + 2 * borderWidth)), // rounded and postprocessed for border width + colIndex = (reversedXAxis ? columnCount - + series.columnIndex : series.columnIndex) || 0, + pointXOffset = pointPadding + (groupPadding + colIndex * + pointOffsetWidth - (categoryWidth / 2)) * + (reversedXAxis ? -1 : 1), + threshold = options.threshold, + translatedThreshold = series.yAxis.getThreshold(threshold), + minPointLength = pick(options.minPointLength, 5); + + // record the new values + each(points, function (point) { + var plotY = point.plotY, + yBottom = pick(point.yBottom, translatedThreshold), + barX = point.plotX + pointXOffset, + barY = mathCeil(mathMin(plotY, yBottom)), + barH = mathCeil(mathMax(plotY, yBottom) - barY), + stack = series.yAxis.stacks[(point.y < 0 ? '-' : '') + series.stackKey], + shapeArgs; + + // Record the offset'ed position and width of the bar to be able to align the stacking total correctly + if (stacking && series.visible && stack && stack[point.x]) { + stack[point.x].setOffset(pointXOffset, barW); + } + + // handle options.minPointLength + if (mathAbs(barH) < minPointLength) { + if (minPointLength) { + barH = minPointLength; + barY = + mathAbs(barY - translatedThreshold) > minPointLength ? // stacked + yBottom - minPointLength : // keep position + translatedThreshold - (plotY <= translatedThreshold ? minPointLength : 0); + } + } + + extend(point, { + barX: barX, + barY: barY, + barW: barW, + barH: barH, + pointWidth: pointWidth + }); + + // create shape type and shape args that are reused in drawPoints and drawTracker + point.shapeType = 'rect'; + point.shapeArgs = shapeArgs = chart.renderer.Element.prototype.crisp.call(0, borderWidth, barX, barY, barW, barH); + + if (borderWidth % 2) { // correct for shorting in crisp method, visible in stacked columns with 1px border + shapeArgs.y -= 1; + shapeArgs.height += 1; + } + + // make small columns responsive to mouse + point.trackerArgs = mathAbs(barH) < 3 && merge(point.shapeArgs, { + height: 6, + y: barY - 3 + }); + }); + + }, + + getSymbol: function () { + }, + + /** + * Use a solid rectangle like the area series types + */ + drawLegendSymbol: AreaSeries.prototype.drawLegendSymbol, + + + /** + * 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.points, function (point) { + var plotY = point.plotY; + if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { + graphic = point.graphic; + shapeArgs = point.shapeArgs; + if (graphic) { // update + stop(graphic); + graphic.animate(merge(shapeArgs)); + + } else { + point.graphic = graphic = renderer[point.shapeType](shapeArgs) + .attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]) + .add(series.group) + .shadow(options.shadow, null, options.stacking && !options.borderRadius); + } + + } + }); + }, + /** + * 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(), + options = series.options, + cursor = options.cursor, + css = cursor && { cursor: cursor }, + trackerGroup = series.drawTrackerGroup(), + rel, + plotY, + validPlotY; + + each(series.points, function (point) { + tracker = point.tracker; + shapeArgs = point.trackerArgs || point.shapeArgs; + plotY = point.plotY; + validPlotY = !series.isCartesian || (plotY !== UNDEFINED && !isNaN(plotY)); + delete shapeArgs.strokeWidth; + if (point.y !== null && validPlotY) { + if (tracker) {// update + tracker.attr(shapeArgs); + + } else { + point.tracker = + renderer[point.shapeType](shapeArgs) + .attr({ + isTracker: trackerLabel, + fill: TRACKER_FILL, + visibility: series.visible ? VISIBLE : HIDDEN + }) + .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 (!options.stickyTracking) { + rel = event.relatedTarget || event.toElement; + if (attr(rel, 'isTracker') !== trackerLabel) { + series.onMouseOut(); + } + } + }) + .css(css) + .add(point.group || trackerGroup); // pies have point group - see issue #118 + } + } + }); + }, + + + /** + * 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, + points = series.points, + options = series.options; + + 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(points, function (point) { + var graphic = point.graphic, + shapeArgs = point.shapeArgs, + yAxis = series.yAxis, + threshold = options.threshold; + + if (graphic) { + // start values + graphic.attr({ + height: 0, + y: defined(threshold) ? + yAxis.getThreshold(threshold) : + yAxis.translate(yAxis.getExtremes().min, 0, 1, 0, 1) + }); + + // animate + graphic.animate({ + height: shapeArgs.height, + y: shapeArgs.y + }, 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; +/** + * Set the default options for bar + */ +defaultPlotOptions.bar = merge(defaultPlotOptions.column, { + dataLabels: { + align: 'left', + x: 5, + y: null, + verticalAlign: 'middle' + } +}); +/** + * The Bar series class + */ +var BarSeries = extendClass(ColumnSeries, { + type: 'bar', + inverted: true +}); +seriesTypes.bar = BarSeries; + +/** + * Set the default options for scatter + */ +defaultPlotOptions.scatter = merge(defaultSeriesOptions, { + lineWidth: 0, + states: { + hover: { + lineWidth: 0 + } + }, + tooltip: { + headerFormat: '<span style="font-size: 10px; color:{series.color}">{series.name}</span><br/>', + pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>' + } +}); + +/** + * The scatter series class + */ +var ScatterSeries = extendClass(Series, { + type: 'scatter', + sorted: false, + /** + * 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.points, function (point) { + point.shapeType = 'circle'; + point.shapeArgs = { + x: point.plotX, + y: point.plotY, + r: series.chart.options.tooltip.snap + }; + }); + }, + + /** + * Add tracking event listener to the series group, so the point graphics + * themselves act as trackers + */ + drawTracker: function () { + var series = this, + cursor = series.options.cursor, + css = cursor && { cursor: cursor }, + points = series.points, + i = points.length, + graphic; + + // Set an expando property for the point index, used below + while (i--) { + graphic = points[i].graphic; + if (graphic) { // doesn't exist for null points + graphic.element._i = i; + } + } + + // Add the event listeners, we need to do this only once + if (!series._hasTracking) { + series.group + .attr({ + isTracker: true + }) + .on(hasTouch ? 'touchstart' : 'mouseover', function (e) { + series.onMouseOver(); + if (e.target._i !== UNDEFINED) { // undefined on graph in scatterchart + points[e.target._i].onMouseOver(); + } + }) + .on('mouseout', function () { + if (!series.options.stickyTracking) { + series.onMouseOut(); + } + }) + .css(css); + } else { + series._hasTracking = true; + } + + } +}); +seriesTypes.scatter = ScatterSeries; + +/** + * Set the default options for pie + */ +defaultPlotOptions.pie = merge(defaultSeriesOptions, { + borderColor: '#FFFFFF', + borderWidth: 1, + center: ['50%', '50%'], + colorByPoint: true, // always true for pies + dataLabels: { + // align: null, + // connectorWidth: 1, + // connectorColor: point.color, + // connectorPadding: 5, + distance: 30, + enabled: true, + formatter: function () { + return this.point.name; + }, + // softConnector: true, + 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 + } + } +}); + +/** + * 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, + shadowGroup = point.shadowGroup, + 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 (shadowGroup) { + shadowGroup[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, + translation; + + 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; + + translation = { + translateX: (sliced ? slicedTranslation[0] : chart.plotLeft), + translateY: (sliced ? slicedTranslation[1] : chart.plotTop) + }; + point.group.animate(translation); + if (point.shadowGroup) { + point.shadowGroup.animate(translation); + } + + } +}); + +/** + * The Pie series class + */ +var PieSeries = { + 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 = this.chart.counters.color; + }, + + /** + * Animate the pies in + */ + animate: function () { + var series = this, + points = series.points; + + each(points, 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; + + }, + + /** + * Extend the basic setData method by running processData and generatePoints immediately, + * in order to access the points from the legend. + */ + setData: function (data, redraw) { + Series.prototype.setData.call(this, data, false); + this.processData(); + this.generatePoints(); + if (pick(redraw, true)) { + this.chart.redraw(); + } + }, + + /** + * Get the center of the pie based on the size and center options relative to the + * plot area. Borrowed by the polar and gauge series types. + */ + getCenter: function () { + + var options = this.options, + chart = this.chart, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + positions = options.center.concat([options.size, options.innerSize || 0]), + smallestSize = mathMin(plotWidth, plotHeight), + isPercent; + + return 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 + // i == 4: innerSize, relative to smallestSize + [plotWidth, plotHeight, smallestSize, smallestSize][i] * + pInt(length) / 100 : + length; + }); + }, + + /** + * Do translation for pie slices + */ + translate: function () { + this.generatePoints(); + + 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, + chart = series.chart, + start, + end, + angle, + points = series.points, + circ = 2 * mathPI, + fraction, + 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 + series.center = positions = series.getCenter(); + + // 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)); + }; + + // get the total sum + each(points, function (point) { + total += point.y; + }); + + each(points, 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(); + } + + // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see + 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, + shadow = series.options.shadow, + shadowGroup, + shapeArgs; + + // draw the slices + each(series.points, function (point) { + graphic = point.graphic; + shapeArgs = point.shapeArgs; + group = point.group; + shadowGroup = point.shadowGroup; + + // put the shadow behind all points + if (shadow && !shadowGroup) { + shadowGroup = point.shadowGroup = renderer.g('shadow') + .attr({ zIndex: 4 }) + .add(); + } + + // 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]); + if (shadowGroup) { + shadowGroup.translate(groupTranslation[0], groupTranslation[1]); + } + + // draw the slice + if (graphic) { + graphic.animate(shapeArgs); + } else { + point.graphic = graphic = renderer.arc(shapeArgs) + .setRadialReference(series.center) + .attr(extend( + point.pointAttr[NORMAL_STATE], + { 'stroke-linejoin': 'round' } + )) + .add(point.group) + .shadow(shadow, shadowGroup); + + } + + // 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, + softConnector = pick(options.softConnector, true), + distanceOption = options.distance, + seriesCenter = series.center, + radius = seriesCenter[2] / 2, + centerY = seriesCenter[1], + outside = distanceOption > 0, + dataLabel, + labelPos, + labelHeight, + halves = [// divide the points into right and left halves for anti collision + [], // right + [] // left + ], + x, + y, + visibility, + rankArr, + sort, + i = 2, + j; + + // get out if not enabled + if (!options.enabled) { + return; + } + + // run parent method + Series.prototype.drawDataLabels.apply(series); + + // arrange points for detection collision + each(data, function (point) { + if (point.dataLabel) { // it may have been cancelled in the base method (#407) + halves[ + point.labelPos[7] < mathPI / 2 ? 0 : 1 + ].push(point); + } + }); + halves[1].reverse(); + + // define the sorting algorithm + sort = function (a, b) { + return b.y - a.y; + }; + + // assume equal label heights + labelHeight = halves[0][0] && halves[0][0].dataLabel && (halves[0][0].dataLabel.getBBox().height || 21); // 21 is for #968 + + /* Loop over the points in each half, starting from the top and bottom + * of the pie to detect overlapping labels. + */ + while (i--) { + + var slots = [], + slotsLength, + usedSlots = [], + points = halves[i], + pos, + length = points.length, + slotIndex; + + // Only do anti-collision when we are outside the pie and have connectors (#856) + if (distanceOption > 0) { + + // build the slots + for (pos = centerY - radius - distanceOption; pos <= centerY + radius + distanceOption; pos += labelHeight) { + slots.push(pos); + // visualize the slot + /* + var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0), + slotY = pos + chart.plotTop; + if (!isNaN(slotX)) { + chart.renderer.rect(slotX, slotY - 7, 100, labelHeight) + .attr({ + 'stroke-width': 1, + stroke: 'silver' + }) + .add(); + chart.renderer.text('Slot '+ (slots.length - 1), slotX, slotY + 4) + .attr({ + fill: 'silver' + }).add(); + } + // */ + } + slotsLength = slots.length; + + // if there are more values than available slots, remove lowest values + if (length > slotsLength) { + // create an array for sorting and ranking the points within each quarter + rankArr = [].concat(points); + rankArr.sort(sort); + j = length; + while (j--) { + rankArr[j].rank = j; + } + j = length; + while (j--) { + if (points[j].rank >= slotsLength) { + points.splice(j, 1); + } + } + length = points.length; + } + + // The label goes to the nearest open slot, but not closer to the edge than + // the label's index. + for (j = 0; j < length; j++) { + + point = points[j]; + labelPos = point.labelPos; + + var closest = 9999, + distance, + slotI; + + // find the closest slot index + for (slotI = 0; slotI < slotsLength; slotI++) { + distance = mathAbs(slots[slotI] - labelPos[1]); + if (distance < closest) { + closest = distance; + slotIndex = slotI; + } + } + + // if that slot index is closer to the edges of the slots, move it + // to the closest appropriate slot + if (slotIndex < j && slots[j] !== null) { // cluster at the top + slotIndex = j; + } else if (slotsLength < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom + slotIndex = slotsLength - length + j; + while (slots[slotIndex] === null) { // make sure it is not taken + slotIndex++; + } + } else { + // Slot is taken, find next free slot below. In the next run, the next slice will find the + // slot above these, because it is the closest one + while (slots[slotIndex] === null) { // make sure it is not taken + slotIndex++; + } + } + + usedSlots.push({ i: slotIndex, y: slots[slotIndex] }); + slots[slotIndex] = null; // mark as taken + } + // sort them in order to fill in from the top + usedSlots.sort(sort); + } + + // now the used slots are sorted, fill them up sequentially + for (j = 0; j < length; j++) { + + var slot, naturalY; + + point = points[j]; + labelPos = point.labelPos; + dataLabel = point.dataLabel; + visibility = point.visible === false ? HIDDEN : VISIBLE; + naturalY = labelPos[1]; + + if (distanceOption > 0) { + slot = usedSlots.pop(); + slotIndex = slot.i; + + // if the slot next to currrent slot is free, the y value is allowed + // to fall back to the natural position + y = slot.y; + if ((naturalY > y && slots[slotIndex + 1] !== null) || + (naturalY < y && slots[slotIndex - 1] !== null)) { + y = naturalY; + } + + } else { + y = naturalY; + } + + // get the x - use the natural x position for first and last slot, to prevent the top + // and botton slice connectors from touching each other on either side + x = options.justify ? + seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) : + series.getX(slotIndex === 0 || slotIndex === slots.length - 1 ? naturalY : y, i); + + // 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 = softConnector ? [ + M, + x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label + 'C', + x, y, // first break, next to the label + 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5], + labelPos[2], labelPos[3], // second break + L, + labelPos[4], labelPos[5] // base + ] : [ + M, + x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at 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 || point.color || '#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, + + /** + * Use a simple symbol from column prototype + */ + drawLegendSymbol: AreaSeries.prototype.drawLegendSymbol, + + /** + * Pies don't have point marker symbols + */ + getSymbol: function () {} + +}; +PieSeries = extendClass(Series, PieSeries); +seriesTypes.pie = PieSeries; + + +// global variables +extend(Highcharts, { + + // Constructors + Axis: Axis, + CanVGRenderer: CanVGRenderer, + Chart: Chart, + Color: Color, + Legend: Legend, + Point: Point, + Tick: Tick, + Tooltip: Tooltip, + Renderer: Renderer, + Series: Series, + SVGRenderer: SVGRenderer, + VMLRenderer: VMLRenderer, + + // Various + dateFormat: dateFormat, + pathAnim: pathAnim, + getOptions: getOptions, + hasBidiBug: hasBidiBug, + numberFormat: numberFormat, + seriesTypes: seriesTypes, + setOptions: setOptions, + addEvent: addEvent, + removeEvent: removeEvent, + createElement: createElement, + discardElement: discardElement, + css: css, + each: each, + extend: extend, + map: map, + merge: merge, + pick: pick, + splat: splat, + extendClass: extendClass, + pInt: pInt, + product: 'Highcharts', + version: '2.2.5' +}); +}()); diff --git a/app/controllers/products_controller.rb b/app/controllers/products_controller.rb index c6926b8..b93504b 100644 --- a/app/controllers/products_controller.rb +++ b/app/controllers/products_controller.rb @@ -1,7 +1,8 @@ class ProductsController < ApplicationController def index - @products = Product.includes(:user).all + @products = Product.includes(:user).first(20) + @sum_price = Product.sum(:price).to_f end def create diff --git a/app/views/products/index.html.erb b/app/views/products/index.html.erb index 2b450ba..d6700c0 100644 --- a/app/views/products/index.html.erb +++ b/app/views/products/index.html.erb @@ -1,5 +1,5 @@ <h2> Products in the market </h2> - +<div id="highChartContainer" style="min-width: 400px; height: 400px; margin: 0 auto"></div> <table> <tbody> <tr> @@ -13,16 +13,74 @@ <tr> <td><%= product.id %></td> <td><%= product.name %></td> - <td>$<%= product.price %></td> - <td><%= product.user.name if product.user.present? %></td> - </tr> + <td data-product='<%= product.id %>'>$<%= product.price %></td> + <td><%= product.user.name if product.user.present? %></td> + </tr> + <td><%= link_to "+", product_path(product), + 'data-price' => product.price, + 'data-incr-price' => product.id, + :method => :put %></td> + +</script> <% end %> + </tbody> + </table> +<script type='text/javascript'> + $('[data-incr-price]').on('click', function(e){ + e.preventDefault(); + e.stopPropagation(); - - - - + url = $(this).attr('href'); + price = $(this).attr('data-price'); + newPrice = parseInt(price) + 1; + + $.ajax({ url: url, + type: 'put', + dataType: 'json', + data: { product: {price: newPrice} } + }).success(function(data, textStatus, jqXHR) { + $('[data-product='+ data.id +']').html("$" + data.price); + $('[data-incr-price='+ data.id +']').attr('data-price', data.price); + }); + }) +</script> + <script type='text/javascript'> + console.log('the number of products is ' + '<%= @products.count %>'); +</script> + + +<a href="#" data-hide-element='chart'>Chart</a> +<script type='text/javascript'> + $('[data-hide-element=chart]').on('click', function(){ + $('#highChartContainer').toggle() + }); +</script> +<script type='text/javascript'> + $(document).ready(function(){ + chart = new Highcharts.Chart({ + chart: { + renderTo: 'highChartContainer', + plotBackgroundColor: null, + plotBorderWidth: null, + plotShadow: false + }, + title: { + text: 'Browser market shares at a specific website, 2010' + }, + series: [{ + type: 'pie', + name: 'Product Share', + data: + <%= + @products.map do |product| + [product.name, product.price/@sum_price] +end.to_json.html_safe + %> + }] + }); +}); +</script>