2 // @compilation_level SIMPLE_OPTIMIZATIONS
5 * @license Highstock JS v1.1.4 (2012-02-15)
7 * (c) 2009-2011 Torstein Hønsi
9 * License: www.highcharts.com/license
13 /*global Highcharts, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $ */
16 // encapsulated variables
21 mathRound = math.round,
22 mathFloor = math.floor,
30 deg2rad = mathPI * 2 / 360,
34 userAgent = navigator.userAgent,
35 isIE = /msie/i.test(userAgent) && !win.opera,
36 docMode8 = doc.documentMode === 8,
37 isWebKit = /AppleWebKit/.test(userAgent),
38 isFirefox = /Firefox/.test(userAgent),
39 SVG_NS = 'http://www.w3.org/2000/svg',
40 hasSVG = !!doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect,
41 hasRtlBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4, // issue #38
43 hasTouch = doc.documentElement.ontouchstart !== UNDEFINED,
48 dateFormat, // function
53 // some constants for frequently used strings
55 ABSOLUTE = 'absolute',
56 RELATIVE = 'relative',
58 PREFIX = 'highcharts-',
65 * Empirical lowest possible opacities for TRACKER_FILL
69 * IE9: 0.00000000001 (unlimited)
70 * FF: 0.00000000001 (unlimited)
73 * Opera: 0.00000000001 (unlimited)
75 TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.000001 : 0.002) + ')', // invisible but clickable
76 //TRACKER_FILL = 'rgba(192,192,192,0.5)',
78 HOVER_STATE = 'hover',
79 SELECT_STATE = 'select',
80 MILLISECOND = 'millisecond',
89 // constants for attributes
91 LINEAR_GRADIENT = 'linearGradient',
94 STROKE_WIDTH = 'stroke-width',
96 // time methods, changed based on whether or not UTC is used
110 // check for a custom HighchartsAdapter defined prior to this file
111 globalAdapter = win.HighchartsAdapter,
112 adapter = globalAdapter || {},
114 // Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object
115 // and all the utility functions will be null. In that case they are populated by the
116 // default adapters below.
119 offset = adapter.offset,
121 merge = adapter.merge,
122 addEvent = adapter.addEvent,
123 removeEvent = adapter.removeEvent,
124 fireEvent = adapter.fireEvent,
125 animate = adapter.animate,
128 // lookup over the types and the associated classes
131 // The Highcharts namespace
135 * Extend an object with the members of another
136 * @param {Object} a The object to be extended
137 * @param {Object} b The object to add to the first one
139 function extend(a, b) {
151 * Take an array and turn into a hash with even number arguments as keys and odd numbers as
152 * values. Allows creating constants for commonly used style properties, attributes etc.
153 * Avoid it in performance critical situations like looping
158 length = args.length,
160 for (; i < length; i++) {
161 obj[args[i++]] = args[i];
167 * Shortcut for parseInt
169 * @param {Number} mag Magnitude
171 function pInt(s, mag) {
172 return parseInt(s, mag || 10);
179 function isString(s) {
180 return typeof s === 'string';
185 * @param {Object} obj
187 function isObject(obj) {
188 return typeof obj === 'object';
193 * @param {Object} obj
195 function isArray(obj) {
196 return Object.prototype.toString.call(obj) === '[object Array]';
203 function isNumber(n) {
204 return typeof n === 'number';
207 function log2lin(num) {
208 return math.log(num) / math.LN10;
210 function lin2log(num) {
211 return math.pow(10, num);
215 * Remove last occurence of an item from an array
217 * @param {Mixed} item
219 function erase(arr, item) {
222 if (arr[i] === item) {
231 * Returns true if the object is not null or undefined. Like MooTools' $.defined.
232 * @param {Object} obj
234 function defined(obj) {
235 return obj !== UNDEFINED && obj !== null;
239 * Set or get an attribute or an object of attributes. Can't use jQuery attr because
240 * it attempts to set expando properties on the SVG element, which is not allowed.
242 * @param {Object} elem The DOM element to receive the attribute(s)
243 * @param {String|Object} prop The property or an abject of key-value pairs
244 * @param {String} value The value if a single property is set
246 function attr(elem, prop, value) {
248 setAttribute = 'setAttribute',
251 // if the prop is a string
252 if (isString(prop)) {
254 if (defined(value)) {
256 elem[setAttribute](prop, value);
259 } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo...
260 ret = elem.getAttribute(prop);
263 // else if prop is defined, it is a hash of key/value pairs
264 } else if (defined(prop) && isObject(prop)) {
266 elem[setAttribute](key, prop[key]);
272 * Check if an element is an array, and if not, make it into an array. Like
275 function splat(obj) {
276 return isArray(obj) ? obj : [obj];
281 * Return the first value that is defined. Like MooTools' $.pick.
284 var args = arguments,
287 length = args.length;
288 for (i = 0; i < length; i++) {
290 if (typeof arg !== 'undefined' && arg !== null) {
297 * Set CSS on a given element
299 * @param {Object} styles Style object with camel case property names
301 function css(el, styles) {
303 if (styles && styles.opacity !== UNDEFINED) {
304 styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')';
307 extend(el.style, styles);
311 * Utility function to create element with attributes and styles
312 * @param {Object} tag
313 * @param {Object} attribs
314 * @param {Object} styles
315 * @param {Object} parent
316 * @param {Object} nopad
318 function createElement(tag, attribs, styles, parent, nopad) {
319 var el = doc.createElement(tag);
324 css(el, {padding: 0, border: NONE, margin: 0});
330 parent.appendChild(el);
336 * Extend a prototyped class by new members
337 * @param {Object} parent
338 * @param {Object} members
340 function extendClass(parent, members) {
341 var object = function () {};
342 object.prototype = new parent();
343 extend(object.prototype, members);
348 * Format a number and return a string based on input settings
349 * @param {Number} number The input number to format
350 * @param {Number} decimals The amount of decimals
351 * @param {String} decPoint The decimal point, defaults to the one given in the lang options
352 * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options
354 function numberFormat(number, decimals, decPoint, thousandsSep) {
355 var lang = defaultOptions.lang,
356 // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/
358 c = isNaN(decimals = mathAbs(decimals)) ? 2 : decimals,
359 d = decPoint === undefined ? lang.decimalPoint : decPoint,
360 t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep,
361 s = n < 0 ? "-" : "",
362 i = String(pInt(n = mathAbs(+n || 0).toFixed(c))),
363 j = i.length > 3 ? i.length % 3 : 0;
365 return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) +
366 (c ? d + mathAbs(n - i).toFixed(c).slice(2) : "");
370 * Based on http://www.php.net/manual/en/function.strftime.php
371 * @param {String} format
372 * @param {Number} timestamp
373 * @param {Boolean} capitalize
375 dateFormat = function (format, timestamp, capitalize) {
376 function pad(number, length) {
378 number = number.toString().replace(/^([0-9])$/, '0$1');
381 number = number.toString().replace(/^([0-9]{2})$/, '0$1');
386 if (!defined(timestamp) || isNaN(timestamp)) {
387 return 'Invalid date';
389 format = pick(format, '%Y-%m-%d %H:%M:%S');
391 var date = new Date(timestamp),
392 key, // used in for constuct below
393 // get the basic time values
394 hours = date[getHours](),
395 day = date[getDay](),
396 dayOfMonth = date[getDate](),
397 month = date[getMonth](),
398 fullYear = date[getFullYear](),
399 lang = defaultOptions.lang,
400 langWeekdays = lang.weekdays,
401 /* // uncomment this and the 'W' format key below to enable week numbers
402 weekNumber = function () {
403 var clone = new Date(date.valueOf()),
404 day = clone[getDay]() == 0 ? 7 : clone[getDay](),
406 clone.setDate(clone[getDate]() + 4 - day);
407 dayNumber = mathFloor((clone.getTime() - new Date(clone[getFullYear](), 0, 1, -6)) / 86400000);
408 return 1 + mathFloor(dayNumber / 7);
412 // list all format keys
416 'a': langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon'
417 'A': langWeekdays[day], // Long weekday, like 'Monday'
418 'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31
419 'e': dayOfMonth, // Day of the month, 1 through 31
421 // Week (none implemented)
425 'b': lang.shortMonths[month], // Short month, like 'Jan'
426 'B': lang.months[month], // Long month, like 'January'
427 'm': pad(month + 1), // Two digit month number, 01 through 12
430 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009
431 'Y': fullYear, // Four digits year, like 2009
434 'H': pad(hours), // Two digits hours in 24h format, 00 through 23
435 'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11
436 'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12
437 'M': pad(date[getMinutes]()), // Two digits minutes, 00 through 59
438 'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM
439 'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM
440 'S': pad(date.getSeconds()), // Two digits seconds, 00 through 59
441 'L': pad(timestamp % 1000, 3) // Milliseconds (naming from Ruby)
446 for (key in replacements) {
447 format = format.replace('%' + key, replacements[key]);
450 // Optionally capitalize the string and return
451 return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format;
455 * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5
456 * @param {Number} interval
457 * @param {Array} multiples
458 * @param {Number} magnitude
459 * @param {Object} options
461 function normalizeTickInterval(interval, multiples, magnitude, options) {
464 // round to a tenfold of 1, 2, 2.5 or 5
465 //magnitude = multiples ? 1 : math.pow(10, mathFloor(math.log(interval) / math.LN10));
466 magnitude = pick(magnitude, 1);
467 normalized = interval / magnitude;
469 // multiples for a linear scale
471 multiples = [1, 2, 2.5, 5, 10];
472 //multiples = [1, 2, 2.5, 4, 5, 7.5, 10];
474 // the allowDecimals option
475 if (options && (options.allowDecimals === false || options.type === 'logarithmic')) {
476 if (magnitude === 1) {
477 multiples = [1, 2, 5, 10];
478 } else if (magnitude <= 0.1) {
479 multiples = [1 / magnitude];
484 // normalize the interval to the nearest multiple
485 for (i = 0; i < multiples.length; i++) {
486 interval = multiples[i];
487 if (normalized <= (multiples[i] + (multiples[i + 1] || multiples[i])) / 2) {
492 // multiply back to the correct magnitude
493 interval *= magnitude;
499 * Get a normalized tick interval for dates. Returns a configuration object with
500 * unit range (interval), count and name. Used to prepare data for getTimeTicks.
501 * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs
502 * of segments in stock charts, the normalizing logic was extracted in order to
503 * prevent it for running over again for each segment having the same interval.
506 function normalizeTimeTickInterval(tickInterval, unitsOption) {
507 var units = unitsOption || [[
508 MILLISECOND, // unit name
509 [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
512 [1, 2, 5, 10, 15, 30]
515 [1, 2, 5, 10, 15, 30]
518 [1, 2, 3, 4, 6, 8, 12]
532 unit = units[units.length - 1], // default unit is years
533 interval = timeUnits[unit[0]],
538 // loop through the units to find the one that best fits the tickInterval
539 for (i = 0; i < units.length; i++) {
541 interval = timeUnits[unit[0]];
546 // lessThan is in the middle between the highest multiple and the next unit.
547 var lessThan = (interval * multiples[multiples.length - 1] +
548 timeUnits[units[i + 1][0]]) / 2;
550 // break and keep the current unit
551 if (tickInterval <= lessThan) {
557 // prevent 2.5 years intervals, though 25, 250 etc. are allowed
558 if (interval === timeUnits[YEAR] && tickInterval < 5 * interval) {
559 multiples = [1, 2, 5];
562 // prevent 2.5 years intervals, though 25, 250 etc. are allowed
563 if (interval === timeUnits[YEAR] && tickInterval < 5 * interval) {
564 multiples = [1, 2, 5];
568 count = normalizeTickInterval(tickInterval / interval, multiples);
578 * Set the tick positions to a time unit that makes sense, for example
579 * on the first of each month or on every Monday. Return an array
580 * with the time positions. Used in datetime axes as well as for grouping
581 * data on a datetime axis.
583 * @param {Object} normalizedInterval The interval in axis values (ms) and the count
584 * @param {Number} min The minimum in axis values
585 * @param {Number} max The maximum in axis values
586 * @param {Number} startOfWeek
588 function getTimeTicks(normalizedInterval, min, max, startOfWeek) {
589 var tickPositions = [],
592 useUTC = defaultOptions.global.useUTC,
593 minYear, // used in months and years as a basis for Date.UTC()
594 minDate = new Date(min),
595 interval = normalizedInterval.unitRange,
596 count = normalizedInterval.count;
598 minDate.setMilliseconds(0);
600 if (interval >= timeUnits[SECOND]) { // second
601 minDate.setSeconds(interval >= timeUnits[MINUTE] ? 0 :
602 count * mathFloor(minDate.getSeconds() / count));
605 if (interval >= timeUnits[MINUTE]) { // minute
606 minDate[setMinutes](interval >= timeUnits[HOUR] ? 0 :
607 count * mathFloor(minDate[getMinutes]() / count));
610 if (interval >= timeUnits[HOUR]) { // hour
611 minDate[setHours](interval >= timeUnits[DAY] ? 0 :
612 count * mathFloor(minDate[getHours]() / count));
615 if (interval >= timeUnits[DAY]) { // day
616 minDate[setDate](interval >= timeUnits[MONTH] ? 1 :
617 count * mathFloor(minDate[getDate]() / count));
620 if (interval >= timeUnits[MONTH]) { // month
621 minDate[setMonth](interval >= timeUnits[YEAR] ? 0 :
622 count * mathFloor(minDate[getMonth]() / count));
623 minYear = minDate[getFullYear]();
626 if (interval >= timeUnits[YEAR]) { // year
627 minYear -= minYear % count;
628 minDate[setFullYear](minYear);
631 // week is a special case that runs outside the hierarchy
632 if (interval === timeUnits[WEEK]) {
633 // get start of current week, independent of count
634 minDate[setDate](minDate[getDate]() - minDate[getDay]() +
635 pick(startOfWeek, 1));
639 // get tick positions
641 minYear = minDate[getFullYear]();
642 var time = minDate.getTime(),
643 minMonth = minDate[getMonth](),
644 minDateDate = minDate[getDate]();
646 // iterate and add tick positions at appropriate values
648 tickPositions.push(time);
650 // if the interval is years, use Date.UTC to increase years
651 if (interval === timeUnits[YEAR]) {
652 time = makeTime(minYear + i * count, 0);
654 // if the interval is months, use Date.UTC to increase months
655 } else if (interval === timeUnits[MONTH]) {
656 time = makeTime(minYear, minMonth + i * count);
658 // if we're using global time, the interval is not fixed as it jumps
659 // one hour at the DST crossover
660 } else if (!useUTC && (interval === timeUnits[DAY] || interval === timeUnits[WEEK])) {
661 time = makeTime(minYear, minMonth, minDateDate +
662 i * count * (interval === timeUnits[DAY] ? 1 : 7));
664 // else, the interval is fixed and we use simple addition
666 time += interval * count;
668 // mark new days if the time is dividable by day
669 if (interval <= timeUnits[HOUR] && time % timeUnits[DAY] === 0) {
670 higherRanks[time] = DAY;
677 // push the last time
678 tickPositions.push(time);
680 // record information on the chosen unit - for dynamic label formatter
681 tickPositions.info = extend(normalizedInterval, {
682 higherRanks: higherRanks,
683 totalRange: interval * count
686 return tickPositions;
690 * Helper class that contains variuos counters that are local to the chart.
692 function ChartCounters() {
697 ChartCounters.prototype = {
699 * Wraps the color counter if it reaches the specified length.
701 wrapColor: function (length) {
702 if (this.color >= length) {
708 * Wraps the symbol counter if it reaches the specified length.
710 wrapSymbol: function (length) {
711 if (this.symbol >= length) {
718 * Utility method extracted from Tooltip code that places a tooltip in a chart without spilling over
719 * and not covering the point it self.
721 function placeBox(boxWidth, boxHeight, outerLeft, outerTop, outerWidth, outerHeight, point, distance, preferRight) {
723 // keep the box within the chart area
724 var pointX = point.x,
726 x = pointX + outerLeft + (preferRight ? distance : -boxWidth - distance),
727 y = pointY - boxHeight + outerTop + 15, // 15 means the point is 15 pixels up from the bottom of the tooltip
730 // it is too far to the left, adjust it
732 x = outerLeft + pointX + distance;
735 // Test to see if the tooltip is too far to the right,
736 // if it is, move it back to be inside and then up to not cover the point.
737 if ((x + boxWidth) > (outerLeft + outerWidth)) {
738 x -= (x + boxWidth) - (outerLeft + outerWidth);
739 y = pointY - boxHeight + outerTop - distance;
743 // if it is now above the plot area, align it to the top of the plot area
744 if (y < outerTop + 5) {
747 // If the tooltip is still covering the point, move it below instead
748 if (alignedRight && pointY >= y && pointY <= (y + boxHeight)) {
749 y = pointY + outerTop + distance; // below
751 } else if (y + boxHeight > outerTop + outerHeight) {
752 y = outerTop + outerHeight - boxHeight - distance; // below
759 * Utility method that sorts an object array and keeping the order of equal items.
760 * ECMA script standard does not specify the behaviour when items are equal.
762 function stableSort(arr, sortFunction) {
763 var length = arr.length,
767 // Add index to each item
768 for (i = 0; i < length; i++) {
769 arr[i].ss_i = i; // stable sort index
772 arr.sort(function (a, b) {
773 sortValue = sortFunction(a, b);
774 return sortValue === 0 ? a.ss_i - b.ss_i : sortValue;
777 // Remove index from items
778 for (i = 0; i < length; i++) {
779 delete arr[i].ss_i; // stable sort index
784 * Non-recursive method to find the lowest member of an array. Math.min raises a maximum
785 * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This
786 * method is slightly slower, but safe.
788 function arrayMin(data) {
801 * Non-recursive method to find the lowest member of an array. Math.min raises a maximum
802 * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This
803 * method is slightly slower, but safe.
805 function arrayMax(data) {
818 * Utility method that destroys any SVGElement or VMLElement that are properties on the given object.
819 * It loops all properties and invokes destroy if there is a destroy method. The property is
822 function destroyObjectProperties(obj) {
825 // If the object is non-null and destroy is defined
826 if (obj[n] && obj[n].destroy) {
827 // Invoke the destroy
831 // Delete the property from the object.
838 * Discard an element by moving it to the bin and delete
839 * @param {Object} The HTML node to discard
841 function discardElement(element) {
842 // create a garbage bin element, not part of the DOM
844 garbageBin = createElement(DIV);
847 // move the node and empty bin
849 garbageBin.appendChild(element);
851 garbageBin.innerHTML = '';
855 * The time unit lookup
857 /*jslint white: true*/
864 WEEK, 7 * 24 * 3600000,
865 MONTH, 30 * 24 * 3600000,
868 /*jslint white: false*/
870 * Path interpolation algorithm used across adapters
874 * Prepare start and end values so that the path can be animated one to one
876 init: function (elem, fromD, toD) {
878 var shift = elem.shift,
879 bezier = fromD.indexOf('C') > -1,
880 numParams = bezier ? 7 : 3,
884 start = fromD.split(' '),
885 end = [].concat(toD), // copy
888 sixify = function (arr) { // in splines make move points have six parameters like bezier curves
892 arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]);
902 // pull out the base lines before padding
904 startBaseLine = start.splice(start.length - 6, 6);
905 endBaseLine = end.splice(end.length - 6, 6);
908 // if shifting points, prepend a dummy point to the end path
911 end = [].concat(end).splice(0, numParams).concat(end);
913 elem.shift = 0; // reset for following animations
915 // copy and append last point until the length matches the end length
917 endLength = end.length;
918 while (start.length < endLength) {
920 //bezier && sixify(start);
921 slice = [].concat(start).splice(start.length - numParams, numParams);
922 if (bezier) { // disable first control point
923 slice[numParams - 6] = slice[numParams - 2];
924 slice[numParams - 5] = slice[numParams - 1];
926 start = start.concat(slice);
930 if (startBaseLine) { // append the base lines for areas
931 start = start.concat(startBaseLine);
932 end = end.concat(endBaseLine);
938 * Interpolate each value of the path and return the array
940 step: function (start, end, pos, complete) {
945 if (pos === 1) { // land on the final path without adjustment points appended in the ends
948 } else if (i === end.length && pos < 1) {
950 startVal = parseFloat(start[i]);
952 isNaN(startVal) ? // a letter instruction like M or L
954 pos * (parseFloat(end[i] - startVal)) + startVal;
957 } else { // if animation is finished or length not matching, land on right value
966 * Set the global animation to either a given value, or fall back to the
967 * given chart's animation option
968 * @param {Object} animation
969 * @param {Object} chart
971 function setAnimation(animation, chart) {
972 globalAnimation = pick(animation, chart.animation);
976 * Define the adapter for frameworks. If an external adapter is not defined,
977 * Highcharts reverts to the built-in jQuery adapter.
979 if (globalAdapter && globalAdapter.init) {
980 // Initialize the adapter with the pathAnim object that takes care
981 // of path animations.
982 globalAdapter.init(pathAnim);
984 if (!globalAdapter && win.jQuery) {
988 * Utility for iterating over an array. Parameters are reversed compared to jQuery.
990 * @param {Function} fn
992 each = function (arr, fn) {
995 for (; i < len; i++) {
996 if (fn.call(arr[i], arr[i], i, arr) === false) {
1009 * @param {Array} arr
1010 * @param {Function} fn
1012 map = function (arr, fn) {
1013 //return jQuery.map(arr, fn);
1017 for (; i < len; i++) {
1018 results[i] = fn.call(arr[i], arr[i], i, arr);
1025 * Deep merge two objects and return a third object
1027 merge = function () {
1028 var args = arguments;
1029 return jQ.extend(true, null, args[0], args[1], args[2], args[3]);
1033 * Get the position of an element relative to the top left of the page
1035 offset = function (el) {
1036 return jQ(el).offset();
1040 * Add an event listener
1041 * @param {Object} el A HTML element or custom object
1042 * @param {String} event The event type
1043 * @param {Function} fn The event handler
1045 addEvent = function (el, event, fn) {
1046 jQ(el).bind(event, fn);
1050 * Remove event added with addEvent
1051 * @param {Object} el The object
1052 * @param {String} eventType The event type. Leave blank to remove all events.
1053 * @param {Function} handler The function to remove
1055 removeEvent = function (el, eventType, handler) {
1056 // workaround for jQuery issue with unbinding custom events:
1057 // http://forum.jquery.com/topic/javascript-error-when-unbinding-a-custom-event-using-jquery-1-4-2
1058 var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent';
1059 if (doc[func] && !el[func]) {
1060 el[func] = function () {};
1063 jQ(el).unbind(eventType, handler);
1067 * Fire an event on a custom object
1068 * @param {Object} el
1069 * @param {String} type
1070 * @param {Object} eventArguments
1071 * @param {Function} defaultFunction
1073 fireEvent = function (el, type, eventArguments, defaultFunction) {
1074 var event = jQ.Event(type),
1075 detachedType = 'detached' + type,
1078 extend(event, eventArguments);
1080 // Prevent jQuery from triggering the object method that is named the
1081 // same as the event. For example, if the event is 'select', jQuery
1082 // attempts calling el.select and it goes into a loop.
1084 el[detachedType] = el[type];
1088 // Wrap preventDefault and stopPropagation in try/catch blocks in
1089 // order to prevent JS errors when cancelling events on non-DOM
1091 each(['preventDefault', 'stopPropagation'], function (fn) {
1092 var base = event[fn];
1093 event[fn] = function () {
1097 if (fn === 'preventDefault') {
1098 defaultPrevented = true;
1105 jQ(el).trigger(event);
1107 // attach the method
1108 if (el[detachedType]) {
1109 el[type] = el[detachedType];
1110 el[detachedType] = null;
1113 if (defaultFunction && !event.isDefaultPrevented() && !defaultPrevented) {
1114 defaultFunction(event);
1119 * Animate a HTML element or SVG element wrapper
1120 * @param {Object} el
1121 * @param {Object} params
1122 * @param {Object} options jQuery-like animation options: duration, easing, callback
1124 animate = function (el, params, options) {
1127 el.toD = params.d; // keep the array form for paths, used in jQ.fx.step.d
1128 params.d = 1; // because in jQuery, animating to an array has a different meaning
1132 $el.animate(params, options);
1136 * Stop running animation
1138 stop = function (el) {
1143 //=== Extend jQuery on init
1145 /*jslint unparam: true*//* allow unused param x in this function */
1146 jQ.extend(jQ.easing, {
1147 easeOutQuad: function (x, t, b, c, d) {
1148 return -c * (t /= d) * (t - 2) + b;
1151 /*jslint unparam: false*/
1153 // extend the animate function to allow SVG animations
1154 var jFx = jQuery.fx,
1157 // extend some methods to check for elem.attr, which means it is a Highcharts SVG object
1158 each(['cur', '_default', 'width', 'height'], function (fn, i) {
1159 var obj = i ? jStep : jFx.prototype, // 'cur', the getter' relates to jFx.prototype
1163 if (base) { // step.width and step.height don't exist in jQuery < 1.7
1165 // create the extended function replacement
1166 obj[fn] = function (fx) {
1168 // jFx.prototype.cur does not use fx argument
1174 // jFX.prototype.cur returns the current value. The other ones are setters
1175 // and returning a value has no effect.
1176 return elem.attr ? // is SVG element wrapper
1177 elem.attr(fx.prop, fx.now) : // apply the SVG wrapper's method
1178 base.apply(this, arguments); // use jQuery's built-in method
1184 jStep.d = function (fx) {
1188 // Normally start and end should be set in state == 0, but sometimes,
1189 // for reasons unknown, this doesn't happen. Perhaps state == 0 is skipped
1192 var ends = pathAnim.init(elem, elem.d, elem.toD);
1199 // interpolate each value of the path
1200 elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD));
1205 /* ****************************************************************************
1206 * Handle the options *
1207 *****************************************************************************/
1210 defaultLabelOptions = {
1216 /*formatter: function () {
1227 colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE',
1228 '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'],
1229 symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],
1231 loading: 'Loading...',
1232 months: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
1233 'August', 'September', 'October', 'November', 'December'],
1234 shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
1235 weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
1237 resetZoom: 'Reset zoom',
1238 resetZoomTitle: 'Reset zoom level 1:1',
1246 //alignTicks: false,
1249 //events: { load, selection },
1252 //marginRight: null,
1253 //marginBottom: null,
1255 borderColor: '#4572A7',
1258 defaultSeriesType: 'line',
1259 ignoreHiddenSeries: true,
1267 fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
1270 backgroundColor: '#FFFFFF',
1271 //plotBackgroundColor: null,
1272 plotBorderColor: '#C0C0C0',
1273 //plotBorderWidth: 0,
1274 //plotShadow: false,
1276 resetZoomButton: { // docs
1283 //verticalAlign: 'top',
1290 text: 'Chart title',
1295 // verticalAlign: 'top',
1308 // verticalAlign: 'top',
1316 line: { // base series options
1317 allowPointSelect: false,
1318 showCheckbox: false,
1322 //connectNulls: false,
1323 //cursor: 'default',
1326 //enableMouseTracking: true,
1337 lineColor: '#FFFFFF',
1339 states: { // states for a single point
1344 fillColor: '#FFFFFF',
1345 lineColor: '#000000',
1353 dataLabels: merge(defaultLabelOptions, {
1356 formatter: function () {
1360 cropThreshold: 300, // draw points outside the plot area when the number of points is less than this
1365 states: { // states for the entire series
1368 //lineWidth: base + 1,
1370 // lineWidth: base + 1,
1378 stickyTracking: true
1380 //pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b>'
1381 //valueDecimals: null,
1382 //xDateFormat: '%A, %b %e, %Y',
1386 // turboThreshold: 1000
1393 //font: defaultFont,
1402 layout: 'horizontal',
1403 labelFormatter: function () {
1407 borderColor: '#909090',
1412 // backgroundColor: null,
1421 //cursor: 'pointer', removed as of #601
1427 itemCheckboxStyle: {
1429 width: '13px', // for IE precision
1432 // itemWidth: undefined,
1435 verticalAlign: 'bottom',
1436 // width: undefined,
1442 // hideDuration: 100,
1451 backgroundColor: 'white',
1460 backgroundColor: 'rgba(255, 255, 255, .85)',
1463 //formatter: defaultFormatter,
1464 headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>',
1465 pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b><br/>',
1468 snap: hasTouch ? 25 : 10,
1473 whiteSpace: 'nowrap'
1475 //xDateFormat: '%A, %b %e, %Y',
1476 //valueDecimals: null,
1483 text: 'Highcharts.com',
1484 href: 'http://www.highcharts.com',
1488 verticalAlign: 'bottom',
1500 /*jslint white: true*/
1501 var defaultXAxisOptions = {
1502 // allowDecimals: null,
1503 // alternateGridColor: null,
1505 dateTimeLabelFormats: hash(
1506 MILLISECOND, '%H:%M:%S.%L',
1516 gridLineColor: '#C0C0C0',
1517 // gridLineDashStyle: 'solid',
1518 // gridLineWidth: 0,
1521 labels: defaultLabelOptions,
1523 lineColor: '#C0D0E0',
1531 minorGridLineColor: '#E0E0E0',
1532 // minorGridLineDashStyle: null,
1533 minorGridLineWidth: 1,
1534 minorTickColor: '#A0A0A0',
1535 //minorTickInterval: null,
1537 minorTickPosition: 'outside', // inside or outside
1538 //minorTickWidth: 0,
1544 // labels: { align, x, verticalAlign, y, style, rotation, textAlign }
1550 // labels: { align, x, verticalAlign, y, style, rotation, textAlign }
1553 // showFirstLabel: true,
1554 // showLastLabel: true,
1557 tickColor: '#C0D0E0',
1558 //tickInterval: null,
1560 tickmarkPlacement: 'between', // on or between
1561 tickPixelInterval: 100,
1562 tickPosition: 'outside',
1566 align: 'middle', // low, middle or high
1567 //margin: 0 for horizontal, 10 for vertical axes,
1572 //font: defaultFont.replace('normal', 'bold')
1578 type: 'linear' // linear, logarithmic or datetime
1581 defaultYAxisOptions = merge(defaultXAxisOptions, {
1584 tickPixelInterval: 72,
1585 showLastLabel: true,
1605 //verticalAlign: dynamic,
1606 //textAlign: dynamic,
1608 formatter: function () {
1611 style: defaultLabelOptions.style
1615 defaultLeftAxisOptions = {
1625 defaultRightAxisOptions = {
1635 defaultBottomAxisOptions = { // horizontal axis
1640 // staggerLines: null
1646 defaultTopAxisOptions = merge(defaultBottomAxisOptions, {
1649 // staggerLines: null
1652 /*jslint white: false*/
1657 var defaultPlotOptions = defaultOptions.plotOptions,
1658 defaultSeriesOptions = defaultPlotOptions.line;
1659 //defaultPlotOptions.line = merge(defaultSeriesOptions);
1660 defaultPlotOptions.spline = merge(defaultSeriesOptions);
1661 defaultPlotOptions.scatter = merge(defaultSeriesOptions, {
1669 headerFormat: '<span style="font-size: 10px; color:{series.color}">{series.name}</span><br/>',
1670 pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>'
1673 defaultPlotOptions.area = merge(defaultSeriesOptions, {
1675 // lineColor: null, // overrides color, but lets fillColor be unaltered
1676 // fillOpacity: 0.75,
1680 defaultPlotOptions.areaspline = merge(defaultPlotOptions.area);
1681 defaultPlotOptions.column = merge(defaultSeriesOptions, {
1682 borderColor: '#FFFFFF',
1685 //colorByPoint: undefined,
1687 marker: null, // point options are specified in the base options
1691 cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes
1692 pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories
1700 borderColor: '#000000',
1710 defaultPlotOptions.bar = merge(defaultPlotOptions.column, {
1717 defaultPlotOptions.pie = merge(defaultSeriesOptions, {
1718 //dragType: '', // n/a
1719 borderColor: '#FFFFFF',
1721 center: ['50%', '50%'],
1722 colorByPoint: true, // always true for pies
1725 // connectorWidth: 1,
1726 // connectorColor: point.color,
1727 // connectorPadding: 5,
1730 formatter: function () {
1731 return this.point.name;
1733 // softConnector: true,
1737 legendType: 'point',
1738 marker: null, // point options are specified in the base options
1740 showInLegend: false,
1751 // set the default time methods
1757 * Set the time methods globally based on the useUTC option. Time method can be either
1758 * local time or UTC (default).
1760 function setTimeMethods() {
1761 var useUTC = defaultOptions.global.useUTC,
1762 GET = useUTC ? 'getUTC' : 'get',
1763 SET = useUTC ? 'setUTC' : 'set';
1765 makeTime = useUTC ? Date.UTC : function (year, month, date, hours, minutes, seconds) {
1775 getMinutes = GET + 'Minutes';
1776 getHours = GET + 'Hours';
1777 getDay = GET + 'Day';
1778 getDate = GET + 'Date';
1779 getMonth = GET + 'Month';
1780 getFullYear = GET + 'FullYear';
1781 setMinutes = SET + 'Minutes';
1782 setHours = SET + 'Hours';
1783 setDate = SET + 'Date';
1784 setMonth = SET + 'Month';
1785 setFullYear = SET + 'FullYear';
1790 * Merge the default options with custom options and return the new options structure
1791 * @param {Object} options The new custom options
1793 function setOptions(options) {
1795 // Pull out axis options and apply them to the respective default axis options
1796 defaultXAxisOptions = merge(defaultXAxisOptions, options.xAxis);
1797 defaultYAxisOptions = merge(defaultYAxisOptions, options.yAxis);
1798 options.xAxis = options.yAxis = UNDEFINED;
1800 // Merge in the default options
1801 defaultOptions = merge(defaultOptions, options);
1806 return defaultOptions;
1810 * Get the updated default options. Merely exposing defaultOptions for outside modules
1811 * isn't enough because the setOptions method creates a new object.
1813 function getOptions() {
1814 return defaultOptions;
1820 * Handle color operations. The object methods are chainable.
1821 * @param {String} input The input color in either rbga or hex format
1823 var Color = function (input) {
1824 // declare variables
1825 var rgba = [], result;
1828 * Parse the input color to rgba array
1829 * @param {String} input
1831 function init(input) {
1834 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);
1836 rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)];
1838 result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input);
1840 rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1];
1846 * Return the color a specified format
1847 * @param {String} format
1849 function get(format) {
1852 // it's NaN if gradient colors on a column chart
1853 if (rgba && !isNaN(rgba[0])) {
1854 if (format === 'rgb') {
1855 ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')';
1856 } else if (format === 'a') {
1859 ret = 'rgba(' + rgba.join(',') + ')';
1868 * Brighten the color
1869 * @param {Number} alpha
1871 function brighten(alpha) {
1872 if (isNumber(alpha) && alpha !== 0) {
1874 for (i = 0; i < 3; i++) {
1875 rgba[i] += pInt(alpha * 255);
1880 if (rgba[i] > 255) {
1888 * Set the color's opacity to a given alpha value
1889 * @param {Number} alpha
1891 function setOpacity(alpha) {
1896 // initialize: parse the input
1903 setOpacity: setOpacity
1909 * A wrapper object for SVG elements
1911 function SVGElement() {}
1913 SVGElement.prototype = {
1915 * Initialize the SVG renderer
1916 * @param {Object} renderer
1917 * @param {String} nodeName
1919 init: function (renderer, nodeName) {
1921 wrapper.element = doc.createElementNS(SVG_NS, nodeName);
1922 wrapper.renderer = renderer;
1924 * A collection of attribute setters. These methods, if defined, are called right before a certain
1925 * attribute is set on an element wrapper. Returning false prevents the default attribute
1926 * setter to run. Returning a value causes the default setter to set that value. Used in
1929 wrapper.attrSetters = {};
1932 * Animate a given attribute
1933 * @param {Object} params
1934 * @param {Number} options The same options as in jQuery animation
1935 * @param {Function} complete Function to perform at the end of animation
1937 animate: function (params, options, complete) {
1938 var animOptions = pick(options, globalAnimation, true);
1939 stop(this); // stop regardless of animation actually running, or reverting to .attr (#607)
1941 animOptions = merge(animOptions);
1942 if (complete) { // allows using a callback with the global animation without overwriting it
1943 animOptions.complete = complete;
1945 animate(this, params, animOptions);
1954 * Set or get a given attribute
1955 * @param {Object|String} hash
1956 * @param {Mixed|Undefined} val
1958 attr: function (hash, val) {
1965 element = wrapper.element,
1966 nodeName = element.nodeName,
1967 renderer = wrapper.renderer,
1969 attrSetters = wrapper.attrSetters,
1970 shadows = wrapper.shadows,
1971 htmlNode = wrapper.htmlNode,
1975 // single key-value pair
1976 if (isString(hash) && defined(val)) {
1982 // used as a getter: first argument is a string, second is undefined
1983 if (isString(hash)) {
1985 if (nodeName === 'circle') {
1986 key = { x: 'cx', y: 'cy' }[key] || key;
1987 } else if (key === 'strokeWidth') {
1988 key = 'stroke-width';
1990 ret = attr(element, key) || wrapper[key] || 0;
1992 if (key !== 'd' && key !== 'visibility') { // 'd' is string in animation step
1993 ret = parseFloat(ret);
2000 skipAttr = false; // reset
2003 // check for a specific attribute setter
2004 result = attrSetters[key] && attrSetters[key](value, key);
2006 if (result !== false) {
2008 if (result !== UNDEFINED) {
2009 value = result; // the attribute setter has returned a new value to set
2014 if (value && value.join) { // join path
2015 value = value.join(' ');
2017 if (/(NaN| {2}|^$)/.test(value)) {
2020 wrapper.d = value; // shortcut for animations
2022 // update child tspans x values
2023 } else if (key === 'x' && nodeName === 'text') {
2024 for (i = 0; i < element.childNodes.length; i++) {
2025 child = element.childNodes[i];
2026 // if the x values are equal, the tspan represents a linebreak
2027 if (attr(child, 'x') === attr(element, 'x')) {
2028 //child.setAttribute('x', value);
2029 attr(child, 'x', value);
2033 if (wrapper.rotation) {
2034 attr(element, 'transform', 'rotate(' + wrapper.rotation + ' ' + value + ' ' +
2035 pInt(hash.y || attr(element, 'y')) + ')');
2039 } else if (key === 'fill') {
2040 value = renderer.color(value, element, key);
2043 } else if (nodeName === 'circle' && (key === 'x' || key === 'y')) {
2044 key = { x: 'cx', y: 'cy' }[key] || key;
2046 // rectangle border radius
2047 } else if (nodeName === 'rect' && key === 'r') {
2054 // translation and text rotation
2055 } else if (key === 'translateX' || key === 'translateY' || key === 'rotation' || key === 'verticalAlign') {
2056 wrapper[key] = value;
2057 wrapper.updateTransform();
2060 // apply opacity as subnode (required by legacy WebKit and Batik)
2061 } else if (key === 'stroke') {
2062 value = renderer.color(value, element, key);
2064 // emulate VML's dashstyle implementation
2065 } else if (key === 'dashstyle') {
2066 key = 'stroke-dasharray';
2067 value = value && value.toLowerCase();
2068 if (value === 'solid') {
2072 .replace('shortdashdotdot', '3,1,1,1,1,1,')
2073 .replace('shortdashdot', '3,1,1,1')
2074 .replace('shortdot', '1,1,')
2075 .replace('shortdash', '3,1,')
2076 .replace('longdash', '8,3,')
2077 .replace(/dot/g, '1,3,')
2078 .replace('dash', '4,3,')
2080 .split(','); // ending comma
2084 value[i] = pInt(value[i]) * hash['stroke-width'];
2086 value = value.join(',');
2090 } else if (key === 'isTracker') {
2091 wrapper[key] = value;
2093 // IE9/MooTools combo: MooTools returns objects instead of numbers and IE9 Beta 2
2094 // is unable to cast them. Test again with final IE9.
2095 } else if (key === 'width') {
2096 value = pInt(value);
2099 } else if (key === 'align') {
2100 key = 'text-anchor';
2101 value = { left: 'start', center: 'middle', right: 'end' }[value];
2103 // Title requires a subnode, #431
2104 } else if (key === 'title') {
2105 var title = doc.createElementNS(SVG_NS, 'title');
2106 title.appendChild(doc.createTextNode(value));
2107 element.appendChild(title);
2110 // jQuery animate changes case
2111 if (key === 'strokeWidth') {
2112 key = 'stroke-width';
2115 // Chrome/Win < 6 bug (http://code.google.com/p/chromium/issues/detail?id=15461)
2116 if (isWebKit && key === 'stroke-width' && value === 0) {
2121 if (wrapper.symbolName && /^(x|y|r|start|end|innerR|anchorX|anchorY)/.test(key)) {
2124 if (!hasSetSymbolSize) {
2125 wrapper.symbolAttr(hash);
2126 hasSetSymbolSize = true;
2131 // let the shadow follow the main element
2132 if (shadows && /^(width|height|visibility|x|y|d|transform)$/.test(key)) {
2135 attr(shadows[i], key, value);
2140 if ((key === 'width' || key === 'height') && nodeName === 'rect' && value < 0) {
2147 if (key === 'text') {
2148 // only one node allowed
2149 wrapper.textStr = value;
2150 if (wrapper.added) {
2151 renderer.buildText(wrapper);
2153 } else if (!skipAttr) {
2154 attr(element, key, value);
2160 if (htmlNode && (key === 'x' || key === 'y' ||
2161 key === 'translateX' || key === 'translateY' || key === 'visibility')) {
2163 arr = htmlNode.length ? htmlNode : [this],
2164 length = arr.length,
2168 for (j = 0; j < length; j++) {
2169 itemWrapper = arr[j];
2170 bBox = itemWrapper.getBBox();
2171 htmlNode = itemWrapper.htmlNode; // reassign to child item
2172 css(htmlNode, extend(wrapper.styles, {
2173 left: (bBox.x + (wrapper.translateX || 0)) + PX,
2174 top: (bBox.y + (wrapper.translateY || 0)) + PX
2177 if (key === 'visibility') {
2189 // Workaround for our #732, WebKit's issue https://bugs.webkit.org/show_bug.cgi?id=78385
2190 // TODO: If the WebKit team fix this bug before the final release of Chrome 18, remove the workaround.
2191 if (isWebKit && /Chrome\/(18|19)/.test(userAgent)) {
2192 if (nodeName === 'text' && (hash.x !== UNDEFINED || hash.y !== UNDEFINED)) {
2193 var parent = element.parentNode,
2194 next = element.nextSibling;
2197 parent.removeChild(element);
2199 parent.insertBefore(element, next);
2201 parent.appendChild(element);
2206 // End of workaround for #732
2212 * If one of the symbol size affecting parameters are changed,
2213 * check all the others only once for each call to an element's
2215 * @param {Object} hash
2217 symbolAttr: function (hash) {
2220 each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function (key) {
2221 wrapper[key] = pick(hash[key], wrapper[key]);
2225 d: wrapper.renderer.symbols[wrapper.symbolName](wrapper.x, wrapper.y, wrapper.width, wrapper.height, wrapper)
2230 * Apply a clipping path to this object
2231 * @param {String} id
2233 clip: function (clipRect) {
2234 return this.attr('clip-path', 'url(' + this.renderer.url + '#' + clipRect.id + ')');
2238 * Calculate the coordinates needed for drawing a rectangle crisply and return the
2239 * calculated attributes
2240 * @param {Number} strokeWidth
2243 * @param {Number} width
2244 * @param {Number} height
2246 crisp: function (strokeWidth, x, y, width, height) {
2254 strokeWidth = strokeWidth || wrapper.strokeWidth || (wrapper.attr && wrapper.attr('stroke-width')) || 0;
2255 normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors
2257 // normalize for crisp edges
2258 values.x = mathFloor(x || wrapper.x || 0) + normalizer;
2259 values.y = mathFloor(y || wrapper.y || 0) + normalizer;
2260 values.width = mathFloor((width || wrapper.width || 0) - 2 * normalizer);
2261 values.height = mathFloor((height || wrapper.height || 0) - 2 * normalizer);
2262 values.strokeWidth = strokeWidth;
2264 for (key in values) {
2265 if (wrapper[key] !== values[key]) { // only set attribute if changed
2266 wrapper[key] = attribs[key] = values[key];
2274 * Set styles for the element
2275 * @param {Object} styles
2277 css: function (styles) {
2278 /*jslint unparam: true*//* allow unused param a in the regexp function below */
2279 var elemWrapper = this,
2280 elem = elemWrapper.element,
2281 textWidth = styles && styles.width && elem.nodeName === 'text',
2284 hyphenate = function (a, b) { return '-' + b.toLowerCase(); };
2285 /*jslint unparam: false*/
2288 if (styles && styles.color) {
2289 styles.fill = styles.color;
2292 // Merge the new styles with the old ones
2299 elemWrapper.styles = styles;
2301 // serialize and set style attribute
2302 if (isIE && !hasSVG) { // legacy IE doesn't support setting style attribute
2304 delete styles.width;
2306 css(elemWrapper.element, styles);
2309 serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';';
2312 style: serializedCss
2318 if (textWidth && elemWrapper.added) {
2319 elemWrapper.renderer.buildText(elemWrapper);
2326 * Add an event listener
2327 * @param {String} eventType
2328 * @param {Function} handler
2330 on: function (eventType, handler) {
2333 if (hasTouch && eventType === 'click') {
2334 eventType = 'touchstart';
2340 // simplest possible event model for internal use
2341 this.element['on' + eventType] = fn;
2347 * Move an object and its children by x and y values
2351 translate: function (x, y) {
2359 * Invert a group, rotate and flip
2361 invert: function () {
2363 wrapper.inverted = true;
2364 wrapper.updateTransform();
2369 * Private method to update the transform attribute based on internal
2372 updateTransform: function () {
2374 translateX = wrapper.translateX || 0,
2375 translateY = wrapper.translateY || 0,
2376 inverted = wrapper.inverted,
2377 rotation = wrapper.rotation,
2380 // flipping affects translate as adjustment for flipping around the group's axis
2382 translateX += wrapper.attr('width');
2383 translateY += wrapper.attr('height');
2387 if (translateX || translateY) {
2388 transform.push('translate(' + translateX + ',' + translateY + ')');
2393 transform.push('rotate(90) scale(-1,1)');
2394 } else if (rotation) { // text rotation
2395 transform.push('rotate(' + rotation + ' ' + wrapper.x + ' ' + wrapper.y + ')');
2398 if (transform.length) {
2399 attr(wrapper.element, 'transform', transform.join(' '));
2403 * Bring the element to the front
2405 toFront: function () {
2406 var element = this.element;
2407 element.parentNode.appendChild(element);
2413 * Break down alignment options like align, verticalAlign, x and y
2414 * to x and y relative to the chart.
2416 * @param {Object} alignOptions
2417 * @param {Boolean} alignByTranslate
2418 * @param {Object} box The box to align to, needs a width and height
2421 align: function (alignOptions, alignByTranslate, box) {
2422 var elemWrapper = this;
2424 if (!alignOptions) { // called on resize
2425 alignOptions = elemWrapper.alignOptions;
2426 alignByTranslate = elemWrapper.alignByTranslate;
2427 } else { // first call on instanciate
2428 elemWrapper.alignOptions = alignOptions;
2429 elemWrapper.alignByTranslate = alignByTranslate;
2430 if (!box) { // boxes other than renderer handle this internally
2431 elemWrapper.renderer.alignedObjects.push(elemWrapper);
2435 box = pick(box, elemWrapper.renderer);
2437 var align = alignOptions.align,
2438 vAlign = alignOptions.verticalAlign,
2439 x = (box.x || 0) + (alignOptions.x || 0), // default: left align
2440 y = (box.y || 0) + (alignOptions.y || 0), // default: top align
2445 if (/^(right|center)$/.test(align)) {
2446 x += (box.width - (alignOptions.width || 0)) /
2447 { right: 1, center: 2 }[align];
2449 attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x);
2453 if (/^(bottom|middle)$/.test(vAlign)) {
2454 y += (box.height - (alignOptions.height || 0)) /
2455 ({ bottom: 1, middle: 2 }[vAlign] || 1);
2458 attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y);
2460 // animate only if already placed
2461 elemWrapper[elemWrapper.placed ? 'animate' : 'attr'](attribs);
2462 elemWrapper.placed = true;
2463 elemWrapper.alignAttr = attribs;
2469 * Get the bounding box (width, height, x and y) for the element
2471 getBBox: function () {
2475 rotation = this.rotation,
2476 rad = rotation * deg2rad;
2478 try { // fails in Firefox if the container has display: none
2479 // use extend because IE9 is not allowed to change width and height in case
2480 // of rotation (below)
2481 bBox = extend({}, this.element.getBBox());
2483 bBox = { width: 0, height: 0 };
2486 height = bBox.height;
2488 // adjust for rotated text
2490 bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad));
2491 bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad));
2501 return this.attr({ visibility: VISIBLE });
2508 return this.attr({ visibility: HIDDEN });
2513 * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined
2514 * to append the element to the renderer.box.
2516 add: function (parent) {
2518 var renderer = this.renderer,
2519 parentWrapper = parent || renderer,
2520 parentNode = parentWrapper.element || renderer.box,
2521 childNodes = parentNode.childNodes,
2522 element = this.element,
2523 zIndex = attr(element, 'zIndex'),
2530 this.parentInverted = parent && parent.inverted;
2532 // build formatted text
2533 if (this.textStr !== undefined) {
2534 renderer.buildText(this);
2537 // register html spans in groups
2538 if (parent && this.htmlNode) {
2539 if (!parent.htmlNode) {
2540 parent.htmlNode = [];
2542 parent.htmlNode.push(this);
2545 // mark the container as having z indexed children
2547 parentWrapper.handleZ = true;
2548 zIndex = pInt(zIndex);
2551 // insert according to this and other elements' zIndex
2552 if (parentWrapper.handleZ) { // this element or any of its siblings has a z index
2553 for (i = 0; i < childNodes.length; i++) {
2554 otherElement = childNodes[i];
2555 otherZIndex = attr(otherElement, 'zIndex');
2556 if (otherElement !== element && (
2557 // insert before the first element with a higher zIndex
2558 pInt(otherZIndex) > zIndex ||
2559 // if no zIndex given, insert before the first element with a zIndex
2560 (!defined(zIndex) && defined(otherZIndex))
2563 parentNode.insertBefore(element, otherElement);
2570 // default: append at the end
2572 parentNode.appendChild(element);
2578 // fire an event for internal hooks
2579 fireEvent(this, 'add');
2585 * Removes a child either by removeChild or move to garbageBin.
2586 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
2588 safeRemoveChild: function (element) {
2589 var parentNode = element.parentNode;
2591 parentNode.removeChild(element);
2596 * Destroy the element and element wrapper
2598 destroy: function () {
2600 element = wrapper.element || {},
2601 shadows = wrapper.shadows,
2607 element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = null;
2608 stop(wrapper); // stop running animations
2610 if (wrapper.clipPath) {
2611 wrapper.clipPath = wrapper.clipPath.destroy();
2614 // Destroy stops in case this is a gradient object
2615 if (wrapper.stops) {
2616 for (i = 0; i < wrapper.stops.length; i++) {
2617 wrapper.stops[i] = wrapper.stops[i].destroy();
2619 wrapper.stops = null;
2623 wrapper.safeRemoveChild(element);
2627 each(shadows, function (shadow) {
2628 wrapper.safeRemoveChild(shadow);
2632 // destroy label box
2637 // remove from alignObjects
2638 erase(wrapper.renderer.alignedObjects, wrapper);
2640 for (key in wrapper) {
2641 delete wrapper[key];
2648 * Empty a group element
2650 empty: function () {
2651 var element = this.element,
2652 childNodes = element.childNodes,
2653 i = childNodes.length;
2656 element.removeChild(childNodes[i]);
2661 * Add a shadow to the element. Must be done after the element is added to the DOM
2662 * @param {Boolean} apply
2664 shadow: function (apply, group) {
2668 element = this.element,
2670 // compensate for inverted plot area
2671 transform = this.parentInverted ? '(-1,-1)' : '(1,1)';
2675 for (i = 1; i <= 3; i++) {
2676 shadow = element.cloneNode(0);
2679 'stroke': 'rgb(0, 0, 0)',
2680 'stroke-opacity': 0.05 * i,
2681 'stroke-width': 7 - 2 * i,
2682 'transform': 'translate' + transform,
2687 group.element.appendChild(shadow);
2689 element.parentNode.insertBefore(shadow, element);
2692 shadows.push(shadow);
2695 this.shadows = shadows;
2704 * The default SVG renderer
2706 var SVGRenderer = function () {
2707 this.init.apply(this, arguments);
2709 SVGRenderer.prototype = {
2711 Element: SVGElement,
2714 * Initialize the SVGRenderer
2715 * @param {Object} container
2716 * @param {Number} width
2717 * @param {Number} height
2718 * @param {Boolean} forExport
2720 init: function (container, width, height, forExport) {
2721 var renderer = this,
2725 boxWrapper = renderer.createElement('svg')
2730 container.appendChild(boxWrapper.element);
2732 // object properties
2733 renderer.box = boxWrapper.element;
2734 renderer.boxWrapper = boxWrapper;
2735 renderer.alignedObjects = [];
2736 renderer.url = isIE ? '' : loc.href.replace(/#.*?$/, '')
2737 .replace(/\(/g, '\\(').replace(/\)/g, '\\)'); // Page url used for internal references. #24, #672.
2738 renderer.defs = this.createElement('defs').add();
2739 renderer.forExport = forExport;
2740 renderer.gradients = {}; // Object where gradient SvgElements are stored
2742 renderer.setSize(width, height, false);
2746 * Destroys the renderer and its allocated members.
2748 destroy: function () {
2749 var renderer = this,
2750 rendererDefs = renderer.defs;
2751 renderer.box = null;
2752 renderer.boxWrapper = renderer.boxWrapper.destroy();
2754 // Call destroy on all gradient elements
2755 destroyObjectProperties(renderer.gradients || {});
2756 renderer.gradients = null;
2758 // Defs are null in VMLRenderer
2759 // Otherwise, destroy them here.
2761 renderer.defs = rendererDefs.destroy();
2764 renderer.alignedObjects = null;
2770 * Create a wrapper for an SVG element
2771 * @param {Object} nodeName
2773 createElement: function (nodeName) {
2774 var wrapper = new this.Element();
2775 wrapper.init(this, nodeName);
2781 * Parse a simple HTML string into SVG tspans
2783 * @param {Object} textNode The parent text SVG node
2785 buildText: function (wrapper) {
2786 var textNode = wrapper.element,
2787 lines = pick(wrapper.textStr, '').toString()
2788 .replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
2789 .replace(/<(i|em)>/g, '<span style="font-style:italic">')
2790 .replace(/<a/g, '<span')
2791 .replace(/<\/(b|strong|i|em|a)>/g, '</span>')
2793 childNodes = textNode.childNodes,
2794 styleRegex = /style="([^"]+)"/,
2795 hrefRegex = /href="([^"]+)"/,
2796 parentX = attr(textNode, 'x'),
2797 textStyles = wrapper.styles,
2798 renderAsHtml = textStyles && wrapper.useHTML && !this.forExport,
2799 htmlNode = wrapper.htmlNode,
2800 //arr, issue #38 workaround
2801 width = textStyles && pInt(textStyles.width),
2802 textLineHeight = textStyles && textStyles.lineHeight,
2804 GET_COMPUTED_STYLE = 'getComputedStyle',
2805 i = childNodes.length;
2809 textNode.removeChild(childNodes[i]);
2812 if (width && !wrapper.added) {
2813 this.box.appendChild(textNode); // attach it to the DOM to read offset width
2816 // remove empty line at end
2817 if (lines[lines.length - 1] === '') {
2822 each(lines, function (line, lineNo) {
2823 var spans, spanNo = 0, lineHeight;
2825 line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||');
2826 spans = line.split('|||');
2828 each(spans, function (span) {
2829 if (span !== '' || spans.length === 1) {
2830 var attributes = {},
2831 tspan = doc.createElementNS(SVG_NS, 'tspan');
2832 if (styleRegex.test(span)) {
2836 span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2')
2839 if (hrefRegex.test(span)) {
2840 attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"');
2841 css(tspan, { cursor: 'pointer' });
2844 span = (span.replace(/<(.|\n)*?>/g, '') || ' ')
2845 .replace(/</g, '<')
2846 .replace(/>/g, '>');
2848 // issue #38 workaround.
2853 arr.push(span.charAt(i));
2855 span = arr.join('');
2858 // add the text node
2859 tspan.appendChild(doc.createTextNode(span));
2861 if (!spanNo) { // first span in a line, align it to the left
2862 attributes.x = parentX;
2864 // Firefox ignores spaces at the front or end of the tspan
2865 attributes.dx = 3; // space
2868 // first span on subsequent line, add the line height
2872 // allow getting the right offset height in exporting in IE
2873 if (!hasSVG && wrapper.renderer.forExport) {
2874 css(tspan, { display: 'block' });
2877 // Webkit and opera sometimes return 'normal' as the line height. In that
2878 // case, webkit uses offsetHeight, while Opera falls back to 18
2879 lineHeight = win[GET_COMPUTED_STYLE] &&
2880 pInt(win[GET_COMPUTED_STYLE](lastLine, null).getPropertyValue('line-height'));
2882 if (!lineHeight || isNaN(lineHeight)) {
2883 lineHeight = textLineHeight || lastLine.offsetHeight || 18;
2885 attr(tspan, 'dy', lineHeight);
2887 lastLine = tspan; // record for use in next line
2891 attr(tspan, attributes);
2894 textNode.appendChild(tspan);
2898 // check width and apply soft breaks
2900 var words = span.replace(/-/g, '- ').split(' '),
2905 while (words.length || rest.length) {
2906 actualWidth = wrapper.getBBox().width;
2907 tooLong = actualWidth > width;
2908 if (!tooLong || words.length === 1) { // new line needed
2912 tspan = doc.createElementNS(SVG_NS, 'tspan');
2914 dy: textLineHeight || 16,
2917 textNode.appendChild(tspan);
2919 if (actualWidth > width) { // a single word is pressing it out
2920 width = actualWidth;
2923 } else { // append to existing line tspan
2924 tspan.removeChild(tspan.firstChild);
2925 rest.unshift(words.pop());
2928 tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));
2936 // Fix issue #38 and allow HTML in tooltips and other labels
2939 htmlNode = wrapper.htmlNode = createElement('span', null, extend(textStyles, {
2943 }), this.box.parentNode);
2945 htmlNode.innerHTML = wrapper.textStr;
2947 i = childNodes.length;
2949 childNodes[i].style.visibility = HIDDEN;
2955 * Create a button with preset states
2956 * @param {String} text
2959 * @param {Function} callback
2960 * @param {Object} normalState
2961 * @param {Object} hoverState
2962 * @param {Object} pressedState
2964 button: function (text, x, y, callback, normalState, hoverState, pressedState) {
2965 var label = this.label(text, x, y),
2973 verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 };
2975 // prepare the attributes
2976 /*jslint white: true*/
2977 normalState = merge(hash(
2981 LINEAR_GRADIENT, verticalGradient,
2993 /*jslint white: false*/
2994 normalStyle = normalState[STYLE];
2995 delete normalState[STYLE];
2997 /*jslint white: true*/
2998 hoverState = merge(normalState, hash(
3001 LINEAR_GRADIENT, verticalGradient,
3008 /*jslint white: false*/
3009 hoverStyle = hoverState[STYLE];
3010 delete hoverState[STYLE];
3012 /*jslint white: true*/
3013 pressedState = merge(normalState, hash(
3016 LINEAR_GRADIENT, verticalGradient,
3023 /*jslint white: false*/
3024 pressedStyle = pressedState[STYLE];
3025 delete pressedState[STYLE];
3028 addEvent(label.element, 'mouseenter', function () {
3029 label.attr(hoverState)
3032 addEvent(label.element, 'mouseleave', function () {
3033 stateOptions = [normalState, hoverState, pressedState][curState];
3034 stateStyle = [normalStyle, hoverStyle, pressedStyle][curState];
3035 label.attr(stateOptions)
3039 label.setState = function (state) {
3042 label.attr(normalState)
3044 } else if (state === 2) {
3045 label.attr(pressedState)
3051 .on('click', function () {
3052 callback.call(label);
3055 .css(extend({ cursor: 'default' }, normalStyle));
3059 * Make a straight line crisper by not spilling out to neighbour pixels
3060 * @param {Array} points
3061 * @param {Number} width
3063 crispLine: function (points, width) {
3064 // points format: [M, 0, 0, L, 100, 0]
3065 // normalize to a crisp line
3066 if (points[1] === points[4]) {
3067 points[1] = points[4] = mathRound(points[1]) + (width % 2 / 2);
3069 if (points[2] === points[5]) {
3070 points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2);
3078 * @param {Array} path An SVG path in array form
3080 path: function (path) {
3081 return this.createElement('path').attr({
3088 * Draw and return an SVG circle
3089 * @param {Number} x The x position
3090 * @param {Number} y The y position
3091 * @param {Number} r The radius
3093 circle: function (x, y, r) {
3094 var attr = isObject(x) ?
3102 return this.createElement('circle').attr(attr);
3106 * Draw and return an arc
3107 * @param {Number} x X position
3108 * @param {Number} y Y position
3109 * @param {Number} r Radius
3110 * @param {Number} innerR Inner radius like used in donut charts
3111 * @param {Number} start Starting angle
3112 * @param {Number} end Ending angle
3114 arc: function (x, y, r, innerR, start, end) {
3115 // arcs are defined as symbols for the ability to set
3116 // attributes in attr and animate
3126 return this.symbol('arc', x || 0, y || 0, r || 0, r || 0, {
3127 innerR: innerR || 0,
3134 * Draw and return a rectangle
3135 * @param {Number} x Left position
3136 * @param {Number} y Top position
3137 * @param {Number} width
3138 * @param {Number} height
3139 * @param {Number} r Border corner radius
3140 * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing
3142 rect: function (x, y, width, height, r, strokeWidth) {
3148 strokeWidth = x.strokeWidth;
3151 var wrapper = this.createElement('rect').attr({
3157 return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0)));
3161 * Resize the box and re-align all aligned elements
3162 * @param {Object} width
3163 * @param {Object} height
3164 * @param {Boolean} animate
3167 setSize: function (width, height, animate) {
3168 var renderer = this,
3169 alignedObjects = renderer.alignedObjects,
3170 i = alignedObjects.length;
3172 renderer.width = width;
3173 renderer.height = height;
3175 renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({
3181 alignedObjects[i].align();
3187 * @param {String} name The group will be given a class name of 'highcharts-{name}'.
3188 * This can be used for styling and scripting.
3190 g: function (name) {
3191 var elem = this.createElement('g');
3192 return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem;
3197 * @param {String} src
3200 * @param {Number} width
3201 * @param {Number} height
3203 image: function (src, x, y, width, height) {
3205 preserveAspectRatio: NONE
3209 // optional properties
3210 if (arguments.length > 1) {
3219 elemWrapper = this.createElement('image').attr(attribs);
3221 // set the href in the xlink namespace
3222 if (elemWrapper.element.setAttributeNS) {
3223 elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink',
3226 // could be exporting in IE
3227 // using href throws "not supported" in ie7 and under, requries regex shim to fix later
3228 elemWrapper.element.setAttribute('hc-svg-href', src);
3235 * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object.
3237 * @param {Object} symbol
3240 * @param {Object} radius
3241 * @param {Object} options
3243 symbol: function (symbol, x, y, width, height, options) {
3247 // get the symbol definition function
3248 symbolFn = this.symbols[symbol],
3250 // check if there's a path defined for this symbol
3251 path = symbolFn && symbolFn(
3259 imageRegex = /^url\((.*?)\)$/,
3265 obj = this.path(path);
3266 // expando properties for use in animate and attr
3275 extend(obj, options);
3280 } else if (imageRegex.test(symbol)) {
3282 var centerImage = function (img, size) {
3287 -mathRound(size[0] / 2),
3288 -mathRound(size[1] / 2)
3292 imageSrc = symbol.match(imageRegex)[1];
3293 imageSize = symbolSizes[imageSrc];
3295 // create the image synchronously, add attribs async
3296 obj = this.image(imageSrc)
3303 centerImage(obj, imageSize);
3305 // initialize image to be 0 size so export will still function if there's no cached sizes
3306 obj.attr({ width: 0, height: 0 });
3308 // create a dummy JavaScript image to get the width and height
3309 createElement('img', {
3310 onload: function () {
3313 centerImage(obj, symbolSizes[imageSrc] = [img.width, img.height]);
3324 * An extendable collection of functions for defining symbol paths.
3327 'circle': function (x, y, w, h) {
3328 var cpw = 0.166 * w;
3331 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h,
3332 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y,
3337 'square': function (x, y, w, h) {
3347 'triangle': function (x, y, w, h) {
3356 'triangle-down': function (x, y, w, h) {
3364 'diamond': function (x, y, w, h) {
3367 L, x + w, y + h / 2,
3373 'arc': function (x, y, w, h, options) {
3374 var start = options.start,
3375 radius = options.r || w || h,
3376 end = options.end - 0.000001, // to prevent cos and sin of start and end from becoming equal on 360 arcs
3377 innerRadius = options.innerR,
3378 cosStart = mathCos(start),
3379 sinStart = mathSin(start),
3380 cosEnd = mathCos(end),
3381 sinEnd = mathSin(end),
3382 longArc = options.end - start < mathPI ? 0 : 1;
3386 x + radius * cosStart,
3387 y + radius * sinStart,
3392 longArc, // long or short arc
3394 x + radius * cosEnd,
3395 y + radius * sinEnd,
3397 x + innerRadius * cosEnd,
3398 y + innerRadius * sinEnd,
3400 innerRadius, // x radius
3401 innerRadius, // y radius
3403 longArc, // long or short arc
3405 x + innerRadius * cosStart,
3406 y + innerRadius * sinStart,
3414 * Define a clipping rectangle
3415 * @param {String} id
3418 * @param {Number} width
3419 * @param {Number} height
3421 clipRect: function (x, y, width, height) {
3423 id = PREFIX + idCounter++,
3425 clipPath = this.createElement('clipPath').attr({
3429 wrapper = this.rect(x, y, width, height, 0).add(clipPath);
3431 wrapper.clipPath = clipPath;
3438 * Take a color and return it if it's a string, make it a gradient if it's a
3439 * gradient configuration object. Prior to Highstock, an array was used to define
3440 * a linear gradient with pixel positions relative to the SVG. In newer versions
3441 * we change the coordinates to apply relative to the shape, using coordinates
3442 * 0-1 within the shape. To preserve backwards compatibility, linearGradient
3443 * in this definition is an object of x1, y1, x2 and y2.
3445 * @param {Object} color The color or config object
3447 color: function (color, elem, prop) {
3449 regexRgba = /^rgba/;
3450 if (color && color.linearGradient) {
3451 var renderer = this,
3452 linearGradient = color[LINEAR_GRADIENT],
3453 relativeToShape = !isArray(linearGradient), // keep backwards compatibility
3455 gradients = renderer.gradients,
3457 x1 = linearGradient.x1 || linearGradient[0] || 0,
3458 y1 = linearGradient.y1 || linearGradient[1] || 0,
3459 x2 = linearGradient.x2 || linearGradient[2] || 0,
3460 y2 = linearGradient.y2 || linearGradient[3] || 0,
3463 // Create a unique key in order to reuse gradient objects. #671.
3464 key = [relativeToShape, x1, y1, x2, y2, color.stops.join(',')].join(',');
3466 // If the gradient with the same setup is already created, reuse it
3467 if (gradients[key]) {
3468 id = attr(gradients[key].element, 'id');
3470 // If not, create a new one and keep the reference.
3472 id = PREFIX + idCounter++;
3473 gradientObject = renderer.createElement(LINEAR_GRADIENT)
3480 }, relativeToShape ? null : { gradientUnits: 'userSpaceOnUse' }))
3481 .add(renderer.defs);
3483 // The gradient needs to keep a list of stops to be able to destroy them
3484 gradientObject.stops = [];
3485 each(color.stops, function (stop) {
3487 if (regexRgba.test(stop[1])) {
3488 colorObject = Color(stop[1]);
3489 stopColor = colorObject.get('rgb');
3490 stopOpacity = colorObject.get('a');
3492 stopColor = stop[1];
3495 stopObject = renderer.createElement('stop').attr({
3497 'stop-color': stopColor,
3498 'stop-opacity': stopOpacity
3499 }).add(gradientObject);
3501 // Add the stop element to the gradient
3502 gradientObject.stops.push(stopObject);
3505 // Keep a reference to the gradient object so it is possible to reuse it and
3507 gradients[key] = gradientObject;
3510 return 'url(' + this.url + '#' + id + ')';
3512 // Webkit and Batik can't show rgba.
3513 } else if (regexRgba.test(color)) {
3514 colorObject = Color(color);
3515 attr(elem, prop + '-opacity', colorObject.get('a'));
3517 return colorObject.get('rgb');
3521 // Remove the opacity attribute added above. Does not throw if the attribute is not there.
3522 elem.removeAttribute(prop + '-opacity');
3531 * Add text to the SVG object
3532 * @param {String} str
3533 * @param {Number} x Left position
3534 * @param {Number} y Top position
3535 * @param {Boolean} useHTML Use HTML to render the text
3537 text: function (str, x, y, useHTML) {
3539 // declare variables
3540 var renderer = this,
3541 defaultChartStyle = defaultOptions.chart.style,
3544 x = mathRound(pick(x, 0));
3545 y = mathRound(pick(y, 0));
3547 wrapper = renderer.createElement('text')
3554 fontFamily: defaultChartStyle.fontFamily,
3555 fontSize: defaultChartStyle.fontSize
3560 wrapper.useHTML = useHTML;
3565 * Add a label, a text item that can hold a colored or gradient background
3566 * as well as a border and shadow.
3567 * @param {string} str
3570 * @param {String} shape
3571 * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the
3572 * coordinates it should be pinned to
3573 * @param {Number} anchorY
3575 label: function (str, x, y, shape, anchorX, anchorY) {
3577 var renderer = this,
3578 wrapper = renderer.g(),
3579 text = renderer.text()
3594 attrSetters = wrapper.attrSetters;
3597 * This function runs after the label is added to the DOM (when the bounding box is
3598 * available), and after the text of the label is updated to detect the new bounding
3599 * box and reflect it in the border box.
3601 function updateBoxSize() {
3602 bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) &&
3604 wrapper.width = (width || bBox.width) + 2 * padding;
3605 wrapper.height = (height || bBox.height) + 2 * padding;
3607 // create the border box if it is not already present
3609 wrapper.box = box = shape ?
3610 renderer.symbol(shape, 0, 0, wrapper.width, wrapper.height) :
3611 renderer.rect(0, 0, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]);
3615 // apply the box attributes
3617 width: wrapper.width,
3618 height: wrapper.height
3620 deferredAttr = null;
3624 * This function runs after setting text or padding, but only if padding is changed
3626 function updateTextPadding() {
3627 var styles = wrapper.styles,
3628 textAlign = styles && styles.textAlign,
3630 y = padding + mathRound(pInt(wrapper.element.style.fontSize || 11) * 1.2);
3632 // compensate for alignment
3633 if (defined(width) && (textAlign === 'center' || textAlign === 'right')) {
3634 x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width);
3637 // update if anything changed
3638 if (x !== text.x || y !== text.y) {
3645 // record current values
3651 * Set a box attribute, or defer it if the box is not yet created
3652 * @param {Object} key
3653 * @param {Object} value
3655 function boxAttr(key, value) {
3657 box.attr(key, value);
3659 deferredAttr[key] = value;
3663 function getSizeAfterAdd() {
3665 text: str, // alignment is available now
3674 * After the text element is added, get the desired size of the border box
3675 * and add it before the text in the DOM.
3677 addEvent(wrapper, 'add', getSizeAfterAdd);
3680 * Add specific attribute setters.
3683 // only change local variables
3684 attrSetters.width = function (value) {
3688 attrSetters.height = function (value) {
3692 attrSetters.padding = function (value) {
3694 updateTextPadding();
3699 // change local variable and set attribue as well
3700 attrSetters.align = function (value) {
3702 return false; // prevent setting text-anchor on the group
3705 // apply these to the box and the text alike
3706 attrSetters.text = function (value, key) {
3707 text.attr(key, value);
3709 updateTextPadding();
3713 // apply these to the box but not to the text
3714 attrSetters[STROKE_WIDTH] = function (value, key) {
3715 crispAdjust = value % 2 / 2;
3716 boxAttr(key, value);
3719 attrSetters.stroke = attrSetters.fill = attrSetters.r = function (value, key) {
3720 boxAttr(key, value);
3723 attrSetters.anchorX = function (value, key) {
3725 boxAttr(key, value + crispAdjust - wrapperX);
3728 attrSetters.anchorY = function (value, key) {
3730 boxAttr(key, value - wrapperY);
3734 // rename attributes
3735 attrSetters.x = function (value) {
3737 wrapperX -= { left: 0, center: 0.5, right: 1 }[align] * ((width || bBox.width) + padding);
3739 wrapper.attr('translateX', mathRound(wrapperX));
3742 attrSetters.y = function (value) {
3744 wrapper.attr('translateY', mathRound(value));
3748 // Redirect certain methods to either the box or the text
3749 var baseCss = wrapper.css;
3750 return extend(wrapper, {
3752 * Pick up some properties and apply them to the text instead of the wrapper
3754 css: function (styles) {
3756 var textStyles = {};
3757 styles = merge({}, styles); // create a copy to avoid altering the original object (#537)
3758 each(['fontSize', 'fontWeight', 'fontFamily', 'color', 'lineHeight', 'width'], function (prop) {
3759 if (styles[prop] !== UNDEFINED) {
3760 textStyles[prop] = styles[prop];
3761 delete styles[prop];
3764 text.css(textStyles);
3766 return baseCss.call(wrapper, styles);
3769 * Return the bounding box of the box, not the group
3771 getBBox: function () {
3772 return box.getBBox();
3775 * Apply the shadow to the box
3777 shadow: function (b) {
3782 * Destroy and release memory.
3784 destroy: function () {
3785 removeEvent(wrapper, 'add', getSizeAfterAdd);
3787 // Added by button implementation
3788 removeEvent(wrapper.element, 'mouseenter');
3789 removeEvent(wrapper.element, 'mouseleave');
3792 // Destroy the text element
3793 text = text.destroy();
3795 // Call base implementation to destroy the rest
3796 SVGElement.prototype.destroy.call(wrapper);
3800 }; // end SVGRenderer
3804 Renderer = SVGRenderer;
3807 /* ****************************************************************************
3809 * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
3811 * For applications and websites that don't need IE support, like platform *
3812 * targeted mobile apps and web apps, this code can be removed. *
3814 *****************************************************************************/
3823 * The VML element wrapper.
3825 var VMLElement = extendClass(SVGElement, {
3828 * Initialize a new VML element wrapper. It builds the markup as a string
3829 * to minimize DOM traffic.
3830 * @param {Object} renderer
3831 * @param {Object} nodeName
3833 init: function (renderer, nodeName) {
3835 markup = ['<', nodeName, ' filled="f" stroked="f"'],
3836 style = ['position: ', ABSOLUTE, ';'];
3838 // divs and shapes need size
3839 if (nodeName === 'shape' || nodeName === DIV) {
3840 style.push('left:0;top:0;width:10px;height:10px;');
3843 style.push('visibility: ', nodeName === DIV ? HIDDEN : VISIBLE);
3846 markup.push(' style="', style.join(''), '"/>');
3848 // create element with default attributes and style
3850 markup = nodeName === DIV || nodeName === 'span' || nodeName === 'img' ?
3852 : renderer.prepVML(markup);
3853 wrapper.element = createElement(markup);
3856 wrapper.renderer = renderer;
3857 wrapper.attrSetters = {};
3861 * Add the node to the given parent
3862 * @param {Object} parent
3864 add: function (parent) {
3866 renderer = wrapper.renderer,
3867 element = wrapper.element,
3869 inverted = parent && parent.inverted,
3871 // get the parent node
3872 parentNode = parent ?
3873 parent.element || parent :
3877 // if the parent group is inverted, apply inversion on all children
3878 if (inverted) { // only on groups
3879 renderer.invertChild(element, parentNode);
3882 // issue #140 workaround - related to #61 and #74
3883 if (docMode8 && parentNode.gVis === HIDDEN) {
3884 css(element, { visibility: HIDDEN });
3888 parentNode.appendChild(element);
3890 // align text after adding to be able to read offset
3891 wrapper.added = true;
3892 if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) {
3893 wrapper.updateTransform();
3896 // fire an event for internal hooks
3897 fireEvent(wrapper, 'add');
3903 * In IE8 documentMode 8, we need to recursively set the visibility down in the DOM
3904 * tree for nested groups. Related to #61, #586.
3906 toggleChildren: function (element, visibility) {
3907 var childNodes = element.childNodes,
3908 i = childNodes.length;
3912 // apply the visibility
3913 css(childNodes[i], { visibility: visibility });
3915 // we have a nested group, apply it to its children again
3916 if (childNodes[i].nodeName === 'DIV') {
3917 this.toggleChildren(childNodes[i], visibility);
3923 * Get or set attributes
3925 attr: function (hash, val) {
3931 element = wrapper.element || {},
3932 elemStyle = element.style,
3933 nodeName = element.nodeName,
3934 renderer = wrapper.renderer,
3935 symbolName = wrapper.symbolName,
3937 shadows = wrapper.shadows,
3939 attrSetters = wrapper.attrSetters,
3942 // single key-value pair
3943 if (isString(hash) && defined(val)) {
3949 // used as a getter, val is undefined
3950 if (isString(hash)) {
3952 if (key === 'strokeWidth' || key === 'stroke-width') {
3953 ret = wrapper.strokeweight;
3964 // check for a specific attribute setter
3965 result = attrSetters[key] && attrSetters[key](value, key);
3967 if (result !== false && value !== null) { // #620
3969 if (result !== UNDEFINED) {
3970 value = result; // the attribute setter has returned a new value to set
3976 if (symbolName && /^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.test(key)) {
3977 // if one of the symbol size affecting parameters are changed,
3978 // check all the others only once for each call to an element's
3980 if (!hasSetSymbolSize) {
3982 wrapper.symbolAttr(hash);
3984 hasSetSymbolSize = true;
3988 } else if (key === 'd') {
3989 value = value || [];
3990 wrapper.d = value.join(' '); // used in getter for animation
3994 var convertedPath = [];
3997 // Multiply by 10 to allow subpixel precision.
3998 // Substracting half a pixel seems to make the coordinates
3999 // align with SVG, but this hasn't been tested thoroughly
4000 if (isNumber(value[i])) {
4001 convertedPath[i] = mathRound(value[i] * 10) - 5;
4002 } else if (value[i] === 'Z') { // close the path
4003 convertedPath[i] = 'x';
4005 convertedPath[i] = value[i];
4009 value = convertedPath.join(' ') || 'x';
4010 element.path = value;
4016 shadows[i].path = value;
4021 // directly mapped to css
4022 } else if (key === 'zIndex' || key === 'visibility') {
4024 // workaround for #61 and #586
4025 if (docMode8 && key === 'visibility' && nodeName === 'DIV') {
4026 element.gVis = value;
4027 wrapper.toggleChildren(element, value);
4028 if (value === VISIBLE) { // #74
4034 elemStyle[key] = value;
4042 } else if (key === 'width' || key === 'height') {
4044 value = mathMax(0, value); // don't set width or height below zero (#311)
4046 this[key] = value; // used in getter
4048 // clipping rectangle special
4049 if (wrapper.updateClipping) {
4050 wrapper[key] = value;
4051 wrapper.updateClipping();
4054 elemStyle[key] = value;
4060 } else if (/^(x|y)$/.test(key)) {
4062 wrapper[key] = value; // used in getter
4064 if (element.tagName === 'SPAN') {
4065 wrapper.updateTransform();
4068 elemStyle[{ x: 'left', y: 'top' }[key]] = value;
4072 } else if (key === 'class') {
4073 // IE8 Standards mode has problems retrieving the className
4074 element.className = value;
4077 } else if (key === 'stroke') {
4079 value = renderer.color(value, element, key);
4081 key = 'strokecolor';
4084 } else if (key === 'stroke-width' || key === 'strokeWidth') {
4085 element.stroked = value ? true : false;
4086 key = 'strokeweight';
4087 wrapper[key] = value; // used in getter, issue #113
4088 if (isNumber(value)) {
4093 } else if (key === 'dashstyle') {
4094 var strokeElem = element.getElementsByTagName('stroke')[0] ||
4095 createElement(renderer.prepVML(['<stroke/>']), null, null, element);
4096 strokeElem[key] = value || 'solid';
4097 wrapper.dashstyle = value; /* because changing stroke-width will change the dash length
4098 and cause an epileptic effect */
4102 } else if (key === 'fill') {
4104 if (nodeName === 'SPAN') { // text color
4105 elemStyle.color = value;
4107 element.filled = value !== NONE ? true : false;
4109 value = renderer.color(value, element, key);
4114 // translation for animation
4115 } else if (key === 'translateX' || key === 'translateY' || key === 'rotation' || key === 'align') {
4116 if (key === 'align') {
4119 wrapper[key] = value;
4120 wrapper.updateTransform();
4124 // text for rotated and non-rotated elements
4125 } else if (key === 'text') {
4127 element.innerHTML = value;
4131 // let the shadow follow the main element
4132 if (shadows && key === 'visibility') {
4135 shadows[i].style[key] = value;
4142 if (docMode8) { // IE8 setAttribute bug
4143 element[key] = value;
4145 attr(element, key, value);
4156 * Set the element's clipping to a predefined rectangle
4158 * @param {String} id The id of the clip rectangle
4160 clip: function (clipRect) {
4162 clipMembers = clipRect.members;
4164 clipMembers.push(wrapper);
4165 wrapper.destroyClip = function () {
4166 erase(clipMembers, wrapper);
4168 return wrapper.css(clipRect.getCSS(wrapper.inverted));
4172 * Set styles for the element
4173 * @param {Object} styles
4175 css: function (styles) {
4177 element = wrapper.element,
4178 textWidth = styles && element.tagName === 'SPAN' && styles.width;
4181 delete styles.width;
4182 wrapper.textWidth = textWidth;
4183 wrapper.updateTransform();
4186 wrapper.styles = extend(wrapper.styles, styles);
4187 css(wrapper.element, styles);
4193 * Removes a child either by removeChild or move to garbageBin.
4194 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
4196 safeRemoveChild: function (element) {
4197 // discardElement will detach the node from its parent before attaching it
4198 // to the garbage bin. Therefore it is important that the node is attached and have parent.
4199 var parentNode = element.parentNode;
4201 discardElement(element);
4206 * Extend element.destroy by removing it from the clip members array
4208 destroy: function () {
4211 if (wrapper.destroyClip) {
4212 wrapper.destroyClip();
4215 return SVGElement.prototype.destroy.apply(wrapper);
4219 * Remove all child nodes of a group, except the v:group element
4221 empty: function () {
4222 var element = this.element,
4223 childNodes = element.childNodes,
4224 i = childNodes.length,
4228 node = childNodes[i];
4229 node.parentNode.removeChild(node);
4234 * VML override for calculating the bounding box based on offsets
4235 * @param {Boolean} refresh Whether to force a fresh value from the DOM or to
4236 * use the cached value
4238 * @return {Object} A hash containing values for x, y, width and height
4241 getBBox: function (refresh) {
4243 element = wrapper.element,
4244 bBox = wrapper.bBox;
4246 // faking getBBox in exported SVG in legacy IE
4247 if (!bBox || refresh) {
4248 // faking getBBox in exported SVG in legacy IE
4249 if (element.nodeName === 'text') {
4250 element.style.position = ABSOLUTE;
4253 bBox = wrapper.bBox = {
4254 x: element.offsetLeft,
4255 y: element.offsetTop,
4256 width: element.offsetWidth,
4257 height: element.offsetHeight
4265 * Add an event listener. VML override for normalizing event parameters.
4266 * @param {String} eventType
4267 * @param {Function} handler
4269 on: function (eventType, handler) {
4270 // simplest possible event model for internal use
4271 this.element['on' + eventType] = function () {
4272 var evt = win.event;
4273 evt.target = evt.srcElement;
4281 * VML override private method to update elements based on internal
4282 * properties based on SVG transform
4284 updateTransform: function () {
4285 // aligning non added elements is expensive
4287 this.alignOnAdd = true;
4292 elem = wrapper.element,
4293 translateX = wrapper.translateX || 0,
4294 translateY = wrapper.translateY || 0,
4297 align = wrapper.textAlign || 'left',
4298 alignCorrection = { left: 0, center: 0.5, right: 1 }[align],
4299 nonLeft = align && align !== 'left',
4300 shadows = wrapper.shadows;
4303 if (translateX || translateY) {
4305 marginLeft: translateX,
4306 marginTop: translateY
4308 if (shadows) { // used in labels/tooltip
4309 each(shadows, function (shadow) {
4311 marginLeft: translateX + 1,
4312 marginTop: translateY + 1
4319 if (wrapper.inverted) { // wrapper is a group
4320 each(elem.childNodes, function (child) {
4321 wrapper.renderer.invertChild(child, elem);
4325 if (elem.tagName === 'SPAN') {
4328 rotation = wrapper.rotation,
4334 textWidth = pInt(wrapper.textWidth),
4335 xCorr = wrapper.xCorr || 0,
4336 yCorr = wrapper.yCorr || 0,
4337 currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth].join(',');
4339 if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed
4341 if (defined(rotation)) {
4342 radians = rotation * deg2rad; // deg to rad
4343 costheta = mathCos(radians);
4344 sintheta = mathSin(radians);
4346 // Adjust for alignment and rotation.
4347 // Test case: http://highcharts.com/tests/?file=text-rotation
4349 filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta,
4350 ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta,
4351 ', sizingMethod=\'auto expand\')'].join('') : NONE
4355 width = pick(wrapper.elemWidth, elem.offsetWidth);
4356 height = pick(wrapper.elemHeight, elem.offsetHeight);
4359 if (width > textWidth) {
4361 width: textWidth + PX,
4363 whiteSpace: 'normal'
4369 lineHeight = mathRound((pInt(elem.style.fontSize) || 12) * 1.2);
4370 xCorr = costheta < 0 && -width;
4371 yCorr = sintheta < 0 && -height;
4373 // correct for lineHeight and corners spilling out after rotation
4374 quad = costheta * sintheta < 0;
4375 xCorr += sintheta * lineHeight * (quad ? 1 - alignCorrection : alignCorrection);
4376 yCorr -= costheta * lineHeight * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);
4378 // correct for the length/height of the text
4380 xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);
4382 yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);
4389 // record correction
4390 wrapper.xCorr = xCorr;
4391 wrapper.yCorr = yCorr;
4394 // apply position with correction
4400 // record current text transform
4401 wrapper.cTT = currentTextTransform;
4406 * Apply a drop shadow by copying elements and giving them different strokes
4407 * @param {Boolean} apply
4409 shadow: function (apply, group) {
4412 element = this.element,
4413 renderer = this.renderer,
4415 elemStyle = element.style,
4417 path = element.path;
4419 // some times empty paths are not strings
4420 if (path && typeof path.value !== 'string') {
4425 for (i = 1; i <= 3; i++) {
4426 markup = ['<shape isShadow="true" strokeweight="', (7 - 2 * i),
4427 '" filled="false" path="', path,
4428 '" coordsize="100,100" style="', element.style.cssText, '" />'];
4429 shadow = createElement(renderer.prepVML(markup),
4431 left: pInt(elemStyle.left) + 1,
4432 top: pInt(elemStyle.top) + 1
4436 // apply the opacity
4437 markup = ['<stroke color="black" opacity="', (0.05 * i), '"/>'];
4438 createElement(renderer.prepVML(markup), null, null, shadow);
4443 group.element.appendChild(shadow);
4445 element.parentNode.insertBefore(shadow, element);
4449 shadows.push(shadow);
4453 this.shadows = shadows;
4463 VMLRenderer = function () {
4464 this.init.apply(this, arguments);
4466 VMLRenderer.prototype = merge(SVGRenderer.prototype, { // inherit SVGRenderer
4468 Element: VMLElement,
4469 isIE8: userAgent.indexOf('MSIE 8.0') > -1,
4473 * Initialize the VMLRenderer
4474 * @param {Object} container
4475 * @param {Number} width
4476 * @param {Number} height
4478 init: function (container, width, height) {
4479 var renderer = this,
4482 renderer.alignedObjects = [];
4484 boxWrapper = renderer.createElement(DIV);
4485 container.appendChild(boxWrapper.element);
4488 // generate the containing box
4489 renderer.box = boxWrapper.element;
4490 renderer.boxWrapper = boxWrapper;
4493 renderer.setSize(width, height, false);
4495 // The only way to make IE6 and IE7 print is to use a global namespace. However,
4496 // with IE8 the only way to make the dynamic shapes visible in screen and print mode
4497 // seems to be to add the xmlns attribute and the behaviour style inline.
4498 if (!doc.namespaces.hcv) {
4500 doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml');
4502 // setup default css
4503 doc.createStyleSheet().cssText =
4504 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' +
4505 '{ behavior:url(#default#VML); display: inline-block; } ';
4511 * Define a clipping rectangle. In VML it is accomplished by storing the values
4512 * for setting the CSS style to all associated members.
4516 * @param {Number} width
4517 * @param {Number} height
4519 clipRect: function (x, y, width, height) {
4521 // create a dummy element
4522 var clipRect = this.createElement();
4524 // mimic a rectangle with its style object for automatic updating in attr
4525 return extend(clipRect, {
4531 getCSS: function (inverted) {
4532 var rect = this,//clipRect.element.style,
4535 right = left + rect.width,
4536 bottom = top + rect.height,
4539 mathRound(inverted ? left : top) + 'px,' +
4540 mathRound(inverted ? bottom : right) + 'px,' +
4541 mathRound(inverted ? right : bottom) + 'px,' +
4542 mathRound(inverted ? top : left) + 'px)'
4545 // issue 74 workaround
4546 if (!inverted && docMode8) {
4555 // used in attr and animation to update the clipping of all members
4556 updateClipping: function () {
4557 each(clipRect.members, function (member) {
4558 member.css(clipRect.getCSS(member.inverted));
4567 * Take a color and return it if it's a string, make it a gradient if it's a
4568 * gradient configuration object, and apply opacity.
4570 * @param {Object} color The color or config object
4572 color: function (color, elem, prop) {
4574 regexRgba = /^rgba/,
4577 if (color && color[LINEAR_GRADIENT]) {
4581 linearGradient = color[LINEAR_GRADIENT],
4582 x1 = linearGradient.x1 || linearGradient[0] || 0,
4583 y1 = linearGradient.y1 || linearGradient[1] || 0,
4584 x2 = linearGradient.x2 || linearGradient[2] || 0,
4585 y2 = linearGradient.y2 || linearGradient[3] || 0,
4592 each(color.stops, function (stop, i) {
4593 if (regexRgba.test(stop[1])) {
4594 colorObject = Color(stop[1]);
4595 stopColor = colorObject.get('rgb');
4596 stopOpacity = colorObject.get('a');
4598 stopColor = stop[1];
4604 opacity1 = stopOpacity;
4607 opacity2 = stopOpacity;
4611 // Apply the gradient to fills only.
4612 if (prop === 'fill') {
4613 // calculate the angle based on the linear vector
4614 angle = 90 - math.atan(
4615 (y2 - y1) / // y vector
4616 (x2 - x1) // x vector
4620 // when colors attribute is used, the meanings of opacity and o:opacity2
4622 markup = ['<fill colors="0% ', color1, ',100% ', color2, '" angle="', angle,
4623 '" opacity="', opacity2, '" o:opacity2="', opacity1,
4624 '" type="gradient" focus="100%" method="any" />'];
4625 createElement(this.prepVML(markup), null, null, elem);
4627 // Gradients are not supported for VML stroke, return the first color. #722.
4633 // if the color is an rgba color, split it and add a fill node
4634 // to hold the opacity component
4635 } else if (regexRgba.test(color) && elem.tagName !== 'IMG') {
4637 colorObject = Color(color);
4639 markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>'];
4640 createElement(this.prepVML(markup), null, null, elem);
4642 return colorObject.get('rgb');
4646 var strokeNodes = elem.getElementsByTagName(prop);
4647 if (strokeNodes.length) {
4648 strokeNodes[0].opacity = 1;
4656 * Take a VML string and prepare it for either IE8 or IE6/IE7.
4657 * @param {Array} markup A string array of the VML markup to prepare
4659 prepVML: function (markup) {
4660 var vmlStyle = 'display:inline-block;behavior:url(#default#VML);',
4663 markup = markup.join('');
4665 if (isIE8) { // add xmlns and style inline
4666 markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />');
4667 if (markup.indexOf('style="') === -1) {
4668 markup = markup.replace('/>', ' style="' + vmlStyle + '" />');
4670 markup = markup.replace('style="', 'style="' + vmlStyle);
4673 } else { // add namespace
4674 markup = markup.replace('<', '<hcv:');
4681 * Create rotated and aligned text
4682 * @param {String} str
4686 text: function (str, x, y) {
4688 var defaultChartStyle = defaultOptions.chart.style;
4690 return this.createElement('span')
4697 whiteSpace: 'nowrap',
4698 fontFamily: defaultChartStyle.fontFamily,
4699 fontSize: defaultChartStyle.fontSize
4704 * Create and return a path element
4705 * @param {Array} path
4707 path: function (path) {
4709 return this.createElement('shape').attr({
4710 // subpixel precision down to 0.1 (width and height = 10px)
4711 coordsize: '100 100',
4717 * Create and return a circle element. In VML circles are implemented as
4718 * shapes, which is faster than v:oval
4723 circle: function (x, y, r) {
4724 return this.symbol('circle').attr({ x: x, y: y, r: r});
4728 * Create a group using an outer div and an inner v:group to allow rotating
4729 * and flipping. A simple v:group would have problems with positioning
4730 * child HTML elements and CSS clip.
4732 * @param {String} name The name of the group
4734 g: function (name) {
4738 // set the class name
4740 attribs = { 'className': PREFIX + name, 'class': PREFIX + name };
4743 // the div to hold HTML and clipping
4744 wrapper = this.createElement(DIV).attr(attribs);
4750 * VML override to create a regular HTML image
4751 * @param {String} src
4754 * @param {Number} width
4755 * @param {Number} height
4757 image: function (src, x, y, width, height) {
4758 var obj = this.createElement('img')
4759 .attr({ src: src });
4761 if (arguments.length > 1) {
4773 * VML uses a shape for rect to overcome bugs and rotation problems
4775 rect: function (x, y, width, height, r, strokeWidth) {
4781 strokeWidth = x.strokeWidth;
4784 var wrapper = this.symbol('rect');
4787 return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0)));
4791 * In the VML renderer, each child of an inverted div (group) is inverted
4792 * @param {Object} element
4793 * @param {Object} parentNode
4795 invertChild: function (element, parentNode) {
4796 var parentStyle = parentNode.style;
4800 left: pInt(parentStyle.width) - 10,
4801 top: pInt(parentStyle.height) - 10,
4807 * Symbol definitions that override the parent SVG renderer's symbols
4811 // VML specific arc function
4812 arc: function (x, y, w, h, options) {
4813 var start = options.start,
4815 radius = options.r || w || h,
4816 cosStart = mathCos(start),
4817 sinStart = mathSin(start),
4818 cosEnd = mathCos(end),
4819 sinEnd = mathSin(end),
4820 innerRadius = options.innerR,
4821 circleCorrection = 0.07 / radius,
4822 innerCorrection = (innerRadius && 0.1 / innerRadius) || 0;
4824 if (end - start === 0) { // no angle, don't show it.
4827 //} else if (end - start == 2 * mathPI) { // full circle
4828 } else if (2 * mathPI - end + start < circleCorrection) { // full circle
4829 // empirical correction found by trying out the limits for different radii
4830 cosEnd = -circleCorrection;
4831 } else if (end - start < innerCorrection) { // issue #186, another mysterious VML arc problem
4832 cosEnd = mathCos(start + innerCorrection);
4836 'wa', // clockwise arc to
4839 x + radius, // right
4840 y + radius, // bottom
4841 x + radius * cosStart, // start x
4842 y + radius * sinStart, // start y
4843 x + radius * cosEnd, // end x
4844 y + radius * sinEnd, // end y
4847 'at', // anti clockwise arc to
4848 x - innerRadius, // left
4849 y - innerRadius, // top
4850 x + innerRadius, // right
4851 y + innerRadius, // bottom
4852 x + innerRadius * cosEnd, // start x
4853 y + innerRadius * sinEnd, // start y
4854 x + innerRadius * cosStart, // end x
4855 y + innerRadius * sinStart, // end y
4862 // Add circle symbol path. This performs significantly faster than v:oval.
4863 circle: function (x, y, w, h) {
4866 'wa', // clockwisearcto
4872 y + h / 2, // start y
4875 //'x', // finish path
4880 * Add rectangle symbol path which eases rotation and omits arcsize problems
4881 * compared to the built-in VML roundrect shape
4883 * @param {Number} left Left position
4884 * @param {Number} top Top position
4885 * @param {Number} r Border radius
4886 * @param {Object} options Width and height
4889 rect: function (left, top, width, height, options) {
4890 /*for (var n in r) {
4891 logTime && console .log(n)
4894 if (!defined(options)) {
4897 var right = left + width,
4898 bottom = top + height,
4899 r = mathMin(options.r || 0, width, height);
4916 right - 2 * r, bottom - 2 * r,
4924 left, bottom - 2 * r,
4925 left + 2 * r, bottom,
4933 left + 2 * r, top + 2 * r,
4947 Renderer = VMLRenderer;
4950 /* ****************************************************************************
4952 * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
4954 *****************************************************************************/
4958 * @param {Object} options
4959 * @param {Function} callback Function to run when the chart has loaded
4961 function Chart(options, callback) {
4963 // Handle regular options
4964 var seriesOptions = options.series; // skip merging data points to increase performance
4965 options.series = null;
4966 options = merge(defaultOptions, options); // do the merge
4967 options.series = seriesOptions; // set back the series data
4969 // Define chart variables
4970 var optionsChart = options.chart,
4971 optionsMargin = optionsChart.margin,
4972 margin = isObject(optionsMargin) ?
4974 [optionsMargin, optionsMargin, optionsMargin, optionsMargin],
4975 optionsMarginTop = pick(optionsChart.marginTop, margin[0]),
4976 optionsMarginRight = pick(optionsChart.marginRight, margin[1]),
4977 optionsMarginBottom = pick(optionsChart.marginBottom, margin[2]),
4978 optionsMarginLeft = pick(optionsChart.marginLeft, margin[3]),
4979 spacingTop = optionsChart.spacingTop,
4980 spacingRight = optionsChart.spacingRight,
4981 spacingBottom = optionsChart.spacingBottom,
4982 spacingLeft = optionsChart.spacingLeft,
4985 chartSubtitleOptions,
5006 chartEvents = optionsChart.events,
5007 runChartClick = chartEvents && !!chartEvents.click,
5009 isInsidePlot, // function
5024 hasCartesianSeries = optionsChart.showAxes,
5027 maxTicks, // handle the greatest amount of ticks on grouped axes
5034 drawChartBox, // function
5035 getMargins, // function
5036 resetMargins, // function
5037 setChartSize, // function
5040 zoomOut; // function
5044 * Create a new axis object
5045 * @param {Object} options
5047 function Axis(userOptions) {
5050 var isXAxis = userOptions.isX,
5051 opposite = userOptions.opposite, // needed in setOptions
5052 horiz = inverted ? !isXAxis : isXAxis,
5054 (opposite ? 0 : 2) : // top : bottom
5055 (opposite ? 1 : 3), // right : left
5059 isXAxis ? defaultXAxisOptions : defaultYAxisOptions,
5060 [defaultTopAxisOptions, defaultRightAxisOptions,
5061 defaultBottomAxisOptions, defaultLeftAxisOptions][side],
5067 type = options.type,
5068 isDatetimeAxis = type === 'datetime',
5069 isLog = type === 'logarithmic',
5070 offset = options.offset || 0,
5071 xOrY = isXAxis ? 'x' : 'y',
5074 transA, // translation factor
5075 transB, // translation addend
5076 oldTransA, // used for prerendering
5084 setAxisTranslation, // fn
5085 getPlotLinePath, // fn
5091 minRange = options.minRange || options.maxZoom,
5092 range = options.range,
5101 minPadding = options.minPadding,
5102 maxPadding = options.maxPadding,
5103 minPixelPadding = 0,
5104 isLinked = defined(options.linkedTo),
5105 ignoreMinPadding, // can be set to true by a column or bar series
5108 events = options.events,
5110 plotLinesAndBands = [],
5114 tickPositions, // array containing predefined positions
5115 tickPositioner = options.tickPositioner,
5118 alternateBands = {},
5121 axisTitleMargin,// = options.title.margin,
5122 categories = options.categories,
5123 labelFormatter = options.labels.formatter || // can be overwritten by dynamic format
5125 var value = this.value,
5126 dateTimeLabelFormat = this.dateTimeLabelFormat,
5129 if (dateTimeLabelFormat) { // datetime axis
5130 ret = dateFormat(dateTimeLabelFormat, value);
5132 } else if (tickInterval % 1000000 === 0) { // use M abbreviation
5133 ret = (value / 1000000) + 'M';
5135 } else if (tickInterval % 1000 === 0) { // use k abbreviation
5136 ret = (value / 1000) + 'k';
5138 } else if (!categories && value >= 1000) { // add thousands separators
5139 ret = numberFormat(value, 0);
5141 } else { // strings (categories) and small numbers
5147 staggerLines = horiz && options.labels.staggerLines,
5148 reversed = options.reversed,
5149 tickmarkOffset = (categories && options.tickmarkPlacement === 'between') ? 0.5 : 0;
5154 function Tick(pos, type) {
5157 tick.type = type || '';
5167 * Write the tick label
5169 addLabel: function () {
5172 labelOptions = options.labels,
5174 width = (categories && horiz && categories.length &&
5175 !labelOptions.step && !labelOptions.staggerLines &&
5176 !labelOptions.rotation &&
5177 plotWidth / categories.length) ||
5178 (!horiz && plotWidth / 2),
5179 isFirst = pos === tickPositions[0],
5180 isLast = pos === tickPositions[tickPositions.length - 1],
5182 value = categories && defined(categories[pos]) ? categories[pos] : pos,
5184 tickPositionInfo = tickPositions.info,
5185 dateTimeLabelFormat;
5187 // Set the datetime label format. If a higher rank is set for this position, use that. If not,
5188 // use the general format.
5189 if (isDatetimeAxis && tickPositionInfo) {
5190 dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName];
5193 // set properties for access in render method
5194 tick.isFirst = isFirst;
5195 tick.isLast = isLast;
5198 str = labelFormatter.call({
5200 chart: chart, // docs
5203 dateTimeLabelFormat: dateTimeLabelFormat,
5204 value: isLog ? lin2log(value) : value
5209 css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX };
5210 css = extend(css, labelOptions.style);
5213 if (!defined(label)) {
5215 defined(str) && labelOptions.enabled ?
5220 labelOptions.useHTML
5223 align: labelOptions.align,
5224 rotation: labelOptions.rotation
5226 // without position absolute, IE export sometimes is wrong
5240 * Get the offset height or width of the label
5242 getLabelSize: function () {
5243 var label = this.label;
5245 ((this.labelBBox = label.getBBox()))[horiz ? 'height' : 'width'] :
5249 * Put everything in place
5251 * @param index {Number}
5252 * @param old {Boolean} Use old coordinates to prepare an animation into new position
5254 render: function (index, old) {
5259 labelOptions = options.labels,
5260 gridLine = tick.gridLine,
5261 gridPrefix = type ? type + 'Grid' : 'grid',
5262 tickPrefix = type ? type + 'Tick' : 'tick',
5263 gridLineWidth = options[gridPrefix + 'LineWidth'],
5264 gridLineColor = options[gridPrefix + 'LineColor'],
5265 dashStyle = options[gridPrefix + 'LineDashStyle'],
5266 tickLength = options[tickPrefix + 'Length'],
5267 tickWidth = options[tickPrefix + 'Width'] || 0,
5268 tickColor = options[tickPrefix + 'Color'],
5269 tickPosition = options[tickPrefix + 'Position'],
5273 step = labelOptions.step,
5274 cHeight = (old && oldChartHeight) || chartHeight,
5279 // get x and y position for ticks and labels
5281 translate(pos + tickmarkOffset, null, null, old) + transB :
5282 axisLeft + offset + (opposite ? ((old && oldChartWidth) || chartWidth) - axisRight - axisLeft : 0);
5285 cHeight - axisBottom + offset - (opposite ? axisHeight : 0) :
5286 cHeight - translate(pos + tickmarkOffset, null, null, old) - transB;
5288 // create the grid line
5289 if (gridLineWidth) {
5290 gridLinePath = getPlotLinePath(pos + tickmarkOffset, gridLineWidth, old);
5292 if (gridLine === UNDEFINED) {
5294 stroke: gridLineColor,
5295 'stroke-width': gridLineWidth
5298 attribs.dashstyle = dashStyle;
5303 tick.gridLine = gridLine =
5305 renderer.path(gridLinePath)
5306 .attr(attribs).add(gridGroup) :
5310 // If the parameter 'old' is set, the current call will be followed
5311 // by another call, therefore do not do any animations this time
5312 if (!old && gridLine && gridLinePath) {
5319 // create the tick mark
5322 // negate the length
5323 if (tickPosition === 'inside') {
5324 tickLength = -tickLength;
5327 tickLength = -tickLength;
5330 markPath = renderer.crispLine([
5335 x + (horiz ? 0 : -tickLength),
5336 y + (horiz ? tickLength : 0)
5339 if (mark) { // updating
5343 } else { // first time
5344 tick.mark = renderer.path(
5348 'stroke-width': tickWidth
5353 // the label is created on init - now move it into place
5354 if (label && !isNaN(x)) {
5355 x = x + labelOptions.x - (tickmarkOffset && horiz ?
5356 tickmarkOffset * transA * (reversed ? -1 : 1) : 0);
5357 y = y + labelOptions.y - (tickmarkOffset && !horiz ?
5358 tickmarkOffset * transA * (reversed ? 1 : -1) : 0);
5360 // vertically centered
5361 if (!defined(labelOptions.y)) {
5362 y += pInt(label.styles.lineHeight) * 0.9 - label.getBBox().height / 2;
5366 // correct for staggered labels
5368 y += (index / (step || 1) % staggerLines) * 16;
5371 // apply show first and show last
5372 if ((tick.isFirst && !pick(options.showFirstLabel, 1)) ||
5373 (tick.isLast && !pick(options.showLastLabel, 1))) {
5376 // show those that may have been previously hidden, either by show first/last, or by step
5381 if (step && index % step) {
5382 // show those indices dividable by step
5386 label[tick.isNew ? 'attr' : 'animate']({
5395 * Destructor for the tick prototype
5397 destroy: function () {
5398 destroyObjectProperties(this);
5403 * The object wrapper for plot lines and plot bands
5404 * @param {Object} options
5406 function PlotLineOrBand(options) {
5407 var plotLine = this;
5409 plotLine.options = options;
5410 plotLine.id = options.id;
5417 PlotLineOrBand.prototype = {
5420 * Render the plot line or plot band. If it is already existing,
5423 render: function () {
5424 var plotLine = this,
5425 halfPointRange = (axis.pointRange || 0) / 2,
5426 options = plotLine.options,
5427 optionsLabel = options.label,
5428 label = plotLine.label,
5429 width = options.width,
5431 from = options.from,
5432 value = options.value,
5433 toPath, // bands only
5434 dashStyle = options.dashStyle,
5435 svgElem = plotLine.svgElem,
5443 color = options.color,
5444 zIndex = options.zIndex,
5445 events = options.events,
5448 // logarithmic conversion
5450 from = log2lin(from);
5452 value = log2lin(value);
5457 path = getPlotLinePath(value, width);
5460 'stroke-width': width
5463 attribs.dashstyle = dashStyle;
5465 } else if (defined(from) && defined(to)) { // plot band
5466 // keep within plot area
5467 from = mathMax(from, min - halfPointRange);
5468 to = mathMin(to, max + halfPointRange);
5470 toPath = getPlotLinePath(to);
5471 path = getPlotLinePath(from);
5472 if (path && toPath) {
5479 } else { // outside the axis area
5489 if (defined(zIndex)) {
5490 attribs.zIndex = zIndex;
5493 // common for lines and bands
5498 }, null, svgElem.onGetPath);
5501 svgElem.onGetPath = function () {
5505 } else if (path && path.length) {
5506 plotLine.svgElem = svgElem = renderer.path(path)
5507 .attr(attribs).add();
5511 addEvent = function (eventType) {
5512 svgElem.on(eventType, function (e) {
5513 events[eventType].apply(plotLine, [e]);
5516 for (eventType in events) {
5517 addEvent(eventType);
5522 // the plot band/line label
5523 if (optionsLabel && defined(optionsLabel.text) && path && path.length && axisWidth > 0 && axisHeight > 0) {
5525 optionsLabel = merge({
5526 align: horiz && toPath && 'center',
5527 x: horiz ? !toPath && 4 : 10,
5528 verticalAlign : !horiz && toPath && 'middle',
5529 y: horiz ? toPath ? 16 : 10 : toPath ? 6 : -4,
5530 rotation: horiz && !toPath && 90
5533 // add the SVG element
5535 plotLine.label = label = renderer.text(
5541 align: optionsLabel.textAlign || optionsLabel.align,
5542 rotation: optionsLabel.rotation,
5545 .css(optionsLabel.style)
5549 // get the bounding box and align the label
5550 xs = [path[1], path[4], pick(path[6], path[1])];
5551 ys = [path[2], path[5], pick(path[7], path[2])];
5555 label.align(optionsLabel, false, {
5558 width: arrayMax(xs) - x,
5559 height: arrayMax(ys) - y
5563 } else if (label) { // move out of sight
5572 * Remove the plot line or band
5574 destroy: function () {
5577 destroyObjectProperties(obj);
5579 // remove it from the lookup
5580 erase(plotLinesAndBands, obj);
5585 * The class for stack items
5587 function StackItem(options, isNegative, x, stackOption) {
5588 var stackItem = this;
5590 // Tells if the stack is negative
5591 stackItem.isNegative = isNegative;
5593 // Save the options to be able to style the label
5594 stackItem.options = options;
5596 // Save the x value to be able to position the label later
5599 // Save the stack option on the series configuration object
5600 stackItem.stack = stackOption;
5602 // The align options and text align varies on whether the stack is negative and
5603 // if the chart is inverted or not.
5604 // First test the user supplied value, then use the dynamic.
5605 stackItem.alignOptions = {
5606 align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'),
5607 verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
5608 y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)),
5609 x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0)
5612 stackItem.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center');
5615 StackItem.prototype = {
5616 destroy: function () {
5617 destroyObjectProperties(this);
5621 * Sets the total of this stack. Should be called when a serie is hidden or shown
5622 * since that will affect the total of other stacks.
5624 setTotal: function (total) {
5630 * Renders the stack total label and adds it to the stack label group.
5632 render: function (group) {
5633 var stackItem = this, // aliased this
5634 str = stackItem.options.formatter.call(stackItem); // format the text in the label
5636 // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden
5637 if (stackItem.label) {
5638 stackItem.label.attr({text: str, visibility: HIDDEN});
5642 chart.renderer.text(str, 0, 0) // dummy positions, actual position updated with setOffset method in columnseries
5643 .css(stackItem.options.style) // apply style
5644 .attr({align: stackItem.textAlign, // fix the text-anchor
5645 rotation: stackItem.options.rotation, // rotation
5646 visibility: HIDDEN }) // hidden until setOffset is called
5647 .add(group); // add to the labels-group
5652 * Sets the offset that the stack has from the x value and repositions the label.
5654 setOffset: function (xOffset, xWidth) {
5655 var stackItem = this, // aliased this
5656 neg = stackItem.isNegative, // special treatment is needed for negative stacks
5657 y = axis.translate(stackItem.total), // stack value translated mapped to chart coordinates
5658 yZero = axis.translate(0), // stack origin
5659 h = mathAbs(y - yZero), // stack height
5660 x = chart.xAxis[0].translate(stackItem.x) + xOffset, // stack x position
5661 plotHeight = chart.plotHeight,
5662 stackBox = { // this is the box for the complete stack
5663 x: inverted ? (neg ? y : y - h) : x,
5664 y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y),
5665 width: inverted ? h : xWidth,
5666 height: inverted ? xWidth : h
5669 if (stackItem.label) {
5671 .align(stackItem.alignOptions, null, stackBox) // align the label to the box
5672 .attr({visibility: VISIBLE}); // set visibility
5678 * Get the minimum and maximum for the series of each axis
5680 function getSeriesExtremes() {
5685 // reset dataMin and dataMax in case we're redrawing
5686 dataMin = dataMax = null;
5688 // loop through this axis' series
5689 each(axis.series, function (series) {
5691 if (series.visible || !optionsChart.ignoreHiddenSeries) {
5693 var seriesOptions = series.options,
5704 threshold = seriesOptions.threshold,
5709 // Get dataMin and dataMax for X axes
5711 xData = series.xData;
5713 dataMin = mathMin(pick(dataMin, xData[0]), arrayMin(xData));
5714 dataMax = mathMax(pick(dataMax, xData[0]), arrayMax(xData));
5717 // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data
5722 cropped = series.cropped,
5723 xExtremes = series.xAxis.getExtremes(),
5727 hasModifyValue = !!series.modifyValue;
5731 stacking = seriesOptions.stacking;
5732 usePercentage = stacking === 'percent';
5734 // create a stack for this particular series type
5736 stackOption = seriesOptions.stack;
5737 stackKey = series.type + pick(stackOption, '');
5738 negKey = '-' + stackKey;
5739 series.stackKey = stackKey; // used in translate
5741 posPointStack = posStack[stackKey] || []; // contains the total values for each x
5742 posStack[stackKey] = posPointStack;
5744 negPointStack = negStack[negKey] || [];
5745 negStack[negKey] = negPointStack;
5747 if (usePercentage) {
5753 // processData can alter series.pointRange, so this goes after
5754 //findPointRange = series.pointRange === null;
5756 xData = series.processedXData;
5757 yData = series.processedYData;
5758 yDataLength = yData.length;
5761 // loop over the non-null y values and read them into a local array
5762 for (i = 0; i < yDataLength; i++) {
5765 if (y !== null && y !== UNDEFINED) {
5767 // read stacked values into a stack based on the x value,
5768 // the sign of y and the stack key
5770 isNegative = y < threshold;
5771 pointStack = isNegative ? negPointStack : posPointStack;
5772 key = isNegative ? negKey : stackKey;
5775 defined(pointStack[x]) ?
5776 pointStack[x] + y : y;
5784 // If the StackItem is there, just update the values,
5785 // if not, create one first
5786 if (!stacks[key][x]) {
5787 stacks[key][x] = new StackItem(options.stackLabels, isNegative, x, stackOption);
5789 stacks[key][x].setTotal(y);
5792 // general hook, used for Highstock compare values feature
5793 } else if (hasModifyValue) {
5794 y = series.modifyValue(y);
5797 // get the smallest distance between points
5799 distance = mathAbs(xData[i] - xData[i - 1]);
5800 pointRange = pointRange === UNDEFINED ? distance : mathMin(distance, pointRange);
5803 // for points within the visible range, including the first point outside the
5804 // visible range, consider y extremes
5805 if (cropped || ((xData[i + 1] || x) >= xExtremes.min && (xData[i - 1] || x) <= xExtremes.max)) {
5808 if (j) { // array, like ohlc data
5810 if (y[j] !== null) {
5811 activeYData[activeCounter++] = y[j];
5815 activeYData[activeCounter++] = y;
5821 // record the least unit distance
5822 /*if (findPointRange) {
5823 series.pointRange = pointRange || 1;
5825 series.closestPointRange = pointRange;*/
5828 // Get the dataMin and dataMax so far. If percentage is used, the min and max are
5829 // always 0 and 100. If the length of activeYData is 0, continue with null values.
5830 if (!usePercentage && activeYData.length) {
5831 dataMin = mathMin(pick(dataMin, activeYData[0]), arrayMin(activeYData));
5832 dataMax = mathMax(pick(dataMax, activeYData[0]), arrayMax(activeYData));
5836 // todo: instead of checking useThreshold, just set the threshold to 0
5837 // in area and column-like chart types
5838 if (series.useThreshold && threshold !== null) {
5839 if (dataMin >= threshold) {
5840 dataMin = threshold;
5841 ignoreMinPadding = true;
5842 } else if (dataMax < threshold) {
5843 dataMax = threshold;
5844 ignoreMaxPadding = true;
5854 * Translate from axis value to pixel position on the chart, or back
5857 translate = function (val, backwards, cvsCoord, old, handleLog) {
5860 localA = old ? oldTransA : transA,
5861 localMin = old ? oldMin : min,
5863 postTranslate = options.ordinal || (isLog && handleLog);
5870 sign *= -1; // canvas coordinates inverts the value
5871 cvsOffset = axisLength;
5873 if (reversed) { // reversed axis
5875 cvsOffset -= sign * axisLength;
5878 if (backwards) { // reverse translation
5880 val = axisLength - val;
5882 returnValue = val / localA + localMin; // from chart pixel to value
5883 if (postTranslate) { // log and ordinal axes
5884 returnValue = axis.lin2val(returnValue);
5887 } else { // normal translation, from axis value to pixel, relative to plot
5888 if (postTranslate) { // log and ordinal axes
5889 val = axis.val2lin(val);
5892 returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding);
5899 * Create the path for a plot line that goes from the given value on
5900 * this axis, across the plot to the opposite side
5901 * @param {Number} value
5902 * @param {Number} lineWidth Used for calculation crisp line
5903 * @param {Number] old Use old coordinates (for resizing and rescaling)
5905 getPlotLinePath = function (value, lineWidth, old) {
5910 translatedValue = translate(value, null, null, old),
5911 cHeight = (old && oldChartHeight) || chartHeight,
5912 cWidth = (old && oldChartWidth) || chartWidth,
5915 x1 = x2 = mathRound(translatedValue + transB);
5916 y1 = y2 = mathRound(cHeight - translatedValue - transB);
5918 if (isNaN(translatedValue)) { // no min or max
5923 y2 = cHeight - axisBottom;
5924 if (x1 < axisLeft || x1 > axisLeft + axisWidth) {
5929 x2 = cWidth - axisRight;
5931 if (y1 < axisTop || y1 > axisTop + axisHeight) {
5937 renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 0);
5941 * Fix JS round off float errors
5942 * @param {Number} num
5944 function correctFloat(num) {
5945 var invMag, ret = num;
5946 magnitude = pick(magnitude, math.pow(10, mathFloor(math.log(tickInterval) / math.LN10)));
5948 if (magnitude < 1) {
5949 invMag = mathRound(1 / magnitude) * 10;
5950 ret = mathRound(num * invMag) / invMag;
5956 * Set the tick positions of a linear axis to round values like whole tens or every five.
5958 function setLinearTickPositions() {
5962 roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval),
5963 roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval);
5967 // Populate the intermediate values
5969 while (pos <= roundedMax) {
5971 // Place the tick on the rounded value
5972 tickPositions.push(pos);
5974 // Always add the raw tickInterval, not the corrected one.
5975 pos = correctFloat(pos + tickInterval);
5977 // If the interval is not big enough in the current min - max range to actually increase
5978 // the loop variable, we need to break out to prevent endless loop. Issue #619
5979 if (pos === lastPos) {
5983 // Record the last value
5989 * Adjust the min and max for the minimum range. Keep in mind that the series data is
5990 * not yet processed, so we don't have information on data cropping and grouping, or
5991 * updated axis.pointRange or series.pointRange. The data can't be processed until
5992 * we have finally established min and max.
5994 function adjustForMinRange() {
5996 spaceAvailable = dataMax - dataMin >= minRange,
6005 // Set the automatic minimum range based on the closest point distance
6006 if (isXAxis && minRange === UNDEFINED) {
6008 if (defined(options.min) || defined(options.max)) {
6009 minRange = null; // don't do this again
6013 // Find the closest distance between raw data points, as opposed to
6014 // closestPointRange that applies to processed points (cropped and grouped)
6015 each(axis.series, function (series) {
6016 xData = series.xData;
6017 loopLength = series.xIncrement ? 1 : xData.length - 1;
6018 for (i = loopLength; i > 0; i--) {
6019 distance = xData[i] - xData[i - 1];
6020 if (closestDataRange === UNDEFINED || distance < closestDataRange) {
6021 closestDataRange = distance;
6025 minRange = mathMin(closestDataRange * 5, dataMax - dataMin);
6029 // if minRange is exceeded, adjust
6030 if (max - min < minRange) {
6032 zoomOffset = (minRange - max + min) / 2;
6034 // if min and max options have been set, don't go beyond it
6035 minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)];
6036 if (spaceAvailable) { // if space is available, stay within the data range
6037 minArgs[2] = dataMin;
6039 min = arrayMax(minArgs);
6041 maxArgs = [min + minRange, pick(options.max, min + minRange)];
6042 if (spaceAvailable) { // if space is availabe, stay within the data range
6043 maxArgs[2] = dataMax;
6046 max = arrayMin(maxArgs);
6048 // now if the max is adjusted, adjust the min back
6049 if (max - min < minRange) {
6050 minArgs[0] = max - minRange;
6051 minArgs[1] = pick(options.min, max - minRange);
6052 min = arrayMax(minArgs);
6058 * Set the tick positions to round values and optionally extend the extremes
6059 * to the nearest tick
6061 function setTickPositions(secondPass) {
6065 linkedParentExtremes,
6066 tickIntervalOption = options.tickInterval,
6067 tickPixelIntervalOption = options.tickPixelInterval;
6069 // linked axis gets the extremes from the parent axis
6071 linkedParent = chart[isXAxis ? 'xAxis' : 'yAxis'][options.linkedTo];
6072 linkedParentExtremes = linkedParent.getExtremes();
6073 min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);
6074 max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax);
6075 } else { // initial min and max from the extreme data values
6076 min = pick(userMin, options.min, dataMin);
6077 max = pick(userMax, options.max, dataMax);
6085 // handle zoomed range
6087 userMin = min = mathMax(min, max - range); // #618
6090 range = null; // don't use it when running setExtremes
6094 // adjust min and max for the minimum range
6095 adjustForMinRange();
6097 // pad the values to get clear of the chart's edges
6098 if (!categories && !usePercentage && !isLinked && defined(min) && defined(max)) {
6099 length = (max - min) || 1;
6100 if (!defined(options.min) && !defined(userMin) && minPadding && (dataMin < 0 || !ignoreMinPadding)) {
6101 min -= length * minPadding;
6103 if (!defined(options.max) && !defined(userMax) && maxPadding && (dataMax > 0 || !ignoreMaxPadding)) {
6104 max += length * maxPadding;
6109 if (min === max || min === undefined || max === undefined) {
6111 } else if (isLinked && !tickIntervalOption &&
6112 tickPixelIntervalOption === linkedParent.options.tickPixelInterval) {
6113 tickInterval = linkedParent.tickInterval;
6115 tickInterval = pick(
6117 categories ? // for categoried axis, 1 is default, for linear axis use tickPix
6119 (max - min) * tickPixelIntervalOption / (axisLength || 1)
6123 // Now we're finished detecting min and max, crop and group series data. This
6124 // is in turn needed in order to find tick positions in ordinal axes.
6125 if (isXAxis && !secondPass) {
6126 each(axis.series, function (series) {
6127 series.processData(min !== oldMin || max !== oldMax);
6132 // set the translation factor used in translate function
6133 setAxisTranslation();
6135 // hook for ordinal axes. To do: merge with below
6136 if (axis.beforeSetTickPositions) {
6137 axis.beforeSetTickPositions();
6140 // hook for extensions, used in Highstock ordinal axes
6141 if (axis.postProcessTickInterval) {
6142 tickInterval = axis.postProcessTickInterval(tickInterval);
6145 // for linear axes, get magnitude and normalize the interval
6146 if (!isDatetimeAxis) { // linear
6147 magnitude = math.pow(10, mathFloor(math.log(tickInterval) / math.LN10));
6148 if (!defined(options.tickInterval)) {
6149 tickInterval = normalizeTickInterval(tickInterval, null, magnitude, options);
6153 // record the tick interval for linked axis
6154 axis.tickInterval = tickInterval;
6156 // get minorTickInterval
6157 minorTickInterval = options.minorTickInterval === 'auto' && tickInterval ?
6158 tickInterval / 5 : options.minorTickInterval;
6160 // find the tick positions
6161 tickPositions = options.tickPositions || (tickPositioner && tickPositioner.apply(axis, [min, max]));
6162 if (!tickPositions) {
6163 if (isDatetimeAxis) {
6164 tickPositions = (axis.getNonLinearTimeTicks || getTimeTicks)(
6165 normalizeTimeTickInterval(tickInterval, options.units),
6168 options.startOfWeek,
6169 axis.ordinalPositions,
6170 axis.closestPointRange,
6174 setLinearTickPositions();
6178 // post process positions, used in ordinal axes in Highstock.
6179 // TODO: combine with getNonLinearTimeTicks
6180 fireEvent(axis, 'afterSetTickPositions', {
6181 tickPositions: tickPositions
6187 // reset min/max or remove extremes based on start/end on tick
6188 var roundedMin = tickPositions[0],
6189 roundedMax = tickPositions[tickPositions.length - 1];
6191 if (options.startOnTick) {
6193 } else if (min > roundedMin) {
6194 tickPositions.shift();
6197 if (options.endOnTick) {
6199 } else if (max < roundedMax) {
6200 tickPositions.pop();
6203 // record the greatest number of ticks for multi axis
6204 if (!maxTicks) { // first call, or maxTicks have been reset after a zoom operation
6211 if (!isDatetimeAxis && tickPositions.length > maxTicks[xOrY] && options.alignTicks !== false) {
6212 maxTicks[xOrY] = tickPositions.length;
6218 * When using multiple axes, adjust the number of ticks to match the highest
6219 * number of ticks in that group
6221 function adjustTickAmount() {
6223 if (maxTicks && maxTicks[xOrY] && !isDatetimeAxis && !categories && !isLinked && options.alignTicks !== false) { // only apply to linear scale
6224 var oldTickAmount = tickAmount,
6225 calculatedTickAmount = tickPositions.length;
6227 // set the axis-level tickAmount to use below
6228 tickAmount = maxTicks[xOrY];
6230 if (calculatedTickAmount < tickAmount) {
6231 while (tickPositions.length < tickAmount) {
6232 tickPositions.push(correctFloat(
6233 tickPositions[tickPositions.length - 1] + tickInterval
6236 transA *= (calculatedTickAmount - 1) / (tickAmount - 1);
6237 max = tickPositions[tickPositions.length - 1];
6240 if (defined(oldTickAmount) && tickAmount !== oldTickAmount) {
6241 axis.isDirty = true;
6249 * Set the scale based on data min and max, user set min and max or options
6252 function setScale() {
6259 oldAxisLength = axisLength;
6261 // set the new axisLength
6262 axisLength = horiz ? axisWidth : axisHeight;
6264 // is there new data?
6265 each(axis.series, function (series) {
6266 if (series.isDirtyData || series.isDirty ||
6267 series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well
6272 // do we really need to go through all this?
6273 if (axisLength !== oldAxisLength || isDirtyData || isLinked ||
6274 userMin !== oldUserMin || userMax !== oldUserMax) {
6276 // get data extremes if needed
6277 getSeriesExtremes();
6279 // get fixed positions based on tickInterval
6282 // record old values to decide whether a rescale is necessary later on (#540)
6283 oldUserMin = userMin;
6284 oldUserMax = userMax;
6288 for (type in stacks) {
6289 for (i in stacks[type]) {
6290 stacks[type][i].cum = stacks[type][i].total;
6295 // Mark as dirty if it is not already set to dirty and extremes have changed. #595.
6296 if (!axis.isDirty) {
6297 axis.isDirty = chart.isDirtyBox || min !== oldMin || max !== oldMax;
6303 * Set the extremes and optionally redraw
6304 * @param {Number} newMin
6305 * @param {Number} newMax
6306 * @param {Boolean} redraw
6307 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
6311 function setExtremes(newMin, newMax, redraw, animation) {
6313 redraw = pick(redraw, true); // defaults to true
6315 fireEvent(axis, 'setExtremes', { // fire an event to enable syncing of multiple charts
6318 }, function () { // the default event handler
6325 chart.redraw(animation);
6331 * Update translation information
6333 setAxisTranslation = function () {
6334 var range = max - min,
6337 seriesClosestPointRange;
6339 // adjust translation for padding
6341 each(axis.series, function (series) {
6342 pointRange = mathMax(pointRange, series.pointRange);
6343 seriesClosestPointRange = series.closestPointRange;
6344 if (!series.noSharedTooltip && defined(seriesClosestPointRange)) {
6345 closestPointRange = defined(closestPointRange) ?
6346 mathMin(closestPointRange, seriesClosestPointRange) :
6347 seriesClosestPointRange;
6350 // pointRange means the width reserved for each point, like in a column chart
6351 axis.pointRange = pointRange;
6353 // closestPointRange means the closest distance between points. In columns
6354 // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange
6355 // is some other value
6356 axis.closestPointRange = closestPointRange;
6361 axis.translationSlope = transA = axisLength / ((range + pointRange) || 1);
6362 transB = horiz ? axisLeft : axisBottom; // translation addend
6363 minPixelPadding = transA * (pointRange / 2);
6367 * Update the axis metrics
6369 function setAxisSize() {
6371 var offsetLeft = options.offsetLeft || 0,
6372 offsetRight = options.offsetRight || 0;
6375 axisLeft = pick(options.left, plotLeft + offsetLeft);
6376 axisTop = pick(options.top, plotTop);
6377 axisWidth = pick(options.width, plotWidth - offsetLeft + offsetRight);
6378 axisHeight = pick(options.height, plotHeight);
6379 axisBottom = chartHeight - axisHeight - axisTop;
6380 axisRight = chartWidth - axisWidth - axisLeft;
6381 axisLength = horiz ? axisWidth : axisHeight;
6383 // expose to use in Series object and navigator
6384 axis.left = axisLeft;
6386 axis.len = axisLength;
6391 * Get the actual axis extremes
6393 function getExtremes() {
6405 * Get the zero plane either based on zero or on the min or max value.
6406 * Used in bar and area plots
6408 function getThreshold(threshold) {
6409 if (min > threshold || threshold === null) {
6411 } else if (max < threshold) {
6415 return translate(threshold, 0, 1);
6419 * Add a plot band or plot line after render time
6421 * @param options {Object} The plotBand or plotLine configuration object
6423 function addPlotBandOrLine(options) {
6424 var obj = new PlotLineOrBand(options).render();
6425 plotLinesAndBands.push(obj);
6430 * Render the tick labels to a preliminary position to get their sizes
6432 function getOffset() {
6434 var hasData = axis.series.length && defined(min) && defined(max),
6435 showAxis = hasData || pick(options.showEmpty, true),
6438 axisTitleOptions = options.title,
6439 labelOptions = options.labels,
6440 directionFactor = [-1, 1, 1, -1][side],
6444 axisGroup = renderer.g('axis')
6445 .attr({ zIndex: 7 })
6447 gridGroup = renderer.g('grid')
6448 .attr({ zIndex: options.gridZIndex || 1 })
6452 labelOffset = 0; // reset
6454 if (hasData || isLinked) {
6455 each(tickPositions, function (pos) {
6457 ticks[pos] = new Tick(pos);
6459 ticks[pos].addLabel(); // update labels depending on tick interval
6464 each(tickPositions, function (pos) {
6465 // left side must be align: right and right side must have align: left for labels
6466 if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === labelOptions.align) {
6468 // get the highest offset
6469 labelOffset = mathMax(
6470 ticks[pos].getLabelSize(),
6478 labelOffset += (staggerLines - 1) * 16;
6481 } else { // doesn't have data
6488 if (axisTitleOptions && axisTitleOptions.text) {
6490 axisTitle = axis.axisTitle = renderer.text(
6491 axisTitleOptions.text,
6494 axisTitleOptions.useHTML
6498 rotation: axisTitleOptions.rotation || 0,
6500 axisTitleOptions.textAlign ||
6501 { low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align]
6503 .css(axisTitleOptions.style)
6505 axisTitle.isNew = true;
6509 titleOffset = axisTitle.getBBox()[horiz ? 'height' : 'width'];
6510 titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10);
6513 // hide or show the title depending on whether showEmpty is set
6514 axisTitle[showAxis ? 'show' : 'hide']();
6519 // handle automatic or user set offset
6520 offset = directionFactor * pick(options.offset, axisOffset[side]);
6523 pick(axisTitleOptions.offset,
6524 labelOffset + titleMargin +
6525 (side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x'])
6528 axisOffset[side] = mathMax(
6530 axisTitleMargin + titleOffset + directionFactor * offset
6539 var axisTitleOptions = options.title,
6540 stackLabelOptions = options.stackLabels,
6541 alternateGridColor = options.alternateGridColor,
6542 lineWidth = options.lineWidth,
6546 hasRendered = chart.hasRendered,
6547 slideInTicks = hasRendered && defined(oldMin) && !isNaN(oldMin),
6548 hasData = axis.series.length && defined(min) && defined(max),
6549 showAxis = hasData || pick(options.showEmpty, true);
6551 // If the series has data draw the ticks. Else only the line and title
6552 if (hasData || isLinked) {
6555 if (minorTickInterval && !categories) {
6556 var pos = min + (tickPositions[0] - min) % minorTickInterval;
6557 for (; pos <= max; pos += minorTickInterval) {
6558 if (!minorTicks[pos]) {
6559 minorTicks[pos] = new Tick(pos, 'minor');
6562 // render new ticks in old position
6563 if (slideInTicks && minorTicks[pos].isNew) {
6564 minorTicks[pos].render(null, true);
6568 minorTicks[pos].isActive = true;
6569 minorTicks[pos].render();
6574 each(tickPositions, function (pos, i) {
6575 // linked axes need an extra check to find out if
6576 if (!isLinked || (pos >= min && pos <= max)) {
6579 ticks[pos] = new Tick(pos);
6582 // render new ticks in old position
6583 if (slideInTicks && ticks[pos].isNew) {
6584 ticks[pos].render(i, true);
6587 ticks[pos].isActive = true;
6588 ticks[pos].render(i);
6593 // alternate grid color
6594 if (alternateGridColor) {
6595 each(tickPositions, function (pos, i) {
6596 if (i % 2 === 0 && pos < max) {
6597 if (!alternateBands[pos]) {
6598 alternateBands[pos] = new PlotLineOrBand();
6600 alternateBands[pos].options = {
6602 to: tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : max,
6603 color: alternateGridColor
6605 alternateBands[pos].render();
6606 alternateBands[pos].isActive = true;
6611 // custom plot lines and bands
6612 if (!axis._addedPlotLB) { // only first time
6613 each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) {
6614 //plotLinesAndBands.push(new PlotLineOrBand(plotLineOptions).render());
6615 addPlotBandOrLine(plotLineOptions);
6617 axis._addedPlotLB = true;
6624 // remove inactive ticks
6625 each([ticks, minorTicks, alternateBands], function (coll) {
6628 if (!coll[pos].isActive) {
6629 coll[pos].destroy();
6632 coll[pos].isActive = false; // reset
6640 // Static items. As the axis group is cleared on subsequent calls
6641 // to render, these items are added outside the group.
6644 lineLeft = axisLeft + (opposite ? axisWidth : 0) + offset;
6645 lineTop = chartHeight - axisBottom - (opposite ? axisHeight : 0) + offset;
6647 linePath = renderer.crispLine([
6657 chartWidth - axisRight :
6661 chartHeight - axisBottom
6664 axisLine = renderer.path(linePath)
6666 stroke: options.lineColor,
6667 'stroke-width': lineWidth,
6672 axisLine.animate({ d: linePath });
6675 // show or hide the line depending on options.showEmpty
6676 axisLine[showAxis ? 'show' : 'hide']();
6680 if (axisTitle && showAxis) {
6681 // compute anchor points for each of the title align options
6682 var margin = horiz ? axisLeft : axisTop,
6683 fontSize = pInt(axisTitleOptions.style.fontSize || 12),
6684 // the position in the length direction of the axis
6686 low: margin + (horiz ? 0 : axisLength),
6687 middle: margin + axisLength / 2,
6688 high: margin + (horiz ? axisLength : 0)
6689 }[axisTitleOptions.align],
6691 // the position in the perpendicular direction of the axis
6692 offAxis = (horiz ? axisTop + axisHeight : axisLeft) +
6693 (horiz ? 1 : -1) * // horizontal axis reverses the margin
6694 (opposite ? -1 : 1) * // so does opposite axes
6696 (side === 2 ? fontSize : 0);
6698 axisTitle[axisTitle.isNew ? 'attr' : 'animate']({
6701 offAxis + (opposite ? axisWidth : 0) + offset +
6702 (axisTitleOptions.x || 0), // x
6704 offAxis - (opposite ? axisHeight : 0) + offset :
6705 alongAxis + (axisTitleOptions.y || 0) // y
6707 axisTitle.isNew = false;
6711 if (stackLabelOptions && stackLabelOptions.enabled) {
6712 var stackKey, oneStack, stackCategory,
6713 stackTotalGroup = axis.stackTotalGroup;
6715 // Create a separate group for the stack total labels
6716 if (!stackTotalGroup) {
6717 axis.stackTotalGroup = stackTotalGroup =
6718 renderer.g('stack-labels')
6720 visibility: VISIBLE,
6723 .translate(plotLeft, plotTop)
6727 // Render each stack total
6728 for (stackKey in stacks) {
6729 oneStack = stacks[stackKey];
6730 for (stackCategory in oneStack) {
6731 oneStack[stackCategory].render(stackTotalGroup);
6735 // End stacked totals
6737 axis.isDirty = false;
6741 * Remove a plot band or plot line from the chart by id
6742 * @param {Object} id
6744 function removePlotBandOrLine(id) {
6745 var i = plotLinesAndBands.length;
6747 if (plotLinesAndBands[i].id === id) {
6748 plotLinesAndBands[i].destroy();
6754 * Redraw the axis to reflect changes in the data or axis extremes
6758 // hide tooltip and hover states
6759 if (tracker.resetTracker) {
6760 tracker.resetTracker();
6766 // move plot lines and bands
6767 each(plotLinesAndBands, function (plotLine) {
6771 // mark associated series as dirty and ready for redraw
6772 each(axis.series, function (series) {
6773 series.isDirty = true;
6779 * Set new axis categories and optionally redraw
6780 * @param {Array} newCategories
6781 * @param {Boolean} doRedraw
6783 function setCategories(newCategories, doRedraw) {
6784 // set the categories
6785 axis.categories = userOptions.categories = categories = newCategories;
6787 // force reindexing tooltips
6788 each(axis.series, function (series) {
6790 series.setTooltipPoints(true);
6794 // optionally redraw
6795 axis.isDirty = true;
6797 if (pick(doRedraw, true)) {
6803 * Destroys an Axis instance.
6805 function destroy() {
6808 // Remove the events
6811 // Destroy each stack total
6812 for (stackKey in stacks) {
6813 destroyObjectProperties(stacks[stackKey]);
6815 stacks[stackKey] = null;
6818 // Destroy stack total group
6819 if (axis.stackTotalGroup) {
6820 axis.stackTotalGroup = axis.stackTotalGroup.destroy();
6823 // Destroy collections
6824 each([ticks, minorTicks, alternateBands, plotLinesAndBands], function (coll) {
6825 destroyObjectProperties(coll);
6828 // Destroy local variables
6829 each([axisLine, axisGroup, gridGroup, axisTitle], function (obj) {
6834 axisLine = axisGroup = gridGroup = axisTitle = null;
6842 chart[isXAxis ? 'xAxis' : 'yAxis'].push(axis);
6844 // inverted charts have reversed xAxes as default
6845 if (inverted && isXAxis && reversed === UNDEFINED) {
6850 // expose some variables
6852 addPlotBand: addPlotBandOrLine,
6853 addPlotLine: addPlotBandOrLine,
6854 adjustTickAmount: adjustTickAmount,
6855 categories: categories,
6856 getExtremes: getExtremes,
6857 getPlotLinePath: getPlotLinePath,
6858 getThreshold: getThreshold,
6861 plotLinesAndBands: plotLinesAndBands,
6862 getOffset: getOffset,
6864 setAxisSize: setAxisSize,
6865 setAxisTranslation: setAxisTranslation,
6866 setCategories: setCategories,
6867 setExtremes: setExtremes,
6869 setTickPositions: setTickPositions,
6870 translate: translate,
6872 removePlotBand: removePlotBandOrLine,
6873 removePlotLine: removePlotBandOrLine,
6875 series: [], // populated by Series
6880 // register event listeners
6881 for (eventType in events) {
6882 addEvent(axis, eventType, events[eventType]);
6885 // extend logarithmic axis
6887 axis.val2lin = log2lin;
6888 axis.lin2val = lin2log;
6895 * The tooltip object
6896 * @param {Object} options Tooltip options
6898 function Tooltip(options) {
6900 borderWidth = options.borderWidth,
6901 crosshairsOptions = options.crosshairs,
6903 style = options.style,
6904 shared = options.shared,
6905 padding = pInt(style.padding),
6906 tooltipIsHidden = true,
6910 // remove padding CSS and apply padding on box instead
6914 var label = renderer.label('', 0, 0)
6917 fill: options.backgroundColor,
6918 'stroke-width': borderWidth,
6919 r: options.borderRadius,
6925 .shadow(options.shadow);
6928 * Destroy the tooltip and its elements.
6930 function destroy() {
6931 each(crosshairs, function (crosshair) {
6933 crosshair.destroy();
6937 // Destroy and clear local variables
6939 label = label.destroy();
6944 * In case no user defined formatter is given, this will be used
6946 function defaultFormatter() {
6948 items = pThis.points || splat(pThis),
6949 series = items[0].series,
6953 s = [series.tooltipHeaderFormatter(items[0].key)];
6956 each(items, function (item) {
6957 series = item.series;
6958 s.push((series.tooltipFormatter && series.tooltipFormatter(item)) ||
6959 item.point.tooltipFormatter(series.tooltipOptions.pointFormat));
6965 * Provide a soft movement for the tooltip
6967 * @param {Number} finalX
6968 * @param {Number} finalY
6970 function move(finalX, finalY) {
6972 // get intermediate values for animation
6973 currentX = tooltipIsHidden ? finalX : (2 * currentX + finalX) / 3;
6974 currentY = tooltipIsHidden ? finalY : (currentY + finalY) / 2;
6976 // move to the intermediate value
6977 label.attr({ x: currentX, y: currentY });
6979 // run on next tick of the mouse tracker
6980 if (mathAbs(finalX - currentX) > 1 || mathAbs(finalY - currentY) > 1) {
6981 tooltipTick = function () {
6982 move(finalX, finalY);
6993 if (!tooltipIsHidden) {
6994 var hoverPoints = chart.hoverPoints;
6998 // hide previous hoverPoints and set new
7000 each(hoverPoints, function (point) {
7004 chart.hoverPoints = null;
7007 tooltipIsHidden = true;
7013 * Hide the crosshairs
7015 function hideCrosshairs() {
7016 each(crosshairs, function (crosshair) {
7024 * Refresh the tooltip's text and position.
7025 * @param {Object} point
7028 function refresh(point) {
7037 tooltipPos = point.tooltipPos,
7038 formatter = options.formatter || defaultFormatter,
7039 hoverPoints = chart.hoverPoints,
7042 // shared tooltip, array is sent over
7043 if (shared && !(point.series && point.series.noSharedTooltip)) {
7046 // hide previous hoverPoints and set new
7048 each(hoverPoints, function (point) {
7052 chart.hoverPoints = point;
7054 each(point, function (item) {
7055 item.setState(HOVER_STATE);
7056 plotY += item.plotY; // for average
7058 pointConfig.push(item.getLabelConfig());
7061 plotX = point[0].plotX;
7062 plotY = mathRound(plotY) / point.length; // mathRound because Opera 10 has problems here
7065 x: point[0].category
7067 textConfig.points = pointConfig;
7070 // single point tooltip
7072 textConfig = point.getLabelConfig();
7074 text = formatter.call(textConfig);
7076 // register the current series
7077 currentSeries = point.series;
7079 // get the reference point coordinates (pie charts use tooltipPos)
7080 plotX = pick(plotX, point.plotX);
7081 plotY = pick(plotY, point.plotY);
7083 x = mathRound(tooltipPos ? tooltipPos[0] : (inverted ? plotWidth - plotY : plotX));
7084 y = mathRound(tooltipPos ? tooltipPos[1] : (inverted ? plotHeight - plotX : plotY));
7087 // For line type series, hide tooltip if the point falls outside the plot
7088 show = shared || !currentSeries.isCartesian || currentSeries.tooltipOutsidePlot || isInsidePlot(x, y);
7090 // update the inner HTML
7091 if (text === false || !show) {
7096 if (tooltipIsHidden) {
7098 tooltipIsHidden = false;
7106 // set the stroke color of the box
7108 stroke: options.borderColor || point.color || currentSeries.color || '#606060'
7111 placedTooltipPoint = placeBox(
7119 pick(options.distance, 12),
7124 move(mathRound(placedTooltipPoint.x), mathRound(placedTooltipPoint.y));
7129 if (crosshairsOptions) {
7130 crosshairsOptions = splat(crosshairsOptions); // [x, y]
7133 i = crosshairsOptions.length,
7138 axis = point.series[i ? 'yAxis' : 'xAxis'];
7139 if (crosshairsOptions[i] && axis) {
7141 .getPlotLinePath(point[i ? 'y' : 'x'], 1);
7142 if (crosshairs[i]) {
7143 crosshairs[i].attr({ d: path, visibility: VISIBLE });
7147 'stroke-width': crosshairsOptions[i].width || 1,
7148 stroke: crosshairsOptions[i].color || '#C0C0C0',
7149 zIndex: crosshairsOptions[i].zIndex || 2
7151 if (crosshairsOptions[i].dashStyle) {
7152 attribs.dashstyle = crosshairsOptions[i].dashStyle;
7154 crosshairs[i] = renderer.path(path)
7170 hideCrosshairs: hideCrosshairs,
7176 * The mouse tracker object
7177 * @param {Object} options
7179 function MouseTracker(options) {
7186 zoomType = optionsChart.zoomType,
7187 zoomX = /x/.test(zoomType),
7188 zoomY = /y/.test(zoomType),
7189 zoomHor = (zoomX && !inverted) || (zoomY && inverted),
7190 zoomVert = (zoomY && !inverted) || (zoomX && inverted);
7193 * Add crossbrowser support for chartX and chartY
7194 * @param {Object} e The event object in standard browsers
7196 function normalizeMouseEvent(e) {
7203 // common IE normalizing
7206 e.target = e.srcElement;
7209 // jQuery only copies over some properties. IE needs e.x and iOS needs touches.
7210 if (e.originalEvent) {
7211 e = e.originalEvent;
7214 // The same for MooTools. It renames e.pageX to e.page.x. #445.
7220 ePos = e.touches ? e.touches.item(0) : e;
7222 // get mouse position
7223 chartPosition = offset(container);
7224 chartPosLeft = chartPosition.left;
7225 chartPosTop = chartPosition.top;
7227 // chartX and chartY
7228 if (isIE) { // IE including IE9 that has pageX but in a different meaning
7232 chartX = ePos.pageX - chartPosLeft;
7233 chartY = ePos.pageY - chartPosTop;
7237 chartX: mathRound(chartX),
7238 chartY: mathRound(chartY)
7243 * Get the click position in terms of axis values.
7245 * @param {Object} e A mouse event
7247 function getMouseCoordinates(e) {
7252 each(axes, function (axis) {
7253 var translate = axis.translate,
7254 isXAxis = axis.isXAxis,
7255 isHorizontal = inverted ? !isXAxis : isXAxis;
7257 coordinates[isXAxis ? 'xAxis' : 'yAxis'].push({
7261 e.chartX - plotLeft :
7262 plotHeight - e.chartY + plotTop,
7271 * With line type charts with a single tracker, get the point closest to the mouse
7273 function onmousemove(e) {
7276 hoverPoint = chart.hoverPoint,
7277 hoverSeries = chart.hoverSeries,
7280 distance = chartWidth,
7281 index = inverted ? e.chartY : e.chartX - plotLeft; // wtf?
7284 if (tooltip && options.shared && !(hoverSeries && hoverSeries.noSharedTooltip)) {
7287 // loop over all series and find the ones with points closest to the mouse
7289 for (j = 0; j < i; j++) {
7290 if (series[j].visible &&
7291 series[j].options.enableMouseTracking !== false &&
7292 !series[j].noSharedTooltip && series[j].tooltipPoints.length) {
7293 point = series[j].tooltipPoints[index];
7294 point._dist = mathAbs(index - point.plotX);
7295 distance = mathMin(distance, point._dist);
7299 // remove furthest points
7302 if (points[i]._dist > distance) {
7303 points.splice(i, 1);
7306 // refresh the tooltip if necessary
7307 if (points.length && (points[0].plotX !== hoverX)) {
7308 tooltip.refresh(points);
7309 hoverX = points[0].plotX;
7313 // separate tooltip and general mouse events
7314 if (hoverSeries && hoverSeries.tracker) { // only use for line-type series with common tracker
7317 point = hoverSeries.tooltipPoints[index];
7319 // a new point is hovered, refresh the tooltip
7320 if (point && point !== hoverPoint) {
7322 // trigger the events
7323 point.onMouseOver();
7332 * Reset the tracking by hiding the tooltip, the hover series state and the hover point
7334 function resetTracker() {
7335 var hoverSeries = chart.hoverSeries,
7336 hoverPoint = chart.hoverPoint;
7339 hoverPoint.onMouseOut();
7343 hoverSeries.onMouseOut();
7348 tooltip.hideCrosshairs();
7355 * Mouse up or outside the plot area
7358 if (selectionMarker) {
7359 var selectionData = {
7363 selectionBox = selectionMarker.getBBox(),
7364 selectionLeft = selectionBox.x - plotLeft,
7365 selectionTop = selectionBox.y - plotTop;
7368 // a selection has been made
7371 // record each axis' min and max
7372 each(axes, function (axis) {
7373 if (axis.options.zoomEnabled !== false) {
7374 var translate = axis.translate,
7375 isXAxis = axis.isXAxis,
7376 isHorizontal = inverted ? !isXAxis : isXAxis,
7377 selectionMin = translate(
7380 plotHeight - selectionTop - selectionBox.height,
7386 selectionMax = translate(
7388 selectionLeft + selectionBox.width :
7389 plotHeight - selectionTop,
7396 selectionData[isXAxis ? 'xAxis' : 'yAxis'].push({
7398 min: mathMin(selectionMin, selectionMax), // for reversed axes,
7399 max: mathMax(selectionMin, selectionMax)
7403 fireEvent(chart, 'selection', selectionData, zoom);
7406 selectionMarker = selectionMarker.destroy();
7409 css(container, { cursor: 'auto' });
7411 chart.mouseIsDown = mouseIsDown = hasDragged = false;
7412 removeEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop);
7417 * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea.
7419 function hideTooltipOnMouseMove(e) {
7420 var pageX = defined(e.pageX) ? e.pageX : e.page.x, // In mootools the event is wrapped and the page x/y position is named e.page.x
7421 pageY = defined(e.pageX) ? e.pageY : e.page.y; // Ref: http://mootools.net/docs/core/Types/DOMEvent
7423 if (chartPosition &&
7424 !isInsidePlot(pageX - chartPosition.left - plotLeft,
7425 pageY - chartPosition.top - plotTop)) {
7431 * When mouse leaves the container, hide the tooltip.
7433 function hideTooltipOnMouseLeave() {
7435 chartPosition = null; // also reset the chart position, used in #149 fix
7439 * Set the JS events on the container element
7441 function setDOMEvents() {
7442 var lastWasOutsidePlot = true;
7444 * Record the starting position of a dragoperation
7446 container.onmousedown = function (e) {
7447 e = normalizeMouseEvent(e);
7449 // issue #295, dragging not always working in Firefox
7450 if (!hasTouch && e.preventDefault) {
7454 // record the start position
7455 chart.mouseIsDown = mouseIsDown = true;
7456 chart.mouseDownX = mouseDownX = e.chartX;
7457 mouseDownY = e.chartY;
7459 addEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop);
7462 // The mousemove, touchmove and touchstart event handler
7463 var mouseMove = function (e) {
7465 // let the system handle multitouch operations like two finger scroll
7467 if (e && e.touches && e.touches.length > 1) {
7472 e = normalizeMouseEvent(e);
7473 if (!hasTouch) { // not for touch devices
7474 e.returnValue = false;
7477 var chartX = e.chartX,
7479 isOutsidePlot = !isInsidePlot(chartX - plotLeft, chartY - plotTop);
7481 // on touch devices, only trigger click if a handler is defined
7482 if (hasTouch && e.type === 'touchstart') {
7483 if (attr(e.target, 'isTracker')) {
7484 if (!chart.runTrackerClick) {
7487 } else if (!runChartClick && !isOutsidePlot) {
7492 // cancel on mouse outside
7493 if (isOutsidePlot) {
7495 /*if (!lastWasOutsidePlot) {
7496 // reset the tracker
7500 // drop the selection if any and reset mouseIsDown and hasDragged
7502 if (chartX < plotLeft) {
7504 } else if (chartX > plotLeft + plotWidth) {
7505 chartX = plotLeft + plotWidth;
7508 if (chartY < plotTop) {
7510 } else if (chartY > plotTop + plotHeight) {
7511 chartY = plotTop + plotHeight;
7516 if (mouseIsDown && e.type !== 'touchstart') { // make selection
7518 // determine if the mouse has moved more than 10px
7519 hasDragged = Math.sqrt(
7520 Math.pow(mouseDownX - chartX, 2) +
7521 Math.pow(mouseDownY - chartY, 2)
7523 if (hasDragged > 10) {
7524 var clickedInside = isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop);
7527 if (hasCartesianSeries && (zoomX || zoomY) && clickedInside) {
7528 if (!selectionMarker) {
7529 selectionMarker = renderer.rect(
7532 zoomHor ? 1 : plotWidth,
7533 zoomVert ? 1 : plotHeight,
7537 fill: optionsChart.selectionMarkerFill || 'rgba(69,114,167,0.25)',
7544 // adjust the width of the selection marker
7545 if (selectionMarker && zoomHor) {
7546 var xSize = chartX - mouseDownX;
7547 selectionMarker.attr({
7548 width: mathAbs(xSize),
7549 x: (xSize > 0 ? 0 : xSize) + mouseDownX
7552 // adjust the height of the selection marker
7553 if (selectionMarker && zoomVert) {
7554 var ySize = chartY - mouseDownY;
7555 selectionMarker.attr({
7556 height: mathAbs(ySize),
7557 y: (ySize > 0 ? 0 : ySize) + mouseDownY
7562 if (clickedInside && !selectionMarker && optionsChart.panning) {
7567 } else if (!isOutsidePlot) {
7572 lastWasOutsidePlot = isOutsidePlot;
7574 // when outside plot, allow touch-drag by returning true
7575 return isOutsidePlot || !hasCartesianSeries;
7579 * When the mouse enters the container, run mouseMove
7581 container.onmousemove = mouseMove;
7584 * When the mouse leaves the container, hide the tracking (tooltip).
7586 addEvent(container, 'mouseleave', hideTooltipOnMouseLeave);
7588 // issue #149 workaround
7589 // The mouseleave event above does not always fire. Whenever the mouse is moving
7590 // outside the plotarea, hide the tooltip
7591 addEvent(doc, 'mousemove', hideTooltipOnMouseMove);
7593 container.ontouchstart = function (e) {
7594 // For touch devices, use touchmove to zoom
7595 if (zoomX || zoomY) {
7596 container.onmousedown(e);
7598 // Show tooltip and prevent the lower mouse pseudo event
7603 * Allow dragging the finger over the chart to read the values on touch
7606 container.ontouchmove = mouseMove;
7609 * Allow dragging the finger over the chart to read the values on touch
7612 container.ontouchend = function () {
7619 // MooTools 1.2.3 doesn't fire this in IE when using addEvent
7620 container.onclick = function (e) {
7621 var hoverPoint = chart.hoverPoint;
7622 e = normalizeMouseEvent(e);
7624 e.cancelBubble = true; // IE specific
7628 if (hoverPoint && attr(e.target, 'isTracker')) {
7629 var plotX = hoverPoint.plotX,
7630 plotY = hoverPoint.plotY;
7632 // add page position info
7633 extend(hoverPoint, {
7634 pageX: chartPosition.left + plotLeft +
7635 (inverted ? plotWidth - plotY : plotX),
7636 pageY: chartPosition.top + plotTop +
7637 (inverted ? plotHeight - plotX : plotY)
7640 // the series click event
7641 fireEvent(hoverPoint.series, 'click', extend(e, {
7645 // the point click event
7646 hoverPoint.firePointEvent('click', e);
7649 extend(e, getMouseCoordinates(e));
7651 // fire a click event in the chart
7652 if (isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {
7653 fireEvent(chart, 'click', e);
7659 // reset mouseIsDown and hasDragged
7666 * Destroys the MouseTracker object and disconnects DOM events.
7668 function destroy() {
7669 // Destroy the tracker group element
7670 if (chart.trackerGroup) {
7671 chart.trackerGroup = trackerGroup = chart.trackerGroup.destroy();
7674 removeEvent(container, 'mouseleave', hideTooltipOnMouseLeave);
7675 removeEvent(doc, 'mousemove', hideTooltipOnMouseMove);
7676 container.onclick = container.onmousedown = container.onmousemove = container.ontouchstart = container.ontouchend = container.ontouchmove = null;
7680 * Create the image map that listens for mouseovers
7682 placeTrackerGroup = function () {
7684 // first create - plot positions is not final at this stage
7685 if (!trackerGroup) {
7686 chart.trackerGroup = trackerGroup = renderer.g('tracker')
7687 .attr({ zIndex: 9 })
7690 // then position - this happens on load and after resizing and changing
7691 // axis or box positions
7693 trackerGroup.translate(plotLeft, plotTop);
7696 width: chart.plotWidth,
7697 height: chart.plotHeight
7705 placeTrackerGroup();
7706 if (options.enabled) {
7707 chart.tooltip = tooltip = Tooltip(options);
7709 // set the fixed interval ticking for the smooth tooltip
7710 tooltipInterval = setInterval(function () {
7719 // expose properties
7723 resetTracker: resetTracker,
7724 normalizeMouseEvent: normalizeMouseEvent,
7732 * The overview of the chart's series
7734 var Legend = function () {
7736 var options = chart.options.legend;
7738 if (!options.enabled) {
7742 var horizontal = options.layout === 'horizontal',
7743 symbolWidth = options.symbolWidth,
7744 symbolPadding = options.symbolPadding,
7746 style = options.style,
7747 itemStyle = options.itemStyle,
7748 itemHoverStyle = options.itemHoverStyle,
7749 itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle),
7750 padding = options.padding || pInt(style.padding),
7752 initialItemX = 4 + padding + symbolWidth + symbolPadding,
7757 itemMarginTop = options.itemMarginTop || 0,
7758 itemMarginBottom = options.itemMarginBottom || 0,
7760 legendBorderWidth = options.borderWidth,
7761 legendBackgroundColor = options.backgroundColor,
7764 widthOption = options.width,
7765 series = chart.series,
7766 reversedLegend = options.reversed;
7771 * Set the colors for the legend item
7772 * @param {Object} item A Series or Point instance
7773 * @param {Object} visible Dimmed or colored
7775 function colorizeItem(item, visible) {
7776 var legendItem = item.legendItem,
7777 legendLine = item.legendLine,
7778 legendSymbol = item.legendSymbol,
7779 hiddenColor = itemHiddenStyle.color,
7780 textColor = visible ? options.itemStyle.color : hiddenColor,
7781 symbolColor = visible ? item.color : hiddenColor;
7784 legendItem.css({ fill: textColor });
7787 legendLine.attr({ stroke: symbolColor });
7791 stroke: symbolColor,
7798 * Position the legend item
7799 * @param {Object} item A Series or Point instance
7800 * @param {Object} visible Dimmed or colored
7802 function positionItem(item, itemX, itemY) {
7803 var legendItem = item.legendItem,
7804 legendLine = item.legendLine,
7805 legendSymbol = item.legendSymbol,
7806 checkbox = item.checkbox;
7814 legendLine.translate(itemX, itemY - 4);
7818 x: itemX + legendSymbol.xOff,
7819 y: itemY + legendSymbol.yOff
7829 * Destroy a single legend item
7830 * @param {Object} item The series or point
7832 function destroyItem(item) {
7833 var checkbox = item.checkbox;
7835 // destroy SVG elements
7836 each(['legendItem', 'legendLine', 'legendSymbol'], function (key) {
7838 item[key].destroy();
7843 discardElement(item.checkbox);
7850 * Destroys the legend.
7852 function destroy() {
7854 box = box.destroy();
7858 legendGroup = legendGroup.destroy();
7863 * Position the checkboxes after the width is determined
7865 function positionCheckboxes() {
7866 each(allItems, function (item) {
7867 var checkbox = item.checkbox,
7868 alignAttr = legendGroup.alignAttr;
7871 left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 40) + PX,
7872 top: (alignAttr.translateY + checkbox.y - 11) + PX
7879 * Render a single specific legend item
7880 * @param {Object} item A series or point
7882 function renderItem(item) {
7890 li = item.legendItem,
7891 series = item.series || item,
7892 itemOptions = series.options,
7893 strokeWidth = (itemOptions && itemOptions.borderWidth) || 0;
7896 if (!li) { // generate it once, later move it
7898 // let these series types use a simple symbol
7899 simpleSymbol = /^(bar|pie|area|column)$/.test(series.type);
7901 // generate the list item text
7902 item.legendItem = li = renderer.text(
7903 options.labelFormatter.call(item),
7907 .css(item.visible ? itemStyle : itemHiddenStyle)
7908 .on('mouseover', function () {
7909 item.setState(HOVER_STATE);
7910 li.css(itemHoverStyle);
7912 .on('mouseout', function () {
7913 li.css(item.visible ? itemStyle : itemHiddenStyle);
7916 .on('click', function () {
7917 var strLegendItemClick = 'legendItemClick',
7918 fnLegendItemClick = function () {
7922 // click the name or symbol
7923 if (item.firePointEvent) { // point
7924 item.firePointEvent(strLegendItemClick, null, fnLegendItemClick);
7926 fireEvent(item, strLegendItemClick, null, fnLegendItemClick);
7929 .attr({ zIndex: 2 })
7933 if (!simpleSymbol && itemOptions && itemOptions.lineWidth) {
7935 'stroke-width': itemOptions.lineWidth,
7938 if (itemOptions.dashStyle) {
7939 attrs.dashstyle = itemOptions.dashStyle;
7941 item.legendLine = renderer.path([
7943 -symbolWidth - symbolPadding,
7953 // draw a simple symbol
7954 if (simpleSymbol) { // bar|pie|area|column
7956 legendSymbol = renderer.rect(
7957 (symbolX = -symbolWidth - symbolPadding),
7963 //'stroke-width': 0,
7965 }).add(legendGroup);
7966 } else if (itemOptions && itemOptions.marker && itemOptions.marker.enabled) { // draw the marker
7967 radius = itemOptions.marker.radius;
7968 legendSymbol = renderer.symbol(
7970 (symbolX = -symbolWidth / 2 - symbolPadding - radius),
7971 (symbolY = -4 - radius),
7975 .attr(item.pointAttr[NORMAL_STATE])
7976 .attr({ zIndex: 3 })
7981 legendSymbol.xOff = symbolX + (strokeWidth % 2 / 2);
7982 legendSymbol.yOff = symbolY + (strokeWidth % 2 / 2);
7985 item.legendSymbol = legendSymbol;
7987 // colorize the items
7988 colorizeItem(item, item.visible);
7991 // add the HTML checkbox on top
7992 if (itemOptions && itemOptions.showCheckbox) {
7993 item.checkbox = createElement('input', {
7995 checked: item.selected,
7996 defaultChecked: item.selected // required by IE7
7997 }, options.itemCheckboxStyle, container);
7999 addEvent(item.checkbox, 'click', function (event) {
8000 var target = event.target;
8001 fireEvent(item, 'checkboxClick', {
8002 checked: target.checked
8013 // calculate the positions for the next line
8014 bBox = li.getBBox();
8016 itemWidth = item.legendItemWidth =
8017 options.itemWidth || symbolWidth + symbolPadding + bBox.width + padding;
8018 itemHeight = bBox.height;
8020 // if the item exceeds the width, start a new line
8021 if (horizontal && itemX - initialItemX + itemWidth >
8022 (widthOption || (chartWidth - 2 * padding - initialItemX))) {
8023 itemX = initialItemX;
8024 itemY += itemMarginTop + itemHeight + itemMarginBottom;
8026 lastItemY = itemY + itemMarginBottom;
8028 // position the newly generated or reordered items
8029 positionItem(item, itemX, itemY);
8035 itemY += itemMarginTop + itemHeight + itemMarginBottom;
8038 // the width of the widest item
8039 offsetWidth = widthOption || mathMax(
8040 horizontal ? itemX - initialItemX : itemWidth,
8047 * Render the legend. This method can be called both before and after
8048 * chart.render. If called after, it will only rearrange items instead
8049 * of creating new ones.
8051 function renderLegend() {
8052 itemX = initialItemX;
8053 itemY = padding + itemMarginTop + y - 5; // 5 is the number of pixels above the text
8058 legendGroup = renderer.g('legend')
8059 .attr({ zIndex: 10 }) // in front of trackers, #414
8064 // add each series or point
8066 each(series, function (serie) {
8067 var seriesOptions = serie.options;
8069 if (!seriesOptions.showInLegend) {
8073 // use points or series for the legend item depending on legendType
8074 allItems = allItems.concat(
8075 serie.legendItems ||
8076 (seriesOptions.legendType === 'point' ?
8083 // sort by legendIndex
8084 stableSort(allItems, function (a, b) {
8085 return (a.options.legendIndex || 0) - (b.options.legendIndex || 0);
8089 if (reversedLegend) {
8094 each(allItems, renderItem);
8098 legendWidth = widthOption || offsetWidth;
8099 legendHeight = lastItemY - y + itemHeight;
8101 if (legendBorderWidth || legendBackgroundColor) {
8102 legendWidth += 2 * padding;
8103 legendHeight += 2 * padding;
8106 box = renderer.rect(
8111 options.borderRadius,
8112 legendBorderWidth || 0
8114 stroke: options.borderColor,
8115 'stroke-width': legendBorderWidth || 0,
8116 fill: legendBackgroundColor || NONE
8119 .shadow(options.shadow);
8122 } else if (legendWidth > 0 && legendHeight > 0) {
8123 box[box.isNew ? 'attr' : 'animate'](
8124 box.crisp(null, null, null, legendWidth, legendHeight)
8129 // hide the border if no items
8130 box[allItems.length ? 'show' : 'hide']();
8133 // 1.x compatibility: positioning based on style
8134 var props = ['left', 'right', 'top', 'bottom'],
8139 if (style[prop] && style[prop] !== 'auto') {
8140 options[i < 2 ? 'align' : 'verticalAlign'] = prop;
8141 options[i < 2 ? 'x' : 'y'] = pInt(style[prop]) * (i % 2 ? -1 : 1);
8145 if (allItems.length) {
8146 legendGroup.align(extend(options, {
8148 height: legendHeight
8149 }), true, spacingBox);
8153 positionCheckboxes();
8162 addEvent(chart, 'endResize', positionCheckboxes);
8166 colorizeItem: colorizeItem,
8167 destroyItem: destroyItem,
8168 renderLegend: renderLegend,
8179 * Initialize an individual series, called internally before render time
8181 function initSeries(options) {
8182 var type = options.type || optionsChart.type || optionsChart.defaultSeriesType,
8183 typeClass = seriesTypes[type],
8185 hasRendered = chart.hasRendered;
8187 // an inverted chart can't take a column series and vice versa
8189 if (inverted && type === 'column') {
8190 typeClass = seriesTypes.bar;
8191 } else if (!inverted && type === 'bar') {
8192 typeClass = seriesTypes.column;
8196 serie = new typeClass();
8198 serie.init(chart, options);
8200 // set internal chart properties
8201 if (!hasRendered && serie.inverted) {
8204 if (serie.isCartesian) {
8205 hasCartesianSeries = serie.isCartesian;
8214 * Add a series dynamically after time
8216 * @param {Object} options The config options
8217 * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true.
8218 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
8221 * @return {Object} series The newly created series object
8223 function addSeries(options, redraw, animation) {
8227 setAnimation(animation, chart);
8228 redraw = pick(redraw, true); // defaults to true
8230 fireEvent(chart, 'addSeries', { options: options }, function () {
8231 series = initSeries(options);
8232 series.isDirty = true;
8234 chart.isDirtyLegend = true; // the series array is out of sync with the display
8245 * Check whether a given point is within the plot area
8247 * @param {Number} x Pixel x relative to the plot area
8248 * @param {Number} y Pixel y relative to the plot area
8250 isInsidePlot = function (x, y) {
8258 * Adjust all axes tick amounts
8260 function adjustTickAmounts() {
8261 if (optionsChart.alignTicks !== false) {
8262 each(axes, function (axis) {
8263 axis.adjustTickAmount();
8270 * Redraw legend, axes or series based on updated data
8272 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
8275 function redraw(animation) {
8276 var redrawLegend = chart.isDirtyLegend,
8278 isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed?
8279 seriesLength = series.length,
8281 clipRect = chart.clipRect,
8284 setAnimation(animation, chart);
8286 // link stacked series
8289 if (serie.isDirty && serie.options.stacking) {
8290 hasStackedSeries = true;
8294 if (hasStackedSeries) { // mark others as dirty
8298 if (serie.options.stacking) {
8299 serie.isDirty = true;
8304 // handle updated data in the series
8305 each(series, function (serie) {
8306 if (serie.isDirty) { // prepare the data so axis can read it
8307 if (serie.options.legendType === 'point') {
8308 redrawLegend = true;
8313 // handle added or removed series
8314 if (redrawLegend && legend.renderLegend) { // series or pie points are added or removed
8315 // draw legend graphics
8316 legend.renderLegend();
8318 chart.isDirtyLegend = false;
8322 if (hasCartesianSeries) {
8329 each(axes, function (axis) {
8333 adjustTickAmounts();
8337 each(axes, function (axis) {
8338 fireEvent(axis, 'afterSetExtremes', axis.getExtremes()); // #747, #751
8347 // the plot areas size has changed
8350 placeTrackerGroup();
8355 clipRect.animate({ // for chart resize
8356 width: chart.plotSizeX,
8357 height: chart.plotSizeY + 1
8364 // redraw affected series
8365 each(series, function (serie) {
8366 if (serie.isDirty && serie.visible &&
8367 (!serie.isCartesian || serie.xAxis)) { // issue #153
8373 // hide tooltip and hover states
8374 if (tracker && tracker.resetTracker) {
8375 tracker.resetTracker();
8379 fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw
8385 * Dim the chart and show a loading text or symbol
8386 * @param {String} str An optional text to show in the loading label instead of the default one
8388 function showLoading(str) {
8389 var loadingOptions = options.loading;
8391 // create the layer at the first call
8393 loadingDiv = createElement(DIV, {
8394 className: PREFIX + 'loading'
8395 }, extend(loadingOptions.style, {
8396 left: plotLeft + PX,
8398 width: plotWidth + PX,
8399 height: plotHeight + PX,
8404 loadingSpan = createElement(
8407 loadingOptions.labelStyle,
8414 loadingSpan.innerHTML = str || options.lang.loading;
8417 if (!loadingShown) {
8418 css(loadingDiv, { opacity: 0, display: '' });
8419 animate(loadingDiv, {
8420 opacity: loadingOptions.style.opacity
8422 duration: loadingOptions.showDuration || 0
8424 loadingShown = true;
8428 * Hide the loading layer
8430 function hideLoading() {
8432 animate(loadingDiv, {
8435 duration: options.loading.hideDuration || 100,
8436 complete: function () {
8437 css(loadingDiv, { display: NONE });
8441 loadingShown = false;
8445 * Get an axis, series or point object by id.
8446 * @param id {String} The id as given in the configuration options
8454 for (i = 0; i < axes.length; i++) {
8455 if (axes[i].options.id === id) {
8461 for (i = 0; i < series.length; i++) {
8462 if (series[i].options.id === id) {
8468 for (i = 0; i < series.length; i++) {
8469 points = series[i].points || [];
8470 for (j = 0; j < points.length; j++) {
8471 if (points[j].id === id) {
8480 * Create the Axis instances based on the config options
8482 function getAxes() {
8483 var xAxisOptions = options.xAxis || {},
8484 yAxisOptions = options.yAxis || {},
8488 // make sure the options are arrays and add some members
8489 xAxisOptions = splat(xAxisOptions);
8490 each(xAxisOptions, function (axis, i) {
8495 yAxisOptions = splat(yAxisOptions);
8496 each(yAxisOptions, function (axis, i) {
8500 // concatenate all axis options into one array
8501 optionsArray = xAxisOptions.concat(yAxisOptions);
8503 each(optionsArray, function (axisOptions) {
8504 axis = new Axis(axisOptions);
8507 adjustTickAmounts();
8512 * Get the currently selected points from all series
8514 function getSelectedPoints() {
8516 each(series, function (serie) {
8517 points = points.concat(grep(serie.points, function (point) {
8518 return point.selected;
8525 * Get the currently selected series
8527 function getSelectedSeries() {
8528 return grep(series, function (serie) {
8529 return serie.selected;
8534 * Display the zoom button
8536 function showResetZoom() {
8537 var lang = defaultOptions.lang,
8538 btnOptions = optionsChart.resetZoomButton,
8539 box = btnOptions.relativeTo === 'plot' && {
8546 chart.resetZoomButton = renderer.button(lang.resetZoom, null, null, zoomOut, btnOptions.theme)
8548 align: btnOptions.position.align,
8549 title: lang.resetZoomTitle
8552 .align(btnOptions.position, false, box);
8558 zoomOut = function () {
8559 var resetZoomButton = chart.resetZoomButton;
8561 fireEvent(chart, 'selection', { resetSelection: true }, zoom);
8562 if (resetZoomButton) {
8563 chart.resetZoomButton = resetZoomButton.destroy();
8567 * Zoom into a given portion of the chart given by axis coordinates
8568 * @param {Object} event
8570 zoom = function (event) {
8572 // add button to reset selection
8573 var animate = chart.pointCount < 100,
8576 if (chart.resetZoomEnabled !== false && !chart.resetZoomButton) { // hook for Stock charts etc.
8580 // if zoom is called with no arguments, reset the axes
8581 if (!event || event.resetSelection) {
8582 each(axes, function (axis) {
8583 if (axis.options.zoomEnabled !== false) {
8584 axis.setExtremes(null, null, false);
8588 } else { // else, zoom in on all axes
8589 each(event.xAxis.concat(event.yAxis), function (axisData) {
8590 var axis = axisData.axis;
8592 // don't zoom more than minRange
8593 if (chart.tracker[axis.isXAxis ? 'zoomX' : 'zoomY']) {
8594 axis.setExtremes(axisData.min, axisData.max, false);
8602 redraw(true, animate);
8607 * Pan the chart by dragging the mouse across the pane. This function is called
8608 * on mouse move, and the distance to pan is computed from chartX compared to
8609 * the first chartX position in the dragging operation.
8611 chart.pan = function (chartX) {
8613 var xAxis = chart.xAxis[0],
8614 mouseDownX = chart.mouseDownX,
8615 halfPointRange = xAxis.pointRange / 2,
8616 extremes = xAxis.getExtremes(),
8617 newMin = xAxis.translate(mouseDownX - chartX, true) + halfPointRange,
8618 newMax = xAxis.translate(mouseDownX + plotWidth - chartX, true) - halfPointRange,
8619 hoverPoints = chart.hoverPoints;
8621 // remove active points for shared tooltip
8623 each(hoverPoints, function (point) {
8628 if (newMin > mathMin(extremes.dataMin, extremes.min) && newMax < mathMax(extremes.dataMax, extremes.max)) {
8629 xAxis.setExtremes(newMin, newMax, true, false);
8632 chart.mouseDownX = chartX; // set new reference for next run
8633 css(container, { cursor: 'move' });
8637 * Show the title and subtitle of the chart
8639 * @param titleOptions {Object} New title options
8640 * @param subtitleOptions {Object} New subtitle options
8643 function setTitle(titleOptions, subtitleOptions) {
8645 chartTitleOptions = merge(options.title, titleOptions);
8646 chartSubtitleOptions = merge(options.subtitle, subtitleOptions);
8648 // add title and subtitle
8650 ['title', titleOptions, chartTitleOptions],
8651 ['subtitle', subtitleOptions, chartSubtitleOptions]
8654 title = chart[name],
8655 titleOptions = arr[1],
8656 chartTitleOptions = arr[2];
8658 if (title && titleOptions) {
8659 title = title.destroy(); // remove old
8661 if (chartTitleOptions && chartTitleOptions.text && !title) {
8662 chart[name] = renderer.text(
8663 chartTitleOptions.text,
8666 chartTitleOptions.useHTML
8669 align: chartTitleOptions.align,
8670 'class': PREFIX + name,
8673 .css(chartTitleOptions.style)
8675 .align(chartTitleOptions, false, spacingBox);
8682 * Get chart width and height according to options and container size
8684 function getChartSize() {
8686 containerWidth = (renderToClone || renderTo).offsetWidth;
8687 containerHeight = (renderToClone || renderTo).offsetHeight;
8688 chart.chartWidth = chartWidth = optionsChart.width || containerWidth || 600;
8689 chart.chartHeight = chartHeight = optionsChart.height ||
8690 // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7:
8691 (containerHeight > 19 ? containerHeight : 400);
8696 * Get the containing element, determine the size and create the inner container
8697 * div to hold the chart
8699 function getContainer() {
8700 renderTo = optionsChart.renderTo;
8701 containerId = PREFIX + idCounter++;
8703 if (isString(renderTo)) {
8704 renderTo = doc.getElementById(renderTo);
8707 // remove previous chart
8708 renderTo.innerHTML = '';
8710 // If the container doesn't have an offsetWidth, it has or is a child of a node
8711 // that has display:none. We need to temporarily move it out to a visible
8712 // state to determine the size, else the legend and tooltips won't render
8714 if (!renderTo.offsetWidth) {
8715 renderToClone = renderTo.cloneNode(0);
8716 css(renderToClone, {
8721 doc.body.appendChild(renderToClone);
8724 // get the width and height
8727 // create the inner container
8728 chart.container = container = createElement(DIV, {
8729 className: PREFIX + 'container' +
8730 (optionsChart.className ? ' ' + optionsChart.className : ''),
8734 overflow: HIDDEN, // needed for context menu (avoid scrollbars) and
8735 // content overflow in IE
8736 width: chartWidth + PX,
8737 height: chartHeight + PX,
8739 lineHeight: 'normal' // #427
8740 }, optionsChart.style),
8741 renderToClone || renderTo
8744 chart.renderer = renderer =
8745 optionsChart.forExport ? // force SVG, used for SVG export
8746 new SVGRenderer(container, chartWidth, chartHeight, true) :
8747 new Renderer(container, chartWidth, chartHeight);
8749 // Issue 110 workaround:
8750 // In Firefox, if a div is positioned by percentage, its pixel position may land
8751 // between pixels. The container itself doesn't display this, but an SVG element
8752 // inside this container will be drawn at subpixel precision. In order to draw
8753 // sharp lines, this must be compensated for. This doesn't seem to work inside
8754 // iframes though (like in jsFiddle).
8755 var subPixelFix, rect;
8756 if (isFirefox && container.getBoundingClientRect) {
8757 subPixelFix = function () {
8758 css(container, { left: 0, top: 0 });
8759 rect = container.getBoundingClientRect();
8761 left: (-(rect.left - pInt(rect.left))) + PX,
8762 top: (-(rect.top - pInt(rect.top))) + PX
8770 addEvent(win, 'resize', subPixelFix);
8772 // remove it on chart destroy
8773 addEvent(chart, 'destroy', function () {
8774 removeEvent(win, 'resize', subPixelFix);
8780 * Calculate margins by rendering axis labels in a preliminary position. Title,
8781 * subtitle and legend have already been rendered at this stage, but will be
8782 * moved into their final positions
8784 getMargins = function () {
8785 var legendOptions = options.legend,
8786 legendMargin = pick(legendOptions.margin, 10),
8787 legendX = legendOptions.x,
8788 legendY = legendOptions.y,
8789 align = legendOptions.align,
8790 verticalAlign = legendOptions.verticalAlign,
8795 // adjust for title and subtitle
8796 if ((chart.title || chart.subtitle) && !defined(optionsMarginTop)) {
8797 titleOffset = mathMax(
8798 (chart.title && !chartTitleOptions.floating && !chartTitleOptions.verticalAlign && chartTitleOptions.y) || 0,
8799 (chart.subtitle && !chartSubtitleOptions.floating && !chartSubtitleOptions.verticalAlign && chartSubtitleOptions.y) || 0
8802 plotTop = mathMax(plotTop, titleOffset + pick(chartTitleOptions.margin, 15) + spacingTop);
8805 // adjust for legend
8806 if (legendOptions.enabled && !legendOptions.floating) {
8807 if (align === 'right') { // horizontal alignment handled first
8808 if (!defined(optionsMarginRight)) {
8809 marginRight = mathMax(
8811 legendWidth - legendX + legendMargin + spacingRight
8814 } else if (align === 'left') {
8815 if (!defined(optionsMarginLeft)) {
8818 legendWidth + legendX + legendMargin + spacingLeft
8822 } else if (verticalAlign === 'top') {
8823 if (!defined(optionsMarginTop)) {
8826 legendHeight + legendY + legendMargin + spacingTop
8830 } else if (verticalAlign === 'bottom') {
8831 if (!defined(optionsMarginBottom)) {
8832 marginBottom = mathMax(
8834 legendHeight - legendY + legendMargin + spacingBottom
8840 // adjust for scroller
8841 if (chart.extraBottomMargin) {
8842 marginBottom += chart.extraBottomMargin;
8844 if (chart.extraTopMargin) {
8845 plotTop += chart.extraTopMargin;
8848 // pre-render axes to get labels offset width
8849 if (hasCartesianSeries) {
8850 each(axes, function (axis) {
8855 if (!defined(optionsMarginLeft)) {
8856 plotLeft += axisOffset[3];
8858 if (!defined(optionsMarginTop)) {
8859 plotTop += axisOffset[0];
8861 if (!defined(optionsMarginBottom)) {
8862 marginBottom += axisOffset[2];
8864 if (!defined(optionsMarginRight)) {
8865 marginRight += axisOffset[1];
8873 * Add the event handlers necessary for auto resizing
8876 function initReflow() {
8878 function reflow(e) {
8879 var width = optionsChart.width || renderTo.offsetWidth,
8880 height = optionsChart.height || renderTo.offsetHeight,
8883 // Width and height checks for display:none. Target is doc in IE8 and Opera,
8884 // win in Firefox, Chrome and IE9.
8885 if (width && height && (target === win || target === doc)) {
8887 if (width !== containerWidth || height !== containerHeight) {
8888 clearTimeout(reflowTimeout);
8889 reflowTimeout = setTimeout(function () {
8890 resize(width, height, false);
8893 containerWidth = width;
8894 containerHeight = height;
8897 addEvent(win, 'resize', reflow);
8898 addEvent(chart, 'destroy', function () {
8899 removeEvent(win, 'resize', reflow);
8904 * Fires endResize event on chart instance.
8906 function fireEndResize() {
8908 fireEvent(chart, 'endResize', null, function () {
8915 * Resize the chart to a given width and height
8916 * @param {Number} width
8917 * @param {Number} height
8918 * @param {Object|Boolean} animation
8920 resize = function (width, height, animation) {
8921 var chartTitle = chart.title,
8922 chartSubtitle = chart.subtitle;
8926 // set the animation for the current process
8927 setAnimation(animation, chart);
8929 oldChartHeight = chartHeight;
8930 oldChartWidth = chartWidth;
8931 if (defined(width)) {
8932 chart.chartWidth = chartWidth = mathRound(width);
8934 if (defined(height)) {
8935 chart.chartHeight = chartHeight = mathRound(height);
8939 width: chartWidth + PX,
8940 height: chartHeight + PX
8942 renderer.setSize(chartWidth, chartHeight, animation);
8944 // update axis lengths for more correct tick intervals:
8945 plotWidth = chartWidth - plotLeft - marginRight;
8946 plotHeight = chartHeight - plotTop - marginBottom;
8950 each(axes, function (axis) {
8951 axis.isDirty = true;
8955 // make sure non-cartesian series are also handled
8956 each(series, function (serie) {
8957 serie.isDirty = true;
8960 chart.isDirtyLegend = true; // force legend redraw
8961 chart.isDirtyBox = true; // force redraw of plot and chart border
8967 chartTitle.align(null, null, spacingBox);
8969 if (chartSubtitle) {
8970 chartSubtitle.align(null, null, spacingBox);
8976 oldChartHeight = null;
8977 fireEvent(chart, 'resize');
8979 // fire endResize and set isResizing back
8980 // If animation is disabled, fire without delay
8981 if (globalAnimation === false) {
8983 } else { // else set a timeout with the animation duration
8984 setTimeout(fireEndResize, (globalAnimation && globalAnimation.duration) || 500);
8989 * Set the public chart properties. This is done before and after the pre-render
8990 * to determine margin sizes
8992 setChartSize = function () {
8994 chart.plotLeft = plotLeft = mathRound(plotLeft);
8995 chart.plotTop = plotTop = mathRound(plotTop);
8996 chart.plotWidth = plotWidth = mathRound(chartWidth - plotLeft - marginRight);
8997 chart.plotHeight = plotHeight = mathRound(chartHeight - plotTop - marginBottom);
8999 chart.plotSizeX = inverted ? plotHeight : plotWidth;
9000 chart.plotSizeY = inverted ? plotWidth : plotHeight;
9005 width: chartWidth - spacingLeft - spacingRight,
9006 height: chartHeight - spacingTop - spacingBottom
9009 each(axes, function (axis) {
9011 axis.setAxisTranslation();
9016 * Initial margins before auto size margins are applied
9018 resetMargins = function () {
9019 plotTop = pick(optionsMarginTop, spacingTop);
9020 marginRight = pick(optionsMarginRight, spacingRight);
9021 marginBottom = pick(optionsMarginBottom, spacingBottom);
9022 plotLeft = pick(optionsMarginLeft, spacingLeft);
9023 axisOffset = [0, 0, 0, 0]; // top, right, bottom, left
9027 * Draw the borders and backgrounds for chart and plot area
9029 drawChartBox = function () {
9030 var chartBorderWidth = optionsChart.borderWidth || 0,
9031 chartBackgroundColor = optionsChart.backgroundColor,
9032 plotBackgroundColor = optionsChart.plotBackgroundColor,
9033 plotBackgroundImage = optionsChart.plotBackgroundImage,
9043 mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);
9045 if (chartBorderWidth || chartBackgroundColor) {
9046 if (!chartBackground) {
9047 chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn,
9048 optionsChart.borderRadius, chartBorderWidth)
9050 stroke: optionsChart.borderColor,
9051 'stroke-width': chartBorderWidth,
9052 fill: chartBackgroundColor || NONE
9055 .shadow(optionsChart.shadow);
9057 chartBackground.animate(
9058 chartBackground.crisp(null, null, null, chartWidth - mgn, chartHeight - mgn)
9065 if (plotBackgroundColor) {
9066 if (!plotBackground) {
9067 plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0)
9069 fill: plotBackgroundColor
9072 .shadow(optionsChart.plotShadow);
9074 plotBackground.animate(plotSize);
9077 if (plotBackgroundImage) {
9079 plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)
9082 plotBGImage.animate(plotSize);
9087 if (optionsChart.plotBorderWidth) {
9089 plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, optionsChart.plotBorderWidth)
9091 stroke: optionsChart.plotBorderColor,
9092 'stroke-width': optionsChart.plotBorderWidth,
9098 plotBorder.crisp(null, plotLeft, plotTop, plotWidth, plotHeight)
9104 chart.isDirtyBox = false;
9108 * Detect whether the chart is inverted, either by setting the chart.inverted option
9109 * or adding a bar series to the configuration options
9111 function setInverted() {
9114 inverted || // it is set before
9115 optionsChart.inverted ||
9116 optionsChart.type === BAR || // default series type
9117 optionsChart.defaultSeriesType === BAR // backwards compatible
9119 seriesOptions = options.series,
9120 i = seriesOptions && seriesOptions.length;
9122 // check if a bar series is present in the config options
9123 while (!isInverted && i--) {
9124 if (seriesOptions[i].type === BAR) {
9129 // set the chart property and the chart scope variable
9130 chart.inverted = inverted = isInverted;
9134 * Render all graphics for the chart
9137 var labels = options.labels,
9138 credits = options.credits,
9146 legend = chart.legend = new Legend();
9148 // Get margins by pre-rendering axes
9150 each(axes, function (axis) {
9154 each(axes, function (axis) {
9155 axis.setTickPositions(true); // update to reflect the new margins
9157 adjustTickAmounts();
9158 getMargins(); // second pass to check for new labels
9161 // Draw the borders and backgrounds
9165 if (hasCartesianSeries) {
9166 each(axes, function (axis) {
9173 if (!chart.seriesGroup) {
9174 chart.seriesGroup = renderer.g('series-group')
9175 .attr({ zIndex: 3 })
9178 each(series, function (serie) {
9180 serie.setTooltipPoints();
9187 each(labels.items, function () {
9188 var style = extend(labels.style, this.style),
9189 x = pInt(style.left) + plotLeft,
9190 y = pInt(style.top) + plotTop + 12;
9192 // delete to prevent rewriting in IE
9201 .attr({ zIndex: 2 })
9209 if (credits.enabled && !chart.credits) {
9210 creditsHref = credits.href;
9211 chart.credits = renderer.text(
9216 .on('click', function () {
9218 location.href = creditsHref;
9222 align: credits.position.align,
9227 .align(credits.position);
9230 placeTrackerGroup();
9233 chart.hasRendered = true;
9235 // If the chart was rendered outside the top container, put it back in
9236 if (renderToClone) {
9237 renderTo.appendChild(container);
9238 discardElement(renderToClone);
9239 //updatePosition(container);
9244 * Clean up memory usage
9246 function destroy() {
9248 parentNode = container && container.parentNode;
9250 // If the chart is destroyed already, do nothing.
9251 // This will happen if if a script invokes chart.destroy and
9252 // then it will be called again on win.unload
9253 if (chart === null) {
9257 // fire the chart.destoy event
9258 fireEvent(chart, 'destroy');
9263 // ==== Destroy collections:
9267 axes[i] = axes[i].destroy();
9270 // Destroy each series
9273 series[i] = series[i].destroy();
9276 // ==== Destroy chart properties:
9277 each(['title', 'subtitle', 'seriesGroup', 'clipRect', 'credits', 'tracker', 'scroller', 'rangeSelector'], function (name) {
9278 var prop = chart[name];
9281 chart[name] = prop.destroy();
9285 // ==== Destroy local variables:
9286 each([chartBackground, plotBorder, plotBackground, legend, tooltip, renderer, tracker], function (obj) {
9287 if (obj && obj.destroy) {
9291 chartBackground = plotBorder = plotBackground = legend = tooltip = renderer = tracker = null;
9293 // remove container and all SVG
9294 if (container) { // can break in IE when destroyed before finished loading
9295 container.innerHTML = '';
9296 removeEvent(container);
9298 discardElement(container);
9305 // memory and CPU leak
9306 clearInterval(tooltipInterval);
9317 * Prepare for first rendering after all data are loaded
9319 function firstRender() {
9321 // VML namespaces can't be added until after complete. Listening
9322 // for Perini's doScroll hack is not enough.
9323 var ONREADYSTATECHANGE = 'onreadystatechange',
9324 COMPLETE = 'complete';
9325 // Note: in spite of JSLint's complaints, win == win.top is required
9326 /*jslint eqeq: true*/
9327 if (!hasSVG && win == win.top && doc.readyState !== COMPLETE) {
9328 /*jslint eqeq: false*/
9329 doc.attachEvent(ONREADYSTATECHANGE, function () {
9330 doc.detachEvent(ONREADYSTATECHANGE, firstRender);
9331 if (doc.readyState === COMPLETE) {
9338 // create the container
9341 // Run an early event after the container and renderer are established
9342 fireEvent(chart, 'init');
9344 // Initialize range selector for stock charts
9345 if (Highcharts.RangeSelector && options.rangeSelector.enabled) {
9346 chart.rangeSelector = new Highcharts.RangeSelector(chart);
9352 // Set the common inversion and transformation for inverted series after initSeries
9358 // Initialize the series
9359 each(options.series || [], function (serieOptions) {
9360 initSeries(serieOptions);
9363 // Run an event where series and axes can be added
9364 //fireEvent(chart, 'beforeRender');
9366 // Initialize scroller for stock charts
9367 if (Highcharts.Scroller && (options.navigator.enabled || options.scrollbar.enabled)) {
9368 chart.scroller = new Highcharts.Scroller(chart);
9371 chart.render = render;
9373 // depends on inverted and on margins being set
9374 chart.tracker = tracker = new MouseTracker(options.tooltip);
9381 callback.apply(chart, [chart]);
9383 each(chart.callbacks, function (fn) {
9384 fn.apply(chart, [chart]);
9387 fireEvent(chart, 'load');
9393 // Set up auto resize
9394 if (optionsChart.reflow !== false) {
9395 addEvent(chart, 'load', initReflow);
9398 // Chart event handlers
9400 for (eventType in chartEvents) {
9401 addEvent(chart, eventType, chartEvents[eventType]);
9406 chart.options = options;
9407 chart.series = series;
9416 // Expose methods and variables
9417 chart.addSeries = addSeries;
9418 chart.animation = pick(optionsChart.animation, true);
9420 chart.destroy = destroy;
9422 chart.getSelectedPoints = getSelectedPoints;
9423 chart.getSelectedSeries = getSelectedSeries;
9424 chart.hideLoading = hideLoading;
9425 chart.initSeries = initSeries;
9426 chart.isInsidePlot = isInsidePlot;
9427 chart.redraw = redraw;
9428 chart.setSize = resize;
9429 chart.setTitle = setTitle;
9430 chart.showLoading = showLoading;
9431 chart.pointCount = 0;
9432 chart.counters = new ChartCounters();
9434 if ($) $(function () {
9435 $container = $('#container');
9439 $('<button>+</button>')
9440 .insertBefore($container)
9441 .click(function () {
9442 if (origChartWidth === UNDEFINED) {
9443 origChartWidth = chartWidth;
9444 origChartHeight = chartHeight;
9446 chart.resize(chartWidth *= 1.1, chartHeight *= 1.1);
9448 $('<button>-</button>')
9449 .insertBefore($container)
9450 .click(function () {
9451 if (origChartWidth === UNDEFINED) {
9452 origChartWidth = chartWidth;
9453 origChartHeight = chartHeight;
9455 chart.resize(chartWidth *= 0.9, chartHeight *= 0.9);
9457 $('<button>1:1</button>')
9458 .insertBefore($container)
9459 .click(function () {
9460 if (origChartWidth === UNDEFINED) {
9461 origChartWidth = chartWidth;
9462 origChartHeight = chartHeight;
9464 chart.resize(origChartWidth, origChartHeight);
9478 // Hook for exporting module
9479 Chart.prototype.callbacks = [];
9481 * The Point object and prototype. Inheritable and used as base for PiePoint
9483 var Point = function () {};
9487 * Initialize the point
9488 * @param {Object} series The series object containing this point
9489 * @param {Object} options The data in either number, array or object format
9491 init: function (series, options, x) {
9493 counters = series.chart.counters,
9495 point.series = series;
9496 point.applyOptions(options, x);
9497 point.pointAttr = {};
9499 if (series.options.colorByPoint) {
9500 defaultColors = series.chart.options.colors;
9501 if (!point.options) {
9504 point.color = point.options.color = point.color || defaultColors[counters.color++];
9506 // loop back to zero
9507 counters.wrapColor(defaultColors.length);
9510 series.chart.pointCount++;
9514 * Apply the options containing the x and y data and possible some extra properties.
9515 * This is called on point init or from point.update.
9517 * @param {Object} options
9519 applyOptions: function (options, x) {
9521 series = point.series,
9522 optionsType = typeof options;
9524 point.config = options;
9526 // onedimensional array input
9527 if (optionsType === 'number' || options === null) {
9529 } else if (typeof options[0] === 'number') { // two-dimentional array
9530 point.x = options[0];
9531 point.y = options[1];
9532 } else if (optionsType === 'object' && typeof options.length !== 'number') { // object input
9533 // copy options directly to point
9534 extend(point, options);
9535 point.options = options;
9536 } else if (typeof options[0] === 'string') { // categorized data with name in first position
9537 point.name = options[0];
9538 point.y = options[1];
9542 * If no x is set by now, get auto incremented value. All points must have an
9543 * x value, however the y value can be null to create a gap in the series
9546 // todo: skip this? It is only used in applyOptions, in translate it should not be used
9547 if (point.x === UNDEFINED) {
9548 point.x = x === UNDEFINED ? series.autoIncrement() : x;
9554 * Destroy a point to clear memory. Its reference still stays in series.data.
9556 destroy: function () {
9558 series = point.series,
9559 hoverPoints = series.chart.hoverPoints,
9562 series.chart.pointCount--;
9566 erase(hoverPoints, point);
9568 if (point === series.chart.hoverPoint) {
9571 series.chart.hoverPoints = null;
9573 // remove all events
9574 if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive
9576 point.destroyElements();
9579 if (point.legendItem) { // pies have legend items
9580 point.series.chart.legend.destroyItem(point);
9583 for (prop in point) {
9591 * Destroy SVG elements associated with the point
9593 destroyElements: function () {
9595 props = ['graphic', 'tracker', 'dataLabel', 'group', 'connector', 'shadowGroup'],
9601 point[prop] = point[prop].destroy();
9607 * Return the configuration hash needed for the data label and tooltip formatters
9609 getLabelConfig: function () {
9614 key: point.name || point.category,
9615 series: point.series,
9617 percentage: point.percentage,
9618 total: point.total || point.stackTotal
9623 * Toggle the selection status of a point
9624 * @param {Boolean} selected Whether to select or unselect the point.
9625 * @param {Boolean} accumulate Whether to add to the previous selection. By default,
9626 * this happens if the control key (Cmd on Mac) was pressed during clicking.
9628 select: function (selected, accumulate) {
9630 series = point.series,
9631 chart = series.chart;
9633 selected = pick(selected, !point.selected);
9635 // fire the event with the defalut handler
9636 point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () {
9637 point.selected = selected;
9638 point.setState(selected && SELECT_STATE);
9640 // unselect all other points unless Ctrl or Cmd + click
9642 each(chart.getSelectedPoints(), function (loopPoint) {
9643 if (loopPoint.selected && loopPoint !== point) {
9644 loopPoint.selected = false;
9645 loopPoint.setState(NORMAL_STATE);
9646 loopPoint.firePointEvent('unselect');
9653 onMouseOver: function () {
9655 series = point.series,
9656 chart = series.chart,
9657 tooltip = chart.tooltip,
9658 hoverPoint = chart.hoverPoint;
9660 // set normal state to previous series
9661 if (hoverPoint && hoverPoint !== point) {
9662 hoverPoint.onMouseOut();
9665 // trigger the event
9666 point.firePointEvent('mouseOver');
9668 // update the tooltip
9669 if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
9670 tooltip.refresh(point);
9674 point.setState(HOVER_STATE);
9675 chart.hoverPoint = point;
9678 onMouseOut: function () {
9680 point.firePointEvent('mouseOut');
9683 point.series.chart.hoverPoint = null;
9687 * Extendable method for formatting each point's tooltip line
9689 * @return {String} A string to be concatenated in to the common tooltip text
9691 tooltipFormatter: function (pointFormat) {
9693 series = point.series,
9694 seriesTooltipOptions = series.tooltipOptions,
9695 split = String(point.y).split('.'),
9696 originalDecimals = split[1] ? split[1].length : 0,
9697 match = pointFormat.match(/\{(series|point)\.[a-zA-Z]+\}/g),
9704 // loop over the variables defined on the form {series.name}, {point.y} etc
9708 if (isString(key) && key !== pointFormat) { // IE matches more than just the variables
9709 obj = key.indexOf('point') === 1 ? point : series;
9711 if (key === '{point.y}') { // add some preformatting
9712 replacement = (seriesTooltipOptions.valuePrefix || seriesTooltipOptions.yPrefix || '') +
9713 numberFormat(point.y, pick(seriesTooltipOptions.valueDecimals, seriesTooltipOptions.yDecimals, originalDecimals)) +
9714 (seriesTooltipOptions.valueSuffix || seriesTooltipOptions.ySuffix || '');
9716 } else { // automatic replacement
9717 replacement = obj[match[i].split(splitter)[1]];
9720 pointFormat = pointFormat.replace(match[i], replacement);
9728 * Update the point with new options (typically x/y data) and optionally redraw the series.
9730 * @param {Object} options Point options as defined in the series.data array
9731 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
9732 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
9736 update: function (options, redraw, animation) {
9738 series = point.series,
9739 graphic = point.graphic,
9742 dataLength = data.length,
9743 chart = series.chart;
9745 redraw = pick(redraw, true);
9747 // fire the event with a default handler of doing the update
9748 point.firePointEvent('update', { options: options }, function () {
9750 point.applyOptions(options);
9753 if (isObject(options)) {
9754 series.getAttribs();
9756 graphic.attr(point.pointAttr[series.state]);
9760 // record changes in the parallel arrays
9761 for (i = 0; i < dataLength; i++) {
9762 if (data[i] === point) {
9763 series.xData[i] = point.x;
9764 series.yData[i] = point.y;
9765 series.options.data[i] = options;
9771 series.isDirty = true;
9772 series.isDirtyData = true;
9774 chart.redraw(animation);
9780 * Remove a point and optionally redraw the series and if necessary the axes
9781 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
9782 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
9785 remove: function (redraw, animation) {
9787 series = point.series,
9788 chart = series.chart,
9791 dataLength = data.length;
9793 setAnimation(animation, chart);
9794 redraw = pick(redraw, true);
9796 // fire the event with a default handler of removing the point
9797 point.firePointEvent('remove', null, function () {
9799 //erase(series.data, point);
9801 for (i = 0; i < dataLength; i++) {
9802 if (data[i] === point) {
9804 // splice all the parallel arrays
9806 series.options.data.splice(i, 1);
9807 series.xData.splice(i, 1);
9808 series.yData.splice(i, 1);
9817 series.isDirty = true;
9818 series.isDirtyData = true;
9828 * Fire an event on the Point object. Must not be renamed to fireEvent, as this
9829 * causes a name clash in MooTools
9830 * @param {String} eventType
9831 * @param {Object} eventArgs Additional event arguments
9832 * @param {Function} defaultFunction Default event handler
9834 firePointEvent: function (eventType, eventArgs, defaultFunction) {
9836 series = this.series,
9837 seriesOptions = series.options;
9839 // load event handlers on demand to save time on mouseover/out
9840 if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) {
9841 this.importEvents();
9844 // add default handler if in selection mode
9845 if (eventType === 'click' && seriesOptions.allowPointSelect) {
9846 defaultFunction = function (event) {
9847 // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
9848 point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
9852 fireEvent(this, eventType, eventArgs, defaultFunction);
9855 * Import events from the series' and point's options. Only do it on
9856 * demand, to save processing time on hovering.
9858 importEvents: function () {
9859 if (!this.hasImportedEvents) {
9861 options = merge(point.series.options.point, point.options),
9862 events = options.events,
9865 point.events = events;
9867 for (eventType in events) {
9868 addEvent(point, eventType, events[eventType]);
9870 this.hasImportedEvents = true;
9876 * Set the point's state
9877 * @param {String} state
9879 setState: function (state) {
9881 plotX = point.plotX,
9882 plotY = point.plotY,
9883 series = point.series,
9884 stateOptions = series.options.states,
9885 markerOptions = defaultPlotOptions[series.type].marker && series.options.marker,
9886 normalDisabled = markerOptions && !markerOptions.enabled,
9887 markerStateOptions = markerOptions && markerOptions.states[state],
9888 stateDisabled = markerStateOptions && markerStateOptions.enabled === false,
9889 stateMarkerGraphic = series.stateMarkerGraphic,
9890 chart = series.chart,
9892 pointAttr = point.pointAttr;
9894 state = state || NORMAL_STATE; // empty string
9897 // already has this state
9898 state === point.state ||
9899 // selected points don't respond to hover
9900 (point.selected && state !== SELECT_STATE) ||
9901 // series' state options is disabled
9902 (stateOptions[state] && stateOptions[state].enabled === false) ||
9903 // point marker's state options is disabled
9904 (state && (stateDisabled || (normalDisabled && !markerStateOptions.enabled)))
9910 // apply hover styles to the existing point
9911 if (point.graphic) {
9912 radius = point.graphic.symbolName && pointAttr[state].r;
9913 point.graphic.attr(merge(
9915 radius ? { // new symbol attributes (#507, #612)
9923 // if a graphic is not applied to each point in the normal state, create a shared
9924 // graphic for the hover state
9926 if (!stateMarkerGraphic) {
9927 radius = markerOptions.radius;
9928 series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
9935 .attr(pointAttr[state])
9939 stateMarkerGraphic.translate(
9945 if (stateMarkerGraphic) {
9946 stateMarkerGraphic[state ? 'show' : 'hide']();
9950 point.state = state;
9955 * @classDescription The base function which all other series types inherit from. The data in the series is stored
9956 * in various arrays.
9958 * - First, series.options.data contains all the original config options for
9959 * each point whether added by options or methods like series.addPoint.
9960 * - Next, series.data contains those values converted to points, but in case the series data length
9961 * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It
9962 * only contains the points that have been created on demand.
9963 * - Then there's series.points that contains all currently visible point objects. In case of cropping,
9964 * the cropped-away points are not part of this array. The series.points array starts at series.cropStart
9965 * compared to series.data and series.options.data. If however the series data is grouped, these can't
9966 * be correlated one to one.
9967 * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points.
9968 * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points.
9970 * @param {Object} chart
9971 * @param {Object} options
9973 var Series = function () {};
9975 Series.prototype = {
9980 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
9981 stroke: 'lineColor',
9982 'stroke-width': 'lineWidth',
9986 init: function (chart, options) {
9991 index = chart.series.length;
9993 series.chart = chart;
9994 series.options = options = series.setOptions(options); // merge with plotOptions
9999 // set some variables
10002 name: options.name || 'Series ' + (index + 1),
10003 state: NORMAL_STATE,
10005 visible: options.visible !== false, // true by default
10006 selected: options.selected === true // false by default
10009 // register event listeners
10010 events = options.events;
10011 for (eventType in events) {
10012 addEvent(series, eventType, events[eventType]);
10015 (events && events.click) ||
10016 (options.point && options.point.events && options.point.events.click) ||
10017 options.allowPointSelect
10019 chart.runTrackerClick = true;
10023 series.getSymbol();
10026 series.setData(options.data, false);
10033 * Set the xAxis and yAxis properties of cartesian series, and register the series
10034 * in the axis.series array
10036 bindAxes: function () {
10038 seriesOptions = series.options,
10039 chart = series.chart,
10042 if (series.isCartesian) {
10044 each(['xAxis', 'yAxis'], function (AXIS) { // repeat for xAxis and yAxis
10046 each(chart[AXIS], function (axis) { // loop through the chart's axis objects
10048 axisOptions = axis.options;
10050 // apply if the series xAxis or yAxis option mathches the number of the
10051 // axis, or if undefined, use the first axis
10052 if ((seriesOptions[AXIS] === axisOptions.index) ||
10053 (seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) {
10055 // register this series in the axis.series lookup
10056 axis.series.push(series);
10058 // set this series.xAxis or series.yAxis reference
10059 series[AXIS] = axis;
10061 // mark dirty for redraw
10062 axis.isDirty = true;
10072 * Return an auto incremented x value based on the pointStart and pointInterval options.
10073 * This is only used if an x value is not given for the point that calls autoIncrement.
10075 autoIncrement: function () {
10077 options = series.options,
10078 xIncrement = series.xIncrement;
10080 xIncrement = pick(xIncrement, options.pointStart, 0);
10082 series.pointInterval = pick(series.pointInterval, options.pointInterval, 1);
10084 series.xIncrement = xIncrement + series.pointInterval;
10089 * Divide the series data into segments divided by null values.
10091 getSegments: function () {
10096 points = series.points,
10097 pointsLength = points.length;
10099 if (pointsLength) { // no action required for []
10101 // if connect nulls, just remove null points
10102 if (series.options.connectNulls) {
10105 if (points[i].y === null) {
10106 points.splice(i, 1);
10109 segments = [points];
10111 // else, split on null points
10113 each(points, function (point, i) {
10114 if (point.y === null) {
10115 if (i > lastNull + 1) {
10116 segments.push(points.slice(lastNull + 1, i));
10119 } else if (i === pointsLength - 1) { // last value
10120 segments.push(points.slice(lastNull + 1, i + 1));
10127 series.segments = segments;
10130 * Set the series options by merging from the options tree
10131 * @param {Object} itemOptions
10133 setOptions: function (itemOptions) {
10135 chart = series.chart,
10136 chartOptions = chart.options,
10137 plotOptions = chartOptions.plotOptions,
10138 data = itemOptions.data,
10141 itemOptions.data = null; // remove from merge to prevent looping over the data set
10144 plotOptions[this.type],
10145 plotOptions.series,
10149 // Re-insert the data array to the options and the original config (#717)
10150 options.data = itemOptions.data = data;
10152 // the tooltip options are merged between global and series specific options
10153 series.tooltipOptions = merge(chartOptions.tooltip, options.tooltip);
10159 * Get the series' color
10161 getColor: function () {
10162 var defaultColors = this.chart.options.colors,
10163 counters = this.chart.counters;
10164 this.color = this.options.color || defaultColors[counters.color++] || '#0000ff';
10165 counters.wrapColor(defaultColors.length);
10168 * Get the series' symbol
10170 getSymbol: function () {
10172 seriesMarkerOption = series.options.marker,
10173 chart = series.chart,
10174 defaultSymbols = chart.options.symbols,
10175 counters = chart.counters;
10176 series.symbol = seriesMarkerOption.symbol || defaultSymbols[counters.symbol++];
10178 // don't substract radius in image symbols (#604)
10179 if (/^url/.test(series.symbol)) {
10180 seriesMarkerOption.radius = 0;
10182 counters.wrapSymbol(defaultSymbols.length);
10186 * Add a point dynamically after chart load time
10187 * @param {Object} options Point options as given in series.data
10188 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
10189 * @param {Boolean} shift If shift is true, a point is shifted off the start
10190 * of the series as one is appended to the end.
10191 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
10194 addPoint: function (options, redraw, shift, animation) {
10196 data = series.data,
10197 graph = series.graph,
10198 area = series.area,
10199 chart = series.chart,
10200 xData = series.xData,
10201 yData = series.yData,
10202 currentShift = (graph && graph.shift) || 0,
10203 dataOptions = series.options.data,
10205 //point = (new series.pointClass()).init(series, options);
10207 setAnimation(animation, chart);
10209 if (graph && shift) { // make graph animate sideways
10210 graph.shift = currentShift + 1;
10213 area.shift = currentShift + 1;
10214 area.isArea = true;
10216 redraw = pick(redraw, true);
10219 // Get options and push the point to xData, yData and series.options. In series.generatePoints
10220 // the Point instance will be created on demand and pushed to the series.data array.
10221 point = { series: series };
10222 series.pointClass.prototype.applyOptions.apply(point, [options]);
10223 xData.push(point.x);
10224 yData.push(series.valueCount === 4 ? [point.open, point.high, point.low, point.close] : point.y);
10225 dataOptions.push(options);
10228 // Shift the first point off the parallel arrays
10229 // todo: consider series.removePoint(i) method
10232 data[0].remove(false);
10237 dataOptions.shift();
10240 series.getAttribs();
10243 series.isDirty = true;
10244 series.isDirtyData = true;
10251 * Replace the series data with a new set of data
10252 * @param {Object} data
10253 * @param {Object} redraw
10255 setData: function (data, redraw) {
10257 oldData = series.points,
10258 options = series.options,
10259 initialColor = series.initialColor,
10260 chart = series.chart,
10264 // reset properties
10265 series.xIncrement = null;
10266 series.pointRange = (series.xAxis && series.xAxis.categories && 1) || options.pointRange;
10268 if (defined(initialColor)) { // reset colors for pie
10269 chart.counters.color = initialColor;
10275 dataLength = data ? data.length : [],
10276 turboThreshold = options.turboThreshold || 1000,
10278 ohlc = series.valueCount === 4;
10280 // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The
10281 // first value is tested, and we assume that all the rest are defined the same
10282 // way. Although the 'for' loops are similar, they are repeated inside each
10283 // if-else conditional for max performance.
10284 if (dataLength > turboThreshold) {
10286 // find the first non-null point
10288 while (firstPoint === null && i < dataLength) {
10289 firstPoint = data[i];
10294 if (isNumber(firstPoint)) { // assume all points are numbers
10295 var x = pick(options.pointStart, 0),
10296 pointInterval = pick(options.pointInterval, 1);
10298 for (i = 0; i < dataLength; i++) {
10300 yData[i] = data[i];
10301 x += pointInterval;
10303 series.xIncrement = x;
10304 } else if (isArray(firstPoint)) { // assume all points are arrays
10305 if (ohlc) { // [x, o, h, l, c]
10306 for (i = 0; i < dataLength; i++) {
10309 yData[i] = pt.slice(1, 5);
10312 for (i = 0; i < dataLength; i++) {
10319 error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
10322 for (i = 0; i < dataLength; i++) {
10323 pt = { series: series };
10324 series.pointClass.prototype.applyOptions.apply(pt, [data[i]]);
10326 yData[i] = ohlc ? [pt.open, pt.high, pt.low, pt.close] : pt.y;
10331 series.options.data = data;
10332 series.xData = xData;
10333 series.yData = yData;
10335 // destroy old points
10336 i = (oldData && oldData.length) || 0;
10338 if (oldData[i] && oldData[i].destroy) {
10339 oldData[i].destroy();
10344 series.isDirty = series.isDirtyData = chart.isDirtyBox = true;
10345 if (pick(redraw, true)) {
10346 chart.redraw(false);
10351 * Remove a series and optionally redraw the chart
10353 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
10354 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
10358 remove: function (redraw, animation) {
10360 chart = series.chart;
10361 redraw = pick(redraw, true);
10363 if (!series.isRemoving) { /* prevent triggering native event in jQuery
10364 (calling the remove function from the remove event) */
10365 series.isRemoving = true;
10367 // fire the event with a default handler of removing the point
10368 fireEvent(series, 'remove', null, function () {
10371 // destroy elements
10376 chart.isDirtyLegend = chart.isDirtyBox = true;
10378 chart.redraw(animation);
10383 series.isRemoving = false;
10387 * Process the data by cropping away unused data points if the series is longer
10388 * than the crop threshold. This saves computing time for lage series.
10390 processData: function (force) {
10392 processedXData = series.xData, // copied during slice operation below
10393 processedYData = series.yData,
10394 dataLength = processedXData.length,
10396 cropEnd = dataLength,
10400 xAxis = series.xAxis,
10401 i, // loop variable
10402 options = series.options,
10403 cropThreshold = options.cropThreshold;
10405 // If the series data or axes haven't changed, don't go through this. Return false to pass
10406 // the message on to override methods like in data grouping.
10407 if (series.isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) {
10411 // optionally filter out points outside the plot area
10412 if (!cropThreshold || dataLength > cropThreshold || series.forceCrop) {
10413 var extremes = xAxis.getExtremes(),
10414 min = extremes.min,
10415 max = extremes.max;
10417 // it's outside current extremes
10418 if (processedXData[dataLength - 1] < min || processedXData[0] > max) {
10419 processedXData = [];
10420 processedYData = [];
10422 // only crop if it's actually spilling out
10423 } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) {
10425 // iterate up to find slice start
10426 for (i = 0; i < dataLength; i++) {
10427 if (processedXData[i] >= min) {
10428 cropStart = mathMax(0, i - 1);
10432 // proceed to find slice end
10433 for (; i < dataLength; i++) {
10434 if (processedXData[i] > max) {
10440 processedXData = processedXData.slice(cropStart, cropEnd);
10441 processedYData = processedYData.slice(cropStart, cropEnd);
10447 // Find the closest distance between processed points
10448 for (i = processedXData.length - 1; i > 0; i--) {
10449 distance = processedXData[i] - processedXData[i - 1];
10450 if (closestPointRange === UNDEFINED || distance < closestPointRange) {
10451 closestPointRange = distance;
10455 // Record the properties
10456 series.cropped = cropped; // undefined or true
10457 series.cropStart = cropStart;
10458 series.processedXData = processedXData;
10459 series.processedYData = processedYData;
10461 if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC
10462 series.pointRange = closestPointRange || 1;
10464 series.closestPointRange = closestPointRange;
10469 * Generate the data point after the data has been processed by cropping away
10470 * unused points and optionally grouped in Highcharts Stock.
10472 generatePoints: function () {
10474 options = series.options,
10475 dataOptions = options.data,
10476 data = series.data,
10478 processedXData = series.processedXData,
10479 processedYData = series.processedYData,
10480 pointClass = series.pointClass,
10481 processedDataLength = processedXData.length,
10482 cropStart = series.cropStart || 0,
10484 hasGroupedData = series.hasGroupedData,
10489 if (!data && !hasGroupedData) {
10491 arr.length = dataOptions.length;
10492 data = series.data = arr;
10495 for (i = 0; i < processedDataLength; i++) {
10496 cursor = cropStart + i;
10497 if (!hasGroupedData) {
10498 if (data[cursor]) {
10499 point = data[cursor];
10501 data[cursor] = point = (new pointClass()).init(series, dataOptions[cursor], processedXData[i]);
10505 // splat the y data in case of ohlc data array
10506 points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
10510 // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when
10511 // swithching view from non-grouped data to grouped data (#637)
10512 if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) {
10513 for (i = 0; i < dataLength; i++) {
10514 if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points
10515 i += processedDataLength;
10518 data[i].destroyElements();
10523 series.data = data;
10524 series.points = points;
10528 * Translate data points from raw data values to chart specific positioning data
10529 * needed later in drawPoints, drawGraph and drawTracker.
10531 translate: function () {
10532 if (!this.processedXData) { // hidden series
10533 this.processData();
10535 this.generatePoints();
10537 chart = series.chart,
10538 options = series.options,
10539 stacking = options.stacking,
10540 xAxis = series.xAxis,
10541 categories = xAxis.categories,
10542 yAxis = series.yAxis,
10543 points = series.points,
10544 dataLength = points.length,
10545 hasModifyValue = !!series.modifyValue,
10546 isLastSeries = series.index === yAxis.series.length - 1,
10549 for (i = 0; i < dataLength; i++) {
10550 var point = points[i],
10553 yBottom = point.low,
10554 stack = yAxis.stacks[(yValue < options.threshold ? '-' : '') + series.stackKey],
10558 // get the plotX translation
10559 point.plotX = mathRound(xAxis.translate(xValue) * 10) / 10; // Math.round fixes #591
10561 // calculate the bottom y value for stacked series
10562 if (stacking && series.visible && stack && stack[xValue]) {
10563 pointStack = stack[xValue];
10564 pointStackTotal = pointStack.total;
10565 pointStack.cum = yBottom = pointStack.cum - yValue; // start from top
10566 yValue = yBottom + yValue;
10568 if (isLastSeries) {
10569 yBottom = options.threshold;
10572 if (stacking === 'percent') {
10573 yBottom = pointStackTotal ? yBottom * 100 / pointStackTotal : 0;
10574 yValue = pointStackTotal ? yValue * 100 / pointStackTotal : 0;
10577 point.percentage = pointStackTotal ? point.y * 100 / pointStackTotal : 0;
10578 point.stackTotal = pointStackTotal;
10581 if (defined(yBottom)) {
10582 point.yBottom = yAxis.translate(yBottom, 0, 1, 0, 1);
10585 // general hook, used for Highstock compare mode
10586 if (hasModifyValue) {
10587 yValue = series.modifyValue(yValue, point);
10591 if (yValue !== null) {
10592 point.plotY = mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10; // Math.round fixes #591
10595 // set client related positions for mouse tracking
10596 point.clientX = chart.inverted ?
10597 chart.plotHeight - point.plotX :
10598 point.plotX; // for mouse tracking
10601 point.category = categories && categories[point.x] !== UNDEFINED ?
10602 categories[point.x] : point.x;
10607 // now that we have the cropped data, build the segments
10608 series.getSegments();
10611 * Memoize tooltip texts and positions
10613 setTooltipPoints: function (renew) {
10615 chart = series.chart,
10616 inverted = chart.inverted,
10619 plotSize = mathRound((inverted ? chart.plotTop : chart.plotLeft) + chart.plotSizeX),
10622 xAxis = series.xAxis,
10625 tooltipPoints = []; // a lookup array for each pixel in the x dimension
10627 // don't waste resources if tracker is disabled
10628 if (series.options.enableMouseTracking === false) {
10634 series.tooltipPoints = null;
10637 // concat segments to overcome null values
10638 each(series.segments || series.points, function (segment) {
10639 points = points.concat(segment);
10642 // loop the concatenated points and apply each point to all the closest
10644 if (xAxis && xAxis.reversed) {
10645 points = points.reverse();//reverseArray(points);
10648 //each(points, function (point, i) {
10649 pointsLength = points.length;
10650 for (i = 0; i < pointsLength; i++) {
10652 low = points[i - 1] ? points[i - 1]._high + 1 : 0;
10653 high = point._high = points[i + 1] ?
10654 (mathFloor((point.plotX + (points[i + 1] ? points[i + 1].plotX : plotSize)) / 2)) :
10657 while (low <= high) {
10658 tooltipPoints[inverted ? plotSize - low++ : low++] = point;
10661 series.tooltipPoints = tooltipPoints;
10665 * Format the header of the tooltip
10667 tooltipHeaderFormatter: function (key) {
10669 tooltipOptions = series.tooltipOptions,
10670 xDateFormat = tooltipOptions.xDateFormat || '%A, %b %e, %Y',
10671 xAxis = series.xAxis,
10672 isDateTime = xAxis && xAxis.options.type === 'datetime';
10674 return tooltipOptions.headerFormat
10675 .replace('{point.key}', isDateTime ? dateFormat(xDateFormat, key) : key)
10676 .replace('{series.name}', series.name)
10677 .replace('{series.color}', series.color);
10681 * Series mouse over handler
10683 onMouseOver: function () {
10685 chart = series.chart,
10686 hoverSeries = chart.hoverSeries;
10688 if (!hasTouch && chart.mouseIsDown) {
10692 // set normal state to previous series
10693 if (hoverSeries && hoverSeries !== series) {
10694 hoverSeries.onMouseOut();
10697 // trigger the event, but to save processing time,
10699 if (series.options.events.mouseOver) {
10700 fireEvent(series, 'mouseOver');
10704 series.setState(HOVER_STATE);
10705 chart.hoverSeries = series;
10709 * Series mouse out handler
10711 onMouseOut: function () {
10712 // trigger the event only if listeners exist
10714 options = series.options,
10715 chart = series.chart,
10716 tooltip = chart.tooltip,
10717 hoverPoint = chart.hoverPoint;
10719 // trigger mouse out on the point, which must be in this series
10721 hoverPoint.onMouseOut();
10724 // fire the mouse out event
10725 if (series && options.events.mouseOut) {
10726 fireEvent(series, 'mouseOut');
10730 // hide the tooltip
10731 if (tooltip && !options.stickyTracking && !tooltip.shared) {
10735 // set normal state
10737 chart.hoverSeries = null;
10741 * Animate in the series
10743 animate: function (init) {
10745 chart = series.chart,
10746 clipRect = series.clipRect,
10747 animation = series.options.animation;
10749 if (animation && !isObject(animation)) {
10753 if (init) { // initialize the animation
10754 if (!clipRect.isAnimating) { // apply it only for one of the series
10755 clipRect.attr('width', 0);
10756 clipRect.isAnimating = true;
10759 } else { // run the animation
10761 width: chart.plotSizeX
10764 // delete this function to allow it only once
10765 this.animate = null;
10773 drawPoints: function () {
10776 points = series.points,
10777 chart = series.chart,
10787 if (series.options.marker.enabled) {
10791 plotX = point.plotX;
10792 plotY = point.plotY;
10793 graphic = point.graphic;
10795 // only draw the point if y is defined
10796 if (plotY !== UNDEFINED && !isNaN(plotY)) {
10799 pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE];
10800 radius = pointAttr.r;
10801 symbol = pick(point.marker && point.marker.symbol, series.symbol);
10802 isImage = symbol.indexOf('url') === 0;
10804 if (graphic) { // update
10805 graphic.animate(extend({
10808 }, graphic.symbolName ? { // don't apply to image symbols #507
10812 } else if (radius > 0 || isImage) {
10813 point.graphic = chart.renderer.symbol(
10821 .add(series.group);
10830 * Convert state properties from API naming conventions to SVG attributes
10832 * @param {Object} options API options object
10833 * @param {Object} base1 SVG attribute object to inherit from
10834 * @param {Object} base2 Second level SVG attribute object to inherit from
10836 convertAttribs: function (options, base1, base2, base3) {
10837 var conversion = this.pointAttrToOptions,
10842 options = options || {};
10843 base1 = base1 || {};
10844 base2 = base2 || {};
10845 base3 = base3 || {};
10847 for (attr in conversion) {
10848 option = conversion[attr];
10849 obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]);
10855 * Get the state attributes. Each series type has its own set of attributes
10856 * that are allowed to change on a point's state change. Series wide attributes are stored for
10857 * all series, and additionally point specific attributes are stored for all
10858 * points with individual marker options. If such options are not defined for the point,
10859 * a reference to the series wide attributes is stored in point.pointAttr.
10861 getAttribs: function () {
10863 normalOptions = defaultPlotOptions[series.type].marker ? series.options.marker : series.options,
10864 stateOptions = normalOptions.states,
10865 stateOptionsHover = stateOptions[HOVER_STATE],
10866 pointStateOptionsHover,
10867 seriesColor = series.color,
10869 stroke: seriesColor,
10872 points = series.points,
10875 seriesPointAttr = [],
10877 pointAttrToOptions = series.pointAttrToOptions,
10878 hasPointSpecificOptions,
10881 // series type specific modifications
10882 if (series.options.marker) { // line, spline, area, areaspline, scatter
10884 // if no hover radius is given, default to normal radius + 2
10885 stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + 2;
10886 stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + 1;
10888 } else { // column, bar, pie
10890 // if no hover color is given, brighten the normal color
10891 stateOptionsHover.color = stateOptionsHover.color ||
10892 Color(stateOptionsHover.color || seriesColor)
10893 .brighten(stateOptionsHover.brightness).get();
10896 // general point attributes for the series normal state
10897 seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults);
10899 // HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius
10900 each([HOVER_STATE, SELECT_STATE], function (state) {
10901 seriesPointAttr[state] =
10902 series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]);
10906 series.pointAttr = seriesPointAttr;
10909 // Generate the point-specific attribute collections if specific point
10910 // options are given. If not, create a referance to the series wide point
10915 normalOptions = (point.options && point.options.marker) || point.options;
10916 if (normalOptions && normalOptions.enabled === false) {
10917 normalOptions.radius = 0;
10919 hasPointSpecificOptions = false;
10921 // check if the point has specific visual options
10922 if (point.options) {
10923 for (key in pointAttrToOptions) {
10924 if (defined(normalOptions[pointAttrToOptions[key]])) {
10925 hasPointSpecificOptions = true;
10932 // a specific marker config object is defined for the individual point:
10933 // create it's own attribute collection
10934 if (hasPointSpecificOptions) {
10937 stateOptions = normalOptions.states || {}; // reassign for individual point
10938 pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {};
10940 // if no hover color is given, brighten the normal color
10941 if (!series.options.marker) { // column, bar, point
10942 pointStateOptionsHover.color =
10943 Color(pointStateOptionsHover.color || point.options.color)
10944 .brighten(pointStateOptionsHover.brightness ||
10945 stateOptionsHover.brightness).get();
10949 // normal point state inherits series wide normal state
10950 pointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, seriesPointAttr[NORMAL_STATE]);
10952 // inherit from point normal and series hover
10953 pointAttr[HOVER_STATE] = series.convertAttribs(
10954 stateOptions[HOVER_STATE],
10955 seriesPointAttr[HOVER_STATE],
10956 pointAttr[NORMAL_STATE]
10958 // inherit from point normal and series hover
10959 pointAttr[SELECT_STATE] = series.convertAttribs(
10960 stateOptions[SELECT_STATE],
10961 seriesPointAttr[SELECT_STATE],
10962 pointAttr[NORMAL_STATE]
10967 // no marker config object is created: copy a reference to the series-wide
10968 // attribute collection
10970 pointAttr = seriesPointAttr;
10973 point.pointAttr = pointAttr;
10981 * Clear DOM objects and free up memory
10983 destroy: function () {
10985 chart = series.chart,
10986 seriesClipRect = series.clipRect,
10987 issue134 = /AppleWebKit\/533/.test(userAgent),
10990 data = series.data || [],
10996 fireEvent(series, 'destroy');
10998 // remove all events
10999 removeEvent(series);
11002 each(['xAxis', 'yAxis'], function (AXIS) {
11003 axis = series[AXIS];
11005 erase(axis.series, series);
11006 axis.isDirty = true;
11010 // remove legend items
11011 if (series.legendItem) {
11012 series.chart.legend.destroyItem(series);
11015 // destroy all points with their elements
11019 if (point && point.destroy) {
11023 series.points = null;
11025 // If this series clipRect is not the global one (which is removed on chart.destroy) we
11026 // destroy it here.
11027 if (seriesClipRect && seriesClipRect !== chart.clipRect) {
11028 series.clipRect = seriesClipRect.destroy();
11031 // destroy all SVGElements associated to the series
11032 each(['area', 'graph', 'dataLabelsGroup', 'group', 'tracker'], function (prop) {
11033 if (series[prop]) {
11035 // issue 134 workaround
11036 destroy = issue134 && prop === 'group' ?
11040 series[prop][destroy]();
11044 // remove from hoverSeries
11045 if (chart.hoverSeries === series) {
11046 chart.hoverSeries = null;
11048 erase(chart.series, series);
11050 // clear all members
11051 for (prop in series) {
11052 delete series[prop];
11057 * Draw the data labels
11059 drawDataLabels: function () {
11060 if (this.options.dataLabels.enabled) {
11064 points = series.points,
11065 seriesOptions = series.options,
11066 options = seriesOptions.dataLabels,
11070 dataLabelsGroup = series.dataLabelsGroup,
11071 chart = series.chart,
11072 xAxis = series.xAxis,
11073 groupLeft = xAxis ? xAxis.left : chart.plotLeft,
11074 yAxis = series.yAxis,
11075 groupTop = yAxis ? yAxis.top : chart.plotTop,
11076 renderer = chart.renderer,
11077 inverted = chart.inverted,
11078 seriesType = series.type,
11079 stacking = seriesOptions.stacking,
11080 isBarLike = seriesType === 'column' || seriesType === 'bar',
11081 vAlignIsNull = options.verticalAlign === null,
11082 yIsNull = options.y === null,
11087 // In stacked series the default label placement is inside the bars
11088 if (vAlignIsNull) {
11089 options = merge(options, {verticalAlign: 'middle'});
11092 // If no y delta is specified, try to create a good default
11094 options = merge(options, {y: {top: 14, middle: 4, bottom: -6}[options.verticalAlign]});
11097 // In non stacked series the default label placement is on top of the bars
11098 if (vAlignIsNull) {
11099 options = merge(options, {verticalAlign: 'top'});
11105 // create a separate group for the data labels to avoid rotation
11106 if (!dataLabelsGroup) {
11107 dataLabelsGroup = series.dataLabelsGroup =
11108 renderer.g('data-labels')
11110 visibility: series.visible ? VISIBLE : HIDDEN,
11113 .translate(groupLeft, groupTop)
11116 dataLabelsGroup.translate(groupLeft, groupTop);
11119 // make the labels for each point
11120 generalOptions = options;
11121 each(points, function (point) {
11123 dataLabel = point.dataLabel;
11125 // Merge in individual options from point // docs
11126 options = generalOptions; // reset changes from previous points
11127 pointOptions = point.options;
11128 if (pointOptions && pointOptions.dataLabels) {
11129 options = merge(options, pointOptions.dataLabels);
11132 // If the point is outside the plot area, destroy it. #678
11133 if (dataLabel && series.isCartesian && !chart.isInsidePlot(point.plotX, point.plotY)) {
11134 point.dataLabel = dataLabel.destroy();
11136 // Individual labels are disabled if the are explicitly disabled
11137 // in the point options, or if they fall outside the plot area.
11138 } else if (options.enabled) {
11141 str = options.formatter.call(point.getLabelConfig(), options);
11143 var barX = point.barX,
11144 plotX = (barX && barX + point.barW / 2) || point.plotX || -999,
11145 plotY = pick(point.plotY, -999),
11146 align = options.align,
11147 individualYDelta = yIsNull ? (point.y >= 0 ? -6 : 12) : options.y;
11149 // Postprocess the positions
11150 x = (inverted ? chart.plotWidth - plotY : plotX) + options.x;
11151 y = (inverted ? chart.plotHeight - plotX : plotY) + individualYDelta;
11153 // in columns, align the string to the column
11154 if (seriesType === 'column') {
11155 x += { left: -1, right: 1 }[align] * point.barW / 2 || 0;
11158 if (!stacking && inverted && point.y < 0) {
11163 // Determine the color
11164 options.style.color = pick(options.color, options.style.color, series.color, 'black');
11167 // update existing label
11169 // vertically centered
11170 if (inverted && !options.y) {
11171 y = y + pInt(dataLabel.styles.lineHeight) * 0.9 - dataLabel.getBBox().height / 2;
11180 // create new label
11181 } else if (defined(str)) {
11182 dataLabel = point.dataLabel = renderer.text(
11189 rotation: options.rotation,
11192 .css(options.style)
11193 .add(dataLabelsGroup);
11194 // vertically centered
11195 if (inverted && !options.y) {
11197 y: y + pInt(dataLabel.styles.lineHeight) * 0.9 - dataLabel.getBBox().height / 2
11202 if (isBarLike && seriesOptions.stacking && dataLabel) {
11203 var barY = point.barY,
11207 dataLabel.align(options, null,
11209 x: inverted ? chart.plotWidth - barY - barH : barX,
11210 y: inverted ? chart.plotHeight - barX - barW : barY,
11211 width: inverted ? barH : barW,
11212 height: inverted ? barW : barH
11223 * Draw the actual graph
11225 drawGraph: function () {
11227 options = series.options,
11228 chart = series.chart,
11229 graph = series.graph,
11232 area = series.area,
11233 group = series.group,
11234 color = options.lineColor || series.color,
11235 lineWidth = options.lineWidth,
11236 dashStyle = options.dashStyle,
11238 renderer = chart.renderer,
11239 translatedThreshold = series.yAxis.getThreshold(options.threshold),
11240 useArea = /^area/.test(series.type),
11241 singlePoints = [], // used in drawTracker
11246 // divide into segments and build graph and area paths
11247 each(series.segments, function (segment) {
11250 // build the segment line
11251 each(segment, function (point, i) {
11253 if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
11254 segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i));
11258 // moveTo or lineTo
11259 segmentPath.push(i ? L : M);
11262 if (i && options.step) {
11263 var lastPoint = segment[i - 1];
11270 // normal line to next point
11278 // add the segment to the graph, or a single point for tracking
11279 if (segment.length > 1) {
11280 graphPath = graphPath.concat(segmentPath);
11282 singlePoints.push(segment[0]);
11287 var areaSegmentPath = [],
11289 segLength = segmentPath.length;
11290 for (i = 0; i < segLength; i++) {
11291 areaSegmentPath.push(segmentPath[i]);
11293 if (segLength === 3) { // for animation from 1 to two points
11294 areaSegmentPath.push(L, segmentPath[1], segmentPath[2]);
11296 if (options.stacking && series.type !== 'areaspline') {
11298 // Follow stack back. Todo: implement areaspline. A general solution could be to
11299 // reverse the entire graphPath of the previous series, though may be hard with
11300 // splines and with series with different extremes
11301 for (i = segment.length - 1; i >= 0; i--) {
11304 if (i < segment.length - 1 && options.step) {
11305 areaSegmentPath.push(segment[i + 1].plotX, segment[i].yBottom);
11308 areaSegmentPath.push(segment[i].plotX, segment[i].yBottom);
11311 } else { // follow zero line back
11312 areaSegmentPath.push(
11314 segment[segment.length - 1].plotX,
11315 translatedThreshold,
11318 translatedThreshold
11321 areaPath = areaPath.concat(areaSegmentPath);
11325 // used in drawTracker:
11326 series.graphPath = graphPath;
11327 series.singlePoints = singlePoints;
11329 // draw the area if area series or areaspline
11333 Color(series.color).setOpacity(options.fillOpacity || 0.75).get()
11336 area.animate({ d: areaPath });
11340 series.area = series.chart.renderer.path(areaPath)
11349 stop(graph); // cancel running animations, #459
11350 graph.animate({ d: graphPath });
11356 'stroke-width': lineWidth
11359 attribs.dashstyle = dashStyle;
11362 series.graph = renderer.path(graphPath)
11363 .attr(attribs).add(group).shadow(options.shadow);
11370 * Render the graph and markers
11372 render: function () {
11374 chart = series.chart,
11377 options = series.options,
11378 doClip = options.clip !== false,
11379 animation = options.animation,
11380 doAnimation = animation && series.animate,
11381 duration = doAnimation ? (animation && animation.duration) || 500 : 0,
11382 clipRect = series.clipRect,
11383 renderer = chart.renderer;
11386 // Add plot area clipping rectangle. If this is before chart.hasRendered,
11387 // create one shared clipRect.
11389 // Todo: since creating the clip property, the clipRect is created but
11390 // never used when clip is false. A better way would be that the animation
11391 // would run, then the clipRect destroyed.
11393 clipRect = series.clipRect = !chart.hasRendered && chart.clipRect ?
11395 renderer.clipRect(0, 0, chart.plotSizeX, chart.plotSizeY + 1);
11396 if (!chart.clipRect) {
11397 chart.clipRect = clipRect;
11403 if (!series.group) {
11404 group = series.group = renderer.g('series');
11406 if (chart.inverted) {
11407 setInvert = function () {
11409 width: chart.plotWidth,
11410 height: chart.plotHeight
11414 setInvert(); // do it now
11415 addEvent(chart, 'resize', setInvert); // do it on resize
11416 addEvent(series, 'destroy', function () {
11417 removeEvent(chart, 'resize', setInvert);
11422 group.clip(clipRect);
11425 visibility: series.visible ? VISIBLE : HIDDEN,
11426 zIndex: options.zIndex
11428 .translate(series.xAxis.left, series.yAxis.top)
11429 .add(chart.seriesGroup);
11432 series.drawDataLabels();
11434 // initiate the animation
11436 series.animate(true);
11439 // cache attributes for shapes
11440 series.getAttribs();
11442 // draw the graph if any
11443 if (series.drawGraph) {
11444 series.drawGraph();
11448 series.drawPoints();
11450 // draw the mouse tracking area
11451 if (series.options.enableMouseTracking !== false) {
11452 series.drawTracker();
11455 // run the animation
11460 // finish the individual clipRect
11461 setTimeout(function () {
11462 clipRect.isAnimating = false;
11463 group = series.group; // can be destroyed during the timeout
11464 if (group && clipRect !== chart.clipRect && clipRect.renderer) {
11466 group.clip((series.clipRect = chart.clipRect));
11468 clipRect.destroy();
11472 series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
11473 // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
11478 * Redraw the series after an update in the axes.
11480 redraw: function () {
11482 chart = series.chart,
11483 wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after
11484 group = series.group;
11486 // reposition on resize
11488 if (chart.inverted) {
11490 width: chart.plotWidth,
11491 height: chart.plotHeight
11496 translateX: series.xAxis.left,
11497 translateY: series.yAxis.top
11501 series.translate();
11502 series.setTooltipPoints(true);
11505 if (wasDirtyData) {
11506 fireEvent(series, 'updatedData');
11511 * Set the state of the graph
11513 setState: function (state) {
11515 options = series.options,
11516 graph = series.graph,
11517 stateOptions = options.states,
11518 lineWidth = options.lineWidth;
11520 state = state || NORMAL_STATE;
11522 if (series.state !== state) {
11523 series.state = state;
11525 if (stateOptions[state] && stateOptions[state].enabled === false) {
11530 lineWidth = stateOptions[state].lineWidth || lineWidth + 1;
11533 if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
11534 graph.attr({ // use attr because animate will cause any other animation on the graph to stop
11535 'stroke-width': lineWidth
11536 }, state ? 0 : 500);
11542 * Set the visibility of the graph
11544 * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED,
11545 * the visibility is toggled.
11547 setVisible: function (vis, redraw) {
11549 chart = series.chart,
11550 legendItem = series.legendItem,
11551 seriesGroup = series.group,
11552 seriesTracker = series.tracker,
11553 dataLabelsGroup = series.dataLabelsGroup,
11556 points = series.points,
11558 ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
11559 oldVisibility = series.visible;
11561 // if called without an argument, toggle visibility
11562 series.visible = vis = vis === UNDEFINED ? !oldVisibility : vis;
11563 showOrHide = vis ? 'show' : 'hide';
11565 // show or hide series
11566 if (seriesGroup) { // pies don't have one
11567 seriesGroup[showOrHide]();
11570 // show or hide trackers
11571 if (seriesTracker) {
11572 seriesTracker[showOrHide]();
11573 } else if (points) {
11577 if (point.tracker) {
11578 point.tracker[showOrHide]();
11584 if (dataLabelsGroup) {
11585 dataLabelsGroup[showOrHide]();
11589 chart.legend.colorizeItem(series, vis);
11593 // rescale or adapt to resized chart
11594 series.isDirty = true;
11595 // in a stack, all other series are affected
11596 if (series.options.stacking) {
11597 each(chart.series, function (otherSeries) {
11598 if (otherSeries.options.stacking && otherSeries.visible) {
11599 otherSeries.isDirty = true;
11604 if (ignoreHiddenSeries) {
11605 chart.isDirtyBox = true;
11607 if (redraw !== false) {
11611 fireEvent(series, showOrHide);
11617 show: function () {
11618 this.setVisible(true);
11624 hide: function () {
11625 this.setVisible(false);
11630 * Set the selected state of the graph
11632 * @param selected {Boolean} True to select the series, false to unselect. If
11633 * UNDEFINED, the selection state is toggled.
11635 select: function (selected) {
11637 // if called without an argument, toggle
11638 series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected;
11640 if (series.checkbox) {
11641 series.checkbox.checked = selected;
11644 fireEvent(series, selected ? 'select' : 'unselect');
11649 * Draw the tracker object that sits above all data labels and markers to
11650 * track mouse events on the graph or points. For the line type charts
11651 * the tracker uses the same graphPath, but with a greater stroke width
11652 * for better control.
11654 drawTracker: function () {
11656 options = series.options,
11657 trackerPath = [].concat(series.graphPath),
11658 trackerPathLength = trackerPath.length,
11659 chart = series.chart,
11660 renderer = chart.renderer,
11661 snap = chart.options.tooltip.snap,
11662 tracker = series.tracker,
11663 cursor = options.cursor,
11664 css = cursor && { cursor: cursor },
11665 singlePoints = series.singlePoints,
11670 // Extend end points. A better way would be to use round linecaps,
11671 // but those are not clickable in VML.
11672 if (trackerPathLength) {
11673 i = trackerPathLength + 1;
11675 if (trackerPath[i] === M) { // extend left side
11676 trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L);
11678 if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side
11679 trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]);
11684 // handle single points
11685 for (i = 0; i < singlePoints.length; i++) {
11686 singlePoint = singlePoints[i];
11687 trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
11688 L, singlePoint.plotX + snap, singlePoint.plotY);
11693 // draw the tracker
11695 tracker.attr({ d: trackerPath });
11698 group = renderer.g()
11699 .clip(chart.clipRect)
11700 .add(chart.trackerGroup);
11702 series.tracker = renderer.path(trackerPath)
11705 stroke: TRACKER_FILL,
11707 'stroke-linejoin': 'bevel',
11708 'stroke-width' : options.lineWidth + 2 * snap,
11709 visibility: series.visible ? VISIBLE : HIDDEN,
11710 zIndex: options.zIndex || 1
11712 .on(hasTouch ? 'touchstart' : 'mouseover', function () {
11713 if (chart.hoverSeries !== series) {
11714 series.onMouseOver();
11717 .on('mouseout', function () {
11718 if (!options.stickyTracking) {
11719 series.onMouseOut();
11728 }; // end Series prototype
11732 * LineSeries object
11734 var LineSeries = extendClass(Series);
11735 seriesTypes.line = LineSeries;
11738 * AreaSeries object
11740 var AreaSeries = extendClass(Series, {
11744 seriesTypes.area = AreaSeries;
11750 * SplineSeries object
11752 var SplineSeries = extendClass(Series, {
11756 * Draw the actual graph
11758 getPointSpline: function (segment, point, i) {
11759 var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc
11760 denom = smoothing + 1,
11761 plotX = point.plotX,
11762 plotY = point.plotY,
11763 lastPoint = segment[i - 1],
11764 nextPoint = segment[i + 1],
11771 // find control points
11772 if (i && i < segment.length - 1) {
11773 var lastX = lastPoint.plotX,
11774 lastY = lastPoint.plotY,
11775 nextX = nextPoint.plotX,
11776 nextY = nextPoint.plotY,
11779 leftContX = (smoothing * plotX + lastX) / denom;
11780 leftContY = (smoothing * plotY + lastY) / denom;
11781 rightContX = (smoothing * plotX + nextX) / denom;
11782 rightContY = (smoothing * plotY + nextY) / denom;
11784 // have the two control points make a straight line through main point
11785 correction = ((rightContY - leftContY) * (rightContX - plotX)) /
11786 (rightContX - leftContX) + plotY - rightContY;
11788 leftContY += correction;
11789 rightContY += correction;
11791 // to prevent false extremes, check that control points are between
11792 // neighbouring points' y values
11793 if (leftContY > lastY && leftContY > plotY) {
11794 leftContY = mathMax(lastY, plotY);
11795 rightContY = 2 * plotY - leftContY; // mirror of left control point
11796 } else if (leftContY < lastY && leftContY < plotY) {
11797 leftContY = mathMin(lastY, plotY);
11798 rightContY = 2 * plotY - leftContY;
11800 if (rightContY > nextY && rightContY > plotY) {
11801 rightContY = mathMax(nextY, plotY);
11802 leftContY = 2 * plotY - rightContY;
11803 } else if (rightContY < nextY && rightContY < plotY) {
11804 rightContY = mathMin(nextY, plotY);
11805 leftContY = 2 * plotY - rightContY;
11808 // record for drawing in next point
11809 point.rightContX = rightContX;
11810 point.rightContY = rightContY;
11814 // moveTo or lineTo
11816 ret = [M, plotX, plotY];
11817 } else { // curve from last point to this
11820 lastPoint.rightContX || lastPoint.plotX,
11821 lastPoint.rightContY || lastPoint.plotY,
11822 leftContX || plotX,
11823 leftContY || plotY,
11827 lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later
11832 seriesTypes.spline = SplineSeries;
11837 * AreaSplineSeries object
11839 var AreaSplineSeries = extendClass(SplineSeries, {
11840 type: 'areaspline',
11843 seriesTypes.areaspline = AreaSplineSeries;
11846 * ColumnSeries object
11848 var ColumnSeries = extendClass(Series, {
11850 useThreshold: true,
11851 tooltipOutsidePlot: true,
11852 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
11853 stroke: 'borderColor',
11854 'stroke-width': 'borderWidth',
11858 init: function () {
11859 Series.prototype.init.apply(this, arguments);
11862 chart = series.chart;
11864 // if the series is added dynamically, force redraw of other
11865 // series affected by a new column
11866 if (chart.hasRendered) {
11867 each(chart.series, function (otherSeries) {
11868 if (otherSeries.type === series.type) {
11869 otherSeries.isDirty = true;
11876 * Translate each point to the plot area coordinate system and find shape positions
11878 translate: function () {
11880 chart = series.chart,
11881 options = series.options,
11882 stacking = options.stacking,
11883 borderWidth = options.borderWidth,
11885 xAxis = series.xAxis,
11886 reversedXAxis = xAxis.reversed,
11891 Series.prototype.translate.apply(series);
11893 // Get the total number of column type series.
11894 // This is called on every series. Consider moving this logic to a
11895 // chart.orderStacks() function and call it on init, addSeries and removeSeries
11896 each(chart.series, function (otherSeries) {
11897 if (otherSeries.type === series.type && otherSeries.visible &&
11898 series.options.group === otherSeries.options.group) { // used in Stock charts navigator series
11899 if (otherSeries.options.stacking) {
11900 stackKey = otherSeries.stackKey;
11901 if (stackGroups[stackKey] === UNDEFINED) {
11902 stackGroups[stackKey] = columnCount++;
11904 columnIndex = stackGroups[stackKey];
11906 columnIndex = columnCount++;
11908 otherSeries.columnIndex = columnIndex;
11912 // calculate the width and position of each column based on
11913 // the number of column series in the plot, the groupPadding
11914 // and the pointPadding options
11915 var points = series.points,
11916 categoryWidth = mathAbs(xAxis.translationSlope) * (xAxis.ordinalSlope || xAxis.closestPointRange || 1),
11917 groupPadding = categoryWidth * options.groupPadding,
11918 groupWidth = categoryWidth - 2 * groupPadding,
11919 pointOffsetWidth = groupWidth / columnCount,
11920 optionPointWidth = options.pointWidth,
11921 pointPadding = defined(optionPointWidth) ? (pointOffsetWidth - optionPointWidth) / 2 :
11922 pointOffsetWidth * options.pointPadding,
11923 pointWidth = mathCeil(mathMax(pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), 1)),
11924 colIndex = (reversedXAxis ? columnCount -
11925 series.columnIndex : series.columnIndex) || 0,
11926 pointXOffset = pointPadding + (groupPadding + colIndex *
11927 pointOffsetWidth - (categoryWidth / 2)) *
11928 (reversedXAxis ? -1 : 1),
11929 threshold = options.threshold,
11930 translatedThreshold = series.yAxis.getThreshold(threshold),
11931 minPointLength = pick(options.minPointLength, 5);
11933 // record the new values
11934 each(points, function (point) {
11935 var plotY = point.plotY,
11936 yBottom = point.yBottom || translatedThreshold,
11937 barX = point.plotX + pointXOffset,
11938 barY = mathCeil(mathMin(plotY, yBottom)),
11939 barH = mathCeil(mathMax(plotY, yBottom) - barY),
11940 stack = series.yAxis.stacks[(point.y < 0 ? '-' : '') + series.stackKey],
11943 // Record the offset'ed position and width of the bar to be able to align the stacking total correctly
11944 if (stacking && series.visible && stack && stack[point.x]) {
11945 stack[point.x].setOffset(pointXOffset, pointWidth);
11948 // handle options.minPointLength
11949 if (mathAbs(barH) < minPointLength) {
11950 if (minPointLength) {
11951 barH = minPointLength;
11953 mathAbs(barY - translatedThreshold) > minPointLength ? // stacked
11954 yBottom - minPointLength : // keep position
11955 translatedThreshold - (plotY <= translatedThreshold ? minPointLength : 0);
11966 // create shape type and shape args that are reused in drawPoints and drawTracker
11967 point.shapeType = 'rect';
11968 shapeArgs = extend(chart.renderer.Element.prototype.crisp.apply({}, [
11975 r: options.borderRadius
11977 if (borderWidth % 2) { // correct for shorting in crisp method, visible in stacked columns with 1px border
11979 shapeArgs.height += 1;
11981 point.shapeArgs = shapeArgs;
11983 // make small columns responsive to mouse
11984 point.trackerArgs = mathAbs(barH) < 3 && merge(point.shapeArgs, {
11992 getSymbol: function () {
11996 * Columns have no graph
11998 drawGraph: function () {},
12001 * Draw the columns. For bars, the series.group is rotated, so the same coordinates
12002 * apply for columns and bars. This method is inherited by scatter series.
12005 drawPoints: function () {
12007 options = series.options,
12008 renderer = series.chart.renderer,
12013 // draw the columns
12014 each(series.points, function (point) {
12015 var plotY = point.plotY;
12016 if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
12017 graphic = point.graphic;
12018 shapeArgs = point.shapeArgs;
12019 if (graphic) { // update
12021 graphic.animate(shapeArgs);
12024 point.graphic = graphic = renderer[point.shapeType](shapeArgs)
12025 .attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE])
12027 .shadow(options.shadow);
12034 * Draw the individual tracker elements.
12035 * This method is inherited by scatter and pie charts too.
12037 drawTracker: function () {
12039 chart = series.chart,
12040 renderer = chart.renderer,
12043 trackerLabel = +new Date(),
12044 options = series.options,
12045 cursor = options.cursor,
12046 css = cursor && { cursor: cursor },
12050 // Add a series specific group to allow clipping the trackers
12051 if (series.isCartesian) {
12052 group = renderer.g()
12053 .clip(chart.clipRect)
12054 .add(chart.trackerGroup);
12057 each(series.points, function (point) {
12058 tracker = point.tracker;
12059 shapeArgs = point.trackerArgs || point.shapeArgs;
12060 delete shapeArgs.strokeWidth;
12061 if (point.y !== null) {
12062 if (tracker) {// update
12063 tracker.attr(shapeArgs);
12067 renderer[point.shapeType](shapeArgs)
12069 isTracker: trackerLabel,
12070 fill: TRACKER_FILL,
12071 visibility: series.visible ? VISIBLE : HIDDEN,
12072 zIndex: options.zIndex || 1
12074 .on(hasTouch ? 'touchstart' : 'mouseover', function (event) {
12075 rel = event.relatedTarget || event.fromElement;
12076 if (chart.hoverSeries !== series && attr(rel, 'isTracker') !== trackerLabel) {
12077 series.onMouseOver();
12079 point.onMouseOver();
12082 .on('mouseout', function (event) {
12083 if (!options.stickyTracking) {
12084 rel = event.relatedTarget || event.toElement;
12085 if (attr(rel, 'isTracker') !== trackerLabel) {
12086 series.onMouseOut();
12091 .add(point.group || group); // pies have point group - see issue #118
12099 * Animate the column heights one by one from zero
12100 * @param {Boolean} init Whether to initialize the animation or run it
12102 animate: function (init) {
12104 points = series.points;
12106 if (!init) { // run the animation
12108 * Note: Ideally the animation should be initialized by calling
12109 * series.group.hide(), and then calling series.group.show()
12110 * after the animation was started. But this rendered the shadows
12111 * invisible in IE8 standards mode. If the columns flicker on large
12112 * datasets, this is the cause.
12115 each(points, function (point) {
12116 var graphic = point.graphic,
12117 shapeArgs = point.shapeArgs;
12123 y: series.yAxis.translate(0, 0, 1)
12128 height: shapeArgs.height,
12130 }, series.options.animation);
12135 // delete this function to allow it only once
12136 series.animate = null;
12141 * Remove this series from the chart
12143 remove: function () {
12145 chart = series.chart;
12147 // column and bar series affects other series of the same type
12148 // as they are either stacked or grouped
12149 if (chart.hasRendered) {
12150 each(chart.series, function (otherSeries) {
12151 if (otherSeries.type === series.type) {
12152 otherSeries.isDirty = true;
12157 Series.prototype.remove.apply(series, arguments);
12160 seriesTypes.column = ColumnSeries;
12162 var BarSeries = extendClass(ColumnSeries, {
12164 init: function () {
12165 this.inverted = true;
12166 ColumnSeries.prototype.init.apply(this, arguments);
12169 seriesTypes.bar = BarSeries;
12172 * The scatter series class
12174 var ScatterSeries = extendClass(Series, {
12178 * Extend the base Series' translate method by adding shape type and
12179 * arguments for the point trackers
12181 translate: function () {
12184 Series.prototype.translate.apply(series);
12186 each(series.points, function (point) {
12187 point.shapeType = 'circle';
12188 point.shapeArgs = {
12191 r: series.chart.options.tooltip.snap
12197 * Add tracking event listener to the series group, so the point graphics
12198 * themselves act as trackers
12200 drawTracker: function () {
12202 cursor = series.options.cursor,
12203 css = cursor && { cursor: cursor },
12204 points = series.points,
12208 // Set an expando property for the point index, used below
12210 graphic = points[i].graphic;
12211 if (graphic) { // doesn't exist for null points
12212 graphic.element._index = i;
12216 // Add the event listeners, we need to do this only once
12217 if (!series._hasTracking) {
12219 .on(hasTouch ? 'touchstart' : 'mouseover', function (e) {
12220 series.onMouseOver();
12221 points[e.target._index].onMouseOver();
12223 .on('mouseout', function () {
12224 if (!series.options.stickyTracking) {
12225 series.onMouseOut();
12230 series._hasTracking = true;
12235 seriesTypes.scatter = ScatterSeries;
12238 * Extended point object for pies
12240 var PiePoint = extendClass(Point, {
12242 * Initiate the pie slice
12244 init: function () {
12246 Point.prototype.init.apply(this, arguments);
12251 //visible: options.visible !== false,
12253 visible: point.visible !== false,
12254 name: pick(point.name, 'Slice')
12257 // add event listener for select
12258 toggleSlice = function () {
12261 addEvent(point, 'select', toggleSlice);
12262 addEvent(point, 'unselect', toggleSlice);
12268 * Toggle the visibility of the pie slice
12269 * @param {Boolean} vis Whether to show the slice or not. If undefined, the
12270 * visibility is toggled
12272 setVisible: function (vis) {
12274 chart = point.series.chart,
12275 tracker = point.tracker,
12276 dataLabel = point.dataLabel,
12277 connector = point.connector,
12278 shadowGroup = point.shadowGroup,
12281 // if called without an argument, toggle visibility
12282 point.visible = vis = vis === UNDEFINED ? !point.visible : vis;
12284 method = vis ? 'show' : 'hide';
12286 point.group[method]();
12291 dataLabel[method]();
12294 connector[method]();
12297 shadowGroup[method]();
12299 if (point.legendItem) {
12300 chart.legend.colorizeItem(point, vis);
12305 * Set or toggle whether the slice is cut out from the pie
12306 * @param {Boolean} sliced When undefined, the slice state is toggled
12307 * @param {Boolean} redraw Whether to redraw the chart. True by default.
12309 slice: function (sliced, redraw, animation) {
12311 series = point.series,
12312 chart = series.chart,
12313 slicedTranslation = point.slicedTranslation,
12316 setAnimation(animation, chart);
12318 // redraw is true by default
12319 redraw = pick(redraw, true);
12321 // if called without an argument, toggle
12322 sliced = point.sliced = defined(sliced) ? sliced : !point.sliced;
12325 translateX: (sliced ? slicedTranslation[0] : chart.plotLeft),
12326 translateY: (sliced ? slicedTranslation[1] : chart.plotTop)
12328 point.group.animate(translation);
12329 if (point.shadowGroup) {
12330 point.shadowGroup.animate(translation);
12337 * The Pie series class
12339 var PieSeries = extendClass(Series, {
12341 isCartesian: false,
12342 pointClass: PiePoint,
12343 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
12344 stroke: 'borderColor',
12345 'stroke-width': 'borderWidth',
12350 * Pies have one color each point
12352 getColor: function () {
12353 // record first color for use in setData
12354 this.initialColor = this.chart.counters.color;
12358 * Animate the column heights one by one from zero
12360 animate: function () {
12362 points = series.points;
12364 each(points, function (point) {
12365 var graphic = point.graphic,
12366 args = point.shapeArgs,
12382 }, series.options.animation);
12386 // delete this function to allow it only once
12387 series.animate = null;
12392 * Extend the basic setData method by running processData and generatePoints immediately,
12393 * in order to access the points from the legend.
12395 setData: function () {
12396 Series.prototype.setData.apply(this, arguments);
12397 this.processData();
12398 this.generatePoints();
12401 * Do translation for pie slices
12403 translate: function () {
12404 this.generatePoints();
12408 cumulative = -0.25, // start at top
12409 precision = 1000, // issue #172
12410 options = series.options,
12411 slicedOffset = options.slicedOffset,
12412 connectorOffset = slicedOffset + options.borderWidth,
12413 positions = options.center.concat([options.size, options.innerSize || 0]),
12414 chart = series.chart,
12415 plotWidth = chart.plotWidth,
12416 plotHeight = chart.plotHeight,
12420 points = series.points,
12423 smallestSize = mathMin(plotWidth, plotHeight),
12425 radiusX, // the x component of the radius vector for a given point
12427 labelDistance = options.dataLabels.distance;
12429 // get positions - either an integer or a percentage string must be given
12430 positions = map(positions, function (length, i) {
12432 isPercent = /%$/.test(length);
12434 // i == 0: centerX, relative to width
12435 // i == 1: centerY, relative to height
12436 // i == 2: size, relative to smallestSize
12437 // i == 4: innerSize, relative to smallestSize
12438 [plotWidth, plotHeight, smallestSize, smallestSize][i] *
12439 pInt(length) / 100 :
12443 // utility for getting the x value from a given y, used for anticollision logic in data labels
12444 series.getX = function (y, left) {
12446 angle = math.asin((y - positions[1]) / (positions[2] / 2 + labelDistance));
12448 return positions[0] +
12450 (mathCos(angle) * (positions[2] / 2 + labelDistance));
12453 // set center for later use
12454 series.center = positions;
12456 // get the total sum
12457 each(points, function (point) {
12461 each(points, function (point) {
12462 // set start and end angle
12463 fraction = total ? point.y / total : 0;
12464 start = mathRound(cumulative * circ * precision) / precision;
12465 cumulative += fraction;
12466 end = mathRound(cumulative * circ * precision) / precision;
12469 point.shapeType = 'arc';
12470 point.shapeArgs = {
12473 r: positions[2] / 2,
12474 innerR: positions[3] / 2,
12479 // center for the sliced out slice
12480 angle = (end + start) / 2;
12481 point.slicedTranslation = map([
12482 mathCos(angle) * slicedOffset + chart.plotLeft,
12483 mathSin(angle) * slicedOffset + chart.plotTop
12486 // set the anchor point for tooltips
12487 radiusX = mathCos(angle) * positions[2] / 2;
12488 radiusY = mathSin(angle) * positions[2] / 2;
12489 point.tooltipPos = [
12490 positions[0] + radiusX * 0.7,
12491 positions[1] + radiusY * 0.7
12494 // set the anchor point for data labels
12496 positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector
12497 positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a
12498 positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie
12499 positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a
12500 positions[0] + radiusX, // landing point for connector
12501 positions[1] + radiusY, // a/a
12502 labelDistance < 0 ? // alignment
12504 angle < circ / 4 ? 'left' : 'right', // alignment
12505 angle // center angle
12509 point.percentage = fraction * 100;
12510 point.total = total;
12515 this.setTooltipPoints();
12519 * Render the slices
12521 render: function () {
12524 // cache attributes for shapes
12525 series.getAttribs();
12529 // draw the mouse tracking area
12530 if (series.options.enableMouseTracking !== false) {
12531 series.drawTracker();
12534 this.drawDataLabels();
12536 if (series.options.animation && series.animate) {
12540 // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
12541 series.isDirty = false; // means data is in accordance with what you see
12545 * Draw the data points
12547 drawPoints: function () {
12549 chart = series.chart,
12550 renderer = chart.renderer,
12555 shadow = series.options.shadow,
12560 each(series.points, function (point) {
12561 graphic = point.graphic;
12562 shapeArgs = point.shapeArgs;
12563 group = point.group;
12564 shadowGroup = point.shadowGroup;
12566 // put the shadow behind all points
12567 if (shadow && !shadowGroup) {
12568 shadowGroup = point.shadowGroup = renderer.g('shadow')
12569 .attr({ zIndex: 4 })
12573 // create the group the first time
12575 group = point.group = renderer.g('point')
12576 .attr({ zIndex: 5 })
12580 // if the point is sliced, use special translation, else use plot area traslation
12581 groupTranslation = point.sliced ? point.slicedTranslation : [chart.plotLeft, chart.plotTop];
12582 group.translate(groupTranslation[0], groupTranslation[1]);
12584 shadowGroup.translate(groupTranslation[0], groupTranslation[1]);
12589 graphic.animate(shapeArgs);
12592 renderer.arc(shapeArgs)
12594 point.pointAttr[NORMAL_STATE],
12595 { 'stroke-linejoin': 'round' }
12598 .shadow(shadow, shadowGroup);
12601 // detect point specific visibility
12602 if (point.visible === false) {
12603 point.setVisible(false);
12611 * Override the base drawDataLabels method by pie specific functionality
12613 drawDataLabels: function () {
12615 data = series.data,
12617 chart = series.chart,
12618 options = series.options.dataLabels,
12619 connectorPadding = pick(options.connectorPadding, 10),
12620 connectorWidth = pick(options.connectorWidth, 1),
12623 softConnector = pick(options.softConnector, true),
12624 distanceOption = options.distance,
12625 seriesCenter = series.center,
12626 radius = seriesCenter[2] / 2,
12627 centerY = seriesCenter[1],
12628 outside = distanceOption > 0,
12632 halves = [// divide the points into right and left halves for anti collision
12644 // get out if not enabled
12645 if (!options.enabled) {
12649 // run parent method
12650 Series.prototype.drawDataLabels.apply(series);
12652 // arrange points for detection collision
12653 each(data, function (point) {
12654 if (point.dataLabel) { // it may have been cancelled in the base method (#407)
12656 point.labelPos[7] < mathPI / 2 ? 0 : 1
12660 halves[1].reverse();
12662 // define the sorting algorithm
12663 sort = function (a, b) {
12667 // assume equal label heights
12668 labelHeight = halves[0][0] && halves[0][0].dataLabel && pInt(halves[0][0].dataLabel.styles.lineHeight);
12670 /* Loop over the points in each quartile, starting from the top and bottom
12671 * of the pie to detect overlapping labels.
12678 points = halves[i],
12680 length = points.length,
12685 for (pos = centerY - radius - distanceOption; pos <= centerY + radius + distanceOption; pos += labelHeight) {
12687 // visualize the slot
12689 var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0),
12690 slotY = pos + chart.plotTop;
12691 if (!isNaN(slotX)) {
12692 chart.renderer.rect(slotX, slotY - 7, 100, labelHeight)
12698 chart.renderer.text('Slot '+ (slots.length - 1), slotX, slotY + 4)
12705 slotsLength = slots.length;
12707 // if there are more values than available slots, remove lowest values
12708 if (length > slotsLength) {
12709 // create an array for sorting and ranking the points within each quarter
12710 rankArr = [].concat(points);
12711 rankArr.sort(sort);
12714 rankArr[j].rank = j;
12718 if (points[j].rank >= slotsLength) {
12719 points.splice(j, 1);
12722 length = points.length;
12725 // The label goes to the nearest open slot, but not closer to the edge than
12726 // the label's index.
12727 for (j = 0; j < length; j++) {
12730 labelPos = point.labelPos;
12732 var closest = 9999,
12736 // find the closest slot index
12737 for (slotI = 0; slotI < slotsLength; slotI++) {
12738 distance = mathAbs(slots[slotI] - labelPos[1]);
12739 if (distance < closest) {
12740 closest = distance;
12745 // if that slot index is closer to the edges of the slots, move it
12746 // to the closest appropriate slot
12747 if (slotIndex < j && slots[j] !== null) { // cluster at the top
12749 } else if (slotsLength < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom
12750 slotIndex = slotsLength - length + j;
12751 while (slots[slotIndex] === null) { // make sure it is not taken
12755 // Slot is taken, find next free slot below. In the next run, the next slice will find the
12756 // slot above these, because it is the closest one
12757 while (slots[slotIndex] === null) { // make sure it is not taken
12762 usedSlots.push({ i: slotIndex, y: slots[slotIndex] });
12763 slots[slotIndex] = null; // mark as taken
12765 // sort them in order to fill in from the top
12766 usedSlots.sort(sort);
12769 // now the used slots are sorted, fill them up sequentially
12770 for (j = 0; j < length; j++) {
12773 labelPos = point.labelPos;
12774 dataLabel = point.dataLabel;
12775 var slot = usedSlots.pop(),
12776 naturalY = labelPos[1];
12778 visibility = point.visible === false ? HIDDEN : VISIBLE;
12779 slotIndex = slot.i;
12781 // if the slot next to currrent slot is free, the y value is allowed
12782 // to fall back to the natural position
12784 if ((naturalY > y && slots[slotIndex + 1] !== null) ||
12785 (naturalY < y && slots[slotIndex - 1] !== null)) {
12789 // get the x - use the natural x position for first and last slot, to prevent the top
12790 // and botton slice connectors from touching each other on either side
12791 x = series.getX(slotIndex === 0 || slotIndex === slots.length - 1 ? naturalY : y, i);
12793 // move or place the data label
12796 visibility: visibility,
12798 })[dataLabel.moved ? 'animate' : 'attr']({
12800 ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0),
12803 dataLabel.moved = true;
12805 // draw the connector
12806 if (outside && connectorWidth) {
12807 connector = point.connector;
12809 connectorPath = softConnector ? [
12811 x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
12813 x, y, // first break, next to the label
12814 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
12815 labelPos[2], labelPos[3], // second break
12817 labelPos[4], labelPos[5] // base
12820 x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
12822 labelPos[2], labelPos[3], // second break
12824 labelPos[4], labelPos[5] // base
12828 connector.animate({ d: connectorPath });
12829 connector.attr('visibility', visibility);
12832 point.connector = connector = series.chart.renderer.path(connectorPath).attr({
12833 'stroke-width': connectorWidth,
12834 stroke: options.connectorColor || point.color || '#606060',
12835 visibility: visibility,
12838 .translate(chart.plotLeft, chart.plotTop)
12847 * Draw point specific tracker objects. Inherit directly from column series.
12849 drawTracker: ColumnSeries.prototype.drawTracker,
12852 * Pies don't have point marker symbols
12854 getSymbol: function () {}
12857 seriesTypes.pie = PieSeries;
12859 /* ****************************************************************************
12860 * Start data grouping module *
12861 ******************************************************************************/
12862 /*jslint white:true */
12863 var DATA_GROUPING = 'dataGrouping',
12864 seriesProto = Series.prototype,
12865 baseProcessData = seriesProto.processData,
12866 baseGeneratePoints = seriesProto.generatePoints,
12867 baseDestroy = seriesProto.destroy,
12868 baseTooltipHeaderFormatter = seriesProto.tooltipHeaderFormatter,
12872 approximation: 'average', // average, open, high, low, close, sum
12873 //forced: undefined,
12874 groupPixelWidth: 2,
12875 // the first one is the point or start value, the second is the start value if we're dealing with range,
12876 // the third one is the end value if dealing with a range
12877 dateTimeLabelFormats: hash(
12878 MILLISECOND, ['%A, %b %e, %H:%M:%S.%L', '%A, %b %e, %H:%M:%S.%L', '-%H:%M:%S.%L'],
12879 SECOND, ['%A, %b %e, %H:%M:%S', '%A, %b %e, %H:%M:%S', '-%H:%M:%S'],
12880 MINUTE, ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'],
12881 HOUR, ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'],
12882 DAY, ['%A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'],
12883 WEEK, ['Week from %A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'],
12884 MONTH, ['%B %Y', '%B', '-%B %Y'],
12885 YEAR, ['%Y', '%Y', '-%Y']
12887 // smoothed = false, // enable this for navigator series only
12890 // units are defined in a separate array to allow complete overriding in case of a user option
12891 defaultDataGroupingUnits = [[
12892 MILLISECOND, // unit name
12893 [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
12896 [1, 2, 5, 10, 15, 30]
12899 [1, 2, 5, 10, 15, 30]
12902 [1, 2, 3, 4, 6, 8, 12]
12919 * Define the available approximation types. The data grouping approximations takes an array
12920 * or numbers as the first parameter. In case of ohlc, four arrays are sent in as four parameters.
12921 * Each array consists only of numbers. In case null values belong to the group, the property
12922 * .hasNulls will be set to true on the array.
12925 sum: function (arr) {
12926 var len = arr.length,
12929 // 1. it consists of nulls exclusively
12930 if (!len && arr.hasNulls) {
12932 // 2. it has a length and real values
12939 // 3. it has zero length, so just return undefined
12944 average: function (arr) {
12945 var len = arr.length,
12946 ret = approximations.sum(arr);
12948 // If we have a number, return it divided by the length. If not, return
12949 // null or undefined based on what the sum method finds.
12950 if (typeof ret === NUMBER && len) {
12956 open: function (arr) {
12957 return arr.length ? arr[0] : (arr.hasNulls ? null : UNDEFINED);
12959 high: function (arr) {
12960 return arr.length ? arrayMax(arr) : (arr.hasNulls ? null : UNDEFINED);
12962 low: function (arr) {
12963 return arr.length ? arrayMin(arr) : (arr.hasNulls ? null : UNDEFINED);
12965 close: function (arr) {
12966 return arr.length ? arr[arr.length - 1] : (arr.hasNulls ? null : UNDEFINED);
12968 // ohlc is a special case where a multidimensional array is input and an array is output
12969 ohlc: function (open, high, low, close) {
12970 open = approximations.open(open);
12971 high = approximations.high(high);
12972 low = approximations.low(low);
12973 close = approximations.close(close);
12975 if (typeof open === NUMBER || typeof high === NUMBER || typeof low === NUMBER || typeof close === NUMBER) {
12976 return [open, high, low, close];
12978 // else, return is undefined
12982 /*jslint white:false */
12985 * Takes parallel arrays of x and y data and groups the data into intervals defined by groupPositions, a collection
12986 * of starting x values for each group.
12988 seriesProto.groupData = function (xData, yData, groupPositions, approximation) {
12990 data = series.data,
12991 dataOptions = series.options.data,
12994 dataLength = xData.length,
12998 handleYData = !!yData, // when grouping the fake extended axis for panning, we don't need to consider y
13003 approximationFn = typeof approximation === 'function' ? approximation : approximations[approximation],
13006 for (i = 0; i <= dataLength; i++) {
13008 // when a new group is entered, summarize and initiate the previous group
13009 while ((groupPositions[1] !== UNDEFINED && xData[i] >= groupPositions[1]) ||
13010 i === dataLength) { // get the last group
13012 // get group x and y
13013 pointX = groupPositions.shift();
13014 groupedY = approximationFn(values1, values2, values3, values4);
13016 // push the grouped data
13017 if (groupedY !== UNDEFINED) {
13018 groupedXData.push(pointX);
13019 groupedYData.push(groupedY);
13022 // reset the aggregate arrays
13028 // don't loop beyond the last group
13029 if (i === dataLength) {
13035 if (i === dataLength) {
13039 // for each raw data point, push it to an array that contains all values for this specific group
13040 pointY = handleYData ? yData[i] : null;
13041 if (approximation === 'ohlc') {
13043 var index = series.cropStart + i,
13044 point = (data && data[index]) || series.pointClass.prototype.applyOptions.apply({}, [dataOptions[index]]),
13048 close = point.close;
13051 if (typeof open === NUMBER) {
13052 values1.push(open);
13053 } else if (open === null) {
13054 values1.hasNulls = true;
13057 if (typeof high === NUMBER) {
13058 values2.push(high);
13059 } else if (high === null) {
13060 values2.hasNulls = true;
13063 if (typeof low === NUMBER) {
13065 } else if (low === null) {
13066 values3.hasNulls = true;
13069 if (typeof close === NUMBER) {
13070 values4.push(close);
13071 } else if (close === null) {
13072 values4.hasNulls = true;
13075 if (typeof pointY === NUMBER) {
13076 values1.push(pointY);
13077 } else if (pointY === null) {
13078 values1.hasNulls = true;
13083 return [groupedXData, groupedYData];
13087 * Extend the basic processData method, that crops the data to the current zoom
13088 * range, with data grouping logic.
13090 seriesProto.processData = function () {
13092 options = series.options,
13093 dataGroupingOptions = options[DATA_GROUPING],
13094 groupingEnabled = dataGroupingOptions && dataGroupingOptions.enabled,
13095 groupedData = series.groupedData,
13099 series.forceCrop = groupingEnabled; // #334
13101 // skip if processData returns false or if grouping is disabled (in that order)
13102 if (baseProcessData.apply(series, arguments) === false || !groupingEnabled) {
13106 // clear previous groups, #622, #740
13107 each(groupedData || [], function (point, i) {
13109 groupedData[i] = point.destroy ? point.destroy() : null;
13115 chart = series.chart,
13116 processedXData = series.processedXData,
13117 processedYData = series.processedYData,
13118 plotSizeX = chart.plotSizeX,
13119 xAxis = series.xAxis,
13120 groupPixelWidth = pick(xAxis.groupPixelWidth, dataGroupingOptions.groupPixelWidth),
13121 dataLength = processedXData.length,
13122 chartSeries = chart.series,
13123 nonGroupedPointRange = series.pointRange;
13125 // attempt to solve #334: if multiple series are compared on the same x axis, give them the same
13126 // group pixel width
13127 if (!xAxis.groupPixelWidth) {
13128 i = chartSeries.length;
13130 if (chartSeries[i].xAxis === xAxis && chartSeries[i].options[DATA_GROUPING]) {
13131 groupPixelWidth = mathMax(groupPixelWidth, chartSeries[i].options[DATA_GROUPING].groupPixelWidth);
13134 xAxis.groupPixelWidth = groupPixelWidth;
13138 // Execute grouping if the amount of points is greater than the limit defined in groupPixelWidth
13139 if (dataLength > (plotSizeX / groupPixelWidth) || dataGroupingOptions.forced) {
13140 hasGroupedData = true;
13142 series.points = null; // force recreation of point instances in series.translate
13144 var extremes = xAxis.getExtremes(),
13145 xMin = extremes.min,
13146 xMax = extremes.max,
13147 groupIntervalFactor = (xAxis.getGroupIntervalFactor && xAxis.getGroupIntervalFactor(xMin, xMax, processedXData)) || 1,
13148 interval = (groupPixelWidth * (xMax - xMin) / plotSizeX) * groupIntervalFactor,
13149 groupPositions = (xAxis.getNonLinearTimeTicks || getTimeTicks)(
13150 normalizeTimeTickInterval(interval, dataGroupingOptions.units || defaultDataGroupingUnits),
13155 series.closestPointRange
13157 groupedXandY = seriesProto.groupData.apply(series, [processedXData, processedYData, groupPositions, dataGroupingOptions.approximation]),
13158 groupedXData = groupedXandY[0],
13159 groupedYData = groupedXandY[1];
13161 // prevent the smoothed data to spill out left and right, and make
13162 // sure data is not shifted to the left
13163 if (dataGroupingOptions.smoothed) {
13164 i = groupedXData.length - 1;
13165 groupedXData[i] = xMax;
13166 while (i-- && i > 0) {
13167 groupedXData[i] += interval / 2;
13169 groupedXData[0] = xMin;
13172 // record what data grouping values were used
13173 series.currentDataGrouping = groupPositions.info;
13174 if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC
13175 series.pointRange = groupPositions.info.totalRange;
13177 series.closestPointRange = groupPositions.info.totalRange;
13179 // set series props
13180 series.processedXData = groupedXData;
13181 series.processedYData = groupedYData;
13183 series.currentDataGrouping = null;
13184 series.pointRange = nonGroupedPointRange;
13187 series.hasGroupedData = hasGroupedData;
13190 seriesProto.generatePoints = function () {
13193 baseGeneratePoints.apply(series);
13195 // record grouped data in order to let it be destroyed the next time processData runs
13196 series.groupedData = series.hasGroupedData ? series.points : null;
13200 * Make the tooltip's header reflect the grouped range
13202 seriesProto.tooltipHeaderFormatter = function (key) {
13204 options = series.options,
13205 tooltipOptions = series.tooltipOptions,
13206 dataGroupingOptions = options.dataGrouping,
13207 xDateFormat = tooltipOptions.xDateFormat,
13209 xAxis = series.xAxis,
13210 currentDataGrouping,
13211 dateTimeLabelFormats,
13217 // apply only to grouped series
13218 if (xAxis && xAxis.options.type === 'datetime' && dataGroupingOptions) {
13221 currentDataGrouping = series.currentDataGrouping;
13222 dateTimeLabelFormats = dataGroupingOptions.dateTimeLabelFormats;
13224 // if we have grouped data, use the grouping information to get the right format
13225 if (currentDataGrouping) {
13226 labelFormats = dateTimeLabelFormats[currentDataGrouping.unitName];
13227 if (currentDataGrouping.count === 1) {
13228 xDateFormat = labelFormats[0];
13230 xDateFormat = labelFormats[1];
13231 xDateFormatEnd = labelFormats[2];
13233 // if not grouped, and we don't have set the xDateFormat option, get the best fit,
13234 // so if the least distance between points is one minute, show it, but if the
13235 // least distance is one day, skip hours and minutes etc.
13236 } else if (!xDateFormat) {
13237 for (n in timeUnits) {
13238 if (timeUnits[n] >= xAxis.closestPointRange) {
13239 xDateFormat = dateTimeLabelFormats[n][0];
13245 // now format the key
13246 formattedKey = dateFormat(xDateFormat, key);
13247 if (xDateFormatEnd) {
13248 formattedKey += dateFormat(xDateFormatEnd, key + currentDataGrouping.totalRange - 1);
13251 // return the replaced format
13252 ret = tooltipOptions.headerFormat.replace('{point.key}', formattedKey);
13254 // else, fall back to the regular formatter
13256 ret = baseTooltipHeaderFormatter.apply(series, [key]);
13263 * Extend the series destroyer
13265 seriesProto.destroy = function () {
13267 groupedData = series.groupedData || [],
13268 i = groupedData.length;
13271 if (groupedData[i]) {
13272 groupedData[i].destroy();
13275 baseDestroy.apply(series);
13279 // Extend the plot options
13282 defaultPlotOptions.line[DATA_GROUPING] =
13283 defaultPlotOptions.spline[DATA_GROUPING] =
13284 defaultPlotOptions.area[DATA_GROUPING] =
13285 defaultPlotOptions.areaspline[DATA_GROUPING] = commonOptions;
13287 // bar-like types (OHLC and candleticks inherit this as the classes are not yet built)
13288 defaultPlotOptions.column[DATA_GROUPING] = merge(commonOptions, {
13289 approximation: 'sum',
13290 groupPixelWidth: 10
13292 /* ****************************************************************************
13293 * End data grouping module *
13294 ******************************************************************************//* ****************************************************************************
13295 * Start OHLC series code *
13296 *****************************************************************************/
13298 // 1 - Set default options
13299 defaultPlotOptions.ohlc = merge(defaultPlotOptions.column, {
13302 approximation: 'ohlc',
13304 groupPixelWidth: 5 // allows to be packed tighter than candlesticks
13313 // 2- Create the OHLCPoint object
13314 var OHLCPoint = extendClass(Point, {
13316 * Apply the options containing the x and OHLC data and possible some extra properties.
13317 * This is called on point init or from point.update. Extends base Point by adding
13318 * multiple y-like values.
13320 * @param {Object} options
13322 applyOptions: function (options) {
13324 series = point.series,
13328 // object input for example:
13329 // { x: Date(2010, 0, 1), open: 7.88, high: 7.99, low: 7.02, close: 7.65 }
13330 if (typeof options === 'object' && typeof options.length !== 'number') {
13332 // copy options directly to point
13333 extend(point, options);
13335 point.options = options;
13336 } else if (options.length) { // array
13337 // with leading x value
13338 if (options.length === 5) {
13339 if (typeof options[0] === 'string') {
13340 point.name = options[0];
13341 } else if (typeof options[0] === 'number') {
13342 point.x = options[0];
13346 point.open = options[i++];
13347 point.high = options[i++];
13348 point.low = options[i++];
13349 point.close = options[i++];
13353 * If no x is set by now, get auto incremented value. All points must have an
13354 * x value, however the y value can be null to create a gap in the series
13356 point.y = point.high;
13357 if (point.x === UNDEFINED && series) {
13358 point.x = series.autoIncrement();
13364 * A specific OHLC tooltip formatter
13366 tooltipFormatter: function () {
13368 series = point.series;
13370 return ['<span style="color:' + series.color + ';font-weight:bold">', (point.name || series.name), '</span><br/>',
13371 'Open: ', point.open, '<br/>',
13372 'High: ', point.high, '<br/>',
13373 'Low: ', point.low, '<br/>',
13374 'Close: ', point.close, '<br/>'].join('');
13380 // 3 - Create the OHLCSeries object
13381 var OHLCSeries = extendClass(seriesTypes.column, {
13383 valueCount: 4, // four values per point
13384 pointClass: OHLCPoint,
13385 useThreshold: false,
13387 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
13389 'stroke-width': 'lineWidth'
13394 * Translate data points from raw values x and y to plotX and plotY
13396 translate: function () {
13398 yAxis = series.yAxis;
13400 seriesTypes.column.prototype.translate.apply(series);
13402 // do the translation
13403 each(series.points, function (point) {
13405 if (point.open !== null) {
13406 point.plotOpen = yAxis.translate(point.open, 0, 1, 0, 1);
13408 if (point.close !== null) {
13409 point.plotClose = yAxis.translate(point.close, 0, 1, 0, 1);
13416 * Draw the data points
13418 drawPoints: function () {
13420 points = series.points,
13421 chart = series.chart,
13432 each(points, function (point) {
13433 if (point.plotY !== UNDEFINED) {
13435 graphic = point.graphic;
13436 pointAttr = point.pointAttr[point.selected ? 'selected' : ''];
13438 // crisp vector coordinates
13439 crispCorr = (pointAttr['stroke-width'] % 2) / 2;
13440 crispX = mathRound(point.plotX) + crispCorr;
13441 halfWidth = mathRound(point.barW / 2);
13443 // the vertical stem
13446 crispX, mathRound(point.yBottom),
13448 crispX, mathRound(point.plotY)
13452 if (point.open !== null) {
13453 plotOpen = mathRound(point.plotOpen) + crispCorr;
13459 crispX - halfWidth,
13465 if (point.close !== null) {
13466 plotClose = mathRound(point.plotClose) + crispCorr;
13472 crispX + halfWidth,
13477 // create and/or update the graphic
13479 graphic.animate({ d: path });
13481 point.graphic = chart.renderer.path(path)
13483 .add(series.group);
13494 * Disable animation
13500 seriesTypes.ohlc = OHLCSeries;
13501 /* ****************************************************************************
13502 * End OHLC series code *
13503 *****************************************************************************/
13504 /* ****************************************************************************
13505 * Start Candlestick series code *
13506 *****************************************************************************/
13508 // 1 - set default options
13509 defaultPlotOptions.candlestick = merge(defaultPlotOptions.column, {
13511 approximation: 'ohlc',
13514 lineColor: 'black',
13524 // 2 - Create the CandlestickSeries object
13525 var CandlestickSeries = extendClass(OHLCSeries, {
13526 type: 'candlestick',
13529 * One-to-one mapping from options to SVG attributes
13531 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
13533 stroke: 'lineColor',
13534 'stroke-width': 'lineWidth'
13538 * Postprocess mapping between options and SVG attributes
13540 getAttribs: function () {
13541 OHLCSeries.prototype.getAttribs.apply(this, arguments);
13543 options = series.options,
13544 stateOptions = options.states,
13545 upColor = options.upColor,
13546 seriesDownPointAttr = merge(series.pointAttr);
13548 seriesDownPointAttr[''].fill = upColor;
13549 seriesDownPointAttr.hover.fill = stateOptions.hover.upColor || upColor;
13550 seriesDownPointAttr.select.fill = stateOptions.select.upColor || upColor;
13552 each(series.points, function (point) {
13553 if (point.open < point.close) {
13554 point.pointAttr = seriesDownPointAttr;
13560 * Draw the data points
13562 drawPoints: function () {
13563 var series = this, //state = series.state,
13564 points = series.points,
13565 chart = series.chart,
13578 each(points, function (point) {
13580 graphic = point.graphic;
13581 if (point.plotY !== UNDEFINED) {
13583 pointAttr = point.pointAttr[point.selected ? 'selected' : ''];
13585 // crisp vector coordinates
13586 crispCorr = (pointAttr['stroke-width'] % 2) / 2;
13587 crispX = mathRound(point.plotX) + crispCorr;
13588 plotOpen = mathRound(point.plotOpen) + crispCorr;
13589 plotClose = mathRound(point.plotClose) + crispCorr;
13590 topBox = math.min(plotOpen, plotClose);
13591 bottomBox = math.max(plotOpen, plotClose);
13592 halfWidth = mathRound(point.barW / 2);
13597 crispX - halfWidth, bottomBox,
13599 crispX - halfWidth, topBox,
13601 crispX + halfWidth, topBox,
13603 crispX + halfWidth, bottomBox,
13605 crispX - halfWidth, bottomBox,
13609 crispX, mathRound(point.yBottom),
13613 crispX, mathRound(point.plotY),
13618 graphic.animate({ d: path });
13620 point.graphic = chart.renderer.path(path)
13622 .add(series.group);
13633 seriesTypes.candlestick = CandlestickSeries;
13635 /* ****************************************************************************
13636 * End Candlestick series code *
13637 *****************************************************************************/
13638 /* ****************************************************************************
13639 * Start Flags series code *
13640 *****************************************************************************/
13642 var symbols = SVGRenderer.prototype.symbols;
13644 // 1 - set default options
13645 defaultPlotOptions.flags = merge(defaultPlotOptions.column, {
13646 dataGrouping: null,
13647 fillColor: 'white',
13649 pointRange: 0, // #673
13655 lineColor: 'black',
13656 fillColor: '#FCFFC5'
13661 fontWeight: 'bold',
13662 textAlign: 'center'
13667 // 2 - Create the CandlestickSeries object
13668 seriesTypes.flags = extendClass(seriesTypes.column, {
13670 noSharedTooltip: true,
13671 useThreshold: false,
13673 * Inherit the initialization from base Series
13675 init: Series.prototype.init,
13678 * One-to-one mapping from options to SVG attributes
13680 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
13683 'stroke-width': 'lineWidth',
13688 * Extend the translate method by placing the point on the related series
13690 translate: function () {
13692 seriesTypes.column.prototype.translate.apply(this);
13695 options = series.options,
13696 chart = series.chart,
13697 points = series.points,
13698 cursor = points.length - 1,
13702 optionsOnSeries = options.onSeries,
13703 onSeries = optionsOnSeries && chart.get(optionsOnSeries),
13709 // relate to a master series
13711 onData = onSeries.points;
13714 // sort the data points
13715 points.sort(function (a, b) {
13716 return (a.x - b.x);
13719 while (i-- && points[cursor]) {
13720 point = points[cursor];
13721 leftPoint = onData[i];
13722 if (leftPoint.x <= point.x) {
13723 point.plotY = leftPoint.plotY;
13725 // interpolate between points, #666
13726 if (leftPoint.x < point.x) {
13727 rightPoint = onData[i + 1];
13730 ((point.x - leftPoint.x) / (rightPoint.x - leftPoint.x)) * // the distance ratio, between 0 and 1
13731 (rightPoint.plotY - leftPoint.plotY); // the y distance
13736 i++; // check again for points in the same x position
13744 each(points, function (point, i) {
13745 // place on y axis or custom position
13747 point.plotY = point.y === UNDEFINED ? chart.plotHeight : point.plotY;
13749 // if multiple flags appear at the same x, order them into a stack
13750 lastPoint = points[i - 1];
13751 if (lastPoint && lastPoint.plotX === point.plotX) {
13752 if (lastPoint.stackIndex === UNDEFINED) {
13753 lastPoint.stackIndex = 0;
13755 point.stackIndex = lastPoint.stackIndex + 1;
13766 drawPoints: function () {
13769 points = series.points,
13770 chart = series.chart,
13771 renderer = chart.renderer,
13774 options = series.options,
13775 optionsY = options.y,
13776 shape = options.shape,
13784 crisp = (options.lineWidth % 2 / 2),
13791 plotX = point.plotX + crisp;
13792 stackIndex = point.stackIndex;
13793 plotY = point.plotY + optionsY + crisp - (stackIndex !== UNDEFINED && stackIndex * options.stackDistance);
13794 // outside to the left, on series but series is clipped
13795 if (isNaN(plotY)) {
13798 anchorX = stackIndex ? UNDEFINED : point.plotX + crisp; // skip connectors for higher level stacked points
13799 anchorY = stackIndex ? UNDEFINED : point.plotY;
13801 graphic = point.graphic;
13802 connector = point.connector;
13804 // only draw the point if y is defined
13805 if (plotY !== UNDEFINED) {
13807 pointAttr = point.pointAttr[point.selected ? 'select' : ''];
13808 if (graphic) { // update
13817 graphic = point.graphic = renderer.label(
13818 point.options.title || options.title || 'A',
13825 .css(merge(options.style, point.style))
13828 align: shape === 'flag' ? 'left' : 'center',
13829 width: options.width,
13830 height: options.height
13833 .shadow(options.shadow);
13837 // get the bounding box
13839 bBox = box.getBBox();
13841 // set the shape arguments for the tracker element
13842 point.shapeArgs = extend(
13845 x: plotX - (shape === 'flag' ? 0 : box.attr('width') / 2), // flags align left, else align center
13857 * Extend the column trackers with listeners to expand and contract stacks
13859 drawTracker: function () {
13862 seriesTypes.column.prototype.drawTracker.apply(series);
13864 // put each point in front on mouse over, this allows readability of vertically
13865 // stacked elements as well as tight points on the x axis
13866 each(series.points, function (point) {
13867 addEvent(point.tracker.element, 'mouseover', function () {
13868 point.graphic.toFront();
13874 * Override the regular tooltip formatter by returning the point text given
13877 tooltipFormatter: function (item) {
13878 return item.point.text;
13882 * Disable animation
13884 animate: function () {}
13888 // create the flag icon with anchor
13889 symbols.flag = function (x, y, w, h, options) {
13890 var anchorX = (options && options.anchorX) || x,
13891 anchorY = (options && options.anchorY) || y;
13894 'M', anchorX, anchorY,
13900 'M', anchorX, anchorY,
13905 // create the circlepin and squarepin icons with anchor
13906 each(['circle', 'square'], function (shape) {
13907 symbols[shape + 'pin'] = function (x, y, w, h, options) {
13909 var anchorX = options && options.anchorX,
13910 anchorY = options && options.anchorY,
13911 path = symbols[shape](x, y, w, h);
13913 if (anchorX && anchorY) {
13914 path.push('M', anchorX, y + h, 'L', anchorX, anchorY);
13921 // The symbol callbacks are generated on the SVGRenderer object in all browsers. Even
13922 // VML browsers need this in order to generate shapes in export. Now share
13923 // them with the VMLRenderer.
13924 if (Renderer === VMLRenderer) {
13925 each(['flag', 'circlepin', 'squarepin'], function (shape) {
13926 VMLRenderer.prototype.symbols[shape] = symbols[shape];
13930 /* ****************************************************************************
13931 * End Flags series code *
13932 *****************************************************************************/
13935 var MOUSEDOWN = hasTouch ? 'touchstart' : 'mousedown',
13936 MOUSEMOVE = hasTouch ? 'touchmove' : 'mousemove',
13937 MOUSEUP = hasTouch ? 'touchend' : 'mouseup';
13942 /* ****************************************************************************
13943 * Start Scroller code *
13944 *****************************************************************************/
13945 /*jslint white:true */
13946 var buttonGradient = hash(
13947 LINEAR_GRADIENT, { x1: 0, y1: 0, x2: 0, y2: 1 },
13953 units = [].concat(defaultDataGroupingUnits); // copy
13955 // add more resolution to units
13956 units[4] = [DAY, [1, 2, 3, 4]]; // allow more days
13957 units[5] = [WEEK, [1, 2, 3]]; // allow more weeks
13959 extend(defaultOptions, {
13963 backgroundColor: '#FFF',
13964 borderColor: '#666'
13968 maskFill: 'rgba(255, 255, 255, 0.75)',
13969 outlineColor: '#444',
13972 type: 'areaspline',
13977 approximation: 'average',
13978 groupPixelWidth: 2,
13985 id: PREFIX + 'navigator-series',
13986 lineColor: '#4572A7',
13994 //top: undefined, // docs
13999 tickPixelInterval: 200,
14008 startOnTick: false,
14023 height: hasTouch ? 20 : 14,
14024 barBackgroundColor: buttonGradient,
14025 barBorderRadius: 2,
14027 barBorderColor: '#666',
14028 buttonArrowColor: '#666',
14029 buttonBackgroundColor: buttonGradient,
14030 buttonBorderColor: '#666',
14031 buttonBorderRadius: 2,
14032 buttonBorderWidth: 1,
14033 rifleColor: '#666',
14034 trackBackgroundColor: hash(
14035 LINEAR_GRADIENT, { x1: 0, y1: 0, x2: 0, y2: 1 },
14041 trackBorderColor: '#CCC',
14042 trackBorderWidth: 1
14043 // trackBorderRadius: 0
14046 /*jslint white:false */
14049 * The Scroller class
14050 * @param {Object} chart
14052 Highcharts.Scroller = function (chart) {
14054 var renderer = chart.renderer,
14055 chartOptions = chart.options,
14056 navigatorOptions = chartOptions.navigator,
14057 navigatorEnabled = navigatorOptions.enabled,
14062 scrollbarOptions = chartOptions.scrollbar,
14063 scrollbarEnabled = scrollbarOptions.enabled,
14076 bodyStyle = document.body.style,
14079 handlesOptions = navigatorOptions.handles,
14080 height = navigatorEnabled ? navigatorOptions.height : 0,
14081 outlineWidth = navigatorOptions.outlineWidth,
14082 scrollbarHeight = scrollbarEnabled ? scrollbarOptions.height : 0,
14083 outlineHeight = height + scrollbarHeight,
14084 barBorderRadius = scrollbarOptions.barBorderRadius,
14086 halfOutline = outlineWidth / 2,
14091 baseSeriesOption = navigatorOptions.baseSeries,
14092 baseSeries = chart.series[baseSeriesOption] ||
14093 (typeof baseSeriesOption === 'string' && chart.get(baseSeriesOption)) ||
14096 // element wrappers
14105 scrollbarButtons = [],
14106 elementsToDestroy = []; // Array containing the elements to destroy when Scroller is destroyed
14108 chart.resetZoomEnabled = false;
14111 * Return the top of the navigation
14113 function getAxisTop(chartHeight) {
14114 return navigatorOptions.top || chartHeight - height - scrollbarHeight - chartOptions.chart.spacingBottom;
14118 * Draw one of the handles on the side of the zoomed range in the navigator
14119 * @param {Number} x The x center for the handle
14120 * @param {Number} index 0 for left and 1 for right
14122 function drawHandle(x, index) {
14125 fill: handlesOptions.backgroundColor,
14126 stroke: handlesOptions.borderColor,
14131 // create the elements
14135 handles[index] = renderer.g()
14136 .css({ cursor: 'e-resize' })
14137 .attr({ zIndex: 4 - index }) // zIndex = 3 for right handle, 4 for left
14141 tempElem = renderer.rect(-4.5, 0, 9, 16, 3, 1)
14143 .add(handles[index]);
14144 elementsToDestroy.push(tempElem);
14147 tempElem = renderer.path([
14157 .add(handles[index]);
14158 elementsToDestroy.push(tempElem);
14161 handles[index].translate(scrollerLeft + scrollbarHeight + parseInt(x, 10), top + height / 2 - 8);
14165 * Draw the scrollbar buttons with arrows
14166 * @param {Number} index 0 is left, 1 is right
14168 function drawScrollbarButton(index) {
14172 scrollbarButtons[index] = renderer.g().add(scrollbarGroup);
14174 tempElem = renderer.rect(
14177 scrollbarHeight + 1, // +1 to compensate for crispifying in rect method
14178 scrollbarHeight + 1,
14179 scrollbarOptions.buttonBorderRadius,
14180 scrollbarOptions.buttonBorderWidth
14182 stroke: scrollbarOptions.buttonBorderColor,
14183 'stroke-width': scrollbarOptions.buttonBorderWidth,
14184 fill: scrollbarOptions.buttonBackgroundColor
14185 }).add(scrollbarButtons[index]);
14186 elementsToDestroy.push(tempElem);
14188 tempElem = renderer.path([
14190 scrollbarHeight / 2 + (index ? -1 : 1), scrollbarHeight / 2 - 3,
14192 scrollbarHeight / 2 + (index ? -1 : 1), scrollbarHeight / 2 + 3,
14193 scrollbarHeight / 2 + (index ? 2 : -2), scrollbarHeight / 2
14195 fill: scrollbarOptions.buttonArrowColor
14196 }).add(scrollbarButtons[index]);
14197 elementsToDestroy.push(tempElem);
14200 // adjust the right side button to the varying length of the scroll track
14202 scrollbarButtons[index].attr({
14203 translateX: scrollerWidth - scrollbarHeight
14209 * Render the navigator and scroll bar
14210 * @param {Number} min X axis value minimum
14211 * @param {Number} max X axis value maximum
14212 * @param {Number} pxMin Pixel value minimum
14213 * @param {Number} pxMax Pixel value maximum
14215 function render(min, max, pxMin, pxMax) {
14217 // don't render the navigator until we have data (#486)
14223 scrollbarStrokeWidth = scrollbarOptions.barBorderWidth,
14226 outlineTop = top + halfOutline;
14227 navigatorLeft = pick(
14229 chart.plotLeft + scrollbarHeight // in case of scrollbar only, without navigator
14231 navigatorWidth = pick(xAxis.len, chart.plotWidth - 2 * scrollbarHeight);
14232 scrollerLeft = navigatorLeft - scrollbarHeight;
14233 scrollerWidth = navigatorWidth + 2 * scrollbarHeight;
14235 // Set the scroller x axis extremes to reflect the total. The navigator extremes
14236 // should always be the extremes of the union of all series in the chart as
14237 // well as the navigator series.
14238 if (xAxis.getExtremes) {
14239 var baseExtremes = chart.xAxis[0].getExtremes(), // the base
14240 noBase = baseExtremes.dataMin === null,
14241 navExtremes = xAxis.getExtremes(),
14242 newMin = mathMin(baseExtremes.dataMin, navExtremes.dataMin),
14243 newMax = mathMax(baseExtremes.dataMax, navExtremes.dataMax);
14245 if (!noBase && (newMin !== navExtremes.min || newMax !== navExtremes.max)) {
14246 xAxis.setExtremes(newMin, newMax, true, false);
14250 // get the pixel position of the handles
14251 pxMin = pick(pxMin, xAxis.translate(min));
14252 pxMax = pick(pxMax, xAxis.translate(max));
14255 // handles are allowed to cross
14256 zoomedMin = pInt(mathMin(pxMin, pxMax));
14257 zoomedMax = pInt(mathMax(pxMin, pxMax));
14258 range = zoomedMax - zoomedMin;
14260 // on first render, create all elements
14263 if (navigatorEnabled) {
14265 leftShade = renderer.rect()
14267 fill: navigatorOptions.maskFill,
14270 rightShade = renderer.rect()
14272 fill: navigatorOptions.maskFill,
14275 outline = renderer.path()
14277 'stroke-width': outlineWidth,
14278 stroke: navigatorOptions.outlineColor,
14284 if (scrollbarEnabled) {
14286 // draw the scrollbar group
14287 scrollbarGroup = renderer.g().add();
14289 // the scrollbar track
14290 strokeWidth = scrollbarOptions.trackBorderWidth;
14291 scrollbarTrack = renderer.rect().attr({
14292 y: -strokeWidth % 2 / 2,
14293 fill: scrollbarOptions.trackBackgroundColor,
14294 stroke: scrollbarOptions.trackBorderColor,
14295 'stroke-width': strokeWidth,
14296 r: scrollbarOptions.trackBorderRadius || 0,
14297 height: scrollbarHeight
14298 }).add(scrollbarGroup);
14300 // the scrollbar itself
14301 scrollbar = renderer.rect()
14303 y: -scrollbarStrokeWidth % 2 / 2,
14304 height: scrollbarHeight,
14305 fill: scrollbarOptions.barBackgroundColor,
14306 stroke: scrollbarOptions.barBorderColor,
14307 'stroke-width': scrollbarStrokeWidth,
14310 .add(scrollbarGroup);
14312 scrollbarRifles = renderer.path()
14314 stroke: scrollbarOptions.rifleColor,
14317 .add(scrollbarGroup);
14322 if (navigatorEnabled) {
14330 x: navigatorLeft + zoomedMax,
14332 width: navigatorWidth - zoomedMax,
14335 outline.attr({ d: [
14337 scrollerLeft, outlineTop, // left
14339 navigatorLeft + zoomedMin + halfOutline, outlineTop, // upper left of zoomed range
14340 navigatorLeft + zoomedMin + halfOutline, outlineTop + outlineHeight - scrollbarHeight, // lower left of z.r.
14342 navigatorLeft + zoomedMax - halfOutline, outlineTop + outlineHeight - scrollbarHeight, // lower right of z.r.
14344 navigatorLeft + zoomedMax - halfOutline, outlineTop, // upper right of z.r.
14345 scrollerLeft + scrollerWidth, outlineTop // right
14348 drawHandle(zoomedMin + halfOutline, 0);
14349 drawHandle(zoomedMax + halfOutline, 1);
14352 // draw the scrollbar
14353 if (scrollbarEnabled) {
14355 // draw the buttons
14356 drawScrollbarButton(0);
14357 drawScrollbarButton(1);
14359 scrollbarGroup.translate(scrollerLeft, mathRound(outlineTop + height));
14361 scrollbarTrack.attr({
14362 width: scrollerWidth
14366 x: mathRound(scrollbarHeight + zoomedMin) + (scrollbarStrokeWidth % 2 / 2),
14367 width: range - scrollbarStrokeWidth
14370 centerBarX = scrollbarHeight + zoomedMin + range / 2 - 0.5;
14372 scrollbarRifles.attr({ d: [
14374 centerBarX - 3, scrollbarHeight / 4,
14376 centerBarX - 3, 2 * scrollbarHeight / 3,
14378 centerBarX, scrollbarHeight / 4,
14380 centerBarX, 2 * scrollbarHeight / 3,
14382 centerBarX + 3, scrollbarHeight / 4,
14384 centerBarX + 3, 2 * scrollbarHeight / 3
14386 visibility: range > 12 ? VISIBLE : HIDDEN
14394 * Event handler for the mouse down event.
14396 function mouseDownHandler(e) {
14397 e = chart.tracker.normalizeMouseEvent(e);
14398 var chartX = e.chartX,
14400 handleSensitivity = hasTouch ? 10 : 7,
14404 if (chartY > top && chartY < top + height + scrollbarHeight) { // we're vertically inside the navigator
14405 isOnNavigator = !scrollbarEnabled || chartY < top + height;
14407 // grab the left handle
14408 if (isOnNavigator && math.abs(chartX - zoomedMin - navigatorLeft) < handleSensitivity) {
14409 grabbedLeft = true;
14410 otherHandlePos = zoomedMax;
14412 // grab the right handle
14413 } else if (isOnNavigator && math.abs(chartX - zoomedMax - navigatorLeft) < handleSensitivity) {
14414 grabbedRight = true;
14415 otherHandlePos = zoomedMin;
14417 // grab the zoomed range
14418 } else if (chartX > navigatorLeft + zoomedMin && chartX < navigatorLeft + zoomedMax) {
14419 grabbedCenter = chartX;
14420 defaultBodyCursor = bodyStyle.cursor;
14421 bodyStyle.cursor = 'ew-resize';
14423 dragOffset = chartX - zoomedMin;
14425 // shift the range by clicking on shaded areas, scrollbar track or scrollbar buttons
14426 } else if (chartX > scrollerLeft && chartX < scrollerLeft + scrollerWidth) {
14428 if (isOnNavigator) { // center around the clicked point
14429 left = chartX - navigatorLeft - range / 2;
14430 } else { // click on scrollbar
14431 if (chartX < navigatorLeft) { // click left scrollbar button
14432 left = zoomedMin - mathMin(10, range);
14433 } else if (chartX > scrollerLeft + scrollerWidth - scrollbarHeight) {
14434 left = zoomedMin + mathMin(10, range);
14436 // click on scrollbar track, shift the scrollbar by one range
14437 left = chartX < navigatorLeft + zoomedMin ? // on the left
14438 zoomedMin - range :
14444 } else if (left + range > navigatorWidth) {
14445 left = navigatorWidth - range;
14447 if (left !== zoomedMin) { // it has actually moved
14448 chart.xAxis[0].setExtremes(
14449 xAxis.translate(left, true),
14450 xAxis.translate(left + range, true),
14457 if (e.preventDefault) { // tries to drag object when clicking on the shades
14458 e.preventDefault();
14463 * Event handler for the mouse move event.
14465 function mouseMoveHandler(e) {
14466 e = chart.tracker.normalizeMouseEvent(e);
14467 var chartX = e.chartX;
14469 // validation for handle dragging
14470 if (chartX < navigatorLeft) {
14471 chartX = navigatorLeft;
14472 } else if (chartX > scrollerLeft + scrollerWidth - scrollbarHeight) {
14473 chartX = scrollerLeft + scrollerWidth - scrollbarHeight;
14476 // drag left handle
14479 render(0, 0, chartX - navigatorLeft, otherHandlePos);
14481 // drag right handle
14482 } else if (grabbedRight) {
14484 render(0, 0, otherHandlePos, chartX - navigatorLeft);
14486 // drag scrollbar or open area in navigator
14487 } else if (grabbedCenter) {
14489 if (chartX < dragOffset) { // outside left
14490 chartX = dragOffset;
14491 } else if (chartX > navigatorWidth + dragOffset - range) { // outside right
14492 chartX = navigatorWidth + dragOffset - range;
14495 render(0, 0, chartX - dragOffset, chartX - dragOffset + range);
14500 * Event handler for the mouse up event.
14502 function mouseUpHandler() {
14504 chart.xAxis[0].setExtremes(
14505 xAxis.translate(zoomedMin, true),
14506 xAxis.translate(zoomedMax, true),
14511 grabbedLeft = grabbedRight = grabbedCenter = hasDragged = dragOffset = null;
14512 bodyStyle.cursor = defaultBodyCursor;
14515 function updatedDataHandler() {
14516 var baseXAxis = baseSeries.xAxis,
14517 baseExtremes = baseXAxis.getExtremes(),
14518 baseMin = baseExtremes.min,
14519 baseMax = baseExtremes.max,
14520 baseDataMin = baseExtremes.dataMin,
14521 baseDataMax = baseExtremes.dataMax,
14522 range = baseMax - baseMin,
14528 navXData = navigatorSeries.xData,
14529 hasSetExtremes = !!baseXAxis.setExtremes;
14531 // detect whether to move the range
14532 stickToMax = baseMax >= navXData[navXData.length - 1];
14533 stickToMin = baseMin <= baseDataMin;
14535 // set the navigator series data to the new data of the base series
14536 if (!navigatorData) {
14537 navigatorSeries.options.pointStart = baseSeries.xData[0];
14538 navigatorSeries.setData(baseSeries.options.data, false);
14542 // if the zoomed range is already at the min, move it to the right as new data
14545 newMin = baseDataMin;
14546 newMax = newMin + range;
14549 // if the zoomed range is already at the max, move it to the right as new data
14552 newMax = baseDataMax;
14553 if (!stickToMin) { // if stickToMin is true, the new min value is set above
14554 newMin = mathMax(newMax - range, navigatorSeries.xData[0]);
14558 // update the extremes
14559 if (hasSetExtremes && (stickToMin || stickToMax)) {
14560 baseXAxis.setExtremes(newMin, newMax, true, false);
14561 // if it is not at any edge, just move the scroller window to reflect the new series data
14564 chart.redraw(false);
14568 mathMax(baseMin, baseDataMin),
14569 mathMin(baseMax, baseDataMax)
14575 * Set up the mouse and touch events for the navigator and scrollbar
14577 function addEvents() {
14578 addEvent(chart.container, MOUSEDOWN, mouseDownHandler);
14579 addEvent(chart.container, MOUSEMOVE, mouseMoveHandler);
14580 addEvent(document, MOUSEUP, mouseUpHandler);
14584 * Removes the event handlers attached previously with addEvents.
14586 function removeEvents() {
14587 removeEvent(chart.container, MOUSEDOWN, mouseDownHandler);
14588 removeEvent(chart.container, MOUSEMOVE, mouseMoveHandler);
14589 removeEvent(document, MOUSEUP, mouseUpHandler);
14590 if (navigatorEnabled) {
14591 removeEvent(baseSeries, 'updatedData', updatedDataHandler);
14596 * Initiate the Scroller object
14599 var xAxisIndex = chart.xAxis.length,
14600 yAxisIndex = chart.yAxis.length,
14601 baseChartSetSize = chart.setSize;
14603 // make room below the chart
14604 chart.extraBottomMargin = outlineHeight + navigatorOptions.margin;
14605 // get the top offset
14606 top = getAxisTop(chart.chartHeight);
14608 if (navigatorEnabled) {
14609 var baseOptions = baseSeries.options,
14610 mergedNavSeriesOptions,
14611 baseData = baseOptions.data,
14612 navigatorSeriesOptions = navigatorOptions.series;
14614 // remove it to prevent merging one by one
14615 navigatorData = navigatorSeriesOptions.data;
14616 baseOptions.data = navigatorSeriesOptions.data = null;
14619 // an x axis is required for scrollbar also
14620 xAxis = new chart.Axis(merge({
14621 ordinal: baseSeries.xAxis.options.ordinal // inherit base xAxis' ordinal option
14622 }, navigatorOptions.xAxis, {
14626 height: height, // docs + width
14627 top: top, // docs + left
14629 offsetLeft: scrollbarHeight, // docs
14630 offsetRight: -scrollbarHeight, // docs
14631 startOnTick: false,
14638 yAxis = new chart.Axis(merge(navigatorOptions.yAxis, {
14639 alignTicks: false, // docs
14647 // dmerge the series options
14648 mergedNavSeriesOptions = merge(baseSeries.options, navigatorSeriesOptions, {
14649 threshold: null, // docs
14650 clip: false, // docs
14651 enableMouseTracking: false,
14652 group: 'nav', // for columns
14657 showInLegend: false,
14662 // set the data back
14663 baseOptions.data = baseData;
14664 navigatorSeriesOptions.data = navigatorData;
14665 mergedNavSeriesOptions.data = navigatorData || baseData;
14668 navigatorSeries = chart.initSeries(mergedNavSeriesOptions);
14670 // respond to updated data in the base series
14671 // todo: use similiar hook when base series is not yet initialized
14672 addEvent(baseSeries, 'updatedData', updatedDataHandler);
14674 // in case of scrollbar only, fake an x axis to get translation
14677 translate: function (value, reverse) {
14678 var ext = baseSeries.xAxis.getExtremes(),
14679 scrollTrackWidth = chart.plotWidth - 2 * scrollbarHeight,
14680 dataMin = ext.dataMin,
14681 valueRange = ext.dataMax - dataMin;
14684 // from pixel to value
14685 (value * valueRange / scrollTrackWidth) + dataMin :
14686 // from value to pixel
14687 scrollTrackWidth * (value - dataMin) / valueRange;
14693 // Override the chart.setSize method to adjust the xAxis and yAxis top option as well.
14694 // This needs to be done prior to chart.resize
14695 chart.setSize = function (width, height, animation) {
14696 xAxis.options.top = yAxis.options.top = top = getAxisTop(height);
14697 baseChartSetSize.call(chart, width, height, animation);
14704 * Destroys allocated elements.
14706 function destroy() {
14707 // Disconnect events added in addEvents
14710 // Destroy local variables
14711 each([xAxis, yAxis, leftShade, rightShade, outline, scrollbarTrack, scrollbar, scrollbarRifles, scrollbarGroup], function (obj) {
14712 if (obj && obj.destroy) {
14716 xAxis = yAxis = leftShade = rightShade = outline = scrollbarTrack = scrollbar = scrollbarRifles = scrollbarGroup = null;
14718 // Destroy elements in collection
14719 each([scrollbarButtons, handles, elementsToDestroy], function (coll) {
14720 destroyObjectProperties(coll);
14735 /* ****************************************************************************
14736 * End Scroller code *
14737 *****************************************************************************/
14739 /* ****************************************************************************
14740 * Start Range Selector code *
14741 *****************************************************************************/
14742 extend(defaultOptions, {
14745 // buttons: {Object}
14746 // buttonSpacing: 0,
14758 // inputDateFormat: '%b %e, %Y',
14759 // inputEditDateFormat: '%Y-%m-%d',
14760 // inputEnabled: true,
14763 // selected: undefined
14765 // - button styles for normal, hover and select state
14766 // - CSS text styles
14767 // - styles for the inputs and labels
14770 defaultOptions.lang = merge(defaultOptions.lang, {
14771 rangeSelectorZoom: 'Zoom',
14772 rangeSelectorFrom: 'From:',
14773 rangeSelectorTo: 'To:'
14777 * The object constructor for the range selector
14778 * @param {Object} chart
14780 Highcharts.RangeSelector = function (chart) {
14781 var renderer = chart.renderer,
14783 container = chart.container,
14784 lang = defaultOptions.lang,
14788 boxSpanElements = {},
14796 defaultButtons = [{
14819 chart.resetZoomEnabled = false;
14822 * The method to run when one of the buttons in the range selectors is clicked
14823 * @param {Number} i The index of the button
14824 * @param {Object} rangeOptions
14825 * @param {Boolean} redraw
14827 function clickButton(i, rangeOptions, redraw) {
14829 var baseAxis = chart.xAxis[0],
14830 extremes = baseAxis && baseAxis.getExtremes(),
14832 dataMin = extremes && extremes.dataMin,
14833 dataMax = extremes && extremes.dataMax,
14835 newMax = baseAxis && mathMin(extremes.max, dataMax),
14836 date = new Date(newMax),
14837 type = rangeOptions.type,
14838 count = rangeOptions.count,
14843 // these time intervals have a fixed number of milliseconds, as opposed
14844 // to month, ytd and year
14850 day: 24 * 3600 * 1000,
14851 week: 7 * 24 * 3600 * 1000
14854 if (dataMin === null || dataMax === null || // chart has no data, base series is removed
14855 i === selected) { // same button is clicked twice
14859 if (fixedTimes[type]) {
14860 range = fixedTimes[type] * count;
14861 newMin = mathMax(newMax - range, dataMin);
14862 } else if (type === 'month') {
14863 date.setMonth(date.getMonth() - count);
14864 newMin = mathMax(date.getTime(), dataMin);
14865 range = 30 * 24 * 3600 * 1000 * count;
14866 } else if (type === 'ytd') {
14867 date = new Date(0);
14869 year = now.getFullYear();
14870 date.setFullYear(year);
14872 // workaround for IE6 bug, which sets year to next year instead of current
14873 if (String(year) !== dateFormat('%Y', date)) {
14874 date.setFullYear(year - 1);
14877 newMin = rangeMin = mathMax(dataMin || 0, date.getTime());
14878 now = now.getTime();
14879 newMax = mathMin(dataMax || now, now);
14880 } else if (type === 'year') {
14881 date.setFullYear(date.getFullYear() - count);
14882 newMin = mathMax(dataMin, date.getTime());
14883 range = 365 * 24 * 3600 * 1000 * count;
14884 } else if (type === 'all' && baseAxis) {
14889 // mark the button pressed
14891 buttons[i].setState(2);
14894 // update the chart
14895 if (!baseAxis) { // axis not yet instanciated
14896 baseXAxisOptions = chart.options.xAxis;
14897 baseXAxisOptions[0] = merge(
14898 baseXAxisOptions[0],
14906 } else { // existing axis object; after render time
14907 setTimeout(function () { // make sure the visual state is set before the heavy process begins
14908 baseAxis.setExtremes(
14921 * The handler connected to container that handles mousedown.
14923 function mouseDownHandler() {
14933 * Initialize the range selector
14936 chart.extraTopMargin = 25;
14937 options = chart.options.rangeSelector;
14938 buttonOptions = options.buttons || defaultButtons;
14941 var selectedOption = options.selected;
14943 addEvent(container, MOUSEDOWN, mouseDownHandler);
14945 // zoomed range based on a pre-selected button index
14946 if (selectedOption !== UNDEFINED && buttonOptions[selectedOption]) {
14947 clickButton(selectedOption, buttonOptions[selectedOption], false);
14950 // normalize the pressed button whenever a new range is selected
14951 addEvent(chart, 'load', function () {
14952 addEvent(chart.xAxis[0], 'afterSetExtremes', function () {
14953 if (buttons[selected]) {
14954 buttons[selected].setState(0);
14963 * Set the internal and displayed value of a HTML input for the dates
14964 * @param {Object} input
14965 * @param {Number} time
14967 function setInputValue(input, time) {
14968 var format = input.hasFocus ? options.inputEditDateFormat || '%Y-%m-%d' : options.inputDateFormat || '%b %e, %Y';
14970 input.HCTime = time;
14972 input.value = dateFormat(format, input.HCTime);
14976 * Draw either the 'from' or the 'to' HTML input box of the range selector
14977 * @param {Object} name
14979 function drawInput(name) {
14980 var isMin = name === 'min',
14983 // create the text label
14984 boxSpanElements[name] = createElement('span', {
14985 innerHTML: lang[isMin ? 'rangeSelectorFrom' : 'rangeSelectorTo']
14986 }, options.labelStyle, div);
14988 // create the input element
14989 input = createElement('input', {
14991 className: PREFIX + 'range-selector',
14996 border: '1px solid silver',
14998 marginRight: isMin ? '5px' : '0',
14999 textAlign: 'center'
15000 }, options.inputStyle), div);
15003 input.onfocus = input.onblur = function (e) {
15004 e = e || window.event;
15005 input.hasFocus = e.type === 'focus';
15006 setInputValue(input);
15009 // handle changes in the input boxes
15010 input.onchange = function () {
15011 var inputValue = input.value,
15012 value = Date.parse(inputValue),
15013 extremes = chart.xAxis[0].getExtremes();
15015 // if the value isn't parsed directly to a value by the browser's Date.parse method,
15016 // like YYYY-MM-DD in IE, try parsing it a different way
15017 if (isNaN(value)) {
15018 value = inputValue.split('-');
15019 value = Date.UTC(pInt(value[0]), pInt(value[1]) - 1, pInt(value[2]));
15022 if (!isNaN(value) &&
15023 ((isMin && (value >= extremes.dataMin && value <= rightBox.HCTime)) ||
15024 (!isMin && (value <= extremes.dataMax && value >= leftBox.HCTime)))
15026 chart.xAxis[0].setExtremes(
15027 isMin ? value : extremes.min,
15028 isMin ? extremes.max : value
15037 * Render the range selector including the buttons and the inputs. The first time render
15038 * is called, the elements are created and positioned. On subsequent calls, they are
15039 * moved and updated.
15040 * @param {Number} min X axis minimum
15041 * @param {Number} max X axis maximum
15043 function render(min, max) {
15044 var chartStyle = chart.options.chart.style,
15045 buttonTheme = options.buttonTheme,
15046 inputEnabled = options.inputEnabled !== false,
15047 states = buttonTheme && buttonTheme.states,
15048 plotLeft = chart.plotLeft,
15051 // create the elements
15053 zoomText = renderer.text(lang.rangeSelectorZoom, plotLeft, chart.plotTop - 10)
15054 .css(options.labelStyle)
15057 // button starting position
15058 buttonLeft = plotLeft + zoomText.getBBox().width + 5;
15060 each(buttonOptions, function (rangeOptions, i) {
15061 buttons[i] = renderer.button(
15064 chart.plotTop - 25,
15066 clickButton(i, rangeOptions);
15067 this.isActive = true;
15070 states && states.hover,
15071 states && states.select
15074 textAlign: 'center'
15078 // increase button position for the next button
15079 buttonLeft += buttons[i].width + (options.buttonSpacing || 0);
15081 if (selected === i) {
15082 buttons[i].setState(2);
15087 // first create a wrapper outside the container in order to make
15088 // the inputs work and make export correct
15089 if (inputEnabled) {
15090 divRelative = div = createElement('div', null, {
15091 position: 'relative',
15093 fontFamily: chartStyle.fontFamily,
15094 fontSize: chartStyle.fontSize,
15095 zIndex: 1 // above container
15098 container.parentNode.insertBefore(div, container);
15100 // create an absolutely positionied div to keep the inputs
15101 divAbsolute = div = createElement('div', null, extend({
15102 position: 'absolute',
15103 top: (chart.plotTop - 25) + 'px',
15104 right: (chart.chartWidth - chart.plotLeft - chart.plotWidth) + 'px'
15105 }, options.inputBoxStyle), div);
15107 leftBox = drawInput('min');
15109 rightBox = drawInput('max');
15113 if (inputEnabled) {
15114 setInputValue(leftBox, min);
15115 setInputValue(rightBox, max);
15123 * Destroys allocated elements.
15125 function destroy() {
15126 removeEvent(container, MOUSEDOWN, mouseDownHandler);
15128 // Destroy elements in collections
15129 each([buttons], function (coll) {
15130 destroyObjectProperties(coll);
15133 // Destroy zoomText
15135 zoomText = zoomText.destroy();
15138 // Clear input element events
15140 leftBox.onfocus = leftBox.onblur = leftBox.onchange = null;
15143 rightBox.onfocus = rightBox.onblur = rightBox.onchange = null;
15146 // Discard divs and spans
15147 each([leftBox, rightBox, boxSpanElements.min, boxSpanElements.max, divAbsolute, divRelative], function (item) {
15148 discardElement(item);
15150 // Null the references
15151 leftBox = rightBox = boxSpanElements = div = divAbsolute = divRelative = null;
15155 // Run RangeSelector
15165 /* ****************************************************************************
15166 * End Range Selector code *
15167 *****************************************************************************/
15171 Chart.prototype.callbacks.push(function (chart) {
15173 scroller = chart.scroller,
15174 rangeSelector = chart.rangeSelector;
15176 function renderScroller() {
15177 extremes = chart.xAxis[0].getExtremes();
15179 mathMax(extremes.min, extremes.dataMin),
15180 mathMin(extremes.max, extremes.dataMax)
15184 function renderRangeSelector() {
15185 extremes = chart.xAxis[0].getExtremes();
15186 rangeSelector.render(extremes.min, extremes.max);
15189 function afterSetExtremesHandlerScroller(e) {
15190 scroller.render(e.min, e.max);
15193 function afterSetExtremesHandlerRangeSelector(e) {
15194 rangeSelector.render(e.min, e.max);
15197 function destroyEvents() {
15199 removeEvent(chart, 'resize', renderScroller);
15200 removeEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerScroller);
15202 if (rangeSelector) {
15203 removeEvent(chart, 'resize', renderRangeSelector);
15204 removeEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerRangeSelector);
15208 // initiate the scroller
15210 // redraw the scroller on setExtremes
15211 addEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerScroller);
15213 // redraw the scroller chart resize
15214 addEvent(chart, 'resize', renderScroller);
15219 if (rangeSelector) {
15220 // redraw the scroller on setExtremes
15221 addEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerRangeSelector);
15223 // redraw the scroller chart resize
15224 addEvent(chart, 'resize', renderRangeSelector);
15227 renderRangeSelector();
15230 // Remove resize/afterSetExtremes at chart destroy
15231 addEvent(chart, 'destroy', destroyEvents);
15234 * A wrapper for Chart with all the default values for a Stock chart
15236 Highcharts.StockChart = function (options, callback) {
15237 var seriesOptions = options.series, // to increase performance, don't merge the data
15250 // gapSize: 0, // docs
15262 // apply X axis options to both single and multi y axes
15263 options.xAxis = map(splat(options.xAxis || {}), function (xAxisOptions) {
15264 return merge({ // defaults
15271 showLastLabel: true
15272 }, xAxisOptions, // user options
15273 { // forced options
15279 // apply Y axis options to both single and multi y axes
15280 options.yAxis = map(splat(options.yAxis || {}), function (yAxisOptions) {
15281 opposite = yAxisOptions.opposite;
15282 return merge({ // defaults
15284 align: opposite ? 'right' : 'left',
15285 x: opposite ? -2 : 2,
15288 showLastLabel: false,
15292 }, yAxisOptions // user options
15296 options.series = null;
15324 spline: lineOptions,
15326 areaspline: lineOptions,
15337 options, // user's options
15339 { // forced options
15345 options.series = seriesOptions;
15348 return new Chart(options, callback);
15352 /* ****************************************************************************
15353 * Start value compare logic *
15354 *****************************************************************************/
15356 var seriesInit = seriesProto.init,
15357 seriesProcessData = seriesProto.processData,
15358 pointTooltipFormatter = Point.prototype.tooltipFormatter;
15361 * Extend series.init by adding a method to modify the y value used for plotting
15362 * on the y axis. This method is called both from the axis when finding dataMin
15363 * and dataMax, and from the series.translate method.
15365 seriesProto.init = function () {
15367 // call base method
15368 seriesInit.apply(this, arguments);
15372 compare = series.options.compare;
15375 series.modifyValue = function (value, point) {
15376 var compareValue = this.compareValue;
15378 // get the modified value
15379 value = compare === 'value' ?
15380 value - compareValue : // compare value
15381 value = 100 * (value / compareValue) - 100; // compare percent
15383 // record for tooltip etc.
15385 point.change = value;
15394 * Extend series.processData by finding the first y value in the plot area,
15395 * used for comparing the following values
15397 seriesProto.processData = function () {
15400 // call base method
15401 seriesProcessData.apply(this, arguments);
15403 if (series.options.compare) {
15407 processedXData = series.processedXData,
15408 processedYData = series.processedYData,
15409 length = processedYData.length,
15410 min = series.xAxis.getExtremes().min;
15412 // find the first value for comparison
15413 for (; i < length; i++) {
15414 if (typeof processedYData[i] === NUMBER && processedXData[i] >= min) {
15415 series.compareValue = processedYData[i];
15423 * Extend the tooltip formatter by adding support for the point.change variable
15424 * as well as the changeDecimals option
15426 Point.prototype.tooltipFormatter = function (pointFormat) {
15429 pointFormat = pointFormat.replace(
15431 (point.change > 0 ? '+' : '') + numberFormat(point.change, point.series.tooltipOptions.changeDecimals || 2)
15434 return pointTooltipFormatter.apply(this, [pointFormat]);
15437 /* ****************************************************************************
15438 * End value compare logic *
15439 *****************************************************************************/
15441 /* ****************************************************************************
15442 * Start ordinal axis logic *
15443 *****************************************************************************/
15446 var baseInit = seriesProto.init,
15447 baseGetSegments = seriesProto.getSegments;
15449 seriesProto.init = function () {
15454 // call base method
15455 baseInit.apply(series, arguments);
15457 // chart and xAxis are set in base init
15458 chart = series.chart;
15459 xAxis = series.xAxis;
15461 // Destroy the extended ordinal index on updated data
15462 if (xAxis && xAxis.options.ordinal) {
15463 addEvent(series, 'updatedData', function () {
15464 delete xAxis.ordinalIndex;
15469 * Extend the ordinal axis object. If we rewrite the axis object to a prototype model,
15470 * we should add these properties to the prototype instead.
15472 if (xAxis && xAxis.options.ordinal && !xAxis.hasOrdinalExtension) {
15474 xAxis.hasOrdinalExtension = true;
15477 * Calculate the ordinal positions before tick positions are calculated.
15478 * TODO: When we rewrite Axis to use a prototype model, this should be implemented
15479 * as a method extension to avoid overhead in the core.
15481 xAxis.beforeSetTickPositions = function () {
15484 ordinalPositions = [],
15485 useOrdinal = false,
15487 extremes = axis.getExtremes(),
15488 min = extremes.min,
15489 max = extremes.max,
15495 // apply the ordinal logic
15496 if (axis.options.ordinal) {
15498 each(axis.series, function (series, i) {
15500 if (series.visible !== false) {
15502 // concatenate the processed X data into the existing positions, or the empty array
15503 ordinalPositions = ordinalPositions.concat(series.processedXData);
15504 len = ordinalPositions.length;
15506 // if we're dealing with more than one series, remove duplicates
15509 ordinalPositions.sort(function (a, b) {
15510 return a - b; // without a custom function it is sorted as strings
15515 if (ordinalPositions[i] === ordinalPositions[i + 1]) {
15516 ordinalPositions.splice(i, 1);
15524 // cache the length
15525 len = ordinalPositions.length;
15527 // Check if we really need the overhead of mapping axis data against the ordinal positions.
15528 // If the series consist of evenly spaced data any way, we don't need any ordinal logic.
15529 if (len > 2) { // two points have equal distance by default
15530 dist = ordinalPositions[1] - ordinalPositions[0];
15532 while (i-- && !useOrdinal) {
15533 if (ordinalPositions[i + 1] - ordinalPositions[i] !== dist) {
15539 // Record the slope and offset to compute the linear values from the array index.
15540 // Since the ordinal positions may exceed the current range, get the start and
15541 // end positions within it (#719, #665b)
15545 axis.ordinalPositions = ordinalPositions;
15547 // This relies on the ordinalPositions being set
15548 minIndex = xAxis.val2lin(min, true);
15549 maxIndex = xAxis.val2lin(max, true);
15551 // Set the slope and offset of the values compared to the indices in the ordinal positions
15552 axis.ordinalSlope = slope = (max - min) / (maxIndex - minIndex);
15553 axis.ordinalOffset = min - (minIndex * slope);
15556 axis.ordinalPositions = axis.ordinalSlope = axis.ordinalOffset = UNDEFINED;
15562 * Translate from a linear axis value to the corresponding ordinal axis position. If there
15563 * are no gaps in the ordinal axis this will be the same. The translated value is the value
15564 * that the point would have if the axis were linear, using the same min and max.
15566 * @param Number val The axis value
15567 * @param Boolean toIndex Whether to return the index in the ordinalPositions or the new value
15569 xAxis.val2lin = function (val, toIndex) {
15572 ordinalPositions = axis.ordinalPositions;
15574 if (!ordinalPositions) {
15579 var ordinalLength = ordinalPositions.length,
15584 // first look for an exact match in the ordinalpositions array
15587 if (ordinalPositions[i] === val) {
15593 // if that failed, find the intermediate position between the two nearest values
15594 i = ordinalLength - 1;
15596 if (val > ordinalPositions[i] || i === 0) { // interpolate
15597 distance = (val - ordinalPositions[i]) / (ordinalPositions[i + 1] - ordinalPositions[i]); // something between 0 and 1
15598 ordinalIndex = i + distance;
15604 axis.ordinalSlope * (ordinalIndex || 0) + axis.ordinalOffset;
15609 * Translate from linear (internal) to axis value
15611 * @param Number val The linear abstracted value
15612 * @param Boolean fromIndex Translate from an index in the ordinal positions rather than a value
15614 xAxis.lin2val = function (val, fromIndex) {
15616 ordinalPositions = axis.ordinalPositions;
15618 if (!ordinalPositions) { // the visible range contains only equally spaced values
15623 var ordinalSlope = axis.ordinalSlope,
15624 ordinalOffset = axis.ordinalOffset,
15625 i = ordinalPositions.length - 1,
15626 linearEquivalentLeft,
15627 linearEquivalentRight,
15631 // Handle the case where we translate from the index directly, used only
15632 // when panning an ordinal axis
15635 if (val < 0) { // out of range, in effect panning to the left
15636 val = ordinalPositions[0];
15637 } else if (val > i) { // out of range, panning to the right
15638 val = ordinalPositions[i];
15639 } else { // split it up
15640 i = mathFloor(val);
15641 distance = val - i; // the decimal
15644 // Loop down along the ordinal positions. When the linear equivalent of i matches
15645 // an ordinal position, interpolate between the left and right values.
15648 linearEquivalentLeft = (ordinalSlope * i) + ordinalOffset;
15649 if (val >= linearEquivalentLeft) {
15650 linearEquivalentRight = (ordinalSlope * (i + 1)) + ordinalOffset;
15651 distance = (val - linearEquivalentLeft) / (linearEquivalentRight - linearEquivalentLeft); // something between 0 and 1
15657 // If the index is within the range of the ordinal positions, return the associated
15658 // or interpolated value. If not, just return the value
15659 return distance !== UNDEFINED && ordinalPositions[i] !== UNDEFINED ?
15660 ordinalPositions[i] + (distance ? distance * (ordinalPositions[i + 1] - ordinalPositions[i]) : 0) :
15666 * Get the ordinal positions for the entire data set. This is necessary in chart panning
15667 * because we need to find out what points or data groups are available outside the
15668 * visible range. When a panning operation starts, if an index for the given grouping
15669 * does not exists, it is created and cached. This index is deleted on updated data, so
15670 * it will be regenerated the next time a panning operation starts.
15672 xAxis.getExtendedPositions = function () {
15673 var grouping = xAxis.series[0].currentDataGrouping,
15674 ordinalIndex = xAxis.ordinalIndex,
15675 key = grouping ? grouping.count + grouping.unitName : 'raw',
15676 extremes = xAxis.getExtremes(),
15680 // If this is the first time, or the ordinal index is deleted by updatedData,
15682 if (!ordinalIndex) {
15683 ordinalIndex = xAxis.ordinalIndex = {};
15687 if (!ordinalIndex[key]) {
15689 // Create a fake axis object where the extended ordinal positions are emulated
15692 getExtremes: function () {
15694 min: extremes.dataMin,
15695 max: extremes.dataMax
15703 // Add the fake series to hold the full data, then apply processData to it
15704 each(xAxis.series, function (series) {
15707 xData: series.xData,
15710 fakeSeries.options = {
15711 dataGrouping : grouping ? {
15714 approximation: 'open', // doesn't matter which, use the fastest
15715 units: [[grouping.unitName, [grouping.count]]]
15720 series.processData.apply(fakeSeries);
15722 fakeAxis.series.push(fakeSeries);
15725 // Run beforeSetTickPositions to compute the ordinalPositions
15726 xAxis.beforeSetTickPositions.apply(fakeAxis);
15729 ordinalIndex[key] = fakeAxis.ordinalPositions;
15731 return ordinalIndex[key];
15735 * Find the factor to estimate how wide the plot area would have been if ordinal
15736 * gaps were included. This value is used to compute an imagined plot width in order
15737 * to establish the data grouping interval.
15739 * A real world case is the intraday-candlestick
15740 * example. Without this logic, it would show the correct data grouping when viewing
15741 * a range within each day, but once moving the range to include the gap between two
15742 * days, the interval would include the cut-away night hours and the data grouping
15743 * would be wrong. So the below method tries to compensate by identifying the most
15744 * common point interval, in this case days.
15746 * An opposite case is presented in issue #718. We have a long array of daily data,
15747 * then one point is appended one hour after the last point. We expect the data grouping
15750 * In the future, if we find cases where this estimation doesn't work optimally, we
15751 * might need to add a second pass to the data grouping logic, where we do another run
15752 * with a greater interval if the number of data groups is more than a certain fraction
15753 * of the desired group count.
15755 xAxis.getGroupIntervalFactor = function (xMin, xMax, processedXData) {
15757 len = processedXData.length,
15761 // Register all the distances in an array
15762 for (; i < len - 1; i++) {
15763 distances[i] = processedXData[i + 1] - processedXData[i];
15766 // Sort them and find the median
15767 distances.sort(function (a, b) {
15770 median = distances[mathFloor(len / 2)];
15772 // Return the factor needed for data grouping
15773 return (len * median) / (xMax - xMin);
15777 * Make the tick intervals closer because the ordinal gaps make the ticks spread out or cluster
15779 xAxis.postProcessTickInterval = function (tickInterval) {
15780 var ordinalSlope = this.ordinalSlope;
15782 return ordinalSlope ?
15783 tickInterval / (ordinalSlope / xAxis.closestPointRange) :
15788 * In an ordinal axis, there might be areas with dense consentrations of points, then large
15789 * gaps between some. Creating equally distributed ticks over this entire range
15790 * may lead to a huge number of ticks that will later be removed. So instead, break the
15791 * positions up in segments, find the tick positions for each segment then concatenize them.
15792 * This method is used from both data grouping logic and X axis tick position logic.
15794 xAxis.getNonLinearTimeTicks = function (normalizedInterval, min, max, startOfWeek, positions, closestDistance, findHigherRanks) {
15800 hasCrossedHigherRank,
15804 groupPositions = [];
15806 // The positions are not always defined, for example for ordinal positions when data
15807 // has regular interval
15808 if (!positions || min === UNDEFINED) {
15809 return getTimeTicks(normalizedInterval, min, max, startOfWeek);
15812 // Analyze the positions array to split it into segments on gaps larger than 5 times
15813 // the closest distance. The closest distance is already found at this point, so
15814 // we reuse that instead of computing it again.
15815 posLength = positions.length;
15816 for (; end < posLength; end++) {
15818 outsideMax = end && positions[end - 1] > max;
15820 if (positions[end] < min) { // Set the last position before min
15823 } else if (end === posLength - 1 || positions[end + 1] - positions[end] > closestDistance * 5 || outsideMax) {
15825 // For each segment, calculate the tick positions from the getTimeTicks utility
15826 // function. The interval will be the same regardless of how long the segment is.
15827 segmentPositions = getTimeTicks(normalizedInterval, positions[start], positions[end], startOfWeek);
15829 groupPositions = groupPositions.concat(segmentPositions);
15831 // Set start of next segment
15840 // Get the grouping info from the last of the segments. The info is the same for
15842 info = segmentPositions.info;
15844 // Optionally identify ticks with higher rank, for example when the ticks
15845 // have crossed midnight.
15846 if (findHigherRanks && info.unitRange <= timeUnits[HOUR]) {
15847 end = groupPositions.length - 1;
15849 // Compare points two by two
15850 for (start = 1; start < end; start++) {
15851 if (new Date(groupPositions[start])[getDate]() !== new Date(groupPositions[start - 1])[getDate]()) {
15852 higherRanks[groupPositions[start]] = DAY;
15853 hasCrossedHigherRank = true;
15857 // If the complete array has crossed midnight, we want to mark the first
15858 // positions also as higher rank
15859 if (hasCrossedHigherRank) {
15860 higherRanks[groupPositions[0]] = DAY;
15862 info.higherRanks = higherRanks;
15866 groupPositions.info = info;
15870 return groupPositions;
15875 * Post process tick positions. The tickPositions array is altered. Don't show ticks
15876 * within a gap in the ordinal axis, where the space between
15877 * two points is greater than a portion of the tick pixel interval
15879 addEvent(xAxis, 'afterSetTickPositions', function (e) {
15881 var options = xAxis.options,
15882 tickPixelIntervalOption = options.tickPixelInterval,
15883 tickPositions = e.tickPositions;
15885 if (xAxis.ordinalPositions && defined(tickPixelIntervalOption)) { // check for squashed ticks
15886 var i = tickPositions.length,
15890 tickInfo = tickPositions.info,
15891 higherRanks = tickInfo ? tickInfo.higherRanks : [];
15894 translated = xAxis.translate(tickPositions[i]);
15896 // Remove ticks that are closer than 0.6 times the pixel interval from the one to the right
15897 if (lastTranslated && lastTranslated - translated < tickPixelIntervalOption * 0.6) {
15899 // Is this a higher ranked position with a normal position to the right?
15900 if (higherRanks[tickPositions[i]] && !higherRanks[tickPositions[i + 1]]) {
15902 // Yes: remove the lower ranked neighbour to the right
15903 itemToRemove = i + 1;
15904 lastTranslated = translated; // #709
15908 // No: remove this one
15912 tickPositions.splice(itemToRemove, 1);
15915 lastTranslated = translated;
15923 * Overrride the chart.pan method for ordinal axes.
15926 var baseChartPan = chart.pan;
15927 chart.pan = function (chartX) {
15928 var xAxis = chart.xAxis[0],
15930 if (xAxis.options.ordinal) {
15932 var mouseDownX = chart.mouseDownX,
15933 extremes = xAxis.getExtremes(),
15934 dataMax = extremes.dataMax,
15935 min = extremes.min,
15936 max = extremes.max,
15939 hoverPoints = chart.hoverPoints,
15940 closestPointRange = xAxis.closestPointRange,
15941 pointPixelWidth = xAxis.translationSlope * (xAxis.ordinalSlope || closestPointRange),
15942 movedUnits = (mouseDownX - chartX) / pointPixelWidth, // how many ordinal units did we move?
15943 extendedAxis = { ordinalPositions: xAxis.getExtendedPositions() }, // get index of all the chart's points
15946 lin2val = xAxis.lin2val,
15947 val2lin = xAxis.val2lin,
15950 if (!extendedAxis.ordinalPositions) { // we have an ordinal axis, but the data is equally spaced
15953 } else if (mathAbs(movedUnits) > 1) {
15955 // Remove active points for shared tooltip
15957 each(hoverPoints, function (point) {
15962 if (movedUnits < 0) {
15963 searchAxisLeft = extendedAxis;
15964 searchAxisRight = xAxis.ordinalPositions ? xAxis : extendedAxis;
15966 searchAxisLeft = xAxis.ordinalPositions ? xAxis : extendedAxis;
15967 searchAxisRight = extendedAxis;
15970 // In grouped data series, the last ordinal position represents the grouped data, which is
15971 // to the left of the real data max. If we don't compensate for this, we will be allowed
15972 // to pan grouped data series passed the right of the plot area.
15973 ordinalPositions = searchAxisRight.ordinalPositions;
15974 if (dataMax > ordinalPositions[ordinalPositions.length - 1]) {
15975 ordinalPositions.push(dataMax);
15978 // Get the new min and max values by getting the ordinal index for the current extreme,
15979 // then add the moved units and translate back to values. This happens on the
15980 // extended ordinal positions if the new position is out of range, else it happens
15981 // on the current x axis which is smaller and faster.
15982 newMin = lin2val.apply(searchAxisLeft, [
15983 val2lin.apply(searchAxisLeft, [min, true]) + movedUnits, // the new index
15984 true // translate from index
15986 newMax = lin2val.apply(searchAxisRight, [
15987 val2lin.apply(searchAxisRight, [max, true]) + movedUnits, // the new index
15988 true // translate from index
15991 // Apply it if it is within the available data range
15992 if (newMin > mathMin(extremes.dataMin, min) && newMax < mathMax(dataMax, max)) {
15993 xAxis.setExtremes(newMin, newMax, true, false);
15996 chart.mouseDownX = chartX; // set new reference for next run
15997 css(chart.container, { cursor: 'move' });
16004 // revert to the linear chart.pan version
16006 baseChartPan.apply(chart, arguments);
16013 * Extend getSegments by identifying gaps in the ordinal data so that we can draw a gap in the
16016 seriesProto.getSegments = function () {
16020 gapSize = series.options.gapSize;
16022 // call base method
16023 baseGetSegments.apply(series);
16025 if (series.xAxis.options.ordinal && gapSize) {
16028 segments = series.segments;
16030 // extension for ordinal breaks
16031 each(segments, function (segment, no) {
16032 var i = segment.length - 1;
16034 if (segment[i + 1].x - segment[i].x > series.xAxis.closestPointRange * gapSize) {
16035 segments.splice( // insert after this one
16038 segment.splice(i + 1, segment.length - i)
16047 /* ****************************************************************************
16048 * End ordinal axis logic *
16049 *****************************************************************************/
16050 // global variables
16051 extend(Highcharts, {
16053 dateFormat: dateFormat,
16054 pathAnim: pathAnim,
16055 getOptions: getOptions,
16056 hasRtlBug: hasRtlBug,
16057 numberFormat: numberFormat,
16060 Renderer: Renderer,
16061 seriesTypes: seriesTypes,
16062 setOptions: setOptions,
16065 // Expose utility funcitons for modules
16066 addEvent: addEvent,
16067 removeEvent: removeEvent,
16068 createElement: createElement,
16069 discardElement: discardElement,
16077 extendClass: extendClass,
16078 product: 'Highstock',