Add OK and Cancel button for grid editing
[phpmyadmin/arisferyanto.git] / js / highcharts / highcharts.js
blobb3b8e9d5711ba2dda36c01fc4db7d78bff4b6622
1 // ==ClosureCompiler==
2 // @compilation_level SIMPLE_OPTIMIZATIONS
4 /**
5 * @license Highcharts JS v2.1.5 (2011-06-22)
6 *
7 * (c) 2009-2011 Torstein Hønsi
8 *
9 * License: www.highcharts.com/license
12 // JSLint options:
13 /*jslint forin: true */
14 /*global document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $ */
16 (function() {
17 // encapsulated variables
18 var doc = document,
19 win = window,
20 math = Math,
21 mathRound = math.round,
22 mathFloor = math.floor,
23 mathCeil = math.ceil,
24 mathMax = math.max,
25 mathMin = math.min,
26 mathAbs = math.abs,
27 mathCos = math.cos,
28 mathSin = math.sin,
29 mathPI = math.PI,
30 deg2rad = mathPI * 2 / 360,
33 // some variables
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 //hasSVG = win.SVGAngle || doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1"),
40 hasSVG = !!doc.createElementNS && !!doc.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGRect,
41 SVG_NS = 'http://www.w3.org/2000/svg',
42 Renderer,
43 hasTouch = doc.documentElement.ontouchstart !== undefined,
44 colorCounter,
45 symbolCounter,
46 symbolSizes = { },
47 idCounter = 0,
48 timeFactor = 1, // 1 = JavaScript time, 1000 = Unix time
49 garbageBin,
50 defaultOptions,
51 dateFormat, // function
52 globalAnimation,
53 pathAnim,
56 // some constants for frequently used strings
57 UNDEFINED,
58 DIV = 'div',
59 ABSOLUTE = 'absolute',
60 RELATIVE = 'relative',
61 HIDDEN = 'hidden',
62 PREFIX = 'highcharts-',
63 VISIBLE = 'visible',
64 PX = 'px',
65 NONE = 'none',
66 M = 'M',
67 L = 'L',
69 * Empirical lowest possible opacities for TRACKER_FILL
70 * IE6: 0.002
71 * IE7: 0.002
72 * IE8: 0.002
73 * IE9: 0.00000000001 (unlimited)
74 * FF: 0.00000000001 (unlimited)
75 * Chrome: 0.000001
76 * Safari: 0.000001
77 * Opera: 0.00000000001 (unlimited)
79 TRACKER_FILL = 'rgba(192,192,192,'+ (hasSVG ? 0.000001 : 0.002) +')', // invisible but clickable
80 NORMAL_STATE = '',
81 HOVER_STATE = 'hover',
82 SELECT_STATE = 'select',
84 // time methods, changed based on whether or not UTC is used
85 makeTime,
86 getMinutes,
87 getHours,
88 getDay,
89 getDate,
90 getMonth,
91 getFullYear,
92 setMinutes,
93 setHours,
94 setDate,
95 setMonth,
96 setFullYear,
98 // check for a custom HighchartsAdapter defined prior to this file
99 globalAdapter = win.HighchartsAdapter,
100 adapter = globalAdapter || {},
102 // Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object
103 // and all the utility functions will be null. In that case they are populated by the
104 // default adapters below.
105 each = adapter.each,
106 grep = adapter.grep,
107 map = adapter.map,
108 merge = adapter.merge,
109 hyphenate = adapter.hyphenate,
110 addEvent = adapter.addEvent,
111 removeEvent = adapter.removeEvent,
112 fireEvent = adapter.fireEvent,
113 animate = adapter.animate,
114 stop = adapter.stop,
116 // lookup over the types and the associated classes
117 seriesTypes = {},
118 hoverChart;
121 * Extend an object with the members of another
122 * @param {Object} a The object to be extended
123 * @param {Object} b The object to add to the first one
125 function extend(a, b) {
126 var n;
127 if (!a) {
128 a = {};
130 for (n in b) {
131 a[n] = b[n];
133 return a;
137 * Shortcut for parseInt
138 * @param {Object} s
140 function pInt(s, mag) {
141 return parseInt(s, mag || 10);
145 * Check for string
146 * @param {Object} s
148 function isString(s) {
149 return typeof s === 'string';
153 * Check for object
154 * @param {Object} obj
156 function isObject(obj) {
157 return typeof obj === 'object';
161 * Check for number
162 * @param {Object} n
164 function isNumber(n) {
165 return typeof n === 'number';
168 function log2lin(num) {
169 return math.log(num) / math.LN10;
171 function lin2log(num) {
172 return math.pow(10, num);
176 * Remove last occurence of an item from an array
177 * @param {Array} arr
178 * @param {Mixed} item
180 function erase(arr, item) {
181 var i = arr.length;
182 while (i--) {
183 if (arr[i] === item) {
184 arr.splice(i, 1);
185 break;
188 //return arr;
192 * Returns true if the object is not null or undefined. Like MooTools' $.defined.
193 * @param {Object} obj
195 function defined (obj) {
196 return obj !== UNDEFINED && obj !== null;
200 * Set or get an attribute or an object of attributes. Can't use jQuery attr because
201 * it attempts to set expando properties on the SVG element, which is not allowed.
203 * @param {Object} elem The DOM element to receive the attribute(s)
204 * @param {String|Object} prop The property or an abject of key-value pairs
205 * @param {String} value The value if a single property is set
207 function attr(elem, prop, value) {
208 var key,
209 setAttribute = 'setAttribute',
210 ret;
212 // if the prop is a string
213 if (isString(prop)) {
214 // set the value
215 if (defined(value)) {
217 elem[setAttribute](prop, value);
219 // get the value
220 } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo...
221 ret = elem.getAttribute(prop);
224 // else if prop is defined, it is a hash of key/value pairs
225 } else if (defined(prop) && isObject(prop)) {
226 for (key in prop) {
227 elem[setAttribute](key, prop[key]);
230 return ret;
233 * Check if an element is an array, and if not, make it into an array. Like
234 * MooTools' $.splat.
236 function splat(obj) {
237 if (!obj || obj.constructor !== Array) {
238 obj = [obj];
240 return obj;
246 * Return the first value that is defined. Like MooTools' $.pick.
248 function pick() {
249 var args = arguments,
251 arg,
252 length = args.length;
253 for (i = 0; i < length; i++) {
254 arg = args[i];
255 if (typeof arg !== 'undefined' && arg !== null) {
256 return arg;
261 * Make a style string from a JS object
262 * @param {Object} style
264 function serializeCSS(style) {
265 var s = '',
266 key;
267 // serialize the declaration
268 for (key in style) {
269 s += key +':'+ style[key] + ';';
271 return s;
275 * Set CSS on a given element
276 * @param {Object} el
277 * @param {Object} styles Style object with camel case property names
279 function css (el, styles) {
280 if (isIE) {
281 if (styles && styles.opacity !== UNDEFINED) {
282 styles.filter = 'alpha(opacity='+ (styles.opacity * 100) +')';
285 extend(el.style, styles);
288 /* *
289 * Get CSS value on a given element
290 * @param {Object} el DOM object
291 * @param {String} styleProp Camel cased CSS propery
293 function getStyle (el, styleProp) {
294 var ret,
295 CURRENT_STYLE = 'currentStyle',
296 GET_COMPUTED_STYLE = 'getComputedStyle';
297 if (el[CURRENT_STYLE]) {
298 ret = el[CURRENT_STYLE][styleProp];
299 } else if (win[GET_COMPUTED_STYLE]) {
300 ret = win[GET_COMPUTED_STYLE](el, null).getPropertyValue(hyphenate(styleProp));
302 return ret;
306 * Utility function to create element with attributes and styles
307 * @param {Object} tag
308 * @param {Object} attribs
309 * @param {Object} styles
310 * @param {Object} parent
311 * @param {Object} nopad
313 function createElement (tag, attribs, styles, parent, nopad) {
314 var el = doc.createElement(tag);
315 if (attribs) {
316 extend(el, attribs);
318 if (nopad) {
319 css(el, {padding: 0, border: NONE, margin: 0});
321 if (styles) {
322 css(el, styles);
324 if (parent) {
325 parent.appendChild(el);
327 return el;
331 * Extend a prototyped class by new members
332 * @param {Object} parent
333 * @param {Object} members
335 function extendClass(parent, members) {
336 var object = function(){};
337 object.prototype = new parent();
338 extend(object.prototype, members);
339 return object;
343 * Format a number and return a string based on input settings
344 * @param {Number} number The input number to format
345 * @param {Number} decimals The amount of decimals
346 * @param {String} decPoint The decimal point, defaults to the one given in the lang options
347 * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options
349 function numberFormat (number, decimals, decPoint, thousandsSep) {
350 var lang = defaultOptions.lang,
351 // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/
352 n = number, c = isNaN(decimals = mathAbs(decimals)) ? 2 : decimals,
353 d = decPoint === undefined ? lang.decimalPoint : decPoint,
354 t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep, s = n < 0 ? "-" : "",
355 i = String(pInt(n = mathAbs(+n || 0).toFixed(c))),
356 j = i.length > 3 ? i.length % 3 : 0;
358 return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) +
359 (c ? d + mathAbs(n - i).toFixed(c).slice(2) : "");
363 * Based on http://www.php.net/manual/en/function.strftime.php
364 * @param {String} format
365 * @param {Number} timestamp
366 * @param {Boolean} capitalize
368 dateFormat = function (format, timestamp, capitalize) {
369 function pad (number) {
370 return number.toString().replace(/^([0-9])$/, '0$1');
373 if (!defined(timestamp) || isNaN(timestamp)) {
374 return 'Invalid date';
376 format = pick(format, '%Y-%m-%d %H:%M:%S');
378 var date = new Date(timestamp * timeFactor),
379 key, // used in for constuct below
380 // get the basic time values
381 hours = date[getHours](),
382 day = date[getDay](),
383 dayOfMonth = date[getDate](),
384 month = date[getMonth](),
385 fullYear = date[getFullYear](),
386 lang = defaultOptions.lang,
387 langWeekdays = lang.weekdays,
388 langMonths = lang.months,
389 /* // uncomment this and the 'W' format key below to enable week numbers
390 weekNumber = function() {
391 var clone = new Date(date.valueOf()),
392 day = clone[getDay]() == 0 ? 7 : clone[getDay](),
393 dayNumber;
394 clone.setDate(clone[getDate]() + 4 - day);
395 dayNumber = mathFloor((clone.getTime() - new Date(clone[getFullYear](), 0, 1, -6)) / 86400000);
396 return 1 + mathFloor(dayNumber / 7);
400 // list all format keys
401 replacements = {
403 // Day
404 'a': langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon'
405 'A': langWeekdays[day], // Long weekday, like 'Monday'
406 'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31
407 'e': dayOfMonth, // Day of the month, 1 through 31
409 // Week (none implemented)
410 //'W': weekNumber(),
412 // Month
413 'b': langMonths[month].substr(0, 3), // Short month, like 'Jan'
414 'B': langMonths[month], // Long month, like 'January'
415 'm': pad(month + 1), // Two digit month number, 01 through 12
417 // Year
418 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009
419 'Y': fullYear, // Four digits year, like 2009
421 // Time
422 'H': pad(hours), // Two digits hours in 24h format, 00 through 23
423 'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11
424 'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12
425 'M': pad(date[getMinutes]()), // Two digits minutes, 00 through 59
426 'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM
427 'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM
428 'S': pad(date.getSeconds()) // Two digits seconds, 00 through 59
433 // do the replaces
434 for (key in replacements) {
435 format = format.replace('%'+ key, replacements[key]);
438 // Optionally capitalize the string and return
439 return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format;
443 * Loop up the node tree and add offsetWidth and offsetHeight to get the
444 * total page offset for a given element. Used by Opera and iOS on hover and
445 * all browsers on point click.
447 * @param {Object} el
450 function getPosition (el) {
451 var p = { left: el.offsetLeft, top: el.offsetTop };
452 el = el.offsetParent;
453 while (el) {
454 p.left += el.offsetLeft;
455 p.top += el.offsetTop;
456 if (el !== doc.body && el !== doc.documentElement) {
457 p.left -= el.scrollLeft;
458 p.top -= el.scrollTop;
460 el = el.offsetParent;
462 return p;
466 * Set the global animation to either a given value, or fall back to the
467 * given chart's animation option
468 * @param {Object} animation
469 * @param {Object} chart
471 function setAnimation(animation, chart) {
472 globalAnimation = pick(animation, chart.animation);
476 * Define the adapter for frameworks. If an external adapter is not defined,
477 * Highcharts reverts to the built-in jQuery adapter.
479 if (globalAdapter && globalAdapter.init) {
480 globalAdapter.init();
482 if (!globalAdapter && win.jQuery) {
483 var jQ = jQuery;
486 * Utility for iterating over an array. Parameters are reversed compared to jQuery.
487 * @param {Array} arr
488 * @param {Function} fn
490 each = function(arr, fn) {
491 var i = 0,
492 len = arr.length;
493 for (; i < len; i++) {
494 if (fn.call(arr[i], arr[i], i, arr) === false) {
495 return i;
501 * Filter an array
503 grep = jQ.grep;
506 * Map an array
507 * @param {Array} arr
508 * @param {Function} fn
510 map = function(arr, fn){
511 //return jQuery.map(arr, fn);
512 var results = [],
513 i = 0, len = arr.length;
514 for (; i < len; i++) {
515 results[i] = fn.call(arr[i], arr[i], i, arr);
517 return results;
522 * Deep merge two objects and return a third object
524 merge = function(){
525 var args = arguments;
526 return jQ.extend(true, null, args[0], args[1], args[2], args[3]);
530 * Convert a camelCase string to a hyphenated string
531 * @param {String} str
533 hyphenate = function (str) {
534 return str.replace(/([A-Z])/g, function(a, b){ return '-'+ b.toLowerCase(); });
538 * Add an event listener
539 * @param {Object} el A HTML element or custom object
540 * @param {String} event The event type
541 * @param {Function} fn The event handler
543 addEvent = function (el, event, fn){
544 jQ(el).bind(event, fn);
548 * Remove event added with addEvent
549 * @param {Object} el The object
550 * @param {String} eventType The event type. Leave blank to remove all events.
551 * @param {Function} handler The function to remove
553 removeEvent = function(el, eventType, handler) {
554 // workaround for jQuery issue with unbinding custom events:
555 // http://forum.jquery.com/topic/javascript-error-when-unbinding-a-custom-event-using-jquery-1-4-2
556 var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent';
557 if (doc[func] && !el[func]) {
558 el[func] = function() {};
561 jQ(el).unbind(eventType, handler);
565 * Fire an event on a custom object
566 * @param {Object} el
567 * @param {String} type
568 * @param {Object} eventArguments
569 * @param {Function} defaultFunction
571 fireEvent = function(el, type, eventArguments, defaultFunction) {
572 var event = jQ.Event(type),
573 detachedType = 'detached'+ type;
574 extend(event, eventArguments);
576 // Prevent jQuery from triggering the object method that is named the
577 // same as the event. For example, if the event is 'select', jQuery
578 // attempts calling el.select and it goes into a loop.
579 if (el[type]) {
580 el[detachedType] = el[type];
581 el[type] = null;
584 // trigger it
585 jQ(el).trigger(event);
587 // attach the method
588 if (el[detachedType]) {
589 el[type] = el[detachedType];
590 el[detachedType] = null;
593 if (defaultFunction && !event.isDefaultPrevented()) {
594 defaultFunction(event);
599 * Animate a HTML element or SVG element wrapper
600 * @param {Object} el
601 * @param {Object} params
602 * @param {Object} options jQuery-like animation options: duration, easing, callback
604 animate = function (el, params, options) {
605 var $el = jQ(el);
606 if (params.d) {
607 el.toD = params.d; // keep the array form for paths, used in jQ.fx.step.d
608 params.d = 1; // because in jQuery, animating to an array has a different meaning
611 $el.stop();
612 $el.animate(params, options);
616 * Stop running animation
618 stop = function (el) {
619 jQ(el).stop();
623 // extend jQuery
624 jQ.extend( jQ.easing, {
625 easeOutQuad: function (x, t, b, c, d) {
626 return -c *(t/=d)*(t-2) + b;
630 // extend the animate function to allow SVG animations
631 var oldStepDefault = jQuery.fx.step._default,
632 oldCur = jQuery.fx.prototype.cur;
634 // do the step
635 jQ.fx.step._default = function(fx){
636 var elem = fx.elem;
637 if (elem.attr) { // is SVG element wrapper
638 elem.attr(fx.prop, fx.now);
639 } else {
640 oldStepDefault.apply(this, arguments);
643 // animate paths
644 jQ.fx.step.d = function(fx) {
645 var elem = fx.elem;
648 // Normally start and end should be set in state == 0, but sometimes,
649 // for reasons unknown, this doesn't happen. Perhaps state == 0 is skipped
650 // in these cases
651 if (!fx.started) {
652 var ends = pathAnim.init(elem, elem.d, elem.toD);
653 fx.start = ends[0];
654 fx.end = ends[1];
655 fx.started = true;
659 // interpolate each value of the path
660 elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD));
663 // get the current value
664 jQ.fx.prototype.cur = function() {
665 var elem = this.elem,
667 if (elem.attr) { // is SVG element wrapper
668 r = elem.attr(this.prop);
669 } else {
670 r = oldCur.apply(this, arguments);
672 return r;
678 * Add a global listener for mousemove events
680 /*addEvent(doc, 'mousemove', function(e) {
681 if (globalMouseMove) {
682 globalMouseMove(e);
684 });*/
687 * Path interpolation algorithm used across adapters
689 pathAnim = {
691 * Prepare start and end values so that the path can be animated one to one
693 init: function(elem, fromD, toD) {
694 fromD = fromD || '';
695 var shift = elem.shift,
696 bezier = fromD.indexOf('C') > -1,
697 numParams = bezier ? 7 : 3,
698 endLength,
699 slice,
701 start = fromD.split(' '),
702 end = [].concat(toD), // copy
703 startBaseLine,
704 endBaseLine,
705 sixify = function(arr) { // in splines make move points have six parameters like bezier curves
706 i = arr.length;
707 while (i--) {
708 if (arr[i] === M) {
709 arr.splice(i + 1, 0, arr[i+1], arr[i+2], arr[i+1], arr[i+2]);
714 if (bezier) {
715 sixify(start);
716 sixify(end);
719 // pull out the base lines before padding
720 if (elem.isArea) {
721 startBaseLine = start.splice(start.length - 6, 6);
722 endBaseLine = end.splice(end.length - 6, 6);
725 // if shifting points, prepend a dummy point to the end path
726 if (shift) {
728 end = [].concat(end).splice(0, numParams).concat(end);
729 elem.shift = false; // reset for following animations
732 // copy and append last point until the length matches the end length
733 if (start.length) {
734 endLength = end.length;
735 while (start.length < endLength) {
737 //bezier && sixify(start);
738 slice = [].concat(start).splice(start.length - numParams, numParams);
739 if (bezier) { // disable first control point
740 slice[numParams - 6] = slice[numParams - 2];
741 slice[numParams - 5] = slice[numParams - 1];
743 start = start.concat(slice);
747 if (startBaseLine) { // append the base lines for areas
748 start = start.concat(startBaseLine);
749 end = end.concat(endBaseLine);
751 return [start, end];
755 * Interpolate each value of the path and return the array
757 step: function(start, end, pos, complete) {
758 var ret = [],
759 i = start.length,
760 startVal;
762 if (pos === 1) { // land on the final path without adjustment points appended in the ends
763 ret = complete;
765 } else if (i === end.length && pos < 1) {
766 while (i--) {
767 startVal = parseFloat(start[i]);
768 ret[i] =
769 isNaN(startVal) ? // a letter instruction like M or L
770 start[i] :
771 pos * (parseFloat(end[i] - startVal)) + startVal;
774 } else { // if animation is finished or length not matching, land on right value
775 ret = end;
777 return ret;
782 * Set the time methods globally based on the useUTC option. Time method can be either
783 * local time or UTC (default).
785 function setTimeMethods() {
786 var useUTC = defaultOptions.global.useUTC;
788 makeTime = useUTC ? Date.UTC : function(year, month, date, hours, minutes, seconds) {
789 return new Date(
790 year,
791 month,
792 pick(date, 1),
793 pick(hours, 0),
794 pick(minutes, 0),
795 pick(seconds, 0)
796 ).getTime();
798 getMinutes = useUTC ? 'getUTCMinutes' : 'getMinutes';
799 getHours = useUTC ? 'getUTCHours' : 'getHours';
800 getDay = useUTC ? 'getUTCDay' : 'getDay';
801 getDate = useUTC ? 'getUTCDate' : 'getDate';
802 getMonth = useUTC ? 'getUTCMonth' : 'getMonth';
803 getFullYear = useUTC ? 'getUTCFullYear' : 'getFullYear';
804 setMinutes = useUTC ? 'setUTCMinutes' : 'setMinutes';
805 setHours = useUTC ? 'setUTCHours' : 'setHours';
806 setDate = useUTC ? 'setUTCDate' : 'setDate';
807 setMonth = useUTC ? 'setUTCMonth' : 'setMonth';
808 setFullYear = useUTC ? 'setUTCFullYear' : 'setFullYear';
813 * Merge the default options with custom options and return the new options structure
814 * @param {Object} options The new custom options
816 function setOptions(options) {
817 defaultOptions = merge(defaultOptions, options);
819 // apply UTC
820 setTimeMethods();
822 return defaultOptions;
826 * Get the updated default options. Merely exposing defaultOptions for outside modules
827 * isn't enough because the setOptions method creates a new object.
829 function getOptions() {
830 return defaultOptions;
834 * Discard an element by moving it to the bin and delete
835 * @param {Object} The HTML node to discard
837 function discardElement(element) {
838 // create a garbage bin element, not part of the DOM
839 if (!garbageBin) {
840 garbageBin = createElement(DIV);
843 // move the node and empty bin
844 if (element) {
845 garbageBin.appendChild(element);
847 garbageBin.innerHTML = '';
850 /* ****************************************************************************
851 * Handle the options *
852 *****************************************************************************/
853 var
855 defaultLabelOptions = {
856 enabled: true,
857 // rotation: 0,
858 align: 'center',
859 x: 0,
860 y: 15,
861 /*formatter: function() {
862 return this.value;
863 },*/
864 style: {
865 color: '#666',
866 fontSize: '11px',
867 lineHeight: '14px'
871 defaultOptions = {
872 colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE',
873 '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'],
874 symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],
875 lang: {
876 loading: 'Loading...',
877 months: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
878 'August', 'September', 'October', 'November', 'December'],
879 weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
880 decimalPoint: '.',
881 resetZoom: 'Reset zoom',
882 resetZoomTitle: 'Reset zoom level 1:1',
883 thousandsSep: ','
885 global: {
886 useUTC: true
888 chart: {
889 //animation: true,
890 //alignTicks: false,
891 //reflow: true,
892 //className: null,
893 //events: { load, selection },
894 //margin: [null],
895 //marginTop: null,
896 //marginRight: null,
897 //marginBottom: null,
898 //marginLeft: null,
899 borderColor: '#4572A7',
900 //borderWidth: 0,
901 borderRadius: 5,
902 defaultSeriesType: 'line',
903 ignoreHiddenSeries: true,
904 //inverted: false,
905 //shadow: false,
906 spacingTop: 10,
907 spacingRight: 10,
908 spacingBottom: 15,
909 spacingLeft: 10,
910 style: {
911 fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
912 fontSize: '12px'
914 backgroundColor: '#FFFFFF',
915 //plotBackgroundColor: null,
916 plotBorderColor: '#C0C0C0'
917 //plotBorderWidth: 0,
918 //plotShadow: false,
919 //zoomType: ''
921 title: {
922 text: 'Chart title',
923 align: 'center',
924 // floating: false,
925 // margin: 15,
926 // x: 0,
927 // verticalAlign: 'top',
928 y: 15, // docs
929 style: {
930 color: '#3E576F',
931 fontSize: '16px'
935 subtitle: {
936 text: '',
937 align: 'center',
938 // floating: false
939 // x: 0,
940 // verticalAlign: 'top',
941 y: 30, // docs
942 style: {
943 color: '#6D869F'
947 plotOptions: {
948 line: { // base series options
949 allowPointSelect: false,
950 showCheckbox: false,
951 animation: {
952 duration: 1000
954 // connectNulls: false, // docs
955 //cursor: 'default',
956 //dashStyle: null,
957 //enableMouseTracking: true,
958 events: {},
959 //legendIndex: 0, // docs (+ pie points)
960 lineWidth: 2,
961 shadow: true,
962 // stacking: null,
963 marker: {
964 enabled: true,
965 //symbol: null,
966 lineWidth: 0,
967 radius: 4,
968 lineColor: '#FFFFFF',
969 //fillColor: null,
970 states: { // states for a single point
971 hover: {
972 //radius: base + 2
974 select: {
975 fillColor: '#FFFFFF',
976 lineColor: '#000000',
977 lineWidth: 2
981 point: {
982 events: {}
984 dataLabels: merge(defaultLabelOptions, {
985 enabled: false,
986 y: -6,
987 formatter: function() {
988 return this.y;
992 //pointStart: 0,
993 //pointInterval: 1,
994 showInLegend: true,
995 states: { // states for the entire series
996 hover: {
997 //enabled: false,
998 //lineWidth: base + 1,
999 marker: {
1000 // lineWidth: base + 1,
1001 // radius: base + 1
1004 select: {
1005 marker: {}
1008 stickyTracking: true
1009 //zIndex: null
1012 labels: {
1013 //items: [],
1014 style: {
1015 //font: defaultFont,
1016 position: ABSOLUTE,
1017 color: '#3E576F'
1020 legend: {
1021 enabled: true,
1022 align: 'center',
1023 //floating: false,
1024 layout: 'horizontal',
1025 labelFormatter: function() {
1026 return this.name;
1028 // lineHeight: 16, // docs: deprecated
1029 borderWidth: 1,
1030 borderColor: '#909090',
1031 borderRadius: 5,
1032 // margin: 10,
1033 // reversed: false,
1034 shadow: false,
1035 // backgroundColor: null,
1036 style: {
1037 padding: '5px'
1039 itemStyle: {
1040 cursor: 'pointer',
1041 color: '#3E576F'
1043 itemHoverStyle: {
1044 cursor: 'pointer',
1045 color: '#000000'
1047 itemHiddenStyle: {
1048 color: '#C0C0C0'
1050 itemCheckboxStyle: {
1051 position: ABSOLUTE,
1052 width: '13px', // for IE precision
1053 height: '13px'
1055 // itemWidth: undefined,
1056 symbolWidth: 16,
1057 symbolPadding: 5,
1058 verticalAlign: 'bottom',
1059 // width: undefined,
1060 x: 0, // docs
1061 y: 0 // docs
1064 loading: {
1065 hideDuration: 100,
1066 labelStyle: {
1067 fontWeight: 'bold',
1068 position: RELATIVE,
1069 top: '1em'
1071 showDuration: 100,
1072 style: {
1073 position: ABSOLUTE,
1074 backgroundColor: 'white',
1075 opacity: 0.5,
1076 textAlign: 'center'
1080 tooltip: {
1081 enabled: true,
1082 //crosshairs: null,
1083 backgroundColor: 'rgba(255, 255, 255, .85)',
1084 borderWidth: 2,
1085 borderRadius: 5,
1086 //formatter: defaultFormatter,
1087 shadow: true,
1088 //shared: false,
1089 snap: hasTouch ? 25 : 10,
1090 style: {
1091 color: '#333333',
1092 fontSize: '12px',
1093 padding: '5px',
1094 whiteSpace: 'nowrap'
1098 toolbar: {
1099 itemStyle: {
1100 color: '#4572A7',
1101 cursor: 'pointer'
1105 credits: {
1106 enabled: true,
1107 text: 'Highcharts.com',
1108 href: 'http://www.highcharts.com',
1109 position: {
1110 align: 'right',
1111 x: -10,
1112 verticalAlign: 'bottom',
1113 y: -5
1115 style: {
1116 cursor: 'pointer',
1117 color: '#909090',
1118 fontSize: '10px'
1123 // Axis defaults
1124 var defaultXAxisOptions = {
1125 // allowDecimals: null,
1126 // alternateGridColor: null,
1127 // categories: [],
1128 dateTimeLabelFormats: {
1129 second: '%H:%M:%S',
1130 minute: '%H:%M',
1131 hour: '%H:%M',
1132 day: '%e. %b',
1133 week: '%e. %b',
1134 month: '%b \'%y',
1135 year: '%Y'
1137 endOnTick: false,
1138 gridLineColor: '#C0C0C0',
1139 // gridLineDashStyle: 'solid', // docs
1140 // gridLineWidth: 0,
1141 // reversed: false,
1143 labels: defaultLabelOptions,
1144 // { step: null },
1145 lineColor: '#C0D0E0',
1146 lineWidth: 1,
1147 //linkedTo: null,
1148 max: null,
1149 min: null,
1150 minPadding: 0.01,
1151 maxPadding: 0.01,
1152 //maxZoom: null,
1153 minorGridLineColor: '#E0E0E0',
1154 // minorGridLineDashStyle: null,
1155 minorGridLineWidth: 1,
1156 minorTickColor: '#A0A0A0',
1157 //minorTickInterval: null,
1158 minorTickLength: 2,
1159 minorTickPosition: 'outside', // inside or outside
1160 //minorTickWidth: 0,
1161 //opposite: false,
1162 //offset: 0,
1163 //plotBands: [{
1164 // events: {},
1165 // zIndex: 1,
1166 // labels: { align, x, verticalAlign, y, style, rotation, textAlign }
1167 //}],
1168 //plotLines: [{
1169 // events: {}
1170 // dashStyle: {}
1171 // zIndex:
1172 // labels: { align, x, verticalAlign, y, style, rotation, textAlign }
1173 //}],
1174 //reversed: false,
1175 // showFirstLabel: true,
1176 // showLastLabel: false,
1177 startOfWeek: 1,
1178 startOnTick: false,
1179 tickColor: '#C0D0E0',
1180 //tickInterval: null,
1181 tickLength: 5,
1182 tickmarkPlacement: 'between', // on or between
1183 tickPixelInterval: 100,
1184 tickPosition: 'outside',
1185 tickWidth: 1,
1186 title: {
1187 //text: null,
1188 align: 'middle', // low, middle or high
1189 //margin: 0 for horizontal, 10 for vertical axes,
1190 //rotation: 0,
1191 //side: 'outside',
1192 style: {
1193 color: '#6D869F',
1194 //font: defaultFont.replace('normal', 'bold')
1195 fontWeight: 'bold'
1197 //x: 0,
1198 //y: 0
1200 type: 'linear' // linear, logarithmic or datetime // docs
1203 defaultYAxisOptions = merge(defaultXAxisOptions, {
1204 endOnTick: true,
1205 gridLineWidth: 1,
1206 tickPixelInterval: 72,
1207 showLastLabel: true,
1208 labels: {
1209 align: 'right',
1210 x: -8,
1211 y: 3
1213 lineWidth: 0,
1214 maxPadding: 0.05,
1215 minPadding: 0.05,
1216 startOnTick: true,
1217 tickWidth: 0,
1218 title: {
1219 rotation: 270,
1220 text: 'Y-values'
1222 stackLabels: {
1223 enabled: false,
1224 //align: dynamic,
1225 //y: dynamic,
1226 //x: dynamic,
1227 //verticalAlign: dynamic,
1228 //textAlign: dynamic,
1229 //rotation: 0,
1230 formatter: function() {
1231 return this.total;
1233 style: defaultLabelOptions.style
1237 defaultLeftAxisOptions = {
1238 labels: {
1239 align: 'right',
1240 x: -8,
1241 y: null // docs
1243 title: {
1244 rotation: 270
1247 defaultRightAxisOptions = {
1248 labels: {
1249 align: 'left',
1250 x: 8,
1251 y: null // docs
1253 title: {
1254 rotation: 90
1257 defaultBottomAxisOptions = { // horizontal axis
1258 labels: {
1259 align: 'center',
1260 x: 0,
1261 y: 14
1262 // staggerLines: null
1264 title: {
1265 rotation: 0
1268 defaultTopAxisOptions = merge(defaultBottomAxisOptions, {
1269 labels: {
1270 y: -5
1271 // staggerLines: null
1278 // Series defaults
1279 var defaultPlotOptions = defaultOptions.plotOptions,
1280 defaultSeriesOptions = defaultPlotOptions.line;
1281 //defaultPlotOptions.line = merge(defaultSeriesOptions);
1282 defaultPlotOptions.spline = merge(defaultSeriesOptions);
1283 defaultPlotOptions.scatter = merge(defaultSeriesOptions, {
1284 lineWidth: 0,
1285 states: {
1286 hover: {
1287 lineWidth: 0
1291 defaultPlotOptions.area = merge(defaultSeriesOptions, {
1292 // threshold: 0,
1293 // lineColor: null, // overrides color, but lets fillColor be unaltered
1294 // fillOpacity: 0.75,
1295 // fillColor: null
1298 defaultPlotOptions.areaspline = merge(defaultPlotOptions.area);
1299 defaultPlotOptions.column = merge(defaultSeriesOptions, {
1300 borderColor: '#FFFFFF',
1301 borderWidth: 1,
1302 borderRadius: 0,
1303 //colorByPoint: undefined,
1304 groupPadding: 0.2,
1305 marker: null, // point options are specified in the base options
1306 pointPadding: 0.1,
1307 //pointWidth: null,
1308 minPointLength: 0,
1309 states: {
1310 hover: {
1311 brightness: 0.1,
1312 shadow: false
1314 select: {
1315 color: '#C0C0C0',
1316 borderColor: '#000000',
1317 shadow: false
1320 dataLabels: {
1321 y: null,
1322 verticalAlign: null
1325 defaultPlotOptions.bar = merge(defaultPlotOptions.column, {
1326 dataLabels: {
1327 align: 'left',
1328 x: 5,
1329 y: 0
1332 defaultPlotOptions.pie = merge(defaultSeriesOptions, {
1333 //dragType: '', // n/a
1334 borderColor: '#FFFFFF',
1335 borderWidth: 1,
1336 center: ['50%', '50%'],
1337 colorByPoint: true, // always true for pies
1338 dataLabels: {
1339 // align: null,
1340 // connectorWidth: 1,
1341 // connectorColor: '#606060',
1342 // connectorPadding: 5,
1343 distance: 30,
1344 enabled: true,
1345 formatter: function() {
1346 return this.point.name;
1348 y: 5
1350 //innerSize: 0,
1351 legendType: 'point',
1352 marker: null, // point options are specified in the base options
1353 size: '75%',
1354 showInLegend: false,
1355 slicedOffset: 10,
1356 states: {
1357 hover: {
1358 brightness: 0.1,
1359 shadow: false
1365 // set the default time methods
1366 setTimeMethods();
1370 * Handle color operations. The object methods are chainable.
1371 * @param {String} input The input color in either rbga or hex format
1373 var Color = function(input) {
1374 // declare variables
1375 var rgba = [], result;
1378 * Parse the input color to rgba array
1379 * @param {String} input
1381 function init(input) {
1383 // rgba
1384 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);
1385 if (result) {
1386 rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)];
1389 // hex
1390 else {
1391 result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input);
1392 if (result) {
1393 rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1];
1399 * Return the color a specified format
1400 * @param {String} format
1402 function get(format) {
1403 var ret;
1405 // it's NaN if gradient colors on a column chart
1406 if (rgba && !isNaN(rgba[0])) {
1407 if (format === 'rgb') {
1408 ret = 'rgb('+ rgba[0] +','+ rgba[1] +','+ rgba[2] +')';
1409 } else if (format === 'a') {
1410 ret = rgba[3];
1411 } else {
1412 ret = 'rgba('+ rgba.join(',') +')';
1414 } else {
1415 ret = input;
1417 return ret;
1421 * Brighten the color
1422 * @param {Number} alpha
1424 function brighten(alpha) {
1425 if (isNumber(alpha) && alpha !== 0) {
1426 var i;
1427 for (i = 0; i < 3; i++) {
1428 rgba[i] += pInt(alpha * 255);
1430 if (rgba[i] < 0) {
1431 rgba[i] = 0;
1433 if (rgba[i] > 255) {
1434 rgba[i] = 255;
1438 return this;
1441 * Set the color's opacity to a given alpha value
1442 * @param {Number} alpha
1444 function setOpacity(alpha) {
1445 rgba[3] = alpha;
1446 return this;
1449 // initialize: parse the input
1450 init(input);
1452 // public methods
1453 return {
1454 get: get,
1455 brighten: brighten,
1456 setOpacity: setOpacity
1461 * A wrapper object for SVG elements
1463 function SVGElement () {}
1465 SVGElement.prototype = {
1467 * Initialize the SVG renderer
1468 * @param {Object} renderer
1469 * @param {String} nodeName
1471 init: function(renderer, nodeName) {
1472 this.element = doc.createElementNS(SVG_NS, nodeName);
1473 this.renderer = renderer;
1476 * Animate a given attribute
1477 * @param {Object} params
1478 * @param {Number} options The same options as in jQuery animation
1479 * @param {Function} complete Function to perform at the end of animation
1481 animate: function(params, options, complete) {
1482 var animOptions = pick(options, globalAnimation, true);
1483 if (animOptions) {
1484 animOptions = merge(animOptions);
1485 if (complete) { // allows using a callback with the global animation without overwriting it
1486 animOptions.complete = complete;
1488 animate(this, params, animOptions);
1489 } else {
1490 this.attr(params);
1491 if (complete) {
1492 complete();
1497 * Set or get a given attribute
1498 * @param {Object|String} hash
1499 * @param {Mixed|Undefined} val
1501 attr: function(hash, val) {
1502 var key,
1503 value,
1505 child,
1506 element = this.element,
1507 nodeName = element.nodeName,
1508 renderer = this.renderer,
1509 skipAttr,
1510 shadows = this.shadows,
1511 hasSetSymbolSize,
1512 ret = this;
1514 // single key-value pair
1515 if (isString(hash) && defined(val)) {
1516 key = hash;
1517 hash = {};
1518 hash[key] = val;
1521 // used as a getter: first argument is a string, second is undefined
1522 if (isString(hash)) {
1523 key = hash;
1524 if (nodeName === 'circle') {
1525 key = { x: 'cx', y: 'cy' }[key] || key;
1526 } else if (key === 'strokeWidth') {
1527 key = 'stroke-width';
1529 ret = attr(element, key) || this[key] || 0;
1531 if (key !== 'd' && key !== 'visibility') { // 'd' is string in animation step
1532 ret = parseFloat(ret);
1535 // setter
1536 } else {
1538 for (key in hash) {
1539 skipAttr = false; // reset
1540 value = hash[key];
1542 // paths
1543 if (key === 'd') {
1544 if (value && value.join) { // join path
1545 value = value.join(' ');
1547 if (/(NaN| {2}|^$)/.test(value)) {
1548 value = 'M 0 0';
1550 this.d = value; // shortcut for animations
1552 // update child tspans x values
1553 } else if (key === 'x' && nodeName === 'text') {
1554 for (i = 0; i < element.childNodes.length; i++ ) {
1555 child = element.childNodes[i];
1556 // if the x values are equal, the tspan represents a linebreak
1557 if (attr(child, 'x') === attr(element, 'x')) {
1558 //child.setAttribute('x', value);
1559 attr(child, 'x', value);
1563 if (this.rotation) {
1564 attr(element, 'transform', 'rotate('+ this.rotation +' '+ value +' '+
1565 pInt(hash.y || attr(element, 'y')) +')');
1568 // apply gradients
1569 } else if (key === 'fill') {
1570 value = renderer.color(value, element, key);
1572 // circle x and y
1573 } else if (nodeName === 'circle' && (key === 'x' || key === 'y')) {
1574 key = { x: 'cx', y: 'cy' }[key] || key;
1576 // translation and text rotation
1577 } else if (key === 'translateX' || key === 'translateY' || key === 'rotation' || key === 'verticalAlign') {
1578 this[key] = value;
1579 this.updateTransform();
1580 skipAttr = true;
1582 // apply opacity as subnode (required by legacy WebKit and Batik)
1583 } else if (key === 'stroke') {
1584 value = renderer.color(value, element, key);
1586 // emulate VML's dashstyle implementation
1587 } else if (key === 'dashstyle') {
1588 key = 'stroke-dasharray';
1589 value = value && value.toLowerCase();
1590 if (value === 'solid') {
1591 value = NONE;
1592 } else if (value) {
1593 value = value
1594 .replace('shortdashdotdot', '3,1,1,1,1,1,')
1595 .replace('shortdashdot', '3,1,1,1')
1596 .replace('shortdot', '1,1,')
1597 .replace('shortdash', '3,1,')
1598 .replace('longdash', '8,3,')
1599 .replace(/dot/g, '1,3,')
1600 .replace('dash', '4,3,')
1601 .replace(/,$/, '')
1602 .split(','); // ending comma
1604 i = value.length;
1605 while (i--) {
1606 value[i] = pInt(value[i]) * hash['stroke-width'];
1609 value = value.join(',');
1612 // special
1613 } else if (key === 'isTracker') {
1614 this[key] = value;
1616 // IE9/MooTools combo: MooTools returns objects instead of numbers and IE9 Beta 2
1617 // is unable to cast them. Test again with final IE9.
1618 } else if (key === 'width') {
1619 value = pInt(value);
1621 // Text alignment
1622 } else if (key === 'align') {
1623 key = 'text-anchor';
1624 value = { left: 'start', center: 'middle', right: 'end' }[value];
1629 // jQuery animate changes case
1630 if (key === 'strokeWidth') {
1631 key = 'stroke-width';
1634 // Chrome/Win < 6 bug (http://code.google.com/p/chromium/issues/detail?id=15461)
1635 if (isWebKit && key === 'stroke-width' && value === 0) {
1636 value = 0.000001;
1639 // symbols
1640 if (this.symbolName && /^(x|y|r|start|end|innerR)/.test(key)) {
1643 if (!hasSetSymbolSize) {
1644 this.symbolAttr(hash);
1645 hasSetSymbolSize = true;
1647 skipAttr = true;
1650 // let the shadow follow the main element
1651 if (shadows && /^(width|height|visibility|x|y|d)$/.test(key)) {
1652 i = shadows.length;
1653 while (i--) {
1654 attr(shadows[i], key, value);
1658 // validate heights
1659 if ((key === 'width' || key === 'height') && nodeName === 'rect' && value < 0) {
1660 value = 0;
1663 if (key === 'text') {
1664 // only one node allowed
1665 this.textStr = value;
1666 if (this.added) {
1667 renderer.buildText(this);
1669 } else if (!skipAttr) {
1670 //element.setAttribute(key, value);
1671 attr(element, key, value);
1677 return ret;
1681 * If one of the symbol size affecting parameters are changed,
1682 * check all the others only once for each call to an element's
1683 * .attr() method
1684 * @param {Object} hash
1686 symbolAttr: function(hash) {
1687 var wrapper = this;
1689 each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR'], function(key) {
1690 wrapper[key] = pick(hash[key], wrapper[key]);
1693 wrapper.attr({
1694 d: wrapper.renderer.symbols[wrapper.symbolName](
1695 mathRound(wrapper.x * 2) / 2, // Round to halves. Issue #274.
1696 mathRound(wrapper.y * 2) / 2,
1697 wrapper.r,
1699 start: wrapper.start,
1700 end: wrapper.end,
1701 width: wrapper.width,
1702 height: wrapper.height,
1703 innerR: wrapper.innerR
1709 * Apply a clipping path to this object
1710 * @param {String} id
1712 clip: function(clipRect) {
1713 return this.attr('clip-path', 'url('+ this.renderer.url +'#'+ clipRect.id +')');
1717 * Calculate the coordinates needed for drawing a rectangle crisply and return the
1718 * calculated attributes
1719 * @param {Number} strokeWidth
1720 * @param {Number} x
1721 * @param {Number} y
1722 * @param {Number} width
1723 * @param {Number} height
1725 crisp: function(strokeWidth, x, y, width, height) {
1727 var wrapper = this,
1728 key,
1729 attr = {},
1730 values = {},
1731 normalizer;
1733 strokeWidth = strokeWidth || wrapper.strokeWidth || 0;
1734 normalizer = strokeWidth % 2 / 2;
1736 // normalize for crisp edges
1737 values.x = mathFloor(x || wrapper.x || 0) + normalizer;
1738 values.y = mathFloor(y || wrapper.y || 0) + normalizer;
1739 values.width = mathFloor((width || wrapper.width || 0) - 2 * normalizer);
1740 values.height = mathFloor((height || wrapper.height || 0) - 2 * normalizer);
1741 values.strokeWidth = strokeWidth;
1743 for (key in values) {
1744 if (wrapper[key] !== values[key]) { // only set attribute if changed
1745 wrapper[key] = attr[key] = values[key];
1749 return attr;
1753 * Set styles for the element
1754 * @param {Object} styles
1756 css: function(styles) {
1757 var elemWrapper = this,
1758 elem = elemWrapper.element,
1759 textWidth = styles && styles.width && elem.nodeName === 'text',
1760 camelStyles = styles,
1763 // convert legacy
1764 if (styles && styles.color) {
1765 styles.fill = styles.color;
1768 // save the styles in an object
1769 styles = extend(
1770 elemWrapper.styles,
1771 styles
1775 // store object
1776 elemWrapper.styles = styles;
1778 // hyphenate
1779 if (defined(styles)) {
1780 styles = {};
1781 for (n in camelStyles) {
1782 styles[hyphenate(n)] = camelStyles[n];
1786 // serialize and set style attribute
1787 if (isIE && !hasSVG) { // legacy IE doesn't support setting style attribute
1788 if (textWidth) {
1789 delete styles.width;
1791 css(elemWrapper.element, styles);
1792 } else {
1793 elemWrapper.attr({
1794 style: serializeCSS(styles)
1799 // re-build text
1800 if (textWidth && elemWrapper.added) {
1801 elemWrapper.renderer.buildText(elemWrapper);
1804 return elemWrapper;
1808 * Add an event listener
1809 * @param {String} eventType
1810 * @param {Function} handler
1812 on: function(eventType, handler) {
1813 var fn = handler;
1814 // touch
1815 if (hasTouch && eventType === 'click') {
1816 eventType = 'touchstart';
1817 fn = function(e) {
1818 e.preventDefault();
1819 handler();
1822 // simplest possible event model for internal use
1823 this.element['on'+ eventType] = fn;
1824 return this;
1829 * Move an object and its children by x and y values
1830 * @param {Number} x
1831 * @param {Number} y
1833 translate: function(x, y) {
1834 return this.attr({
1835 translateX: x,
1836 translateY: y
1841 * Invert a group, rotate and flip
1843 invert: function() {
1844 var wrapper = this;
1845 wrapper.inverted = true;
1846 wrapper.updateTransform();
1847 return wrapper;
1851 * Private method to update the transform attribute based on internal
1852 * properties
1854 updateTransform: function() {
1855 var wrapper = this,
1856 translateX = wrapper.translateX || 0,
1857 translateY = wrapper.translateY || 0,
1858 inverted = wrapper.inverted,
1859 rotation = wrapper.rotation,
1860 transform = [];
1862 // flipping affects translate as adjustment for flipping around the group's axis
1863 if (inverted) {
1864 translateX += wrapper.attr('width');
1865 translateY += wrapper.attr('height');
1868 if(wrapper.imagesize) {
1869 translateX -= wrapper.imagesize[0]/2;
1870 translateY -= wrapper.imagesize[1]/2;
1873 // apply translate
1874 if (translateX || translateY) {
1875 transform.push('translate('+ translateX +','+ translateY +')');
1878 // apply rotation
1879 if (inverted) {
1880 transform.push('rotate(90) scale(-1,1)');
1881 } else if (rotation) { // text rotation
1882 transform.push('rotate('+ rotation +' '+ wrapper.x +' '+ wrapper.y +')');
1885 if (transform.length) {
1886 attr(wrapper.element, 'transform', transform.join(' '));
1890 * Bring the element to the front
1892 toFront: function() {
1893 var element = this.element;
1894 element.parentNode.appendChild(element);
1895 return this;
1900 * Break down alignment options like align, verticalAlign, x and y
1901 * to x and y relative to the chart.
1903 * @param {Object} alignOptions
1904 * @param {Boolean} alignByTranslate
1905 * @param {Object} box The box to align to, needs a width and height
1908 align: function(alignOptions, alignByTranslate, box) {
1909 var elemWrapper = this;
1911 if (!alignOptions) { // called on resize
1912 alignOptions = elemWrapper.alignOptions;
1913 alignByTranslate = elemWrapper.alignByTranslate;
1914 } else { // first call on instanciate
1915 elemWrapper.alignOptions = alignOptions;
1916 elemWrapper.alignByTranslate = alignByTranslate;
1917 if (!box) { // boxes other than renderer handle this internally
1918 elemWrapper.renderer.alignedObjects.push(elemWrapper);
1922 box = pick(box, elemWrapper.renderer);
1924 var align = alignOptions.align,
1925 vAlign = alignOptions.verticalAlign,
1926 x = (box.x || 0) + (alignOptions.x || 0), // default: left align
1927 y = (box.y || 0) + (alignOptions.y || 0), // default: top align
1928 attribs = {};
1931 // align
1932 if (/^(right|center)$/.test(align)) {
1933 x += (box.width - (alignOptions.width || 0) ) /
1934 { right: 1, center: 2 }[align];
1936 attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x);
1939 // vertical align
1940 if (/^(bottom|middle)$/.test(vAlign)) {
1941 y += (box.height - (alignOptions.height || 0)) /
1942 ({ bottom: 1, middle: 2 }[vAlign] || 1);
1945 attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y);
1947 // animate only if already placed
1948 elemWrapper[elemWrapper.placed ? 'animate' : 'attr'](attribs);
1949 elemWrapper.placed = true;
1950 elemWrapper.alignAttr = attribs;
1952 return elemWrapper;
1956 * Get the bounding box (width, height, x and y) for the element
1958 getBBox: function() {
1959 var bBox,
1960 width,
1961 height,
1962 rotation = this.rotation,
1963 rad = rotation * deg2rad;
1965 try { // fails in Firefox if the container has display: none
1966 // use extend because IE9 is not allowed to change width and height in case
1967 // of rotation (below)
1968 bBox = extend({}, this.element.getBBox());
1969 } catch(e) {
1970 bBox = { width: 0, height: 0 };
1972 width = bBox.width;
1973 height = bBox.height;
1975 // adjust for rotated text
1976 if (rotation) {
1977 bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad));
1978 bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad));
1981 return bBox;
1984 /* *
1985 * Manually compute width and height of rotated text from non-rotated. Shared by SVG and VML
1986 * @param {Object} bBox
1987 * @param {number} rotation
1989 rotateBBox: function(bBox, rotation) {
1990 var rad = rotation * math.PI * 2 / 360, // radians
1991 width = bBox.width,
1992 height = bBox.height;
1995 },*/
1998 * Show the element
2000 show: function() {
2001 return this.attr({ visibility: VISIBLE });
2005 * Hide the element
2007 hide: function() {
2008 return this.attr({ visibility: HIDDEN });
2012 * Add the element
2013 * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined
2014 * to append the element to the renderer.box.
2016 add: function(parent) {
2018 var renderer = this.renderer,
2019 parentWrapper = parent || renderer,
2020 parentNode = parentWrapper.element || renderer.box,
2021 childNodes = parentNode.childNodes,
2022 element = this.element,
2023 zIndex = attr(element, 'zIndex'),
2024 otherElement,
2025 otherZIndex,
2028 // mark as inverted
2029 this.parentInverted = parent && parent.inverted;
2031 // build formatted text
2032 if (this.textStr !== undefined) {
2033 renderer.buildText(this);
2036 // mark the container as having z indexed children
2037 if (zIndex) {
2038 parentWrapper.handleZ = true;
2039 zIndex = pInt(zIndex);
2042 // insert according to this and other elements' zIndex
2043 if (parentWrapper.handleZ) { // this element or any of its siblings has a z index
2044 for (i = 0; i < childNodes.length; i++) {
2045 otherElement = childNodes[i];
2046 otherZIndex = attr(otherElement, 'zIndex');
2047 if (otherElement !== element && (
2048 // insert before the first element with a higher zIndex
2049 pInt(otherZIndex) > zIndex ||
2050 // if no zIndex given, insert before the first element with a zIndex
2051 (!defined(zIndex) && defined(otherZIndex))
2053 )) {
2054 parentNode.insertBefore(element, otherElement);
2055 return this;
2060 // default: append at the end
2061 parentNode.appendChild(element);
2063 this.added = true;
2065 return this;
2069 * Destroy the element and element wrapper
2071 destroy: function() {
2072 var wrapper = this,
2073 element = wrapper.element || {},
2074 shadows = wrapper.shadows,
2075 parentNode = element.parentNode,
2076 key;
2078 // remove events
2079 element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = null;
2080 stop(wrapper); // stop running animations
2082 // remove element
2083 if (parentNode) {
2084 parentNode.removeChild(element);
2087 // destroy shadows
2088 if (shadows) {
2089 each(shadows, function(shadow) {
2090 parentNode = shadow.parentNode;
2091 if (parentNode) { // the entire chart HTML can be overwritten
2092 parentNode.removeChild(shadow);
2097 // remove from alignObjects
2098 erase(wrapper.renderer.alignedObjects, wrapper);
2100 for (key in wrapper) {
2101 delete wrapper[key];
2104 return null;
2108 * Empty a group element
2110 empty: function() {
2111 var element = this.element,
2112 childNodes = element.childNodes,
2113 i = childNodes.length;
2115 while (i--) {
2116 element.removeChild(childNodes[i]);
2121 * Add a shadow to the element. Must be done after the element is added to the DOM
2122 * @param {Boolean} apply
2124 shadow: function(apply, group) {
2125 var shadows = [],
2127 shadow,
2128 element = this.element,
2130 // compensate for inverted plot area
2131 transform = this.parentInverted ? '(-1,-1)' : '(1,1)';
2134 if (apply) {
2135 for (i = 1; i <= 3; i++) {
2136 shadow = element.cloneNode(0);
2137 attr(shadow, {
2138 'isShadow': 'true',
2139 'stroke': 'rgb(0, 0, 0)',
2140 'stroke-opacity': 0.05 * i,
2141 'stroke-width': 7 - 2 * i,
2142 'transform': 'translate'+ transform,
2143 'fill': NONE
2146 if (group) {
2147 group.element.appendChild(shadow);
2148 } else {
2149 element.parentNode.insertBefore(shadow, element);
2152 shadows.push(shadow);
2155 this.shadows = shadows;
2157 return this;
2163 * The default SVG renderer
2165 var SVGRenderer = function() {
2166 this.init.apply(this, arguments);
2168 SVGRenderer.prototype = {
2170 Element: SVGElement,
2173 * Initialize the SVGRenderer
2174 * @param {Object} container
2175 * @param {Number} width
2176 * @param {Number} height
2177 * @param {Boolean} forExport
2179 init: function(container, width, height, forExport) {
2180 var renderer = this,
2181 loc = location,
2182 boxWrapper;
2184 boxWrapper = renderer.createElement('svg')
2185 .attr({
2186 xmlns: SVG_NS,
2187 version: '1.1'
2189 container.appendChild(boxWrapper.element);
2191 // object properties
2192 renderer.box = boxWrapper.element;
2193 renderer.boxWrapper = boxWrapper;
2194 renderer.alignedObjects = [];
2195 renderer.url = isIE ? '' : loc.href.replace(/#.*?$/, ''); // page url used for internal references
2196 renderer.defs = this.createElement('defs').add();
2197 renderer.forExport = forExport;
2199 renderer.setSize(width, height, false);
2205 * Create a wrapper for an SVG element
2206 * @param {Object} nodeName
2208 createElement: function(nodeName) {
2209 var wrapper = new this.Element();
2210 wrapper.init(this, nodeName);
2211 return wrapper;
2215 /**
2216 * Parse a simple HTML string into SVG tspans
2218 * @param {Object} textNode The parent text SVG node
2220 buildText: function(wrapper) {
2221 var textNode = wrapper.element,
2222 lines = pick(wrapper.textStr, '').toString()
2223 .replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
2224 .replace(/<(i|em)>/g, '<span style="font-style:italic">')
2225 .replace(/<a/g, '<span')
2226 .replace(/<\/(b|strong|i|em|a)>/g, '</span>')
2227 .split(/<br.*?>/g),
2228 childNodes = textNode.childNodes,
2229 styleRegex = /style="([^"]+)"/,
2230 hrefRegex = /href="([^"]+)"/,
2231 parentX = attr(textNode, 'x'),
2232 textStyles = wrapper.styles,
2233 reverse = isFirefox && textStyles && textStyles['-hc-direction'] === 'rtl' &&
2234 !this.forExport && pInt(userAgent.split('Firefox/')[1]) < 4, // issue #38
2235 arr,
2236 width = textStyles && pInt(textStyles.width),
2237 textLineHeight = textStyles && textStyles['line-height'],
2238 lastLine,
2239 GET_COMPUTED_STYLE = 'getComputedStyle',
2240 i = childNodes.length;
2242 // remove old text
2243 while (i--) {
2244 textNode.removeChild(childNodes[i]);
2247 if (width && !wrapper.added) {
2248 this.box.appendChild(textNode); // attach it to the DOM to read offset width
2251 each(lines, function(line, lineNo) {
2252 var spans, spanNo = 0, lineHeight;
2254 line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||');
2255 spans = line.split('|||');
2257 each(spans, function (span) {
2258 if (span !== '' || spans.length === 1) {
2259 var attributes = {},
2260 tspan = doc.createElementNS(SVG_NS, 'tspan');
2261 if (styleRegex.test(span)) {
2262 attr(
2263 tspan,
2264 'style',
2265 span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2')
2268 if (hrefRegex.test(span)) {
2269 attr(tspan, 'onclick', 'location.href=\"'+ span.match(hrefRegex)[1] +'\"');
2270 css(tspan, { cursor: 'pointer' });
2273 span = (span.replace(/<(.|\n)*?>/g, '') || ' ')
2274 .replace(/&lt;/g, '<')
2275 .replace(/&gt;/g, '>');
2277 // issue #38 workaround.
2278 if (reverse) {
2279 arr = [];
2280 i = span.length;
2281 while (i--) {
2282 arr.push(span.charAt(i));
2284 span = arr.join('');
2287 // add the text node
2288 tspan.appendChild(doc.createTextNode(span));
2290 if (!spanNo) { // first span in a line, align it to the left
2291 attributes.x = parentX;
2292 } else {
2293 // Firefox ignores spaces at the front or end of the tspan
2294 attributes.dx = 3; // space
2297 // first span on subsequent line, add the line height
2298 if (!spanNo) {
2299 if (lineNo) {
2301 // allow getting the right offset height in exporting in IE
2302 if (!hasSVG && wrapper.renderer.forExport) {
2303 css(tspan, { display: 'block' });
2306 // Webkit and opera sometimes return 'normal' as the line height. In that
2307 // case, webkit uses offsetHeight, while Opera falls back to 18
2308 lineHeight = win[GET_COMPUTED_STYLE] &&
2309 win[GET_COMPUTED_STYLE](lastLine, null).getPropertyValue('line-height');
2311 if (!lineHeight || isNaN(lineHeight)) {
2312 lineHeight = textLineHeight || lastLine.offsetHeight || 18;
2314 attr(tspan, 'dy', lineHeight);
2316 lastLine = tspan; // record for use in next line
2319 // add attributes
2320 attr(tspan, attributes);
2322 // append it
2323 textNode.appendChild(tspan);
2325 spanNo++;
2327 // check width and apply soft breaks
2328 if (width) {
2329 var words = span.replace(/-/g, '- ').split(' '),
2330 tooLong,
2331 actualWidth,
2332 rest = [];
2334 while (words.length || rest.length) {
2335 actualWidth = textNode.getBBox().width;
2336 tooLong = actualWidth > width;
2337 if (!tooLong || words.length === 1) { // new line needed
2338 words = rest;
2339 rest = [];
2340 if (words.length) {
2341 tspan = doc.createElementNS(SVG_NS, 'tspan');
2342 attr(tspan, {
2343 dy: textLineHeight || 16,
2344 x: parentX
2346 textNode.appendChild(tspan);
2348 if (actualWidth > width) { // a single word is pressing it out
2349 width = actualWidth;
2352 } else { // append to existing line tspan
2353 tspan.removeChild(tspan.firstChild);
2354 rest.unshift(words.pop());
2356 if (words.length) {
2357 tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));
2369 * Make a straight line crisper by not spilling out to neighbour pixels
2370 * @param {Array} points
2371 * @param {Number} width
2373 crispLine: function(points, width) {
2374 // points format: [M, 0, 0, L, 100, 0]
2375 // normalize to a crisp line
2376 if (points[1] === points[4]) {
2377 points[1] = points[4] = mathRound(points[1]) + (width % 2 / 2);
2379 if (points[2] === points[5]) {
2380 points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2);
2382 return points;
2387 * Draw a path
2388 * @param {Array} path An SVG path in array form
2390 path: function (path) {
2391 return this.createElement('path').attr({
2392 d: path,
2393 fill: NONE
2398 * Draw and return an SVG circle
2399 * @param {Number} x The x position
2400 * @param {Number} y The y position
2401 * @param {Number} r The radius
2403 circle: function (x, y, r) {
2404 var attr = isObject(x) ?
2407 x: x,
2408 y: y,
2409 r: r
2412 return this.createElement('circle').attr(attr);
2416 * Draw and return an arc
2417 * @param {Number} x X position
2418 * @param {Number} y Y position
2419 * @param {Number} r Radius
2420 * @param {Number} innerR Inner radius like used in donut charts
2421 * @param {Number} start Starting angle
2422 * @param {Number} end Ending angle
2424 arc: function (x, y, r, innerR, start, end) {
2425 // arcs are defined as symbols for the ability to set
2426 // attributes in attr and animate
2428 if (isObject(x)) {
2429 y = x.y;
2430 r = x.r;
2431 innerR = x.innerR;
2432 start = x.start;
2433 end = x.end;
2434 x = x.x;
2437 return this.symbol('arc', x || 0, y || 0, r || 0, {
2438 innerR: innerR || 0,
2439 start: start || 0,
2440 end: end || 0
2445 * Draw and return a rectangle
2446 * @param {Number} x Left position
2447 * @param {Number} y Top position
2448 * @param {Number} width
2449 * @param {Number} height
2450 * @param {Number} r Border corner radius
2451 * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing
2453 rect: function (x, y, width, height, r, strokeWidth) {
2454 if (isObject(x)) {
2455 y = x.y;
2456 width = x.width;
2457 height = x.height;
2458 r = x.r;
2459 strokeWidth = x.strokeWidth;
2460 x = x.x;
2462 var wrapper = this.createElement('rect').attr({
2463 rx: r,
2464 ry: r,
2465 fill: NONE
2468 return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0)));
2472 * Resize the box and re-align all aligned elements
2473 * @param {Object} width
2474 * @param {Object} height
2475 * @param {Boolean} animate
2478 setSize: function(width, height, animate) {
2479 var renderer = this,
2480 alignedObjects = renderer.alignedObjects,
2481 i = alignedObjects.length;
2483 renderer.width = width;
2484 renderer.height = height;
2486 renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({
2487 width: width,
2488 height: height
2489 });
2491 while (i--) {
2492 alignedObjects[i].align();
2497 * Create a group
2498 * @param {String} name The group will be given a class name of 'highcharts-{name}'.
2499 * This can be used for styling and scripting.
2501 g: function(name) {
2502 return this.createElement('g').attr(
2503 defined(name) && { 'class': PREFIX + name }
2508 * Display an image
2509 * @param {String} src
2510 * @param {Number} x
2511 * @param {Number} y
2512 * @param {Number} width
2513 * @param {Number} height
2515 image: function(src, x, y, width, height) {
2516 var attribs = {
2517 preserveAspectRatio: NONE
2519 elemWrapper;
2521 // optional properties
2522 if (arguments.length > 1) {
2523 extend(attribs, {
2524 x: x,
2525 y: y,
2526 width: width,
2527 height: height
2531 elemWrapper = this.createElement('image').attr(attribs);
2533 // set the href in the xlink namespace
2534 if (elemWrapper.element.setAttributeNS) {
2535 elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink',
2536 'href', src);
2537 } else {
2538 // could be exporting in IE
2539 // using href throws "not supported" in ie7 and under, requries regex shim to fix later
2540 elemWrapper.element.setAttribute('hc-svg-href', src);
2543 return elemWrapper;
2547 * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object.
2549 * @param {Object} symbol
2550 * @param {Object} x
2551 * @param {Object} y
2552 * @param {Object} radius
2553 * @param {Object} options
2555 symbol: function(symbol, x, y, radius, options) {
2557 var obj,
2559 // get the symbol definition function
2560 symbolFn = this.symbols[symbol],
2562 // check if there's a path defined for this symbol
2563 path = symbolFn && symbolFn(
2564 mathRound(x),
2565 mathRound(y),
2566 radius,
2567 options
2570 imageRegex = /^url\((.*?)\)$/,
2571 imageSrc,
2572 imageSize;
2574 if (path) {
2576 obj = this.path(path);
2577 // expando properties for use in animate and attr
2578 extend(obj, {
2579 symbolName: symbol,
2580 x: x,
2581 y: y,
2582 r: radius
2584 if (options) {
2585 extend(obj, options);
2589 // image symbols
2590 } else if (imageRegex.test(symbol)) {
2592 var centerImage = function(img, size) {
2593 img.attr({
2594 width: size[0],
2595 height: size[1]
2596 }).translate(
2597 obj.translateX-mathRound(size[0] / 2),
2598 obj.translateY-mathRound(size[1] / 2)
2600 img.imagesize = [size[0], size[1]];
2603 imageSrc = symbol.match(imageRegex)[1];
2604 imageSize = symbolSizes[imageSrc];
2606 // create the image synchronously, add attribs async
2607 obj = this.image(imageSrc)
2608 .attr({
2609 x: x,
2610 y: y
2613 if (imageSize) {
2614 centerImage(obj, imageSize);
2615 } else {
2616 // initialize image to be 0 size so export will still function if there's no cached sizes
2617 obj.attr({ width: 0, height: 0 });
2619 // create a dummy JavaScript image to get the width and height
2620 createElement('img', {
2621 onload: function() {
2622 var img = this;
2623 centerImage(obj, symbolSizes[imageSrc] = [img.width, img.height]);
2625 src: imageSrc
2629 // default circles
2630 } else {
2631 obj = this.circle(x, y, radius);
2634 return obj;
2638 * An extendable collection of functions for defining symbol paths.
2640 symbols: {
2641 'square': function (x, y, radius) {
2642 var len = 0.707 * radius;
2643 return [
2644 M, x-len, y-len,
2645 L, x+len, y-len,
2646 x+len, y+len,
2647 x-len, y+len,
2652 'triangle': function (x, y, radius) {
2653 return [
2654 M, x, y-1.33 * radius,
2655 L, x+radius, y + 0.67 * radius,
2656 x-radius, y + 0.67 * radius,
2661 'triangle-down': function (x, y, radius) {
2662 return [
2663 M, x, y + 1.33 * radius,
2664 L, x-radius, y-0.67 * radius,
2665 x+radius, y-0.67 * radius,
2669 'diamond': function (x, y, radius) {
2670 return [
2671 M, x, y-radius,
2672 L, x+radius, y,
2673 x, y+radius,
2674 x-radius, y,
2678 'arc': function (x, y, radius, options) {
2679 var start = options.start,
2680 end = options.end - 0.000001, // to prevent cos and sin of start and end from becoming equal on 360 arcs
2681 innerRadius = options.innerR,
2682 cosStart = mathCos(start),
2683 sinStart = mathSin(start),
2684 cosEnd = mathCos(end),
2685 sinEnd = mathSin(end),
2686 longArc = options.end - start < mathPI ? 0 : 1;
2688 return [
2690 x + radius * cosStart,
2691 y + radius * sinStart,
2692 'A', // arcTo
2693 radius, // x radius
2694 radius, // y radius
2695 0, // slanting
2696 longArc, // long or short arc
2697 1, // clockwise
2698 x + radius * cosEnd,
2699 y + radius * sinEnd,
2701 x + innerRadius * cosEnd,
2702 y + innerRadius * sinEnd,
2703 'A', // arcTo
2704 innerRadius, // x radius
2705 innerRadius, // y radius
2706 0, // slanting
2707 longArc, // long or short arc
2708 0, // clockwise
2709 x + innerRadius * cosStart,
2710 y + innerRadius * sinStart,
2712 'Z' // close
2718 * Define a clipping rectangle
2719 * @param {String} id
2720 * @param {Number} x
2721 * @param {Number} y
2722 * @param {Number} width
2723 * @param {Number} height
2725 clipRect: function (x, y, width, height) {
2726 var wrapper,
2727 id = PREFIX + idCounter++,
2729 clipPath = this.createElement('clipPath').attr({
2730 id: id
2731 }).add(this.defs);
2733 wrapper = this.rect(x, y, width, height, 0).add(clipPath);
2734 wrapper.id = id;
2736 return wrapper;
2741 * Take a color and return it if it's a string, make it a gradient if it's a
2742 * gradient configuration object
2744 * @param {Object} color The color or config object
2746 color: function(color, elem, prop) {
2747 var colorObject,
2748 regexRgba = /^rgba/;
2749 if (color && color.linearGradient) {
2750 var renderer = this,
2751 strLinearGradient = 'linearGradient',
2752 linearGradient = color[strLinearGradient],
2753 id = PREFIX + idCounter++,
2754 gradientObject,
2755 stopColor,
2756 stopOpacity;
2757 gradientObject = renderer.createElement(strLinearGradient).attr({
2758 id: id,
2759 gradientUnits: 'userSpaceOnUse',
2760 x1: linearGradient[0],
2761 y1: linearGradient[1],
2762 x2: linearGradient[2],
2763 y2: linearGradient[3]
2764 }).add(renderer.defs);
2766 each(color.stops, function(stop) {
2767 if (regexRgba.test(stop[1])) {
2768 colorObject = Color(stop[1]);
2769 stopColor = colorObject.get('rgb');
2770 stopOpacity = colorObject.get('a');
2771 } else {
2772 stopColor = stop[1];
2773 stopOpacity = 1;
2775 renderer.createElement('stop').attr({
2776 offset: stop[0],
2777 'stop-color': stopColor,
2778 'stop-opacity': stopOpacity
2779 }).add(gradientObject);
2782 return 'url('+ this.url +'#'+ id +')';
2784 // Webkit and Batik can't show rgba.
2785 } else if (regexRgba.test(color)) {
2786 colorObject = Color(color);
2787 attr(elem, prop +'-opacity', colorObject.get('a'));
2789 return colorObject.get('rgb');
2792 } else {
2793 return color;
2800 * Add text to the SVG object
2801 * @param {String} str
2802 * @param {Number} x Left position
2803 * @param {Number} y Top position
2805 text: function(str, x, y) {
2807 // declare variables
2808 var defaultChartStyle = defaultOptions.chart.style,
2809 wrapper;
2811 x = mathRound(pick(x, 0));
2812 y = mathRound(pick(y, 0));
2814 wrapper = this.createElement('text')
2815 .attr({
2816 x: x,
2817 y: y,
2818 text: str
2820 .css({
2821 'font-family': defaultChartStyle.fontFamily,
2822 'font-size': defaultChartStyle.fontSize
2825 wrapper.x = x;
2826 wrapper.y = y;
2827 return wrapper;
2829 }; // end SVGRenderer
2831 // general renderer
2832 Renderer = SVGRenderer;
2836 /* ****************************************************************************
2837 * *
2838 * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
2840 * For applications and websites that don't need IE support, like platform *
2841 * targeted mobile apps and web apps, this code can be removed. *
2843 *****************************************************************************/
2844 var VMLRenderer;
2845 if (!hasSVG) {
2848 * The VML element wrapper.
2850 var VMLElement = extendClass( SVGElement, {
2853 * Initialize a new VML element wrapper. It builds the markup as a string
2854 * to minimize DOM traffic.
2855 * @param {Object} renderer
2856 * @param {Object} nodeName
2858 init: function(renderer, nodeName) {
2859 var markup = ['<', nodeName, ' filled="f" stroked="f"'],
2860 style = ['position: ', ABSOLUTE, ';'];
2862 // divs and shapes need size
2863 if (nodeName === 'shape' || nodeName === DIV) {
2864 style.push('left:0;top:0;width:10px;height:10px;');
2866 if (docMode8) {
2867 style.push('visibility: ', nodeName === DIV ? HIDDEN : VISIBLE);
2870 markup.push(' style="', style.join(''), '"/>');
2872 // create element with default attributes and style
2873 if (nodeName) {
2874 markup = nodeName === DIV || nodeName === 'span' || nodeName === 'img' ?
2875 markup.join('')
2876 : renderer.prepVML(markup);
2877 this.element = createElement(markup);
2880 this.renderer = renderer;
2884 * Add the node to the given parent
2885 * @param {Object} parent
2887 add: function(parent) {
2888 var wrapper = this,
2889 renderer = wrapper.renderer,
2890 element = wrapper.element,
2891 box = renderer.box,
2892 inverted = parent && parent.inverted,
2894 // get the parent node
2895 parentNode = parent ?
2896 parent.element || parent :
2897 box;
2900 // if the parent group is inverted, apply inversion on all children
2901 if (inverted) { // only on groups
2902 renderer.invertChild(element, parentNode);
2905 // issue #140 workaround - related to #61 and #74
2906 if (docMode8 && parentNode.gVis === HIDDEN) {
2907 css(element, { visibility: HIDDEN });
2910 // append it
2911 parentNode.appendChild(element);
2913 // align text after adding to be able to read offset
2914 wrapper.added = true;
2915 if (wrapper.alignOnAdd) {
2916 wrapper.updateTransform();
2919 return wrapper;
2923 * Get or set attributes
2925 attr: function(hash, val) {
2926 var key,
2927 value,
2929 element = this.element || {},
2930 elemStyle = element.style,
2931 nodeName = element.nodeName,
2932 renderer = this.renderer,
2933 symbolName = this.symbolName,
2934 childNodes,
2935 hasSetSymbolSize,
2936 shadows = this.shadows,
2937 skipAttr,
2938 ret = this;
2940 // single key-value pair
2941 if (isString(hash) && defined(val)) {
2942 key = hash;
2943 hash = {};
2944 hash[key] = val;
2947 // used as a getter, val is undefined
2948 if (isString(hash)) {
2949 key = hash;
2950 if (key === 'strokeWidth' || key === 'stroke-width') {
2951 ret = this.strokeweight;
2952 } else {
2953 ret = this[key];
2956 // setter
2957 } else {
2958 for (key in hash) {
2959 value = hash[key];
2960 skipAttr = false;
2962 // prepare paths
2963 // symbols
2964 if (symbolName && /^(x|y|r|start|end|width|height|innerR)/.test(key)) {
2965 // if one of the symbol size affecting parameters are changed,
2966 // check all the others only once for each call to an element's
2967 // .attr() method
2968 if (!hasSetSymbolSize) {
2969 this.symbolAttr(hash);
2971 hasSetSymbolSize = true;
2974 skipAttr = true;
2976 } else if (key === 'd') {
2977 value = value || [];
2978 this.d = value.join(' '); // used in getter for animation
2980 // convert paths
2981 i = value.length;
2982 var convertedPath = [];
2983 while (i--) {
2985 // Multiply by 10 to allow subpixel precision.
2986 // Substracting half a pixel seems to make the coordinates
2987 // align with SVG, but this hasn't been tested thoroughly
2988 if (isNumber(value[i])) {
2989 convertedPath[i] = mathRound(value[i] * 10) - 5;
2991 // close the path
2992 else if (value[i] === 'Z') {
2993 convertedPath[i] = 'x';
2995 else {
2996 convertedPath[i] = value[i];
3000 value = convertedPath.join(' ') || 'x';
3001 element.path = value;
3003 // update shadows
3004 if (shadows) {
3005 i = shadows.length;
3006 while (i--) {
3007 shadows[i].path = value;
3010 skipAttr = true;
3012 // directly mapped to css
3013 } else if (key === 'zIndex' || key === 'visibility') {
3015 // issue 61 workaround
3016 if (docMode8 && key === 'visibility' && nodeName === 'DIV') {
3017 element.gVis = value;
3018 childNodes = element.childNodes;
3019 i = childNodes.length;
3020 while (i--) {
3021 css(childNodes[i], { visibility: value });
3023 if (value === VISIBLE) { // issue 74
3024 value = null;
3028 if (value) {
3029 elemStyle[key] = value;
3034 skipAttr = true;
3036 // width and height
3037 } else if (/^(width|height)$/.test(key)) {
3040 // clipping rectangle special
3041 if (this.updateClipping) {
3042 this[key] = value;
3043 this.updateClipping();
3045 } else {
3046 // normal
3047 elemStyle[key] = value;
3050 skipAttr = true;
3052 // x and y
3053 } else if (/^(x|y)$/.test(key)) {
3055 this[key] = value; // used in getter
3057 if (element.tagName === 'SPAN') {
3058 this.updateTransform();
3060 } else {
3061 elemStyle[{ x: 'left', y: 'top' }[key]] = value;
3064 // class name
3065 } else if (key === 'class') {
3066 // IE8 Standards mode has problems retrieving the className
3067 element.className = value;
3069 // stroke
3070 } else if (key === 'stroke') {
3072 value = renderer.color(value, element, key);
3074 key = 'strokecolor';
3076 // stroke width
3077 } else if (key === 'stroke-width' || key === 'strokeWidth') {
3078 element.stroked = value ? true : false;
3079 key = 'strokeweight';
3080 this[key] = value; // used in getter, issue #113
3081 if (isNumber(value)) {
3082 value += PX;
3085 // dashStyle
3086 } else if (key === 'dashstyle') {
3087 var strokeElem = element.getElementsByTagName('stroke')[0] ||
3088 createElement(renderer.prepVML(['<stroke/>']), null, null, element);
3089 strokeElem[key] = value || 'solid';
3090 this.dashstyle = value; /* because changing stroke-width will change the dash length
3091 and cause an epileptic effect */
3092 skipAttr = true;
3094 // fill
3095 } else if (key === 'fill') {
3097 if (nodeName === 'SPAN') { // text color
3098 elemStyle.color = value;
3099 } else {
3100 element.filled = value !== NONE ? true : false;
3102 value = renderer.color(value, element, key);
3104 key = 'fillcolor';
3107 // translation for animation
3108 } else if (key === 'translateX' || key === 'translateY' || key === 'rotation' || key === 'align') {
3109 if (key === 'align') {
3110 key = 'textAlign';
3112 this[key] = value;
3113 this.updateTransform();
3115 skipAttr = true;
3118 // text for rotated and non-rotated elements
3119 else if (key === 'text') {
3120 this.bBox = null;
3121 element.innerHTML = value;
3122 skipAttr = true;
3126 // let the shadow follow the main element
3127 if (shadows && key === 'visibility') {
3128 i = shadows.length;
3129 while (i--) {
3130 shadows[i].style[key] = value;
3136 if (!skipAttr) {
3137 if (docMode8) { // IE8 setAttribute bug
3138 element[key] = value;
3139 } else {
3140 attr(element, key, value);
3145 return ret;
3149 * Set the element's clipping to a predefined rectangle
3151 * @param {String} id The id of the clip rectangle
3153 clip: function(clipRect) {
3154 var wrapper = this,
3155 clipMembers = clipRect.members;
3157 clipMembers.push(wrapper);
3158 wrapper.destroyClip = function() {
3159 erase(clipMembers, wrapper);
3161 return wrapper.css(clipRect.getCSS(wrapper.inverted));
3165 * Set styles for the element
3166 * @param {Object} styles
3168 css: function(styles) {
3169 var wrapper = this,
3170 element = wrapper.element,
3171 textWidth = styles && element.tagName === 'SPAN' && styles.width;
3173 /*if (textWidth) {
3174 extend(styles, {
3175 display: 'block',
3176 whiteSpace: 'normal'
3177 });
3179 if (textWidth) {
3180 delete styles.width;
3181 wrapper.textWidth = textWidth;
3182 wrapper.updateTransform();
3185 wrapper.styles = extend(wrapper.styles, styles);
3186 css(wrapper.element, styles);
3188 return wrapper;
3192 * Extend element.destroy by removing it from the clip members array
3194 destroy: function() {
3195 var wrapper = this;
3197 if (wrapper.destroyClip) {
3198 wrapper.destroyClip();
3201 SVGElement.prototype.destroy.apply(wrapper);
3205 * Remove all child nodes of a group, except the v:group element
3207 empty: function() {
3208 var element = this.element,
3209 childNodes = element.childNodes,
3210 i = childNodes.length,
3211 node;
3213 while (i--) {
3214 node = childNodes[i];
3215 node.parentNode.removeChild(node);
3220 * VML override for calculating the bounding box based on offsets
3222 * @return {Object} A hash containing values for x, y, width and height
3225 getBBox: function() {
3226 var wrapper = this,
3227 element = wrapper.element,
3228 bBox = wrapper.bBox;
3230 if (!bBox) {
3231 // faking getBBox in exported SVG in legacy IE
3232 if (element.nodeName === 'text') {
3233 element.style.position = ABSOLUTE;
3236 bBox = wrapper.bBox = {
3237 x: element.offsetLeft,
3238 y: element.offsetTop,
3239 width: element.offsetWidth,
3240 height: element.offsetHeight
3243 return bBox;
3248 * Add an event listener. VML override for normalizing event parameters.
3249 * @param {String} eventType
3250 * @param {Function} handler
3252 on: function(eventType, handler) {
3253 // simplest possible event model for internal use
3254 this.element['on'+ eventType] = function() {
3255 var evt = win.event;
3256 evt.target = evt.srcElement;
3257 handler(evt);
3259 return this;
3264 * VML override private method to update elements based on internal
3265 * properties based on SVG transform
3267 updateTransform: function(hash) {
3268 // aligning non added elements is expensive
3269 if (!this.added) {
3270 this.alignOnAdd = true;
3271 return;
3274 var wrapper = this,
3275 elem = wrapper.element,
3276 translateX = wrapper.translateX || 0,
3277 translateY = wrapper.translateY || 0,
3278 x = wrapper.x || 0,
3279 y = wrapper.y || 0,
3280 align = wrapper.textAlign || 'left',
3281 alignCorrection = { left: 0, center: 0.5, right: 1 }[align],
3282 nonLeft = align && align !== 'left';
3284 // apply translate
3285 if (translateX || translateY) {
3286 wrapper.css({
3287 marginLeft: translateX,
3288 marginTop: translateY
3292 // apply inversion
3293 if (wrapper.inverted) { // wrapper is a group
3294 each(elem.childNodes, function(child) {
3295 wrapper.renderer.invertChild(child, elem);
3299 if (elem.tagName === 'SPAN') {
3301 var width, height,
3302 rotation = wrapper.rotation,
3303 lineHeight,
3304 radians = 0,
3305 costheta = 1,
3306 sintheta = 0,
3307 quad,
3308 textWidth = pInt(wrapper.textWidth),
3309 xCorr = wrapper.xCorr || 0,
3310 yCorr = wrapper.yCorr || 0,
3311 currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth].join(',');
3313 if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed
3315 if (defined(rotation)) {
3316 radians = rotation * deg2rad; // deg to rad
3317 costheta = mathCos(radians);
3318 sintheta = mathSin(radians);
3320 // Adjust for alignment and rotation.
3321 // Test case: http://highcharts.com/tests/?file=text-rotation
3322 css(elem, {
3323 filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta,
3324 ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta,
3325 ', sizingMethod=\'auto expand\')'].join('') : NONE
3329 width = elem.offsetWidth;
3330 height = elem.offsetHeight;
3332 // update textWidth
3333 if (width > textWidth) {
3334 css(elem, {
3335 width: textWidth +PX,
3336 display: 'block',
3337 whiteSpace: 'normal'
3339 width = textWidth;
3342 // correct x and y
3343 lineHeight = mathRound((pInt(elem.style.fontSize) || 12) * 1.2);
3344 xCorr = costheta < 0 && -width;
3345 yCorr = sintheta < 0 && -height;
3347 // correct for lineHeight and corners spilling out after rotation
3348 quad = costheta * sintheta < 0;
3349 xCorr += sintheta * lineHeight * (quad ? 1 - alignCorrection : alignCorrection);
3350 yCorr -= costheta * lineHeight * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);
3352 // correct for the length/height of the text
3353 if (nonLeft) {
3354 xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);
3355 if (rotation) {
3356 yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);
3358 css(elem, {
3359 textAlign: align
3363 // record correction
3364 wrapper.xCorr = xCorr;
3365 wrapper.yCorr = yCorr;
3368 // apply position with correction
3369 css(elem, {
3370 left: x + xCorr,
3371 top: y + yCorr
3374 // record current text transform
3375 wrapper.cTT = currentTextTransform;
3380 * Apply a drop shadow by copying elements and giving them different strokes
3381 * @param {Boolean} apply
3383 shadow: function(apply, group) {
3384 var shadows = [],
3386 element = this.element,
3387 renderer = this.renderer,
3388 shadow,
3389 elemStyle = element.style,
3390 markup,
3391 path = element.path;
3393 // some times empty paths are not strings
3394 if (path && typeof path.value !== 'string') {
3395 path = 'x';
3398 if (apply) {
3399 for (i = 1; i <= 3; i++) {
3400 markup = ['<shape isShadow="true" strokeweight="', ( 7 - 2 * i ) ,
3401 '" filled="false" path="', path,
3402 '" coordsize="100,100" style="', element.style.cssText, '" />'];
3403 shadow = createElement(renderer.prepVML(markup),
3404 null, {
3405 left: pInt(elemStyle.left) + 1,
3406 top: pInt(elemStyle.top) + 1
3410 // apply the opacity
3411 markup = ['<stroke color="black" opacity="', (0.05 * i), '"/>'];
3412 createElement(renderer.prepVML(markup), null, null, shadow);
3415 // insert it
3416 if (group) {
3417 group.element.appendChild(shadow);
3418 } else {
3419 element.parentNode.insertBefore(shadow, element);
3422 // record it
3423 shadows.push(shadow);
3427 this.shadows = shadows;
3429 return this;
3435 * The VML renderer
3437 VMLRenderer = function() {
3438 this.init.apply(this, arguments);
3440 VMLRenderer.prototype = merge( SVGRenderer.prototype, { // inherit SVGRenderer
3442 Element: VMLElement,
3443 isIE8: userAgent.indexOf('MSIE 8.0') > -1,
3447 * Initialize the VMLRenderer
3448 * @param {Object} container
3449 * @param {Number} width
3450 * @param {Number} height
3452 init: function(container, width, height) {
3453 var renderer = this,
3454 boxWrapper;
3456 renderer.alignedObjects = [];
3458 boxWrapper = renderer.createElement(DIV);
3459 container.appendChild(boxWrapper.element);
3462 // generate the containing box
3463 renderer.box = boxWrapper.element;
3464 renderer.boxWrapper = boxWrapper;
3467 renderer.setSize(width, height, false);
3469 // The only way to make IE6 and IE7 print is to use a global namespace. However,
3470 // with IE8 the only way to make the dynamic shapes visible in screen and print mode
3471 // seems to be to add the xmlns attribute and the behaviour style inline.
3472 if (!doc.namespaces.hcv) {
3474 doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml');
3476 // setup default css
3477 doc.createStyleSheet().cssText =
3478 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke'+
3479 '{ behavior:url(#default#VML); display: inline-block; } ';
3485 * Define a clipping rectangle. In VML it is accomplished by storing the values
3486 * for setting the CSS style to all associated members.
3488 * @param {Number} x
3489 * @param {Number} y
3490 * @param {Number} width
3491 * @param {Number} height
3493 clipRect: function (x, y, width, height) {
3495 // create a dummy element
3496 var clipRect = this.createElement();
3498 // mimic a rectangle with its style object for automatic updating in attr
3499 return extend(clipRect, {
3500 members: [],
3501 left: x,
3502 top: y,
3503 width: width,
3504 height: height,
3505 getCSS: function(inverted) {
3506 var rect = this,//clipRect.element.style,
3507 top = rect.top,
3508 left = rect.left,
3509 right = left + rect.width,
3510 bottom = top + rect.height,
3511 ret = {
3512 clip: 'rect('+
3513 mathRound(inverted ? left : top) + 'px,'+
3514 mathRound(inverted ? bottom : right) + 'px,'+
3515 mathRound(inverted ? right : bottom) + 'px,'+
3516 mathRound(inverted ? top : left) +'px)'
3519 // issue 74 workaround
3520 if (!inverted && docMode8) {
3521 extend(ret, {
3522 width: right +PX,
3523 height: bottom +PX
3526 return ret;
3529 // used in attr and animation to update the clipping of all members
3530 updateClipping: function() {
3531 each(clipRect.members, function(member) {
3532 member.css(clipRect.getCSS(member.inverted));
3541 * Take a color and return it if it's a string, make it a gradient if it's a
3542 * gradient configuration object, and apply opacity.
3544 * @param {Object} color The color or config object
3546 color: function(color, elem, prop) {
3547 var colorObject,
3548 regexRgba = /^rgba/,
3549 markup;
3551 if (color && color.linearGradient) {
3553 var stopColor,
3554 stopOpacity,
3555 linearGradient = color.linearGradient,
3556 angle,
3557 color1,
3558 opacity1,
3559 color2,
3560 opacity2;
3562 each(color.stops, function(stop, i) {
3563 if (regexRgba.test(stop[1])) {
3564 colorObject = Color(stop[1]);
3565 stopColor = colorObject.get('rgb');
3566 stopOpacity = colorObject.get('a');
3567 } else {
3568 stopColor = stop[1];
3569 stopOpacity = 1;
3572 if (!i) { // first
3573 color1 = stopColor;
3574 opacity1 = stopOpacity;
3575 } else {
3576 color2 = stopColor;
3577 opacity2 = stopOpacity;
3583 // calculate the angle based on the linear vector
3584 angle = 90 - math.atan(
3585 (linearGradient[3] - linearGradient[1]) / // y vector
3586 (linearGradient[2] - linearGradient[0]) // x vector
3587 ) * 180 / mathPI;
3589 // when colors attribute is used, the meanings of opacity and o:opacity2
3590 // are reversed.
3591 markup = ['<', prop, ' colors="0% ', color1, ',100% ', color2, '" angle="', angle,
3592 '" opacity="', opacity2, '" o:opacity2="', opacity1,
3593 '" type="gradient" focus="100%" />'];
3594 createElement(this.prepVML(markup), null, null, elem);
3598 // if the color is an rgba color, split it and add a fill node
3599 // to hold the opacity component
3600 } else if (regexRgba.test(color) && elem.tagName !== 'IMG') {
3602 colorObject = Color(color);
3604 markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>'];
3605 createElement(this.prepVML(markup), null, null, elem);
3607 return colorObject.get('rgb');
3610 } else {
3611 return color;
3617 * Take a VML string and prepare it for either IE8 or IE6/IE7.
3618 * @param {Array} markup A string array of the VML markup to prepare
3620 prepVML: function(markup) {
3621 var vmlStyle = 'display:inline-block;behavior:url(#default#VML);',
3622 isIE8 = this.isIE8;
3624 markup = markup.join('');
3626 if (isIE8) { // add xmlns and style inline
3627 markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />');
3628 if (markup.indexOf('style="') === -1) {
3629 markup = markup.replace('/>', ' style="'+ vmlStyle +'" />');
3630 } else {
3631 markup = markup.replace('style="', 'style="'+ vmlStyle);
3634 } else { // add namespace
3635 markup = markup.replace('<', '<hcv:');
3638 return markup;
3642 * Create rotated and aligned text
3643 * @param {String} str
3644 * @param {Number} x
3645 * @param {Number} y
3647 text: function(str, x, y) {
3649 var defaultChartStyle = defaultOptions.chart.style;
3651 return this.createElement('span')
3652 .attr({
3653 text: str,
3654 x: mathRound(x),
3655 y: mathRound(y)
3657 .css({
3658 whiteSpace: 'nowrap',
3659 fontFamily: defaultChartStyle.fontFamily,
3660 fontSize: defaultChartStyle.fontSize
3665 * Create and return a path element
3666 * @param {Array} path
3668 path: function (path) {
3669 // create the shape
3670 return this.createElement('shape').attr({
3671 // subpixel precision down to 0.1 (width and height = 10px)
3672 coordsize: '100 100',
3673 d: path
3678 * Create and return a circle element. In VML circles are implemented as
3679 * shapes, which is faster than v:oval
3680 * @param {Number} x
3681 * @param {Number} y
3682 * @param {Number} r
3684 circle: function(x, y, r) {
3685 return this.symbol('circle').attr({ x: x, y: y, r: r});
3689 * Create a group using an outer div and an inner v:group to allow rotating
3690 * and flipping. A simple v:group would have problems with positioning
3691 * child HTML elements and CSS clip.
3693 * @param {String} name The name of the group
3695 g: function(name) {
3696 var wrapper,
3697 attribs;
3699 // set the class name
3700 if (name) {
3701 attribs = { 'className': PREFIX + name, 'class': PREFIX + name };
3704 // the div to hold HTML and clipping
3705 wrapper = this.createElement(DIV).attr(attribs);
3707 return wrapper;
3711 * VML override to create a regular HTML image
3712 * @param {String} src
3713 * @param {Number} x
3714 * @param {Number} y
3715 * @param {Number} width
3716 * @param {Number} height
3718 image: function(src, x, y, width, height) {
3719 var obj = this.createElement('img')
3720 .attr({ src: src });
3722 if (arguments.length > 1) {
3723 obj.css({
3724 left: x,
3725 top: y,
3726 width: width,
3727 height: height
3730 return obj;
3734 * VML uses a shape for rect to overcome bugs and rotation problems
3736 rect: function(x, y, width, height, r, strokeWidth) {
3738 if (isObject(x)) {
3739 y = x.y;
3740 width = x.width;
3741 height = x.height;
3742 r = x.r;
3743 strokeWidth = x.strokeWidth;
3744 x = x.x;
3746 var wrapper = this.symbol('rect');
3747 wrapper.r = r;
3749 return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0)));
3753 * In the VML renderer, each child of an inverted div (group) is inverted
3754 * @param {Object} element
3755 * @param {Object} parentNode
3757 invertChild: function(element, parentNode) {
3758 var parentStyle = parentNode.style;
3760 css(element, {
3761 flip: 'x',
3762 left: pInt(parentStyle.width) - 10,
3763 top: pInt(parentStyle.height) - 10,
3764 rotation: -90
3769 * Symbol definitions that override the parent SVG renderer's symbols
3772 symbols: {
3773 // VML specific arc function
3774 arc: function (x, y, radius, options) {
3775 var start = options.start,
3776 end = options.end,
3777 cosStart = mathCos(start),
3778 sinStart = mathSin(start),
3779 cosEnd = mathCos(end),
3780 sinEnd = mathSin(end),
3781 innerRadius = options.innerR,
3782 circleCorrection = 0.07 / radius,
3783 innerCorrection = (innerRadius && 0.1 / innerRadius) || 0;
3785 if (end - start === 0) { // no angle, don't show it.
3786 return ['x'];
3788 //} else if (end - start == 2 * mathPI) { // full circle
3789 } else if (2 * mathPI - end + start < circleCorrection) { // full circle
3790 // empirical correction found by trying out the limits for different radii
3791 cosEnd = - circleCorrection;
3792 } else if (end - start < innerCorrection) { // issue #186, another mysterious VML arc problem
3793 cosEnd = mathCos(start + innerCorrection);
3796 return [
3797 'wa', // clockwise arc to
3798 x - radius, // left
3799 y - radius, // top
3800 x + radius, // right
3801 y + radius, // bottom
3802 x + radius * cosStart, // start x
3803 y + radius * sinStart, // start y
3804 x + radius * cosEnd, // end x
3805 y + radius * sinEnd, // end y
3808 'at', // anti clockwise arc to
3809 x - innerRadius, // left
3810 y - innerRadius, // top
3811 x + innerRadius, // right
3812 y + innerRadius, // bottom
3813 x + innerRadius * cosEnd, // start x
3814 y + innerRadius * sinEnd, // start y
3815 x + innerRadius * cosStart, // end x
3816 y + innerRadius * sinStart, // end y
3818 'x', // finish path
3819 'e' // close
3823 // Add circle symbol path. This performs significantly faster than v:oval.
3824 circle: function (x, y, r) {
3825 return [
3826 'wa', // clockwisearcto
3827 x - r, // left
3828 y - r, // top
3829 x + r, // right
3830 y + r, // bottom
3831 x + r, // start x
3832 y, // start y
3833 x + r, // end x
3834 y, // end y
3835 //'x', // finish path
3836 'e' // close
3839 /**
3840 * Add rectangle symbol path which eases rotation and omits arcsize problems
3841 * compared to the built-in VML roundrect shape
3843 * @param {Number} left Left position
3844 * @param {Number} top Top position
3845 * @param {Number} r Border radius
3846 * @param {Object} options Width and height
3849 rect: function (left, top, r, options) {
3850 if (!defined(options)) {
3851 return [];
3853 var width = options.width,
3854 height = options.height,
3855 right = left + width,
3856 bottom = top + height;
3858 r = mathMin(r, width, height);
3860 return [
3862 left + r, top,
3865 right - r, top,
3866 'wa',
3867 right - 2 * r, top,
3868 right, top + 2 * r,
3869 right - r, top,
3870 right, top + r,
3873 right, bottom - r,
3874 'wa',
3875 right - 2 * r, bottom - 2 * r,
3876 right, bottom,
3877 right, bottom - r,
3878 right - r, bottom,
3881 left + r, bottom,
3882 'wa',
3883 left, bottom - 2 * r,
3884 left + 2 * r, bottom,
3885 left + r, bottom,
3886 left, bottom - r,
3889 left, top + r,
3890 'wa',
3891 left, top,
3892 left + 2 * r, top + 2 * r,
3893 left, top + r,
3894 left + r, top,
3897 'x',
3905 // general renderer
3906 Renderer = VMLRenderer;
3908 /* ****************************************************************************
3909 * *
3910 * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
3912 *****************************************************************************/
3916 * The chart class
3917 * @param {Object} options
3918 * @param {Function} callback Function to run when the chart has loaded
3920 function Chart (options, callback) {
3922 defaultXAxisOptions = merge(defaultXAxisOptions, defaultOptions.xAxis);
3923 defaultYAxisOptions = merge(defaultYAxisOptions, defaultOptions.yAxis);
3924 defaultOptions.xAxis = defaultOptions.yAxis = null;
3926 // Handle regular options
3927 options = merge(defaultOptions, options);
3929 // Define chart variables
3930 var optionsChart = options.chart,
3931 optionsMargin = optionsChart.margin,
3932 margin = isObject(optionsMargin) ?
3933 optionsMargin :
3934 [optionsMargin, optionsMargin, optionsMargin, optionsMargin],
3935 optionsMarginTop = pick(optionsChart.marginTop, margin[0]),
3936 optionsMarginRight = pick(optionsChart.marginRight, margin[1]),
3937 optionsMarginBottom = pick(optionsChart.marginBottom, margin[2]),
3938 optionsMarginLeft = pick(optionsChart.marginLeft, margin[3]),
3939 spacingTop = optionsChart.spacingTop,
3940 spacingRight = optionsChart.spacingRight,
3941 spacingBottom = optionsChart.spacingBottom,
3942 spacingLeft = optionsChart.spacingLeft,
3943 spacingBox,
3944 chartTitleOptions,
3945 chartSubtitleOptions,
3946 plotTop,
3947 marginRight,
3948 marginBottom,
3949 plotLeft,
3950 axisOffset,
3951 renderTo,
3952 renderToClone,
3953 container,
3954 containerId,
3955 containerWidth,
3956 containerHeight,
3957 chartWidth,
3958 chartHeight,
3959 oldChartWidth,
3960 oldChartHeight,
3961 chartBackground,
3962 plotBackground,
3963 plotBGImage,
3964 plotBorder,
3965 chart = this,
3966 chartEvents = optionsChart.events,
3967 runChartClick = chartEvents && !!chartEvents.click,
3968 eventType,
3969 isInsidePlot, // function
3970 tooltip,
3971 mouseIsDown,
3972 loadingDiv,
3973 loadingSpan,
3974 loadingShown,
3975 plotHeight,
3976 plotWidth,
3977 tracker,
3978 trackerGroup,
3979 placeTrackerGroup,
3980 legend,
3981 legendWidth,
3982 legendHeight,
3983 chartPosition,// = getPosition(container),
3984 hasCartesianSeries = optionsChart.showAxes,
3985 isResizing = 0,
3986 axes = [],
3987 maxTicks, // handle the greatest amount of ticks on grouped axes
3988 series = [],
3989 inverted,
3990 renderer,
3991 tooltipTick,
3992 tooltipInterval,
3993 hoverX,
3994 drawChartBox, // function
3995 getMargins, // function
3996 resetMargins, // function
3997 setChartSize, // function
3998 resize,
3999 zoom, // function
4000 zoomOut; // function
4004 * Create a new axis object
4005 * @param {Object} chart
4006 * @param {Object} options
4008 function Axis (chart, options) {
4010 // Define variables
4011 var isXAxis = options.isX,
4012 opposite = options.opposite, // needed in setOptions
4013 horiz = inverted ? !isXAxis : isXAxis,
4014 side = horiz ?
4015 (opposite ? 0 /* top */ : 2 /* bottom */) :
4016 (opposite ? 1 /* right*/ : 3 /* left */ ),
4017 stacks = {};
4020 options = merge(
4021 isXAxis ? defaultXAxisOptions : defaultYAxisOptions,
4022 [defaultTopAxisOptions, defaultRightAxisOptions,
4023 defaultBottomAxisOptions, defaultLeftAxisOptions][side],
4024 options
4027 var axis = this,
4028 type = options.type,
4029 isDatetimeAxis = type === 'datetime',
4030 isLog = type === 'logarithmic',
4031 offset = options.offset || 0,
4032 xOrY = isXAxis ? 'x' : 'y',
4033 axisLength,
4034 transA, // translation factor
4035 oldTransA, // used for prerendering
4036 transB = horiz ? plotLeft : marginBottom, // translation addend
4037 translate, // fn
4038 getPlotLinePath, // fn
4039 axisGroup,
4040 gridGroup,
4041 axisLine,
4042 dataMin,
4043 dataMax,
4044 associatedSeries,
4045 userMin,
4046 userMax,
4047 max = null,
4048 min = null,
4049 oldMin,
4050 oldMax,
4051 minPadding = options.minPadding,
4052 maxPadding = options.maxPadding,
4053 isLinked = defined(options.linkedTo),
4054 ignoreMinPadding, // can be set to true by a column or bar series
4055 ignoreMaxPadding,
4056 usePercentage,
4057 events = options.events,
4058 eventType,
4059 plotLinesAndBands = [],
4060 tickInterval,
4061 minorTickInterval,
4062 magnitude,
4063 tickPositions, // array containing predefined positions
4064 ticks = {},
4065 minorTicks = {},
4066 alternateBands = {},
4067 tickAmount,
4068 labelOffset,
4069 axisTitleMargin,// = options.title.margin,
4070 dateTimeLabelFormat,
4071 categories = options.categories,
4072 labelFormatter = options.labels.formatter || // can be overwritten by dynamic format
4073 function() {
4074 var value = this.value,
4075 ret;
4077 if (dateTimeLabelFormat) { // datetime axis
4078 ret = dateFormat(dateTimeLabelFormat, value);
4080 } else if (tickInterval % 1000000 === 0) { // use M abbreviation
4081 ret = (value / 1000000) +'M';
4083 } else if (tickInterval % 1000 === 0) { // use k abbreviation
4084 ret = (value / 1000) +'k';
4086 } else if (!categories && value >= 1000) { // add thousands separators
4087 ret = numberFormat(value, 0);
4089 } else { // strings (categories) and small numbers
4090 ret = value;
4092 return ret;
4095 staggerLines = horiz && options.labels.staggerLines,
4096 reversed = options.reversed,
4097 tickmarkOffset = (categories && options.tickmarkPlacement === 'between') ? 0.5 : 0;
4100 * The Tick class
4102 function Tick(pos, minor) {
4103 var tick = this;
4104 tick.pos = pos;
4105 tick.minor = minor;
4106 tick.isNew = true;
4108 if (!minor) {
4109 tick.addLabel();
4112 Tick.prototype = {
4114 * Write the tick label
4116 addLabel: function() {
4117 var pos = this.pos,
4118 labelOptions = options.labels,
4119 str,
4120 withLabel = !((pos === min && !pick(options.showFirstLabel, 1)) ||
4121 (pos === max && !pick(options.showLastLabel, 0))),
4122 width = (categories && horiz && categories.length &&
4123 !labelOptions.step && !labelOptions.staggerLines &&
4124 !labelOptions.rotation &&
4125 plotWidth / categories.length) ||
4126 (!horiz && plotWidth / 2),
4127 css,
4128 label = this.label;
4131 // get the string
4132 str = labelFormatter.call({
4133 isFirst: pos === tickPositions[0],
4134 isLast: pos === tickPositions[tickPositions.length - 1],
4135 dateTimeLabelFormat: dateTimeLabelFormat,
4136 value: (categories && categories[pos] ? categories[pos] : pos)
4140 // prepare CSS
4141 css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) +PX };
4142 css = extend(css, labelOptions.style);
4144 // first call
4145 if (label === UNDEFINED) {
4146 this.label =
4147 defined(str) && withLabel && labelOptions.enabled ?
4148 renderer.text(
4149 str,
4153 .attr({
4154 align: labelOptions.align,
4155 rotation: labelOptions.rotation
4157 // without position absolute, IE export sometimes is wrong
4158 .css(css)
4159 .add(axisGroup):
4160 null;
4162 // update
4163 } else if (label) {
4164 label.attr({ text: str })
4165 .css(css);
4169 * Get the offset height or width of the label
4171 getLabelSize: function() {
4172 var label = this.label;
4173 return label ?
4174 ((this.labelBBox = label.getBBox()))[horiz ? 'height' : 'width'] :
4178 * Put everything in place
4180 * @param index {Number}
4181 * @param old {Boolean} Use old coordinates to prepare an animation into new position
4183 render: function(index, old) {
4184 var tick = this,
4185 major = !tick.minor,
4186 label = tick.label,
4187 pos = tick.pos,
4188 labelOptions = options.labels,
4189 gridLine = tick.gridLine,
4190 gridLineWidth = major ? options.gridLineWidth : options.minorGridLineWidth,
4191 gridLineColor = major ? options.gridLineColor : options.minorGridLineColor,
4192 dashStyle = major ?
4193 options.gridLineDashStyle :
4194 options.minorGridLineDashStyle,
4195 gridLinePath,
4196 mark = tick.mark,
4197 markPath,
4198 tickLength = major ? options.tickLength : options.minorTickLength,
4199 tickWidth = major ? options.tickWidth : (options.minorTickWidth || 0),
4200 tickColor = major ? options.tickColor : options.minorTickColor,
4201 tickPosition = major ? options.tickPosition : options.minorTickPosition,
4202 step = labelOptions.step,
4203 cHeight = (old && oldChartHeight) || chartHeight,
4204 attribs,
4208 // get x and y position for ticks and labels
4209 x = horiz ?
4210 translate(pos + tickmarkOffset, null, null, old) + transB :
4211 plotLeft + offset + (opposite ? ((old && oldChartWidth) || chartWidth) - marginRight - plotLeft : 0);
4213 y = horiz ?
4214 cHeight - marginBottom + offset - (opposite ? plotHeight : 0) :
4215 cHeight - translate(pos + tickmarkOffset, null, null, old) - transB;
4217 // create the grid line
4218 if (gridLineWidth) {
4219 gridLinePath = getPlotLinePath(pos + tickmarkOffset, gridLineWidth, old);
4221 if (gridLine === UNDEFINED) {
4222 attribs = {
4223 stroke: gridLineColor,
4224 'stroke-width': gridLineWidth
4226 if (dashStyle) {
4227 attribs.dashstyle = dashStyle;
4229 tick.gridLine = gridLine =
4230 gridLineWidth ?
4231 renderer.path(gridLinePath)
4232 .attr(attribs).add(gridGroup) :
4233 null;
4235 if (gridLine && gridLinePath) {
4236 gridLine.animate({
4237 d: gridLinePath
4242 // create the tick mark
4243 if (tickWidth) {
4245 // negate the length
4246 if (tickPosition === 'inside') {
4247 tickLength = -tickLength;
4249 if (opposite) {
4250 tickLength = -tickLength;
4253 markPath = renderer.crispLine([
4258 x + (horiz ? 0 : -tickLength),
4259 y + (horiz ? tickLength : 0)
4260 ], tickWidth);
4262 if (mark) { // updating
4263 mark.animate({
4264 d: markPath
4266 } else { // first time
4267 tick.mark = renderer.path(
4268 markPath
4269 ).attr({
4270 stroke: tickColor,
4271 'stroke-width': tickWidth
4272 }).add(axisGroup);
4276 // the label is created on init - now move it into place
4277 if (label && !isNaN(x)) {
4278 x = x + labelOptions.x - (tickmarkOffset && horiz ?
4279 tickmarkOffset * transA * (reversed ? -1 : 1) : 0);
4280 y = y + labelOptions.y - (tickmarkOffset && !horiz ?
4281 tickmarkOffset * transA * (reversed ? 1 : -1) : 0);
4283 // vertically centered
4284 if (!defined(labelOptions.y)) {
4285 y += pInt(label.styles.lineHeight) * 0.9 - label.getBBox().height / 2;
4289 // correct for staggered labels
4290 if (staggerLines) {
4291 y += (index / (step || 1) % staggerLines) * 16;
4293 // apply step
4294 if (step) {
4295 // show those indices dividable by step
4296 label[index % step ? 'hide' : 'show']();
4299 label[tick.isNew ? 'attr' : 'animate']({
4300 x: x,
4301 y: y
4305 tick.isNew = false;
4308 * Destructor for the tick prototype
4310 destroy: function() {
4311 var tick = this,
4313 for (n in tick) {
4314 if (tick[n] && tick[n].destroy) {
4315 tick[n].destroy();
4322 * The object wrapper for plot lines and plot bands
4323 * @param {Object} options
4325 function PlotLineOrBand(options) {
4326 var plotLine = this;
4327 if (options) {
4328 plotLine.options = options;
4329 plotLine.id = options.id;
4332 //plotLine.render()
4333 return plotLine;
4336 PlotLineOrBand.prototype = {
4339 * Render the plot line or plot band. If it is already existing,
4340 * move it.
4342 render: function () {
4343 var plotLine = this,
4344 options = plotLine.options,
4345 optionsLabel = options.label,
4346 label = plotLine.label,
4347 width = options.width,
4348 to = options.to,
4349 toPath, // bands only
4350 from = options.from,
4351 dashStyle = options.dashStyle,
4352 svgElem = plotLine.svgElem,
4353 path = [],
4354 addEvent,
4355 eventType,
4360 color = options.color,
4361 zIndex = options.zIndex,
4362 events = options.events,
4363 attribs;
4365 // plot line
4366 if (width) {
4367 path = getPlotLinePath(options.value, width);
4368 attribs = {
4369 stroke: color,
4370 'stroke-width': width
4372 if (dashStyle) {
4373 attribs.dashstyle = dashStyle;
4377 // plot band
4378 else if (defined(from) && defined(to)) {
4379 // keep within plot area
4380 from = mathMax(from, min);
4381 to = mathMin(to, max);
4383 toPath = getPlotLinePath(to);
4384 path = getPlotLinePath(from);
4385 if (path && toPath) {
4386 path.push(
4387 toPath[4],
4388 toPath[5],
4389 toPath[1],
4390 toPath[2]
4392 } else { // outside the axis area
4393 path = null;
4395 attribs = {
4396 fill: color
4398 } else {
4399 return;
4401 // zIndex
4402 if (defined(zIndex)) {
4403 attribs.zIndex = zIndex;
4406 // common for lines and bands
4407 if (svgElem) {
4408 if (path) {
4409 svgElem.animate({
4410 d: path
4411 }, null, svgElem.onGetPath);
4412 } else {
4413 svgElem.hide();
4414 svgElem.onGetPath = function() {
4415 svgElem.show();
4418 } else if (path && path.length) {
4419 plotLine.svgElem = svgElem = renderer.path(path)
4420 .attr(attribs).add();
4422 // events
4423 if (events) {
4424 addEvent = function(eventType) {
4425 svgElem.on(eventType, function(e) {
4426 events[eventType].apply(plotLine, [e]);
4429 for (eventType in events) {
4430 addEvent(eventType);
4435 // the plot band/line label
4436 if (optionsLabel && defined(optionsLabel.text) && path && path.length && plotWidth > 0 && plotHeight > 0) {
4437 // apply defaults
4438 optionsLabel = merge({
4439 align: horiz && toPath && 'center',
4440 x: horiz ? !toPath && 4 : 10,
4441 verticalAlign : !horiz && toPath && 'middle',
4442 y: horiz ? toPath ? 16 : 10 : toPath ? 6 : -4,
4443 rotation: horiz && !toPath && 90
4444 }, optionsLabel);
4446 // add the SVG element
4447 if (!label) {
4448 plotLine.label = label = renderer.text(
4449 optionsLabel.text,
4453 .attr({
4454 align: optionsLabel.textAlign || optionsLabel.align,
4455 rotation: optionsLabel.rotation,
4456 zIndex: zIndex
4458 .css(optionsLabel.style)
4459 .add();
4462 // get the bounding box and align the label
4463 xs = [path[1], path[4], pick(path[6], path[1])];
4464 ys = [path[2], path[5], pick(path[7], path[2])];
4465 x = mathMin.apply(math, xs);
4466 y = mathMin.apply(math, ys);
4468 label.align(optionsLabel, false, {
4469 x: x,
4470 y: y,
4471 width: mathMax.apply(math, xs) - x,
4472 height: mathMax.apply(math, ys) - y
4474 label.show();
4476 } else if (label) { // move out of sight
4477 label.hide();
4480 // chainable
4481 return plotLine;
4485 * Remove the plot line or band
4487 destroy: function() {
4488 var obj = this,
4491 for (n in obj) {
4492 if (obj[n] && obj[n].destroy) {
4493 obj[n].destroy(); // destroy SVG wrappers
4495 delete obj[n];
4497 // remove it from the lookup
4498 erase(plotLinesAndBands, obj);
4503 * The class for stack items
4505 function StackItem(options, isNegative, x) {
4506 var stackItem = this;
4508 // Tells if the stack is negative
4509 stackItem.isNegative = isNegative;
4511 // Save the options to be able to style the label
4512 stackItem.options = options;
4514 // Save the x value to be able to position the label later
4515 stackItem.x = x;
4517 // The align options and text align varies on whether the stack is negative and
4518 // if the chart is inverted or not.
4519 // First test the user supplied value, then use the dynamic.
4520 stackItem.alignOptions = {
4521 align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'),
4522 verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
4523 y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)),
4524 x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0)
4527 stackItem.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center');
4530 StackItem.prototype = {
4532 * Sets the total of this stack. Should be called when a serie is hidden or shown
4533 * since that will affect the total of other stacks.
4535 setTotal: function(total) {
4536 this.total = total;
4537 this.cum = total;
4541 * Renders the stack total label and adds it to the stack label group.
4543 render: function(group) {
4544 var stackItem = this, // aliased this
4545 str = stackItem.options.formatter.call(stackItem); // format the text in the label
4547 // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden
4548 if (stackItem.label) {
4549 stackItem.label.attr({text: str, visibility: HIDDEN});
4550 // Create new label
4551 } else {
4552 stackItem.label =
4553 chart.renderer.text(str, 0, 0) // dummy positions, actual position updated with setOffset method in columnseries
4554 .css(stackItem.options.style) // apply style
4555 .attr({align: stackItem.textAlign, // fix the text-anchor
4556 rotation: stackItem.options.rotation, // rotation
4557 visibility: HIDDEN }) // hidden until setOffset is called
4558 .add(group); // add to the labels-group
4563 * Sets the offset that the stack has from the x value and repositions the label.
4565 setOffset: function(xOffset, xWidth) {
4566 var stackItem = this, // aliased this
4567 neg = stackItem.isNegative, // special treatment is needed for negative stacks
4568 y = axis.translate(stackItem.total), // stack value translated mapped to chart coordinates
4569 yZero = axis.translate(0), // stack origin
4570 h = mathAbs(y - yZero), // stack height
4571 x = chart.xAxis[0].translate(stackItem.x) + xOffset, // stack x position
4572 plotHeight = chart.plotHeight,
4573 stackBox = { // this is the box for the complete stack
4574 x: inverted ? (neg ? y : y - h) : x,
4575 y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y),
4576 width: inverted ? h : xWidth,
4577 height: inverted ? xWidth : h
4580 if (stackItem.label) {
4581 stackItem.label
4582 .align(stackItem.alignOptions, null, stackBox) // align the label to the box
4583 .attr({visibility: VISIBLE}); // set visibility
4589 * Get the minimum and maximum for the series of each axis
4591 function getSeriesExtremes() {
4592 var posStack = [],
4593 negStack = [],
4594 run;
4596 // reset dataMin and dataMax in case we're redrawing
4597 dataMin = dataMax = null;
4599 // get an overview of what series are associated with this axis
4600 associatedSeries = [];
4602 each(series, function(serie) {
4603 run = false;
4606 // match this axis against the series' given or implicated axis
4607 each(['xAxis', 'yAxis'], function(strAxis) {
4608 if (
4609 // the series is a cartesian type, and...
4610 serie.isCartesian &&
4611 // we're in the right x or y dimension, and...
4612 ((strAxis === 'xAxis' && isXAxis) || (strAxis === 'yAxis' && !isXAxis)) && (
4613 // the axis number is given in the options and matches this axis index, or
4614 (serie.options[strAxis] === options.index) ||
4615 // the axis index is not given
4616 (serie.options[strAxis] === UNDEFINED && options.index === 0)
4619 serie[strAxis] = axis;
4620 associatedSeries.push(serie);
4622 // the series is visible, run the min/max detection
4623 run = true;
4626 // ignore hidden series if opted
4627 if (!serie.visible && optionsChart.ignoreHiddenSeries) {
4628 run = false;
4631 if (run) {
4633 var stacking,
4634 posPointStack,
4635 negPointStack,
4636 stackKey,
4637 negKey;
4639 if (!isXAxis) {
4640 stacking = serie.options.stacking;
4641 usePercentage = stacking === 'percent';
4643 // create a stack for this particular series type
4644 if (stacking) {
4645 stackKey = serie.type + pick(serie.options.stack, '');
4646 negKey = '-'+ stackKey;
4647 serie.stackKey = stackKey; // used in translate
4649 posPointStack = posStack[stackKey] || []; // contains the total values for each x
4650 posStack[stackKey] = posPointStack;
4652 negPointStack = negStack[negKey] || [];
4653 negStack[negKey] = negPointStack;
4655 if (usePercentage) {
4656 dataMin = 0;
4657 dataMax = 99;
4660 if (serie.isCartesian) { // line, column etc. need axes, pie doesn't
4661 each(serie.data, function(point, i) {
4662 var pointX = point.x,
4663 pointY = point.y,
4664 isNegative = pointY < 0,
4665 pointStack = isNegative ? negPointStack : posPointStack,
4666 key = isNegative ? negKey : stackKey,
4667 totalPos,
4668 pointLow;
4670 // initial values
4671 if (dataMin === null) {
4673 // start out with the first point
4674 dataMin = dataMax = point[xOrY];
4677 // x axis
4678 if (isXAxis) {
4679 if (pointX > dataMax) {
4680 dataMax = pointX;
4681 } else if (pointX < dataMin) {
4682 dataMin = pointX;
4686 // y axis
4687 else if (defined(pointY)) {
4688 if (stacking) {
4689 pointStack[pointX] =
4690 defined(pointStack[pointX]) ?
4691 pointStack[pointX] + pointY : pointY;
4693 totalPos = pointStack ? pointStack[pointX] : pointY;
4694 pointLow = pick(point.low, totalPos);
4695 if (!usePercentage) {
4696 if (totalPos > dataMax) {
4697 dataMax = totalPos;
4698 } else if (pointLow < dataMin) {
4699 dataMin = pointLow;
4702 if (stacking) {
4703 // add the series
4704 if (!stacks[key]) {
4705 stacks[key] = {};
4708 // If the StackItem is there, just update the values,
4709 // if not, create one first
4710 if (!stacks[key][pointX]) {
4711 stacks[key][pointX] = new StackItem(options.stackLabels, isNegative, pointX);
4713 stacks[key][pointX].setTotal(totalPos);
4719 // For column, areas and bars, set the minimum automatically to zero
4720 // and prevent that minPadding is added in setScale
4721 if (/(area|column|bar)/.test(serie.type) && !isXAxis) {
4722 var threshold = 0; // use series.options.threshold?
4723 if (dataMin >= threshold) {
4724 dataMin = threshold;
4725 ignoreMinPadding = true;
4726 } else if (dataMax < threshold) {
4727 dataMax = threshold;
4728 ignoreMaxPadding = true;
4738 * Translate from axis value to pixel position on the chart, or back
4741 translate = function(val, backwards, cvsCoord, old, handleLog) {
4742 var sign = 1,
4743 cvsOffset = 0,
4744 localA = old ? oldTransA : transA,
4745 localMin = old ? oldMin : min,
4746 returnValue;
4748 if (!localA) {
4749 localA = transA;
4752 if (cvsCoord) {
4753 sign *= -1; // canvas coordinates inverts the value
4754 cvsOffset = axisLength;
4756 if (reversed) { // reversed axis
4757 sign *= -1;
4758 cvsOffset -= sign * axisLength;
4761 if (backwards) { // reverse translation
4762 if (reversed) {
4763 val = axisLength - val;
4765 returnValue = val / localA + localMin; // from chart pixel to value
4766 if (isLog && handleLog) {
4767 returnValue = lin2log(returnValue);
4770 } else { // normal translation
4771 if (isLog && handleLog) {
4772 val = log2lin(val);
4774 returnValue = sign * (val - localMin) * localA + cvsOffset; // from value to chart pixel
4777 return returnValue;
4781 * Create the path for a plot line that goes from the given value on
4782 * this axis, across the plot to the opposite side
4783 * @param {Number} value
4784 * @param {Number} lineWidth Used for calculation crisp line
4785 * @param {Number] old Use old coordinates (for resizing and rescaling)
4787 getPlotLinePath = function(value, lineWidth, old) {
4788 var x1,
4789 y1,
4790 x2,
4792 translatedValue = translate(value, null, null, old),
4793 cHeight = (old && oldChartHeight) || chartHeight,
4794 cWidth = (old && oldChartWidth) || chartWidth,
4795 skip;
4797 x1 = x2 = mathRound(translatedValue + transB);
4798 y1 = y2 = mathRound(cHeight - translatedValue - transB);
4800 if (isNaN(translatedValue)) { // no min or max
4801 skip = true;
4803 } else if (horiz) {
4804 y1 = plotTop;
4805 y2 = cHeight - marginBottom;
4806 if (x1 < plotLeft || x1 > plotLeft + plotWidth) {
4807 skip = true;
4809 } else {
4810 x1 = plotLeft;
4811 x2 = cWidth - marginRight;
4812 if (y1 < plotTop || y1 > plotTop + plotHeight) {
4813 skip = true;
4816 return skip ?
4817 null :
4818 renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 0);
4823 * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5
4824 * @param {Number} interval
4826 function normalizeTickInterval(interval, multiples) {
4827 var normalized, i;
4829 // round to a tenfold of 1, 2, 2.5 or 5
4830 magnitude = multiples ? 1 : math.pow(10, mathFloor(math.log(interval) / math.LN10));
4831 normalized = interval / magnitude;
4833 // multiples for a linear scale
4834 if (!multiples) {
4835 multiples = [1, 2, 2.5, 5, 10];
4836 //multiples = [1, 2, 2.5, 4, 5, 7.5, 10];
4838 // the allowDecimals option
4839 if (options.allowDecimals === false || isLog) {
4840 if (magnitude === 1) {
4841 multiples = [1, 2, 5, 10];
4842 } else if (magnitude <= 0.1) {
4843 multiples = [1 / magnitude];
4848 // normalize the interval to the nearest multiple
4849 for (i = 0; i < multiples.length; i++) {
4850 interval = multiples[i];
4851 if (normalized <= (multiples[i] + (multiples[i+1] || multiples[i])) / 2) {
4852 break;
4856 // multiply back to the correct magnitude
4857 interval *= magnitude;
4859 return interval;
4863 * Set the tick positions to a time unit that makes sense, for example
4864 * on the first of each month or on every Monday.
4866 function setDateTimeTickPositions() {
4867 tickPositions = [];
4868 var i,
4869 useUTC = defaultOptions.global.useUTC,
4870 oneSecond = 1000 / timeFactor,
4871 oneMinute = 60000 / timeFactor,
4872 oneHour = 3600000 / timeFactor,
4873 oneDay = 24 * 3600000 / timeFactor,
4874 oneWeek = 7 * 24 * 3600000 / timeFactor,
4875 oneMonth = 30 * 24 * 3600000 / timeFactor,
4876 oneYear = 31556952000 / timeFactor,
4878 units = [[
4879 'second', // unit name
4880 oneSecond, // fixed incremental unit
4881 [1, 2, 5, 10, 15, 30] // allowed multiples
4882 ], [
4883 'minute', // unit name
4884 oneMinute, // fixed incremental unit
4885 [1, 2, 5, 10, 15, 30] // allowed multiples
4886 ], [
4887 'hour', // unit name
4888 oneHour, // fixed incremental unit
4889 [1, 2, 3, 4, 6, 8, 12] // allowed multiples
4890 ], [
4891 'day', // unit name
4892 oneDay, // fixed incremental unit
4893 [1, 2] // allowed multiples
4894 ], [
4895 'week', // unit name
4896 oneWeek, // fixed incremental unit
4897 [1, 2] // allowed multiples
4898 ], [
4899 'month',
4900 oneMonth,
4901 [1, 2, 3, 4, 6]
4902 ], [
4903 'year',
4904 oneYear,
4905 null
4908 unit = units[6], // default unit is years
4909 interval = unit[1],
4910 multiples = unit[2];
4912 // loop through the units to find the one that best fits the tickInterval
4913 for (i = 0; i < units.length; i++) {
4914 unit = units[i];
4915 interval = unit[1];
4916 multiples = unit[2];
4919 if (units[i+1]) {
4920 // lessThan is in the middle between the highest multiple and the next unit.
4921 var lessThan = (interval * multiples[multiples.length - 1] +
4922 units[i + 1][1]) / 2;
4924 // break and keep the current unit
4925 if (tickInterval <= lessThan) {
4926 break;
4931 // prevent 2.5 years intervals, though 25, 250 etc. are allowed
4932 if (interval === oneYear && tickInterval < 5 * interval) {
4933 multiples = [1, 2, 5];
4936 // get the minimum value by flooring the date
4937 var multitude = normalizeTickInterval(tickInterval / interval, multiples),
4938 minYear, // used in months and years as a basis for Date.UTC()
4939 minDate = new Date(min * timeFactor);
4941 minDate.setMilliseconds(0);
4943 if (interval >= oneSecond) { // second
4944 minDate.setSeconds(interval >= oneMinute ? 0 :
4945 multitude * mathFloor(minDate.getSeconds() / multitude));
4948 if (interval >= oneMinute) { // minute
4949 minDate[setMinutes](interval >= oneHour ? 0 :
4950 multitude * mathFloor(minDate[getMinutes]() / multitude));
4953 if (interval >= oneHour) { // hour
4954 minDate[setHours](interval >= oneDay ? 0 :
4955 multitude * mathFloor(minDate[getHours]() / multitude));
4958 if (interval >= oneDay) { // day
4959 minDate[setDate](interval >= oneMonth ? 1 :
4960 multitude * mathFloor(minDate[getDate]() / multitude));
4963 if (interval >= oneMonth) { // month
4964 minDate[setMonth](interval >= oneYear ? 0 :
4965 multitude * mathFloor(minDate[getMonth]() / multitude));
4966 minYear = minDate[getFullYear]();
4969 if (interval >= oneYear) { // year
4970 minYear -= minYear % multitude;
4971 minDate[setFullYear](minYear);
4974 // week is a special case that runs outside the hierarchy
4975 if (interval === oneWeek) {
4976 // get start of current week, independent of multitude
4977 minDate[setDate](minDate[getDate]() - minDate[getDay]() +
4978 options.startOfWeek);
4982 // get tick positions
4983 i = 1; // prevent crash just in case
4984 minYear = minDate[getFullYear]();
4985 var time = minDate.getTime() / timeFactor,
4986 minMonth = minDate[getMonth](),
4987 minDateDate = minDate[getDate]();
4989 // iterate and add tick positions at appropriate values
4990 while (time < max && i < plotWidth) {
4991 tickPositions.push(time);
4993 // if the interval is years, use Date.UTC to increase years
4994 if (interval === oneYear) {
4995 time = makeTime(minYear + i * multitude, 0) / timeFactor;
4997 // if the interval is months, use Date.UTC to increase months
4998 } else if (interval === oneMonth) {
4999 time = makeTime(minYear, minMonth + i * multitude) / timeFactor;
5001 // if we're using global time, the interval is not fixed as it jumps
5002 // one hour at the DST crossover
5003 } else if (!useUTC && (interval === oneDay || interval === oneWeek)) {
5004 time = makeTime(minYear, minMonth, minDateDate +
5005 i * multitude * (interval === oneDay ? 1 : 7));
5007 // else, the interval is fixed and we use simple addition
5008 } else {
5009 time += interval * multitude;
5012 i++;
5014 // push the last time
5015 tickPositions.push(time);
5018 // dynamic label formatter
5019 dateTimeLabelFormat = options.dateTimeLabelFormats[unit[0]];
5023 * Fix JS round off float errors
5024 * @param {Number} num
5026 function correctFloat(num) {
5027 var invMag, ret = num;
5028 magnitude = pick(magnitude, math.pow(10, mathFloor(math.log(tickInterval) / math.LN10)));
5030 if (magnitude < 1) {
5031 invMag = mathRound(1 / magnitude) * 10;
5032 ret = mathRound(num * invMag) / invMag;
5034 return ret;
5038 * Set the tick positions of a linear axis to round values like whole tens or every five.
5040 function setLinearTickPositions() {
5042 var i,
5043 roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval),
5044 roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval);
5046 tickPositions = [];
5048 // populate the intermediate values
5049 i = correctFloat(roundedMin);
5050 while (i <= roundedMax) {
5051 tickPositions.push(i);
5052 i = correctFloat(i + tickInterval);
5058 * Set the tick positions to round values and optionally extend the extremes
5059 * to the nearest tick
5061 function setTickPositions(secondPass) {
5062 var length,
5063 catPad,
5064 linkedParent,
5065 linkedParentExtremes,
5066 tickIntervalOption = options.tickInterval,
5067 tickPixelIntervalOption = options.tickPixelInterval,
5068 maxZoom = options.maxZoom || (
5069 isXAxis && !defined(options.min) && !defined(options.max) ?
5070 mathMin(chart.smallestInterval * 5, dataMax - dataMin) :
5071 null
5073 zoomOffset;
5076 axisLength = horiz ? plotWidth : plotHeight;
5078 // linked axis gets the extremes from the parent axis
5079 if (isLinked) {
5080 linkedParent = chart[isXAxis ? 'xAxis' : 'yAxis'][options.linkedTo];
5081 linkedParentExtremes = linkedParent.getExtremes();
5082 min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);
5083 max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax);
5086 // initial min and max from the extreme data values
5087 else {
5088 min = pick(userMin, options.min, dataMin);
5089 max = pick(userMax, options.max, dataMax);
5092 if (isLog) {
5093 min = log2lin(min);
5094 max = log2lin(max);
5097 // maxZoom exceeded, just center the selection
5098 if (max - min < maxZoom) {
5099 zoomOffset = (maxZoom - max + min) / 2;
5100 // if min and max options have been set, don't go beyond it
5101 min = mathMax(min - zoomOffset, pick(options.min, min - zoomOffset), dataMin);
5102 max = mathMin(min + maxZoom, pick(options.max, min + maxZoom), dataMax);
5105 // pad the values to get clear of the chart's edges
5106 if (!categories && !usePercentage && !isLinked && defined(min) && defined(max)) {
5107 length = (max - min) || 1;
5108 if (!defined(options.min) && !defined(userMin) && minPadding && (dataMin < 0 || !ignoreMinPadding)) {
5109 min -= length * minPadding;
5111 if (!defined(options.max) && !defined(userMax) && maxPadding && (dataMax > 0 || !ignoreMaxPadding)) {
5112 max += length * maxPadding;
5116 // get tickInterval
5117 if (min === max) {
5118 tickInterval = 1;
5119 } else if (isLinked && !tickIntervalOption &&
5120 tickPixelIntervalOption === linkedParent.options.tickPixelInterval) {
5121 tickInterval = linkedParent.tickInterval;
5122 } else {
5123 tickInterval = pick(
5124 tickIntervalOption,
5125 categories ? // for categoried axis, 1 is default, for linear axis use tickPix
5126 1 :
5127 (max - min) * tickPixelIntervalOption / axisLength
5131 if (!isDatetimeAxis && !defined(options.tickInterval)) { // linear
5132 tickInterval = normalizeTickInterval(tickInterval);
5134 axis.tickInterval = tickInterval; // record for linked axis
5136 // get minorTickInterval
5137 minorTickInterval = options.minorTickInterval === 'auto' && tickInterval ?
5138 tickInterval / 5 : options.minorTickInterval;
5140 // find the tick positions
5141 if (isDatetimeAxis) {
5142 setDateTimeTickPositions();
5143 } else {
5144 setLinearTickPositions();
5147 if (!isLinked) {
5148 // pad categorised axis to nearest half unit
5149 if (categories || (isXAxis && chart.hasColumn)) {
5150 catPad = (categories ? 1 : tickInterval) * 0.5;
5151 if (categories || !defined(pick(options.min, userMin))) {
5152 min -= catPad;
5154 if (categories || !defined(pick(options.max, userMax))) {
5155 max += catPad;
5159 // reset min/max or remove extremes based on start/end on tick
5160 var roundedMin = tickPositions[0],
5161 roundedMax = tickPositions[tickPositions.length - 1];
5163 if (options.startOnTick) {
5164 min = roundedMin;
5165 } else if (min > roundedMin) {
5166 tickPositions.shift();
5169 if (options.endOnTick) {
5170 max = roundedMax;
5171 } else if (max < roundedMax) {
5172 tickPositions.pop();
5175 // record the greatest number of ticks for multi axis
5176 if (!maxTicks) { // first call, or maxTicks have been reset after a zoom operation
5177 maxTicks = {
5178 x: 0,
5179 y: 0
5183 if (!isDatetimeAxis && tickPositions.length > maxTicks[xOrY]) {
5184 maxTicks[xOrY] = tickPositions.length;
5192 * When using multiple axes, adjust the number of ticks to match the highest
5193 * number of ticks in that group
5195 function adjustTickAmount() {
5197 if (maxTicks && !isDatetimeAxis && !categories && !isLinked) { // only apply to linear scale
5198 var oldTickAmount = tickAmount,
5199 calculatedTickAmount = tickPositions.length;
5201 // set the axis-level tickAmount to use below
5202 tickAmount = maxTicks[xOrY];
5204 if (calculatedTickAmount < tickAmount) {
5205 while (tickPositions.length < tickAmount) {
5206 tickPositions.push( correctFloat(
5207 tickPositions[tickPositions.length - 1] + tickInterval
5210 transA *= (calculatedTickAmount - 1) / (tickAmount - 1);
5211 max = tickPositions[tickPositions.length - 1];
5214 if (defined(oldTickAmount) && tickAmount !== oldTickAmount) {
5215 axis.isDirty = true;
5222 * Set the scale based on data min and max, user set min and max or options
5225 function setScale() {
5226 var type,
5229 oldMin = min;
5230 oldMax = max;
5232 // get data extremes if needed
5233 getSeriesExtremes();
5235 // get fixed positions based on tickInterval
5236 setTickPositions();
5238 // the translation factor used in translate function
5239 oldTransA = transA;
5240 transA = axisLength / ((max - min) || 1);
5242 // reset stacks
5243 if (!isXAxis) {
5244 for (type in stacks) {
5245 for (i in stacks[type]) {
5246 stacks[type][i].cum = stacks[type][i].total;
5251 // mark as dirty if it is not already set to dirty and extremes have changed
5252 if (!axis.isDirty) {
5253 axis.isDirty = (min !== oldMin || max !== oldMax);
5259 * Set the extremes and optionally redraw
5260 * @param {Number} newMin
5261 * @param {Number} newMax
5262 * @param {Boolean} redraw
5263 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
5264 * configuration
5267 function setExtremes(newMin, newMax, redraw, animation) {
5269 redraw = pick(redraw, true); // defaults to true
5271 fireEvent(axis, 'setExtremes', { // fire an event to enable syncing of multiple charts
5272 min: newMin,
5273 max: newMax
5274 }, function() { // the default event handler
5276 userMin = newMin;
5277 userMax = newMax;
5280 // redraw
5281 if (redraw) {
5282 chart.redraw(animation);
5289 * Get the actual axis extremes
5291 function getExtremes() {
5292 return {
5293 min: min,
5294 max: max,
5295 dataMin: dataMin,
5296 dataMax: dataMax,
5297 userMin: userMin,
5298 userMax: userMax
5303 * Get the zero plane either based on zero or on the min or max value.
5304 * Used in bar and area plots
5306 function getThreshold(threshold) {
5307 if (min > threshold) {
5308 threshold = min;
5309 } else if (max < threshold) {
5310 threshold = max;
5313 return translate(threshold, 0, 1);
5317 * Add a plot band or plot line after render time
5319 * @param options {Object} The plotBand or plotLine configuration object
5321 function addPlotBandOrLine(options) {
5322 var obj = new PlotLineOrBand(options).render();
5323 plotLinesAndBands.push(obj);
5324 return obj;
5328 * Render the tick labels to a preliminary position to get their sizes
5330 function getOffset() {
5332 var hasData = associatedSeries.length && defined(min) && defined(max),
5333 titleOffset = 0,
5334 titleMargin = 0,
5335 axisTitleOptions = options.title,
5336 labelOptions = options.labels,
5337 directionFactor = [-1, 1, 1, -1][side],
5340 if (!axisGroup) {
5341 axisGroup = renderer.g('axis')
5342 .attr({ zIndex: 7 })
5343 .add();
5344 gridGroup = renderer.g('grid')
5345 .attr({ zIndex: 1 })
5346 .add();
5349 labelOffset = 0; // reset
5351 if (hasData || isLinked) {
5352 each(tickPositions, function(pos) {
5353 if (!ticks[pos]) {
5354 ticks[pos] = new Tick(pos);
5355 } else {
5356 ticks[pos].addLabel(); // update labels depending on tick interval
5359 // left side must be align: right and right side must have align: left for labels
5360 if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === labelOptions.align) {
5362 // get the highest offset
5363 labelOffset = mathMax(
5364 ticks[pos].getLabelSize(),
5365 labelOffset
5371 if (staggerLines) {
5372 labelOffset += (staggerLines - 1) * 16;
5375 } else { // doesn't have data
5376 for (n in ticks) {
5377 ticks[n].destroy();
5378 delete ticks[n];
5382 if (axisTitleOptions && axisTitleOptions.text) {
5383 if (!axis.axisTitle) {
5384 axis.axisTitle = renderer.text(
5385 axisTitleOptions.text,
5389 .attr({
5390 zIndex: 7,
5391 rotation: axisTitleOptions.rotation || 0,
5392 align:
5393 axisTitleOptions.textAlign ||
5394 { low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align]
5396 .css(axisTitleOptions.style)
5397 .add();
5400 titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width'];
5401 titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10);
5405 // handle automatic or user set offset
5406 offset = directionFactor * (options.offset || axisOffset[side]);
5408 axisTitleMargin =
5409 labelOffset +
5410 (side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x']) +
5411 titleMargin;
5413 axisOffset[side] = mathMax(
5414 axisOffset[side],
5415 axisTitleMargin + titleOffset + directionFactor * offset
5421 * Render the axis
5423 function render() {
5424 var axisTitleOptions = options.title,
5425 stackLabelOptions = options.stackLabels,
5426 alternateGridColor = options.alternateGridColor,
5427 lineWidth = options.lineWidth,
5428 lineLeft,
5429 lineTop,
5430 linePath,
5431 hasRendered = chart.hasRendered,
5432 slideInTicks = hasRendered && defined(oldMin) && !isNaN(oldMin),
5433 hasData = associatedSeries.length && defined(min) && defined(max);
5435 // update metrics
5436 axisLength = horiz ? plotWidth : plotHeight;
5437 transA = axisLength / ((max - min) || 1);
5438 transB = horiz ? plotLeft : marginBottom; // translation addend
5440 // If the series has data draw the ticks. Else only the line and title
5441 if (hasData || isLinked) {
5443 // minor ticks
5444 if (minorTickInterval && !categories) {
5445 var pos = min + (tickPositions[0] - min) % minorTickInterval;
5446 for (pos; pos <= max; pos += minorTickInterval) {
5447 if (!minorTicks[pos]) {
5448 minorTicks[pos] = new Tick(pos, true);
5451 // render new ticks in old position
5452 if (slideInTicks && minorTicks[pos].isNew) {
5453 minorTicks[pos].render(null, true);
5457 minorTicks[pos].isActive = true;
5458 minorTicks[pos].render();
5462 // major ticks
5463 each(tickPositions, function(pos, i) {
5464 // linked axes need an extra check to find out if
5465 if (!isLinked || (pos >= min && pos <= max)) {
5467 // render new ticks in old position
5468 if (slideInTicks && ticks[pos].isNew) {
5469 ticks[pos].render(i, true);
5472 ticks[pos].isActive = true;
5473 ticks[pos].render(i);
5477 // alternate grid color
5478 if (alternateGridColor) {
5479 each(tickPositions, function(pos, i) {
5480 if (i % 2 === 0 && pos < max) {
5481 /*plotLinesAndBands.push(new PlotLineOrBand({
5482 from: pos,
5483 to: tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : max,
5484 color: alternateGridColor
5485 }));*/
5487 if (!alternateBands[pos]) {
5488 alternateBands[pos] = new PlotLineOrBand();
5490 alternateBands[pos].options = {
5491 from: pos,
5492 to: tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : max,
5493 color: alternateGridColor
5495 alternateBands[pos].render();
5496 alternateBands[pos].isActive = true;
5501 // custom plot bands (behind grid lines)
5502 /*if (!hasRendered) { // only first time
5503 each(options.plotBands || [], function(plotBandOptions) {
5504 plotLinesAndBands.push(new PlotLineOrBand(
5505 extend({ zIndex: 1 }, plotBandOptions)
5506 ).render());
5513 // custom plot lines and bands
5514 if (!hasRendered) { // only first time
5515 each((options.plotLines || []).concat(options.plotBands || []), function(plotLineOptions) {
5516 plotLinesAndBands.push(new PlotLineOrBand(plotLineOptions).render());
5522 } // end if hasData
5524 // remove inactive ticks
5525 each([ticks, minorTicks, alternateBands], function(coll) {
5526 var pos;
5527 for (pos in coll) {
5528 if (!coll[pos].isActive) {
5529 coll[pos].destroy();
5530 delete coll[pos];
5531 } else {
5532 coll[pos].isActive = false; // reset
5540 // Static items. As the axis group is cleared on subsequent calls
5541 // to render, these items are added outside the group.
5542 // axis line
5543 if (lineWidth) {
5544 lineLeft = plotLeft + (opposite ? plotWidth : 0) + offset;
5545 lineTop = chartHeight - marginBottom - (opposite ? plotHeight : 0) + offset;
5547 linePath = renderer.crispLine([
5549 horiz ?
5550 plotLeft:
5551 lineLeft,
5552 horiz ?
5553 lineTop:
5554 plotTop,
5556 horiz ?
5557 chartWidth - marginRight :
5558 lineLeft,
5559 horiz ?
5560 lineTop:
5561 chartHeight - marginBottom
5562 ], lineWidth);
5563 if (!axisLine) {
5564 axisLine = renderer.path(linePath)
5565 .attr({
5566 stroke: options.lineColor,
5567 'stroke-width': lineWidth,
5568 zIndex: 7
5570 .add();
5571 } else {
5572 axisLine.animate({ d: linePath });
5577 if (axis.axisTitle) {
5578 // compute anchor points for each of the title align options
5579 var margin = horiz ? plotLeft : plotTop,
5580 fontSize = pInt(axisTitleOptions.style.fontSize || 12),
5581 // the position in the length direction of the axis
5582 alongAxis = {
5583 low: margin + (horiz ? 0 : axisLength),
5584 middle: margin + axisLength / 2,
5585 high: margin + (horiz ? axisLength : 0)
5586 }[axisTitleOptions.align],
5588 // the position in the perpendicular direction of the axis
5589 offAxis = (horiz ? plotTop + plotHeight : plotLeft) +
5590 (horiz ? 1 : -1) * // horizontal axis reverses the margin
5591 (opposite ? -1 : 1) * // so does opposite axes
5592 axisTitleMargin +
5593 //(isIE ? fontSize / 3 : 0)+ // preliminary fix for vml's centerline
5594 (side === 2 ? fontSize : 0);
5596 axis.axisTitle[hasRendered ? 'animate' : 'attr']({
5597 x: horiz ?
5598 alongAxis:
5599 offAxis + (opposite ? plotWidth : 0) + offset +
5600 (axisTitleOptions.x || 0), // x
5601 y: horiz ?
5602 offAxis - (opposite ? plotHeight : 0) + offset:
5603 alongAxis + (axisTitleOptions.y || 0) // y
5608 // Stacked totals:
5609 if (stackLabelOptions && stackLabelOptions.enabled) {
5610 var stackKey, oneStack, stackCategory,
5611 stackTotalGroup = axis.stackTotalGroup;
5613 // Create a separate group for the stack total labels
5614 if (!stackTotalGroup) {
5615 axis.stackTotalGroup = stackTotalGroup =
5616 renderer.g('stack-labels')
5617 .attr({
5618 visibility: VISIBLE,
5619 zIndex: 6
5621 .translate(plotLeft, plotTop)
5622 .add();
5625 // Render each stack total
5626 for (stackKey in stacks) {
5627 oneStack = stacks[stackKey];
5628 for (stackCategory in oneStack) {
5629 oneStack[stackCategory].render(stackTotalGroup);
5633 // End stacked totals
5635 axis.isDirty = false;
5639 * Remove a plot band or plot line from the chart by id
5640 * @param {Object} id
5642 function removePlotBandOrLine(id) {
5643 var i = plotLinesAndBands.length;
5644 while (i--) {
5645 if (plotLinesAndBands[i].id === id) {
5646 plotLinesAndBands[i].destroy();
5652 * Redraw the axis to reflect changes in the data or axis extremes
5654 function redraw() {
5656 // hide tooltip and hover states
5657 if (tracker.resetTracker) {
5658 tracker.resetTracker();
5661 // render the axis
5662 render();
5664 // move plot lines and bands
5665 each(plotLinesAndBands, function(plotLine) {
5666 plotLine.render();
5669 // mark associated series as dirty and ready for redraw
5670 each(associatedSeries, function(series) {
5671 series.isDirty = true;
5677 * Set new axis categories and optionally redraw
5678 * @param {Array} newCategories
5679 * @param {Boolean} doRedraw
5681 function setCategories(newCategories, doRedraw) {
5682 // set the categories
5683 axis.categories = categories = newCategories;
5685 // force reindexing tooltips
5686 each(associatedSeries, function(series) {
5687 series.translate();
5688 series.setTooltipPoints(true);
5692 // optionally redraw
5693 axis.isDirty = true;
5695 if (pick(doRedraw, true)) {
5696 chart.redraw();
5702 // Run Axis
5704 // inverted charts have reversed xAxes as default
5705 if (inverted && isXAxis && reversed === UNDEFINED) {
5706 reversed = true;
5710 // expose some variables
5711 extend(axis, {
5712 addPlotBand: addPlotBandOrLine,
5713 addPlotLine: addPlotBandOrLine,
5714 adjustTickAmount: adjustTickAmount,
5715 categories: categories,
5716 getExtremes: getExtremes,
5717 getPlotLinePath: getPlotLinePath,
5718 getThreshold: getThreshold,
5719 isXAxis: isXAxis,
5720 options: options,
5721 plotLinesAndBands: plotLinesAndBands,
5722 getOffset: getOffset,
5723 render: render,
5724 setCategories: setCategories,
5725 setExtremes: setExtremes,
5726 setScale: setScale,
5727 setTickPositions: setTickPositions,
5728 translate: translate,
5729 redraw: redraw,
5730 removePlotBand: removePlotBandOrLine,
5731 removePlotLine: removePlotBandOrLine,
5732 reversed: reversed,
5733 stacks: stacks
5736 // register event listeners
5737 for (eventType in events) {
5738 addEvent(axis, eventType, events[eventType]);
5741 // set min and max
5742 setScale();
5744 } // end Axis
5748 * The toolbar object
5750 * @param {Object} chart
5752 function Toolbar(chart) {
5753 var buttons = {};
5755 function add(id, text, title, fn) {
5756 if (!buttons[id]) {
5757 var button = renderer.text(
5758 text,
5762 .css(options.toolbar.itemStyle)
5763 .align({
5764 align: 'right',
5765 x: - marginRight - 20,
5766 y: plotTop + 30
5768 .on('click', fn)
5769 /*.on('touchstart', function(e) {
5770 e.stopPropagation(); // don't fire the container event
5771 fn();
5772 })*/
5773 .attr({
5774 align: 'right',
5775 zIndex: 20
5777 .add();
5778 buttons[id] = button;
5781 function remove(id) {
5782 discardElement(buttons[id].element);
5783 buttons[id] = null;
5786 // public
5787 return {
5788 add: add,
5789 remove: remove
5794 * The tooltip object
5795 * @param {Object} options Tooltip options
5797 function Tooltip (options) {
5798 var currentSeries,
5799 borderWidth = options.borderWidth,
5800 crosshairsOptions = options.crosshairs,
5801 crosshairs = [],
5802 style = options.style,
5803 shared = options.shared,
5804 padding = pInt(style.padding),
5805 boxOffLeft = borderWidth + padding, // off left/top position as IE can't
5806 //properly handle negative positioned shapes
5807 tooltipIsHidden = true,
5808 boxWidth,
5809 boxHeight,
5810 currentX = 0,
5811 currentY = 0;
5813 // remove padding CSS and apply padding on box instead
5814 style.padding = 0;
5816 // create the elements
5817 var group = renderer.g('tooltip')
5818 .attr({ zIndex: 8 })
5819 .add(),
5821 box = renderer.rect(boxOffLeft, boxOffLeft, 0, 0, options.borderRadius, borderWidth)
5822 .attr({
5823 fill: options.backgroundColor,
5824 'stroke-width': borderWidth
5826 .add(group)
5827 .shadow(options.shadow),
5828 label = renderer.text('', padding + boxOffLeft, pInt(style.fontSize) + padding + boxOffLeft)
5829 .attr({ zIndex: 1 })
5830 .css(style)
5831 .add(group);
5833 group.hide();
5836 * In case no user defined formatter is given, this will be used
5838 function defaultFormatter() {
5839 var pThis = this,
5840 items = pThis.points || splat(pThis),
5841 xAxis = items[0].series.xAxis,
5842 x = pThis.x,
5843 isDateTime = xAxis && xAxis.options.type === 'datetime',
5844 useHeader = isString(x) || isDateTime,
5845 series,
5848 // build the header
5849 s = useHeader ?
5850 ['<span style="font-size: 10px">' +
5851 (isDateTime ? dateFormat('%A, %b %e, %Y', x) : x) +
5852 '</span>'] : [];
5854 // build the values
5855 each(items, function(item) {
5856 s.push(item.point.tooltipFormatter(useHeader));
5858 return s.join('<br/>');
5862 * Provide a soft movement for the tooltip
5864 * @param {Number} finalX
5865 * @param {Number} finalY
5867 function move(finalX, finalY) {
5869 currentX = tooltipIsHidden ? finalX : (2 * currentX + finalX) / 3;
5870 currentY = tooltipIsHidden ? finalY : (currentY + finalY) / 2;
5872 group.translate(currentX, currentY);
5875 // run on next tick of the mouse tracker
5876 if (mathAbs(finalX - currentX) > 1 || mathAbs(finalY - currentY) > 1) {
5877 tooltipTick = function() {
5878 move(finalX, finalY);
5880 } else {
5881 tooltipTick = null;
5886 * Hide the tooltip
5888 function hide() {
5889 if (!tooltipIsHidden) {
5890 var hoverPoints = chart.hoverPoints;
5892 group.hide();
5894 each(crosshairs, function(crosshair) {
5895 if (crosshair) {
5896 crosshair.hide();
5900 // hide previous hoverPoints and set new
5901 if (hoverPoints) {
5902 each(hoverPoints, function(point) {
5903 point.setState();
5906 chart.hoverPoints = null;
5909 tooltipIsHidden = true;
5915 * Refresh the tooltip's text and position.
5916 * @param {Object} point
5919 function refresh(point) {
5920 var x,
5922 boxX,
5923 boxY,
5924 show,
5925 bBox,
5926 plotX,
5927 plotY = 0,
5928 textConfig = {},
5929 text,
5930 pointConfig = [],
5931 tooltipPos = point.tooltipPos,
5932 formatter = options.formatter || defaultFormatter,
5933 hoverPoints = chart.hoverPoints;
5935 // shared tooltip, array is sent over
5936 if (shared) {
5938 // hide previous hoverPoints and set new
5939 if (hoverPoints) {
5940 each(hoverPoints, function(point) {
5941 point.setState();
5944 chart.hoverPoints = point;
5946 each(point, function(item, i) {
5947 /*var series = item.series,
5948 hoverPoint = series.hoverPoint;
5949 if (hoverPoint) {
5950 hoverPoint.setState();
5952 series.hoverPoint = item;*/
5953 item.setState(HOVER_STATE);
5954 plotY += item.plotY; // for average
5956 pointConfig.push(item.getLabelConfig());
5959 plotX = point[0].plotX;
5960 plotY = mathRound(plotY) / point.length; // mathRound because Opera 10 has problems here
5962 textConfig = {
5963 x: point[0].category
5965 textConfig.points = pointConfig;
5966 point = point[0];
5968 // single point tooltip
5969 } else {
5970 textConfig = point.getLabelConfig();
5972 text = formatter.call(textConfig);
5974 // register the current series
5975 currentSeries = point.series;
5977 // get the reference point coordinates (pie charts use tooltipPos)
5978 plotX = shared ? plotX : point.plotX;
5979 plotY = shared ? plotY : point.plotY;
5980 x = mathRound(tooltipPos ? tooltipPos[0] : (inverted ? plotWidth - plotY : plotX));
5981 y = mathRound(tooltipPos ? tooltipPos[1] : (inverted ? plotHeight - plotX : plotY));
5984 // hide tooltip if the point falls outside the plot
5985 show = shared || !point.series.isCartesian || isInsidePlot(x, y);
5987 // update the inner HTML
5988 if (text === false || !show) {
5989 hide();
5990 } else {
5992 // show it
5993 if (tooltipIsHidden) {
5994 group.show();
5995 tooltipIsHidden = false;
5998 // update text
5999 label.attr({
6000 text: text
6003 // get the bounding box
6004 bBox = label.getBBox();
6005 boxWidth = bBox.width + 2 * padding;
6006 boxHeight = bBox.height + 2 * padding;
6008 // set the size of the box
6009 box.attr({
6010 width: boxWidth,
6011 height: boxHeight,
6012 stroke: options.borderColor || point.color || currentSeries.color || '#606060'
6015 // keep the box within the chart area
6016 boxX = x - boxWidth + plotLeft - 25;
6017 boxY = y - boxHeight + plotTop + 10;
6019 // it is too far to the left, adjust it
6020 if (boxX < 7) {
6021 boxX = 7;
6022 boxY -= 30;
6026 if (boxY < 5) {
6027 boxY = 5; // above
6028 } else if (boxY + boxHeight > chartHeight) {
6029 boxY = chartHeight - boxHeight - 5; // below
6032 // do the move
6033 move(mathRound(boxX - boxOffLeft), mathRound(boxY - boxOffLeft));
6039 // crosshairs
6040 if (crosshairsOptions) {
6041 crosshairsOptions = splat(crosshairsOptions); // [x, y]
6043 var path,
6044 i = crosshairsOptions.length,
6045 attribs,
6046 axis;
6048 while (i--) {
6049 axis = point.series[i ? 'yAxis' : 'xAxis'];
6050 if (crosshairsOptions[i] && axis) {
6051 path = axis
6052 .getPlotLinePath(point[i ? 'y' : 'x'], 1);
6053 if (crosshairs[i]) {
6054 crosshairs[i].attr({ d: path, visibility: VISIBLE });
6056 } else {
6057 attribs = {
6058 'stroke-width': crosshairsOptions[i].width || 1,
6059 stroke: crosshairsOptions[i].color || '#C0C0C0',
6060 zIndex: 2
6062 if (crosshairsOptions[i].dashStyle) {
6063 attribs.dashstyle = crosshairsOptions[i].dashStyle;
6065 crosshairs[i] = renderer.path(path)
6066 .attr(attribs)
6067 .add();
6076 // public members
6077 return {
6078 shared: shared,
6079 refresh: refresh,
6080 hide: hide
6085 * The mouse tracker object
6086 * @param {Object} chart
6087 * @param {Object} options
6089 function MouseTracker (chart, options) {
6092 var mouseDownX,
6093 mouseDownY,
6094 hasDragged,
6095 selectionMarker,
6096 zoomType = optionsChart.zoomType,
6097 zoomX = /x/.test(zoomType),
6098 zoomY = /y/.test(zoomType),
6099 zoomHor = (zoomX && !inverted) || (zoomY && inverted),
6100 zoomVert = (zoomY && !inverted) || (zoomX && inverted);
6103 * Add crossbrowser support for chartX and chartY
6104 * @param {Object} e The event object in standard browsers
6106 function normalizeMouseEvent(e) {
6107 var ePos,
6108 pageZoomFix = isWebKit && doc.width / doc.documentElement.clientWidth - 1,
6109 chartPosLeft,
6110 chartPosTop,
6111 chartX,
6112 chartY;
6114 // common IE normalizing
6115 e = e || win.event;
6116 if (!e.target) {
6117 e.target = e.srcElement;
6120 // iOS
6121 ePos = e.touches ? e.touches.item(0) : e;
6123 // in certain cases, get mouse position
6124 if (e.type !== 'mousemove' || win.opera || pageZoomFix) { // only Opera needs position on mouse move, see below
6125 chartPosition = getPosition(container);
6126 chartPosLeft = chartPosition.left;
6127 chartPosTop = chartPosition.top;
6130 // chartX and chartY
6131 if (isIE) { // IE including IE9 that has chartX but in a different meaning
6132 chartX = e.x;
6133 chartY = e.y;
6134 } else {
6135 if (ePos.layerX === UNDEFINED) { // Opera and iOS
6136 chartX = ePos.pageX - chartPosLeft;
6137 chartY = ePos.pageY - chartPosTop;
6138 } else {
6139 chartX = e.layerX;
6140 chartY = e.layerY;
6144 // correct for page zoom bug in WebKit
6145 if (pageZoomFix) {
6146 chartX += mathRound((pageZoomFix + 1) * chartPosLeft - chartPosLeft);
6147 chartY += mathRound((pageZoomFix + 1) * chartPosTop - chartPosTop);
6150 return extend(e, {
6151 chartX: chartX,
6152 chartY: chartY
6157 * Get the click position in terms of axis values.
6159 * @param {Object} e A mouse event
6161 function getMouseCoordinates(e) {
6162 var coordinates = {
6163 xAxis: [],
6164 yAxis: []
6166 each(axes, function(axis, i) {
6167 var translate = axis.translate,
6168 isXAxis = axis.isXAxis,
6169 isHorizontal = inverted ? !isXAxis : isXAxis;
6171 coordinates[isXAxis ? 'xAxis' : 'yAxis'].push({
6172 axis: axis,
6173 value: translate(
6174 isHorizontal ?
6175 e.chartX - plotLeft :
6176 plotHeight - e.chartY + plotTop,
6177 true
6181 return coordinates;
6185 * With line type charts with a single tracker, get the point closest to the mouse
6187 function onmousemove (e) {
6188 var point,
6189 points,
6190 hoverPoint = chart.hoverPoint,
6191 hoverSeries = chart.hoverSeries,
6194 distance = chartWidth,
6195 index = inverted ? e.chartY : e.chartX - plotLeft; // wtf?
6197 // shared tooltip
6198 if (tooltip && options.shared) {
6199 points = [];
6201 // loop over all series and find the ones with points closest to the mouse
6202 i = series.length;
6203 for (j = 0; j < i; j++) {
6204 if (series[j].visible && series[j].tooltipPoints.length) {
6205 point = series[j].tooltipPoints[index];
6206 point._dist = mathAbs(index - point.plotX);
6207 distance = mathMin(distance, point._dist);
6208 points.push(point);
6211 // remove furthest points
6212 i = points.length;
6213 while (i--) {
6214 if (points[i]._dist > distance) {
6215 points.splice(i, 1);
6218 // refresh the tooltip if necessary
6219 if (points.length && (points[0].plotX !== hoverX)) {
6220 tooltip.refresh(points);
6221 hoverX = points[0].plotX;
6225 // separate tooltip and general mouse events
6226 if (hoverSeries && hoverSeries.tracker) { // only use for line-type series with common tracker
6228 // get the point
6229 point = hoverSeries.tooltipPoints[index];
6231 // a new point is hovered, refresh the tooltip
6232 if (point && point !== hoverPoint) {
6234 // trigger the events
6235 point.onMouseOver();
6244 * Reset the tracking by hiding the tooltip, the hover series state and the hover point
6246 function resetTracker() {
6247 var hoverSeries = chart.hoverSeries,
6248 hoverPoint = chart.hoverPoint;
6250 if (hoverPoint) {
6251 hoverPoint.onMouseOut();
6254 if (hoverSeries) {
6255 hoverSeries.onMouseOut();
6258 if (tooltip) {
6259 tooltip.hide();
6262 hoverX = null;
6266 * Mouse up or outside the plot area
6268 function drop() {
6269 if (selectionMarker) {
6270 var selectionData = {
6271 xAxis: [],
6272 yAxis: []
6274 selectionBox = selectionMarker.getBBox(),
6275 selectionLeft = selectionBox.x - plotLeft,
6276 selectionTop = selectionBox.y - plotTop;
6279 // a selection has been made
6280 if (hasDragged) {
6282 // record each axis' min and max
6283 each(axes, function(axis, i) {
6284 var translate = axis.translate,
6285 isXAxis = axis.isXAxis,
6286 isHorizontal = inverted ? !isXAxis : isXAxis,
6287 selectionMin = translate(
6288 isHorizontal ?
6289 selectionLeft :
6290 plotHeight - selectionTop - selectionBox.height,
6291 true,
6296 selectionMax = translate(
6297 isHorizontal ?
6298 selectionLeft + selectionBox.width :
6299 plotHeight - selectionTop,
6300 true,
6306 selectionData[isXAxis ? 'xAxis' : 'yAxis'].push({
6307 axis: axis,
6308 min: mathMin(selectionMin, selectionMax), // for reversed axes,
6309 max: mathMax(selectionMin, selectionMax)
6313 fireEvent(chart, 'selection', selectionData, zoom);
6316 selectionMarker = selectionMarker.destroy();
6319 chart.mouseIsDown = mouseIsDown = hasDragged = false;
6320 removeEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop);
6325 * Set the JS events on the container element
6327 function setDOMEvents () {
6328 var lastWasOutsidePlot = true;
6331 * Record the starting position of a dragoperation
6333 container.onmousedown = function(e) {
6334 e = normalizeMouseEvent(e);
6336 // record the start position
6337 //e.preventDefault && e.preventDefault();
6339 chart.mouseIsDown = mouseIsDown = true;
6340 mouseDownX = e.chartX;
6341 mouseDownY = e.chartY;
6343 addEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop);
6346 // The mousemove, touchmove and touchstart event handler
6347 var mouseMove = function(e) {
6349 // let the system handle multitouch operations like two finger scroll
6350 // and pinching
6351 if (e && e.touches && e.touches.length > 1) {
6352 return;
6355 // normalize
6356 e = normalizeMouseEvent(e);
6357 if (!hasTouch) { // not for touch devices
6358 e.returnValue = false;
6361 var chartX = e.chartX,
6362 chartY = e.chartY,
6363 isOutsidePlot = !isInsidePlot(chartX - plotLeft, chartY - plotTop);
6365 // on touch devices, only trigger click if a handler is defined
6366 if (hasTouch && e.type === 'touchstart') {
6367 if (attr(e.target, 'isTracker')) {
6368 if (!chart.runTrackerClick) {
6369 e.preventDefault();
6371 } else if (!runChartClick && !isOutsidePlot) {
6372 e.preventDefault();
6376 // cancel on mouse outside
6377 if (isOutsidePlot) {
6379 if (!lastWasOutsidePlot) {
6380 // reset the tracker
6381 resetTracker();
6384 // drop the selection if any and reset mouseIsDown and hasDragged
6385 //drop();
6386 if (chartX < plotLeft) {
6387 chartX = plotLeft;
6388 } else if (chartX > plotLeft + plotWidth) {
6389 chartX = plotLeft + plotWidth;
6392 if (chartY < plotTop) {
6393 chartY = plotTop;
6394 } else if (chartY > plotTop + plotHeight) {
6395 chartY = plotTop + plotHeight;
6400 if (mouseIsDown && e.type !== 'touchstart') { // make selection
6402 // determine if the mouse has moved more than 10px
6403 hasDragged = Math.sqrt(
6404 Math.pow(mouseDownX - chartX, 2) +
6405 Math.pow(mouseDownY - chartY, 2));
6406 if (hasDragged > 10) {
6408 // make a selection
6409 if (hasCartesianSeries && (zoomX || zoomY) &&
6410 isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop)) {
6411 if (!selectionMarker) {
6412 selectionMarker = renderer.rect(
6413 plotLeft,
6414 plotTop,
6415 zoomHor ? 1 : plotWidth,
6416 zoomVert ? 1 : plotHeight,
6419 .attr({
6420 fill: 'rgba(69,114,167,0.25)',
6421 zIndex: 7
6423 .add();
6427 // adjust the width of the selection marker
6428 if (selectionMarker && zoomHor) {
6429 var xSize = chartX - mouseDownX;
6430 selectionMarker.attr({
6431 width: mathAbs(xSize),
6432 x: (xSize > 0 ? 0 : xSize) + mouseDownX
6435 // adjust the height of the selection marker
6436 if (selectionMarker && zoomVert) {
6437 var ySize = chartY - mouseDownY;
6438 selectionMarker.attr({
6439 height: mathAbs(ySize),
6440 y: (ySize > 0 ? 0 : ySize) + mouseDownY
6445 } else if (!isOutsidePlot) {
6446 // show the tooltip
6447 onmousemove(e);
6450 lastWasOutsidePlot = isOutsidePlot;
6452 // when outside plot, allow touch-drag by returning true
6453 return isOutsidePlot || !hasCartesianSeries;
6457 * When the mouse enters the container, run mouseMove
6459 container.onmousemove = mouseMove;
6462 * When the mouse leaves the container, hide the tracking (tooltip).
6464 addEvent(container, 'mouseleave', resetTracker);
6467 container.ontouchstart = function(e) {
6468 // For touch devices, use touchmove to zoom
6469 if (zoomX || zoomY) {
6470 container.onmousedown(e);
6472 // Show tooltip and prevent the lower mouse pseudo event
6473 mouseMove(e);
6477 * Allow dragging the finger over the chart to read the values on touch
6478 * devices
6480 container.ontouchmove = mouseMove;
6483 * Allow dragging the finger over the chart to read the values on touch
6484 * devices
6486 container.ontouchend = function() {
6487 if (hasDragged) {
6488 resetTracker();
6493 // MooTools 1.2.3 doesn't fire this in IE when using addEvent
6494 container.onclick = function(e) {
6495 var hoverPoint = chart.hoverPoint;
6496 e = normalizeMouseEvent(e);
6498 e.cancelBubble = true; // IE specific
6501 if (!hasDragged) {
6502 if (hoverPoint && attr(e.target, 'isTracker')) {
6503 var plotX = hoverPoint.plotX,
6504 plotY = hoverPoint.plotY;
6506 // add page position info
6507 extend(hoverPoint, {
6508 pageX: chartPosition.left + plotLeft +
6509 (inverted ? plotWidth - plotY : plotX),
6510 pageY: chartPosition.top + plotTop +
6511 (inverted ? plotHeight - plotX : plotY)
6514 // the series click event
6515 fireEvent(hoverPoint.series, 'click', extend(e, {
6516 point: hoverPoint
6517 }));
6519 // the point click event
6520 hoverPoint.firePointEvent('click', e);
6522 } else {
6523 extend(e, getMouseCoordinates(e));
6525 // fire a click event in the chart
6526 if (isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {
6527 fireEvent(chart, 'click', e);
6533 // reset mouseIsDown and hasDragged
6534 hasDragged = false;
6540 * Create the image map that listens for mouseovers
6542 placeTrackerGroup = function() {
6544 // first create - plot positions is not final at this stage
6545 if (!trackerGroup) {
6546 chart.trackerGroup = trackerGroup = renderer.g('tracker')
6547 .attr({ zIndex: 9 })
6548 .add();
6550 // then position - this happens on load and after resizing and changing
6551 // axis or box positions
6552 } else {
6553 trackerGroup.translate(plotLeft, plotTop);
6554 if (inverted) {
6555 trackerGroup.attr({
6556 width: chart.plotWidth,
6557 height: chart.plotHeight
6558 }).invert();
6564 // Run MouseTracker
6565 placeTrackerGroup();
6566 if (options.enabled) {
6567 chart.tooltip = tooltip = Tooltip(options);
6570 setDOMEvents();
6572 // set the fixed interval ticking for the smooth tooltip
6573 tooltipInterval = setInterval(function() {
6574 if (tooltipTick) {
6575 tooltipTick();
6577 }, 32);
6579 // expose properties
6580 extend(this, {
6581 zoomX: zoomX,
6582 zoomY: zoomY,
6583 resetTracker: resetTracker
6590 * The overview of the chart's series
6591 * @param {Object} chart
6593 var Legend = function(chart) {
6595 var options = chart.options.legend;
6597 if (!options.enabled) {
6598 return;
6601 var horizontal = options.layout === 'horizontal',
6602 symbolWidth = options.symbolWidth,
6603 symbolPadding = options.symbolPadding,
6604 allItems,
6605 style = options.style,
6606 itemStyle = options.itemStyle,
6607 itemHoverStyle = options.itemHoverStyle,
6608 itemHiddenStyle = options.itemHiddenStyle,
6609 padding = pInt(style.padding),
6610 rightPadding = 20,
6611 //lineHeight = options.lineHeight || 16,
6612 y = 18,
6613 initialItemX = 4 + padding + symbolWidth + symbolPadding,
6614 itemX,
6615 itemY,
6616 lastItemY,
6617 itemHeight = 0,
6618 box,
6619 legendBorderWidth = options.borderWidth,
6620 legendBackgroundColor = options.backgroundColor,
6621 legendGroup,
6622 offsetWidth,
6623 widthOption = options.width,
6624 series = chart.series,
6625 reversedLegend = options.reversed;
6630 * Set the colors for the legend item
6631 * @param {Object} item A Series or Point instance
6632 * @param {Object} visible Dimmed or colored
6634 function colorizeItem(item, visible) {
6635 var legendItem = item.legendItem,
6636 legendLine = item.legendLine,
6637 legendSymbol = item.legendSymbol,
6638 hiddenColor = itemHiddenStyle.color,
6639 textColor = visible ? options.itemStyle.color : hiddenColor,
6640 lineColor = visible ? item.color : hiddenColor,
6641 symbolAttr = visible ? item.pointAttr[NORMAL_STATE] : {
6642 stroke: hiddenColor,
6643 fill: hiddenColor
6646 if (legendItem) {
6647 legendItem.css({ fill: textColor });
6649 if (legendLine) {
6650 legendLine.attr({ stroke: lineColor });
6652 if (legendSymbol) {
6653 legendSymbol.attr(symbolAttr);
6659 * Position the legend item
6660 * @param {Object} item A Series or Point instance
6661 * @param {Object} visible Dimmed or colored
6663 function positionItem(item, itemX, itemY) {
6664 var legendItem = item.legendItem,
6665 legendLine = item.legendLine,
6666 legendSymbol = item.legendSymbol,
6667 checkbox = item.checkbox;
6668 if (legendItem) {
6669 legendItem.attr({
6670 x: itemX,
6671 y: itemY
6674 if (legendLine) {
6675 legendLine.translate(itemX, itemY - 4);
6677 if (legendSymbol) {
6678 legendSymbol.attr({
6679 x: itemX + legendSymbol.xOff,
6680 y: itemY + legendSymbol.yOff
6683 if (checkbox) {
6684 checkbox.x = itemX;
6685 checkbox.y = itemY;
6690 * Destroy a single legend item
6691 * @param {Object} item The series or point
6693 function destroyItem(item) {
6694 var checkbox = item.checkbox;
6696 // pull out from the array
6697 //erase(allItems, item);
6699 // destroy SVG elements
6700 each(['legendItem', 'legendLine', 'legendSymbol'], function(key) {
6701 if (item[key]) {
6702 item[key].destroy();
6706 if (checkbox) {
6707 discardElement(item.checkbox);
6715 * Position the checkboxes after the width is determined
6717 function positionCheckboxes() {
6718 each(allItems, function(item) {
6719 var checkbox = item.checkbox,
6720 alignAttr = legendGroup.alignAttr;
6721 if (checkbox) {
6722 css(checkbox, {
6723 left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 40) +PX,
6724 top: (alignAttr.translateY + checkbox.y - 11) + PX
6731 * Render a single specific legend item
6732 * @param {Object} item A series or point
6734 function renderItem(item) {
6735 var bBox,
6736 itemWidth,
6737 legendSymbol,
6738 symbolX,
6739 symbolY,
6740 attribs,
6741 simpleSymbol,
6742 li = item.legendItem,
6743 series = item.series || item,
6744 i = allItems.length,
6745 itemOptions = series.options,
6746 strokeWidth = (itemOptions && itemOptions.borderWidth) || 0;
6748 if (!li) { // generate it once, later move it
6750 // let these series types use a simple symbol
6751 simpleSymbol = /^(bar|pie|area|column)$/.test(series.type);
6753 // generate the list item text
6754 item.legendItem = li = renderer.text(
6755 options.labelFormatter.call(item),
6759 .css(item.visible ? itemStyle : itemHiddenStyle)
6760 .on('mouseover', function() {
6761 item.setState(HOVER_STATE);
6762 li.css(itemHoverStyle);
6764 .on('mouseout', function() {
6765 li.css(item.visible ? itemStyle : itemHiddenStyle);
6766 item.setState();
6768 .on('click', function(event) {
6769 var strLegendItemClick = 'legendItemClick',
6770 fnLegendItemClick = function() {
6771 item.setVisible();
6774 // click the name or symbol
6775 if (item.firePointEvent) { // point
6776 item.firePointEvent(strLegendItemClick, null, fnLegendItemClick);
6777 } else {
6778 fireEvent(item, strLegendItemClick, null, fnLegendItemClick);
6781 .attr({ zIndex: 2 })
6782 .add(legendGroup);
6784 // draw the line
6785 if (!simpleSymbol && itemOptions && itemOptions.lineWidth) {
6786 var attrs = {
6787 'stroke-width': itemOptions.lineWidth,
6788 zIndex: 2
6790 if (itemOptions.dashStyle) {
6791 attrs.dashstyle = itemOptions.dashStyle;
6793 item.legendLine = renderer.path([
6795 -symbolWidth - symbolPadding,
6798 -symbolPadding,
6801 .attr(attrs)
6802 .add(legendGroup);
6805 // draw a simple symbol
6806 if (simpleSymbol) { // bar|pie|area|column
6808 legendSymbol = renderer.rect(
6809 (symbolX = -symbolWidth - symbolPadding),
6810 (symbolY = -11),
6811 symbolWidth,
6814 ).attr({
6815 //'stroke-width': 0,
6816 zIndex: 3
6817 }).add(legendGroup);
6820 // draw the marker
6821 else if (itemOptions && itemOptions.marker && itemOptions.marker.enabled) {
6822 legendSymbol = renderer.symbol(
6823 item.symbol,
6824 (symbolX = -symbolWidth / 2 - symbolPadding),
6825 (symbolY = -4),
6826 itemOptions.marker.radius
6828 //.attr(item.pointAttr[NORMAL_STATE])
6829 .attr({ zIndex: 3 })
6830 .add(legendGroup);
6833 if (legendSymbol) {
6834 legendSymbol.xOff = symbolX + (strokeWidth % 2 / 2);
6835 legendSymbol.yOff = symbolY + (strokeWidth % 2 / 2);
6838 item.legendSymbol = legendSymbol;
6840 // colorize the items
6841 colorizeItem(item, item.visible);
6844 // add the HTML checkbox on top
6845 if (itemOptions && itemOptions.showCheckbox) {
6846 item.checkbox = createElement('input', {
6847 type: 'checkbox',
6848 checked: item.selected,
6849 defaultChecked: item.selected // required by IE7
6850 }, options.itemCheckboxStyle, container);
6852 addEvent(item.checkbox, 'click', function(event) {
6853 var target = event.target;
6854 fireEvent(item, 'checkboxClick', {
6855 checked: target.checked
6857 function() {
6858 item.select();
6866 // calculate the positions for the next line
6867 bBox = li.getBBox();
6869 itemWidth = item.legendItemWidth =
6870 options.itemWidth || symbolWidth + symbolPadding + bBox.width + rightPadding;
6871 itemHeight = bBox.height;
6873 // if the item exceeds the width, start a new line
6874 if (horizontal && itemX - initialItemX + itemWidth >
6875 (widthOption || (chartWidth - 2 * padding - initialItemX))) {
6876 itemX = initialItemX;
6877 itemY += itemHeight;
6879 lastItemY = itemY;
6881 // position the newly generated or reordered items
6882 positionItem(item, itemX, itemY);
6884 // advance
6885 if (horizontal) {
6886 itemX += itemWidth;
6887 } else {
6888 itemY += itemHeight;
6891 // the width of the widest item
6892 offsetWidth = widthOption || mathMax(
6893 horizontal ? itemX - initialItemX : itemWidth,
6894 offsetWidth
6899 // add it all to an array to use below
6900 //allItems.push(item);
6904 * Render the legend. This method can be called both before and after
6905 * chart.render. If called after, it will only rearrange items instead
6906 * of creating new ones.
6908 function renderLegend() {
6909 itemX = initialItemX;
6910 itemY = y;
6911 offsetWidth = 0;
6912 lastItemY = 0;
6914 if (!legendGroup) {
6915 legendGroup = renderer.g('legend')
6916 .attr({ zIndex: 7 })
6917 .add();
6921 // add each series or point
6922 allItems = [];
6923 each(series, function(serie) {
6924 var seriesOptions = serie.options;
6926 if (!seriesOptions.showInLegend) {
6927 return;
6930 // use points or series for the legend item depending on legendType
6931 allItems = allItems.concat(seriesOptions.legendType === 'point' ?
6932 serie.data :
6933 serie
6938 // sort by legendIndex
6939 allItems.sort(function(a, b) {
6940 return (a.options.legendIndex || 0) - (b.options.legendIndex || 0);
6943 // reversed legend
6944 if (reversedLegend) {
6945 allItems.reverse();
6948 // render the items
6949 each(allItems, renderItem);
6953 // Draw the border
6954 legendWidth = widthOption || offsetWidth;
6955 legendHeight = lastItemY - y + itemHeight;
6957 if (legendBorderWidth || legendBackgroundColor) {
6958 legendWidth += 2 * padding;
6959 legendHeight += 2 * padding;
6961 if (!box) {
6962 box = renderer.rect(
6965 legendWidth,
6966 legendHeight,
6967 options.borderRadius,
6968 legendBorderWidth || 0
6969 ).attr({
6970 stroke: options.borderColor,
6971 'stroke-width': legendBorderWidth || 0,
6972 fill: legendBackgroundColor || NONE
6974 .add(legendGroup)
6975 .shadow(options.shadow);
6977 } else if (legendWidth > 0 && legendHeight > 0) {
6978 box.animate(
6979 box.crisp(null, null, null, legendWidth, legendHeight)
6983 // hide the border if no items
6984 box[allItems.length ? 'show' : 'hide']();
6987 // 1.x compatibility: positioning based on style
6988 var props = ['left', 'right', 'top', 'bottom'],
6989 prop,
6990 i = 4;
6991 while(i--) {
6992 prop = props[i];
6993 if (style[prop] && style[prop] !== 'auto') {
6994 options[i < 2 ? 'align' : 'verticalAlign'] = prop;
6995 options[i < 2 ? 'x' : 'y'] = pInt(style[prop]) * (i % 2 ? -1 : 1);
6999 legendGroup.align(extend(options, {
7000 width: legendWidth,
7001 height: legendHeight
7002 }), true, spacingBox);
7004 if (!isResizing) {
7005 positionCheckboxes();
7010 // run legend
7011 renderLegend();
7013 // move checkboxes
7014 addEvent(chart, 'endResize', positionCheckboxes);
7016 // expose
7017 return {
7018 colorizeItem: colorizeItem,
7019 destroyItem: destroyItem,
7020 renderLegend: renderLegend
7029 /**
7030 * Initialize an individual series, called internally before render time
7032 function initSeries(options) {
7033 var type = options.type || optionsChart.type || optionsChart.defaultSeriesType,
7034 typeClass = seriesTypes[type],
7035 serie,
7036 hasRendered = chart.hasRendered;
7038 // an inverted chart can't take a column series and vice versa
7039 if (hasRendered) {
7040 if (inverted && type === 'column') {
7041 typeClass = seriesTypes.bar;
7042 } else if (!inverted && type === 'bar') {
7043 typeClass = seriesTypes.column;
7047 serie = new typeClass();
7049 serie.init(chart, options);
7051 // set internal chart properties
7052 if (!hasRendered && serie.inverted) {
7053 inverted = true;
7055 if (serie.isCartesian) {
7056 hasCartesianSeries = serie.isCartesian;
7059 series.push(serie);
7061 return serie;
7065 * Add a series dynamically after time
7067 * @param {Object} options The config options
7068 * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true.
7069 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
7070 * configuration
7072 * @return {Object} series The newly created series object
7074 function addSeries(options, redraw, animation) {
7075 var series;
7077 if (options) {
7078 setAnimation(animation, chart);
7079 redraw = pick(redraw, true); // defaults to true
7081 fireEvent(chart, 'addSeries', { options: options }, function() {
7082 series = initSeries(options);
7083 series.isDirty = true;
7085 chart.isDirtyLegend = true; // the series array is out of sync with the display
7086 if (redraw) {
7087 chart.redraw();
7092 return series;
7096 * Check whether a given point is within the plot area
7098 * @param {Number} x Pixel x relative to the coordinateSystem
7099 * @param {Number} y Pixel y relative to the coordinateSystem
7101 isInsidePlot = function(x, y) {
7102 return x >= 0 &&
7103 x <= plotWidth &&
7104 y >= 0 &&
7105 y <= plotHeight;
7109 * Adjust all axes tick amounts
7111 function adjustTickAmounts() {
7112 if (optionsChart.alignTicks !== false) {
7113 each(axes, function(axis) {
7114 axis.adjustTickAmount();
7117 maxTicks = null;
7121 * Redraw legend, axes or series based on updated data
7123 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
7124 * configuration
7126 function redraw(animation) {
7127 var redrawLegend = chart.isDirtyLegend,
7128 hasStackedSeries,
7129 isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed?
7130 seriesLength = series.length,
7131 i = seriesLength,
7132 clipRect = chart.clipRect,
7133 serie;
7135 setAnimation(animation, chart);
7137 // link stacked series
7138 while (i--) {
7139 serie = series[i];
7140 if (serie.isDirty && serie.options.stacking) {
7141 hasStackedSeries = true;
7142 break;
7145 if (hasStackedSeries) { // mark others as dirty
7146 i = seriesLength;
7147 while (i--) {
7148 serie = series[i];
7149 if (serie.options.stacking) {
7150 serie.isDirty = true;
7155 // handle updated data in the series
7156 each(series, function(serie) {
7157 if (serie.isDirty) { // prepare the data so axis can read it
7158 serie.cleanData();
7159 serie.getSegments();
7161 if (serie.options.legendType === 'point') {
7162 redrawLegend = true;
7167 // handle added or removed series
7168 if (redrawLegend && legend.renderLegend) { // series or pie points are added or removed
7169 // draw legend graphics
7170 legend.renderLegend();
7172 chart.isDirtyLegend = false;
7175 if (hasCartesianSeries) {
7176 if (!isResizing) {
7178 // reset maxTicks
7179 maxTicks = null;
7181 // set axes scales
7182 each(axes, function(axis) {
7183 axis.setScale();
7186 adjustTickAmounts();
7187 getMargins();
7189 // redraw axes
7190 each(axes, function(axis) {
7191 if (axis.isDirty || isDirtyBox) {
7192 axis.redraw();
7193 isDirtyBox = true; // always redraw box to reflect changes in the axis labels
7200 // the plot areas size has changed
7201 if (isDirtyBox) {
7202 drawChartBox();
7203 placeTrackerGroup();
7205 // move clip rect
7206 if (clipRect) {
7207 stop(clipRect);
7208 clipRect.animate({ // for chart resize
7209 width: chart.plotSizeX,
7210 height: chart.plotSizeY
7217 // redraw affected series
7218 each(series, function(serie) {
7219 if (serie.isDirty && serie.visible &&
7220 (!serie.isCartesian || serie.xAxis)) { // issue #153
7221 serie.redraw();
7226 // hide tooltip and hover states
7227 if (tracker && tracker.resetTracker) {
7228 tracker.resetTracker();
7231 // fire the event
7232 fireEvent(chart, 'redraw');
7238 * Dim the chart and show a loading text or symbol
7239 * @param {String} str An optional text to show in the loading label instead of the default one
7241 function showLoading(str) {
7242 var loadingOptions = options.loading;
7244 // create the layer at the first call
7245 if (!loadingDiv) {
7246 loadingDiv = createElement(DIV, {
7247 className: 'highcharts-loading'
7248 }, extend(loadingOptions.style, {
7249 left: plotLeft + PX,
7250 top: plotTop + PX,
7251 width: plotWidth + PX,
7252 height: plotHeight + PX,
7253 zIndex: 10,
7254 display: NONE
7255 }), container);
7257 loadingSpan = createElement(
7258 'span',
7259 null,
7260 loadingOptions.labelStyle,
7261 loadingDiv
7266 // update text
7267 loadingSpan.innerHTML = str || options.lang.loading;
7269 // show it
7270 if (!loadingShown) {
7271 css(loadingDiv, { opacity: 0, display: '' });
7272 animate(loadingDiv, {
7273 opacity: loadingOptions.style.opacity
7274 }, {
7275 duration: loadingOptions.showDuration
7277 loadingShown = true;
7281 * Hide the loading layer
7283 function hideLoading() {
7284 animate(loadingDiv, {
7285 opacity: 0
7286 }, {
7287 duration: options.loading.hideDuration,
7288 complete: function() {
7289 css(loadingDiv, { display: NONE });
7292 loadingShown = false;
7296 * Get an axis, series or point object by id.
7297 * @param id {String} The id as given in the configuration options
7299 function get(id) {
7300 var i,
7302 data;
7304 // search axes
7305 for (i = 0; i < axes.length; i++) {
7306 if (axes[i].options.id === id) {
7307 return axes[i];
7311 // search series
7312 for (i = 0; i < series.length; i++) {
7313 if (series[i].options.id === id) {
7314 return series[i];
7318 // search points
7319 for (i = 0; i < series.length; i++) {
7320 data = series[i].data;
7321 for (j = 0; j < data.length; j++) {
7322 if (data[j].id === id) {
7323 return data[j];
7327 return null;
7330 /**
7331 * Create the Axis instances based on the config options
7333 function getAxes() {
7334 var xAxisOptions = options.xAxis || {},
7335 yAxisOptions = options.yAxis || {},
7336 axis;
7338 // make sure the options are arrays and add some members
7339 xAxisOptions = splat(xAxisOptions);
7340 each(xAxisOptions, function(axis, i) {
7341 axis.index = i;
7342 axis.isX = true;
7345 yAxisOptions = splat(yAxisOptions);
7346 each(yAxisOptions, function(axis, i) {
7347 axis.index = i;
7350 // concatenate all axis options into one array
7351 axes = xAxisOptions.concat(yAxisOptions);
7353 // loop the options and construct axis objects
7354 chart.xAxis = [];
7355 chart.yAxis = [];
7356 axes = map(axes, function(axisOptions) {
7357 axis = new Axis(chart, axisOptions);
7358 chart[axis.isXAxis ? 'xAxis' : 'yAxis'].push(axis);
7360 return axis;
7363 adjustTickAmounts();
7368 * Get the currently selected points from all series
7370 function getSelectedPoints() {
7371 var points = [];
7372 each(series, function(serie) {
7373 points = points.concat( grep( serie.data, function(point) {
7374 return point.selected;
7375 }));
7377 return points;
7381 * Get the currently selected series
7383 function getSelectedSeries() {
7384 return grep(series, function (serie) {
7385 return serie.selected;
7390 * Zoom out to 1:1
7392 zoomOut = function () {
7393 fireEvent(chart, 'selection', { resetSelection: true }, zoom);
7394 chart.toolbar.remove('zoom');
7398 * Zoom into a given portion of the chart given by axis coordinates
7399 * @param {Object} event
7401 zoom = function (event) {
7403 // add button to reset selection
7404 var lang = defaultOptions.lang,
7405 animate = chart.pointCount < 100;
7406 chart.toolbar.add('zoom', lang.resetZoom, lang.resetZoomTitle, zoomOut);
7408 // if zoom is called with no arguments, reset the axes
7409 if (!event || event.resetSelection) {
7410 each(axes, function(axis) {
7411 axis.setExtremes(null, null, false, animate);
7415 // else, zoom in on all axes
7416 else {
7417 each(event.xAxis.concat(event.yAxis), function(axisData) {
7418 var axis = axisData.axis;
7420 // don't zoom more than maxZoom
7421 if (chart.tracker[axis.isXAxis ? 'zoomX' : 'zoomY']) {
7422 axis.setExtremes(axisData.min, axisData.max, false, animate);
7427 // redraw chart
7428 redraw();
7432 * Show the title and subtitle of the chart
7434 * @param titleOptions {Object} New title options
7435 * @param subtitleOptions {Object} New subtitle options
7438 function setTitle (titleOptions, subtitleOptions) {
7440 chartTitleOptions = merge(options.title, titleOptions);
7441 chartSubtitleOptions = merge(options.subtitle, subtitleOptions);
7443 // add title and subtitle
7444 each([
7445 ['title', titleOptions, chartTitleOptions],
7446 ['subtitle', subtitleOptions, chartSubtitleOptions]
7447 ], function(arr) {
7448 var name = arr[0],
7449 title = chart[name],
7450 titleOptions = arr[1],
7451 chartTitleOptions = arr[2];
7453 if (title && titleOptions) {
7454 title.destroy(); // remove old
7455 title = null;
7457 if (chartTitleOptions && chartTitleOptions.text && !title) {
7458 chart[name] = renderer.text(
7459 chartTitleOptions.text,
7463 .attr({
7464 align: chartTitleOptions.align,
7465 'class': 'highcharts-'+ name,
7466 zIndex: 1
7468 .css(chartTitleOptions.style)
7469 .add()
7470 .align(chartTitleOptions, false, spacingBox);
7477 * Get chart width and height according to options and container size
7479 function getChartSize() {
7481 containerWidth = (renderToClone || renderTo).offsetWidth;
7482 containerHeight = (renderToClone || renderTo).offsetHeight;
7483 chart.chartWidth = chartWidth = optionsChart.width || containerWidth || 600;
7484 chart.chartHeight = chartHeight = optionsChart.height ||
7485 // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7:
7486 (containerHeight > 19 ? containerHeight : 400);
7491 * Get the containing element, determine the size and create the inner container
7492 * div to hold the chart
7494 function getContainer() {
7495 renderTo = optionsChart.renderTo;
7496 containerId = PREFIX + idCounter++;
7498 if (isString(renderTo)) {
7499 renderTo = doc.getElementById(renderTo);
7502 // remove previous chart
7503 renderTo.innerHTML = '';
7505 // If the container doesn't have an offsetWidth, it has or is a child of a node
7506 // that has display:none. We need to temporarily move it out to a visible
7507 // state to determine the size, else the legend and tooltips won't render
7508 // properly
7509 if (!renderTo.offsetWidth) {
7510 renderToClone = renderTo.cloneNode(0);
7511 css(renderToClone, {
7512 position: ABSOLUTE,
7513 top: '-9999px',
7514 display: ''
7516 doc.body.appendChild(renderToClone);
7519 // get the width and height
7520 getChartSize();
7522 // create the inner container
7523 chart.container = container = createElement(DIV, {
7524 className: 'highcharts-container' +
7525 (optionsChart.className ? ' '+ optionsChart.className : ''),
7526 id: containerId
7527 }, extend({
7528 position: RELATIVE,
7529 overflow: HIDDEN, // needed for context menu (avoid scrollbars) and
7530 // content overflow in IE
7531 width: chartWidth + PX,
7532 height: chartHeight + PX,
7533 textAlign: 'left'
7534 }, optionsChart.style),
7535 renderToClone || renderTo
7538 chart.renderer = renderer =
7539 optionsChart.forExport ? // force SVG, used for SVG export
7540 new SVGRenderer(container, chartWidth, chartHeight, true) :
7541 new Renderer(container, chartWidth, chartHeight);
7543 // Issue 110 workaround:
7544 // In Firefox, if a div is positioned by percentage, its pixel position may land
7545 // between pixels. The container itself doesn't display this, but an SVG element
7546 // inside this container will be drawn at subpixel precision. In order to draw
7547 // sharp lines, this must be compensated for. This doesn't seem to work inside
7548 // iframes though (like in jsFiddle).
7549 var subPixelFix, rect;
7550 if (isFirefox && container.getBoundingClientRect) {
7551 subPixelFix = function() {
7552 css(container, { left: 0, top: 0 });
7553 rect = container.getBoundingClientRect();
7554 css(container, {
7555 left: (-(rect.left - pInt(rect.left))) + PX,
7556 top: (-(rect.top - pInt(rect.top))) + PX
7560 // run the fix now
7561 subPixelFix();
7563 // run it on resize
7564 addEvent(win, 'resize', subPixelFix);
7566 // remove it on chart destroy
7567 addEvent(chart, 'destroy', function() {
7568 removeEvent(win, 'resize', subPixelFix);
7574 * Calculate margins by rendering axis labels in a preliminary position. Title,
7575 * subtitle and legend have already been rendered at this stage, but will be
7576 * moved into their final positions
7578 getMargins = function() {
7579 var legendOptions = options.legend,
7580 legendMargin = pick(legendOptions.margin, 10),
7581 legendX = legendOptions.x,
7582 legendY = legendOptions.y,
7583 align = legendOptions.align,
7584 verticalAlign = legendOptions.verticalAlign,
7585 titleOffset;
7587 resetMargins();
7589 // adjust for title and subtitle
7590 if ((chart.title || chart.subtitle) && !defined(optionsMarginTop)) {
7591 titleOffset = mathMax(
7592 (chart.title && !chartTitleOptions.floating && !chartTitleOptions.verticalAlign && chartTitleOptions.y) || 0,
7593 (chart.subtitle && !chartSubtitleOptions.floating && !chartSubtitleOptions.verticalAlign && chartSubtitleOptions.y) || 0
7595 if (titleOffset) {
7596 plotTop = mathMax(plotTop, titleOffset + pick(chartTitleOptions.margin, 15) + spacingTop);
7599 // adjust for legend
7600 if (legendOptions.enabled && !legendOptions.floating) {
7601 if (align === 'right') { // horizontal alignment handled first
7602 if (!defined(optionsMarginRight)) {
7603 marginRight = mathMax(
7604 marginRight,
7605 legendWidth - legendX + legendMargin + spacingRight
7608 } else if (align === 'left') {
7609 if (!defined(optionsMarginLeft)) {
7610 plotLeft = mathMax(
7611 plotLeft,
7612 legendWidth + legendX + legendMargin + spacingLeft
7616 } else if (verticalAlign === 'top') {
7617 if (!defined(optionsMarginTop)) {
7618 plotTop = mathMax(
7619 plotTop,
7620 legendHeight + legendY + legendMargin + spacingTop
7624 } else if (verticalAlign === 'bottom') {
7625 if (!defined(optionsMarginBottom)) {
7626 marginBottom = mathMax(
7627 marginBottom,
7628 legendHeight - legendY + legendMargin + spacingBottom
7634 // pre-render axes to get labels offset width
7635 if (hasCartesianSeries) {
7636 each(axes, function(axis) {
7637 axis.getOffset();
7641 if (!defined(optionsMarginLeft)) {
7642 plotLeft += axisOffset[3];
7644 if (!defined(optionsMarginTop)) {
7645 plotTop += axisOffset[0];
7647 if (!defined(optionsMarginBottom)) {
7648 marginBottom += axisOffset[2];
7650 if (!defined(optionsMarginRight)) {
7651 marginRight += axisOffset[1];
7654 setChartSize();
7659 * Add the event handlers necessary for auto resizing
7662 function initReflow() {
7663 var reflowTimeout;
7664 function reflow() {
7665 var width = optionsChart.width || renderTo.offsetWidth,
7666 height = optionsChart.height || renderTo.offsetHeight;
7668 if (width && height) { // means container is display:none
7669 if (width !== containerWidth || height !== containerHeight) {
7670 clearTimeout(reflowTimeout);
7671 reflowTimeout = setTimeout(function() {
7672 resize(width, height, false);
7673 }, 100);
7675 containerWidth = width;
7676 containerHeight = height;
7679 addEvent(win, 'resize', reflow);
7680 addEvent(chart, 'destroy', function() {
7681 removeEvent(win, 'resize', reflow);
7686 * Resize the chart to a given width and height
7687 * @param {Number} width
7688 * @param {Number} height
7689 * @param {Object|Boolean} animation
7691 resize = function(width, height, animation) {
7692 var chartTitle = chart.title,
7693 chartSubtitle = chart.subtitle;
7695 isResizing += 1;
7697 // set the animation for the current process
7698 setAnimation(animation, chart);
7700 oldChartHeight = chartHeight;
7701 oldChartWidth = chartWidth;
7702 chart.chartWidth = chartWidth = mathRound(width);
7703 chart.chartHeight = chartHeight = mathRound(height);
7705 css(container, {
7706 width: chartWidth + PX,
7707 height: chartHeight + PX
7709 renderer.setSize(chartWidth, chartHeight, animation);
7711 // update axis lengths for more correct tick intervals:
7712 plotWidth = chartWidth - plotLeft - marginRight;
7713 plotHeight = chartHeight - plotTop - marginBottom;
7715 // handle axes
7716 maxTicks = null;
7717 each(axes, function(axis) {
7718 axis.isDirty = true;
7719 axis.setScale();
7722 // make sure non-cartesian series are also handled
7723 each(series, function(serie) {
7724 serie.isDirty = true;
7727 chart.isDirtyLegend = true; // force legend redraw
7728 chart.isDirtyBox = true; // force redraw of plot and chart border
7730 getMargins();
7732 // move titles
7733 if (chartTitle) {
7734 chartTitle.align(null, null, spacingBox);
7736 if (chartSubtitle) {
7737 chartSubtitle.align(null, null, spacingBox);
7740 redraw(animation);
7743 oldChartHeight = null;
7744 fireEvent(chart, 'resize');
7746 // fire endResize and set isResizing back
7747 setTimeout(function() {
7748 fireEvent(chart, 'endResize', null, function() {
7749 isResizing -= 1;
7751 }, (globalAnimation && globalAnimation.duration) || 500);
7755 * Set the public chart properties. This is done before and after the pre-render
7756 * to determine margin sizes
7758 setChartSize = function() {
7760 chart.plotLeft = plotLeft = mathRound(plotLeft);
7761 chart.plotTop = plotTop = mathRound(plotTop);
7762 chart.plotWidth = plotWidth = mathRound(chartWidth - plotLeft - marginRight);
7763 chart.plotHeight = plotHeight = mathRound(chartHeight - plotTop - marginBottom);
7765 chart.plotSizeX = inverted ? plotHeight : plotWidth;
7766 chart.plotSizeY = inverted ? plotWidth : plotHeight;
7768 spacingBox = {
7769 x: spacingLeft,
7770 y: spacingTop,
7771 width: chartWidth - spacingLeft - spacingRight,
7772 height: chartHeight - spacingTop - spacingBottom
7777 * Initial margins before auto size margins are applied
7779 resetMargins = function() {
7780 plotTop = pick(optionsMarginTop, spacingTop);
7781 marginRight = pick(optionsMarginRight, spacingRight);
7782 marginBottom = pick(optionsMarginBottom, spacingBottom);
7783 plotLeft = pick(optionsMarginLeft, spacingLeft);
7784 axisOffset = [0, 0, 0, 0]; // top, right, bottom, left
7788 * Draw the borders and backgrounds for chart and plot area
7790 drawChartBox = function() {
7791 var chartBorderWidth = optionsChart.borderWidth || 0,
7792 chartBackgroundColor = optionsChart.backgroundColor,
7793 plotBackgroundColor = optionsChart.plotBackgroundColor,
7794 plotBackgroundImage = optionsChart.plotBackgroundImage,
7795 mgn,
7796 plotSize = {
7797 x: plotLeft,
7798 y: plotTop,
7799 width: plotWidth,
7800 height: plotHeight
7803 // Chart area
7804 mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);
7806 if (chartBorderWidth || chartBackgroundColor) {
7807 if (!chartBackground) {
7808 chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn,
7809 optionsChart.borderRadius, chartBorderWidth)
7810 .attr({
7811 stroke: optionsChart.borderColor,
7812 'stroke-width': chartBorderWidth,
7813 fill: chartBackgroundColor || NONE
7815 .add()
7816 .shadow(optionsChart.shadow);
7817 } else { // resize
7818 chartBackground.animate(
7819 chartBackground.crisp(null, null, null, chartWidth - mgn, chartHeight - mgn)
7825 // Plot background
7826 if (plotBackgroundColor) {
7827 if (!plotBackground) {
7828 plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0)
7829 .attr({
7830 fill: plotBackgroundColor
7832 .add()
7833 .shadow(optionsChart.plotShadow);
7834 } else {
7835 plotBackground.animate(plotSize);
7838 if (plotBackgroundImage) {
7839 if (!plotBGImage) {
7840 plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)
7841 .add();
7842 } else {
7843 plotBGImage.animate(plotSize);
7847 // Plot area border
7848 if (optionsChart.plotBorderWidth) {
7849 if (!plotBorder) {
7850 plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, optionsChart.plotBorderWidth)
7851 .attr({
7852 stroke: optionsChart.plotBorderColor,
7853 'stroke-width': optionsChart.plotBorderWidth,
7854 zIndex: 4
7856 .add();
7857 } else {
7858 plotBorder.animate(
7859 plotBorder.crisp(null, plotLeft, plotTop, plotWidth, plotHeight)
7864 // reset
7865 chart.isDirtyBox = false;
7869 * Render all graphics for the chart
7871 function render () {
7872 var labels = options.labels,
7873 credits = options.credits,
7874 creditsHref;
7876 // Title
7877 setTitle();
7880 // Legend
7881 legend = chart.legend = new Legend(chart);
7883 // Get margins by pre-rendering axes
7884 getMargins();
7885 each(axes, function(axis) {
7886 axis.setTickPositions(true); // update to reflect the new margins
7888 adjustTickAmounts();
7889 getMargins(); // second pass to check for new labels
7892 // Draw the borders and backgrounds
7893 drawChartBox();
7895 // Axes
7896 if (hasCartesianSeries) {
7897 each(axes, function(axis) {
7898 axis.render();
7903 // The series
7904 if (!chart.seriesGroup) {
7905 chart.seriesGroup = renderer.g('series-group')
7906 .attr({ zIndex: 3 })
7907 .add();
7909 each(series, function(serie) {
7910 serie.translate();
7911 serie.setTooltipPoints();
7912 serie.render();
7916 // Labels
7917 if (labels.items) {
7918 each(labels.items, function() {
7919 var style = extend(labels.style, this.style),
7920 x = pInt(style.left) + plotLeft,
7921 y = pInt(style.top) + plotTop + 12;
7923 // delete to prevent rewriting in IE
7924 delete style.left;
7925 delete style.top;
7927 renderer.text(
7928 this.html,
7932 .attr({ zIndex: 2 })
7933 .css(style)
7934 .add();
7939 // Toolbar (don't redraw)
7940 if (!chart.toolbar) {
7941 chart.toolbar = Toolbar(chart);
7944 // Credits
7945 if (credits.enabled && !chart.credits) {
7946 creditsHref = credits.href;
7947 renderer.text(
7948 credits.text,
7952 .on('click', function() {
7953 if (creditsHref) {
7954 location.href = creditsHref;
7957 .attr({
7958 align: credits.position.align,
7959 zIndex: 8
7961 .css(credits.style)
7962 .add()
7963 .align(credits.position);
7966 placeTrackerGroup();
7968 // Set flag
7969 chart.hasRendered = true;
7971 // If the chart was rendered outside the top container, put it back in
7972 if (renderToClone) {
7973 renderTo.appendChild(container);
7974 discardElement(renderToClone);
7975 //updatePosition(container);
7980 * Clean up memory usage
7982 function destroy() {
7983 var i = series.length,
7984 parentNode = container && container.parentNode;
7986 // fire the chart.destoy event
7987 fireEvent(chart, 'destroy');
7989 // remove events
7990 removeEvent(win, 'unload', destroy);
7991 removeEvent(chart);
7993 each(axes, function(axis) {
7994 removeEvent(axis);
7997 // destroy each series
7998 while (i--) {
7999 series[i].destroy();
8002 // remove container and all SVG
8003 if (container) { // can break in IE when destroyed before finished loading
8004 container.innerHTML = '';
8005 removeEvent(container);
8006 if (parentNode) {
8007 parentNode.removeChild(container);
8010 // IE6 leak
8011 container = null;
8014 // IE7 leak
8015 if (renderer) { // can break in IE when destroyed before finished loading
8016 renderer.alignedObjects = null;
8019 // memory and CPU leak
8020 clearInterval(tooltipInterval);
8022 // clean it all up
8023 for (i in chart) {
8024 delete chart[i];
8029 * Prepare for first rendering after all data are loaded
8031 function firstRender() {
8033 // VML namespaces can't be added until after complete. Listening
8034 // for Perini's doScroll hack is not enough.
8035 var ONREADYSTATECHANGE = 'onreadystatechange',
8036 COMPLETE = 'complete';
8037 // Note: in spite of JSLint's complaints, win == win.top is required
8038 if (!hasSVG && win == win.top && doc.readyState !== COMPLETE) {
8039 doc.attachEvent(ONREADYSTATECHANGE, function() {
8040 doc.detachEvent(ONREADYSTATECHANGE, firstRender);
8041 if (doc.readyState === COMPLETE) {
8042 firstRender();
8045 return;
8048 // Set to zero for each new chart
8049 colorCounter = 0;
8050 symbolCounter = 0;
8052 // create the container
8053 getContainer();
8055 resetMargins();
8056 setChartSize();
8058 // Initialize the series
8059 each(options.series || [], function(serieOptions) {
8060 initSeries(serieOptions);
8063 // Set the common inversion and transformation for inverted series after initSeries
8064 chart.inverted = inverted = pick(inverted, options.chart.inverted);
8067 getAxes();
8070 chart.render = render;
8072 // depends on inverted and on margins being set
8073 chart.tracker = tracker = new MouseTracker(chart, options.tooltip);
8075 //globalAnimation = false;
8076 render();
8078 fireEvent(chart, 'load');
8080 //globalAnimation = true;
8082 // run callbacks
8083 if (callback) {
8084 callback.apply(chart, [chart]);
8086 each(chart.callbacks, function(fn) {
8087 fn.apply(chart, [chart]);
8091 // Run chart
8094 // Destroy the chart and free up memory.
8095 addEvent(win, 'unload', destroy);
8097 // Set up auto resize
8098 if (optionsChart.reflow !== false) {
8099 addEvent(chart, 'load', initReflow);
8102 // Chart event handlers
8103 if (chartEvents) {
8104 for (eventType in chartEvents) {
8105 addEvent(chart, eventType, chartEvents[eventType]);
8110 chart.options = options;
8111 chart.series = series;
8118 // Expose methods and variables
8119 chart.addSeries = addSeries;
8120 chart.animation = pick(optionsChart.animation, true);
8121 chart.destroy = destroy;
8122 chart.get = get;
8123 chart.getSelectedPoints = getSelectedPoints;
8124 chart.getSelectedSeries = getSelectedSeries;
8125 chart.hideLoading = hideLoading;
8126 chart.isInsidePlot = isInsidePlot;
8127 chart.redraw = redraw;
8128 chart.setSize = resize;
8129 chart.setTitle = setTitle;
8130 chart.showLoading = showLoading;
8131 chart.pointCount = 0;
8133 if ($) $(function() {
8134 $container = $('#container');
8135 var origChartWidth,
8136 origChartHeight;
8137 if ($container) {
8138 $('<button>+</button>')
8139 .insertBefore($container)
8140 .click(function() {
8141 if (origChartWidth === UNDEFINED) {
8142 origChartWidth = chartWidth;
8143 origChartHeight = chartHeight;
8145 chart.resize(chartWidth *= 1.1, chartHeight *= 1.1);
8147 $('<button>-</button>')
8148 .insertBefore($container)
8149 .click(function() {
8150 if (origChartWidth === UNDEFINED) {
8151 origChartWidth = chartWidth;
8152 origChartHeight = chartHeight;
8154 chart.resize(chartWidth *= 0.9, chartHeight *= 0.9);
8156 $('<button>1:1</button>')
8157 .insertBefore($container)
8158 .click(function() {
8159 if (origChartWidth === UNDEFINED) {
8160 origChartWidth = chartWidth;
8161 origChartHeight = chartHeight;
8163 chart.resize(origChartWidth, origChartHeight);
8172 firstRender();
8175 } // end Chart
8177 // Hook for exporting module
8178 Chart.prototype.callbacks = [];
8180 * The Point object and prototype. Inheritable and used as base for PiePoint
8182 var Point = function() {};
8183 Point.prototype = {
8186 * Initialize the point
8187 * @param {Object} series The series object containing this point
8188 * @param {Object} options The data in either number, array or object format
8190 init: function(series, options) {
8191 var point = this,
8192 defaultColors;
8193 point.series = series;
8194 point.applyOptions(options);
8195 point.pointAttr = {};
8197 if (series.options.colorByPoint) {
8198 defaultColors = series.chart.options.colors;
8199 if (!point.options) {
8200 point.options = {};
8202 point.color = point.options.color = point.color || defaultColors[colorCounter++];
8204 // loop back to zero
8205 if (colorCounter >= defaultColors.length) {
8206 colorCounter = 0;
8210 series.chart.pointCount++;
8211 return point;
8214 * Apply the options containing the x and y data and possible some extra properties.
8215 * This is called on point init or from point.update.
8217 * @param {Object} options
8219 applyOptions: function(options) {
8220 var point = this,
8221 series = point.series;
8223 point.config = options;
8225 // onedimensional array input
8226 if (isNumber(options) || options === null) {
8227 point.y = options;
8230 // object input
8231 else if (isObject(options) && !isNumber(options.length)) {
8233 // copy options directly to point
8234 extend(point, options);
8235 point.options = options;
8238 // categorized data with name in first position
8239 else if (isString(options[0])) {
8240 point.name = options[0];
8241 point.y = options[1];
8244 // two-dimentional array
8245 else if (isNumber(options[0])) {
8246 point.x = options[0];
8247 point.y = options[1];
8251 * If no x is set by now, get auto incremented value. All points must have an
8252 * x value, however the y value can be null to create a gap in the series
8254 if (point.x === UNDEFINED) {
8255 point.x = series.autoIncrement();
8261 * Destroy a point to clear memory. Its reference still stays in series.data.
8263 destroy: function() {
8264 var point = this,
8265 series = point.series,
8266 prop;
8268 series.chart.pointCount--;
8270 if (point === series.chart.hoverPoint) {
8271 point.onMouseOut();
8273 series.chart.hoverPoints = null; // remove reference
8275 // remove all events
8276 removeEvent(point);
8278 each(['graphic', 'tracker', 'group', 'dataLabel', 'connector'], function(prop) {
8279 if (point[prop]) {
8280 point[prop].destroy();
8282 });
8284 if (point.legendItem) { // pies have legend items
8285 point.series.chart.legend.destroyItem(point);
8288 for (prop in point) {
8289 point[prop] = null;
8296 * Return the configuration hash needed for the data label and tooltip formatters
8298 getLabelConfig: function() {
8299 var point = this;
8300 return {
8301 x: point.category,
8302 y: point.y,
8303 series: point.series,
8304 point: point,
8305 percentage: point.percentage,
8306 total: point.total || point.stackTotal
8311 * Toggle the selection status of a point
8312 * @param {Boolean} selected Whether to select or unselect the point.
8313 * @param {Boolean} accumulate Whether to add to the previous selection. By default,
8314 * this happens if the control key (Cmd on Mac) was pressed during clicking.
8316 select: function(selected, accumulate) {
8317 var point = this,
8318 series = point.series,
8319 chart = series.chart;
8321 point.selected = selected = pick(selected, !point.selected);
8323 //series.isDirty = true;
8324 point.firePointEvent(selected ? 'select' : 'unselect');
8325 point.setState(selected && SELECT_STATE);
8327 // unselect all other points unless Ctrl or Cmd + click
8328 if (!accumulate) {
8329 each(chart.getSelectedPoints(), function (loopPoint) {
8330 if (loopPoint.selected && loopPoint !== point) {
8331 loopPoint.selected = false;
8332 loopPoint.setState(NORMAL_STATE);
8333 loopPoint.firePointEvent('unselect');
8340 onMouseOver: function() {
8341 var point = this,
8342 chart = point.series.chart,
8343 tooltip = chart.tooltip,
8344 hoverPoint = chart.hoverPoint;
8346 // set normal state to previous series
8347 if (hoverPoint && hoverPoint !== point) {
8348 hoverPoint.onMouseOut();
8351 // trigger the event
8352 point.firePointEvent('mouseOver');
8354 // update the tooltip
8355 if (tooltip && !tooltip.shared) {
8356 tooltip.refresh(point);
8359 // hover this
8360 point.setState(HOVER_STATE);
8361 chart.hoverPoint = point;
8364 onMouseOut: function() {
8365 var point = this;
8366 point.firePointEvent('mouseOut');
8368 point.setState();
8369 point.series.chart.hoverPoint = null;
8373 * Extendable method for formatting each point's tooltip line
8375 * @param {Boolean} useHeader Whether a common header is used for multiple series in the tooltip
8377 * @return {String} A string to be concatenated in to the common tooltip text
8379 tooltipFormatter: function(useHeader) {
8380 var point = this,
8381 series = point.series;
8383 return ['<span style="color:'+ series.color +'">', (point.name || series.name), '</span>: ',
8384 (!useHeader ? ('<b>x = '+ (point.name || point.x) + ',</b> ') : ''),
8385 '<b>', (!useHeader ? 'y = ' : '' ), point.y, '</b>'].join('');
8390 * Update the point with new options (typically x/y data) and optionally redraw the series.
8392 * @param {Object} options Point options as defined in the series.data array
8393 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
8394 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
8395 * configuration
8398 update: function(options, redraw, animation) {
8399 var point = this,
8400 series = point.series,
8401 dataLabel = point.dataLabel,
8402 graphic = point.graphic,
8403 chart = series.chart;
8405 redraw = pick(redraw, true);
8407 // fire the event with a default handler of doing the update
8408 point.firePointEvent('update', { options: options }, function() {
8410 point.applyOptions(options);
8412 // update visuals
8413 if (isObject(options)) {
8414 series.getAttribs();
8415 if (graphic) {
8416 graphic.attr(point.pointAttr[series.state]);
8420 // redraw
8421 series.isDirty = true;
8422 if (redraw) {
8423 chart.redraw(animation);
8429 * Remove a point and optionally redraw the series and if necessary the axes
8430 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
8431 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
8432 * configuration
8434 remove: function(redraw, animation) {
8435 var point = this,
8436 series = point.series,
8437 chart = series.chart,
8438 data = series.data;
8440 setAnimation(animation, chart);
8441 redraw = pick(redraw, true);
8443 // fire the event with a default handler of removing the point
8444 point.firePointEvent('remove', null, function() {
8446 erase(data, point);
8448 point.destroy();
8451 // redraw
8452 series.isDirty = true;
8453 if (redraw) {
8454 chart.redraw();
8462 * Fire an event on the Point object. Must not be renamed to fireEvent, as this
8463 * causes a name clash in MooTools
8464 * @param {String} eventType
8465 * @param {Object} eventArgs Additional event arguments
8466 * @param {Function} defaultFunction Default event handler
8468 firePointEvent: function(eventType, eventArgs, defaultFunction) {
8469 var point = this,
8470 series = this.series,
8471 seriesOptions = series.options;
8473 // load event handlers on demand to save time on mouseover/out
8474 if (seriesOptions.point.events[eventType] || (
8475 point.options && point.options.events && point.options.events[eventType])) {
8476 this.importEvents();
8479 // add default handler if in selection mode
8480 if (eventType === 'click' && seriesOptions.allowPointSelect) {
8481 defaultFunction = function (event) {
8482 // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
8483 point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
8487 fireEvent(this, eventType, eventArgs, defaultFunction);
8490 * Import events from the series' and point's options. Only do it on
8491 * demand, to save processing time on hovering.
8493 importEvents: function() {
8494 if (!this.hasImportedEvents) {
8495 var point = this,
8496 options = merge(point.series.options.point, point.options),
8497 events = options.events,
8498 eventType;
8500 point.events = events;
8502 for (eventType in events) {
8503 addEvent(point, eventType, events[eventType]);
8505 this.hasImportedEvents = true;
8511 * Set the point's state
8512 * @param {String} state
8514 setState: function(state) {
8515 var point = this,
8516 series = point.series,
8517 stateOptions = series.options.states,
8518 markerOptions = defaultPlotOptions[series.type].marker && series.options.marker,
8519 normalDisabled = markerOptions && !markerOptions.enabled,
8520 markerStateOptions = markerOptions && markerOptions.states[state],
8521 stateDisabled = markerStateOptions && markerStateOptions.enabled === false,
8522 stateMarkerGraphic = series.stateMarkerGraphic,
8523 chart = series.chart,
8524 pointAttr = point.pointAttr;
8526 state = state || NORMAL_STATE; // empty string
8528 if (
8529 // already has this state
8530 state === point.state ||
8531 // selected points don't respond to hover
8532 (point.selected && state !== SELECT_STATE) ||
8533 // series' state options is disabled
8534 (stateOptions[state] && stateOptions[state].enabled === false) ||
8535 // point marker's state options is disabled
8536 (state && (stateDisabled || (normalDisabled && !markerStateOptions.enabled)))
8539 return;
8542 // apply hover styles to the existing point
8543 if (point.graphic) {
8544 point.graphic.attr(pointAttr[state]);
8546 // if a graphic is not applied to each point in the normal state, create a shared
8547 // graphic for the hover state
8548 else {
8549 if (state) {
8550 if (!stateMarkerGraphic) {
8551 series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.circle(
8552 0, 0, pointAttr[state].r
8554 .attr(pointAttr[state])
8555 .add(series.group);
8558 stateMarkerGraphic.translate(
8559 point.plotX,
8560 point.plotY
8564 if (stateMarkerGraphic) {
8565 stateMarkerGraphic[state ? 'show' : 'hide']();
8569 point.state = state;
8574 * The base function which all other series types inherit from
8575 * @param {Object} chart
8576 * @param {Object} options
8578 var Series = function() {};
8580 Series.prototype = {
8582 isCartesian: true,
8583 type: 'line',
8584 pointClass: Point,
8585 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
8586 stroke: 'lineColor',
8587 'stroke-width': 'lineWidth',
8588 fill: 'fillColor',
8589 r: 'radius'
8591 init: function(chart, options) {
8592 var series = this,
8593 eventType,
8594 events,
8595 //pointEvent,
8596 index = chart.series.length;
8598 series.chart = chart;
8599 options = series.setOptions(options); // merge with plotOptions
8601 // set some variables
8602 extend(series, {
8603 index: index,
8604 options: options,
8605 name: options.name || 'Series '+ (index + 1),
8606 state: NORMAL_STATE,
8607 pointAttr: {},
8608 visible: options.visible !== false, // true by default
8609 selected: options.selected === true // false by default
8612 // register event listeners
8613 events = options.events;
8614 for (eventType in events) {
8615 addEvent(series, eventType, events[eventType]);
8617 if (
8618 (events && events.click) ||
8619 (options.point && options.point.events && options.point.events.click) ||
8620 options.allowPointSelect
8622 chart.runTrackerClick = true;
8625 series.getColor();
8626 series.getSymbol();
8629 // set the data
8630 series.setData(options.data, false);
8636 * Return an auto incremented x value based on the pointStart and pointInterval options.
8637 * This is only used if an x value is not given for the point that calls autoIncrement.
8639 autoIncrement: function() {
8640 var series = this,
8641 options = series.options,
8642 xIncrement = series.xIncrement;
8644 xIncrement = pick(xIncrement, options.pointStart, 0);
8646 series.pointInterval = pick(series.pointInterval, options.pointInterval, 1);
8648 series.xIncrement = xIncrement + series.pointInterval;
8649 return xIncrement;
8653 * Sort the data and remove duplicates
8655 cleanData: function() {
8656 var series = this,
8657 chart = series.chart,
8658 data = series.data,
8659 closestPoints,
8660 smallestInterval,
8661 chartSmallestInterval = chart.smallestInterval,
8662 interval,
8665 // sort the data points
8666 data.sort(function(a, b){
8667 return (a.x - b.x);
8670 // remove points with equal x values
8671 // record the closest distance for calculation of column widths
8672 /*for (i = data.length - 1; i >= 0; i--) {
8673 if (data[i - 1]) {
8674 if (data[i - 1].x == data[i].x) {
8675 data[i - 1].destroy();
8676 data.splice(i - 1, 1); // remove the duplicate
8681 // connect nulls
8682 if (series.options.connectNulls) {
8683 for (i = data.length - 1; i >= 0; i--) {
8684 if (data[i].y === null && data[i - 1] && data[i + 1]) {
8685 data.splice(i, 1);
8690 // find the closes pair of points
8691 for (i = data.length - 1; i >= 0; i--) {
8692 if (data[i - 1]) {
8693 interval = data[i].x - data[i - 1].x;
8694 if (interval > 0 && (smallestInterval === UNDEFINED || interval < smallestInterval)) {
8695 smallestInterval = interval;
8696 closestPoints = i;
8701 if (chartSmallestInterval === UNDEFINED || smallestInterval < chartSmallestInterval) {
8702 chart.smallestInterval = smallestInterval;
8704 series.closestPoints = closestPoints;
8708 * Divide the series data into segments divided by null values. Also sort
8709 * the data points and delete duplicate values.
8711 getSegments: function() {
8712 var lastNull = -1,
8713 segments = [],
8714 data = this.data;
8716 // create the segments
8717 each(data, function(point, i) {
8718 if (point.y === null) {
8719 if (i > lastNull + 1) {
8720 segments.push(data.slice(lastNull + 1, i));
8722 lastNull = i;
8723 } else if (i === data.length - 1) { // last value
8724 segments.push(data.slice(lastNull + 1, i + 1));
8727 this.segments = segments;
8732 * Set the series options by merging from the options tree
8733 * @param {Object} itemOptions
8735 setOptions: function(itemOptions) {
8736 var plotOptions = this.chart.options.plotOptions,
8737 options = merge(
8738 plotOptions[this.type],
8739 plotOptions.series,
8740 itemOptions
8743 return options;
8747 * Get the series' color
8749 getColor: function(){
8750 var defaultColors = this.chart.options.colors;
8751 this.color = this.options.color || defaultColors[colorCounter++] || '#0000ff';
8752 if (colorCounter >= defaultColors.length) {
8753 colorCounter = 0;
8757 * Get the series' symbol
8759 getSymbol: function(){
8760 var defaultSymbols = this.chart.options.symbols,
8761 symbol = this.options.marker.symbol || defaultSymbols[symbolCounter++];
8762 this.symbol = symbol;
8763 if (symbolCounter >= defaultSymbols.length) {
8764 symbolCounter = 0;
8769 * Add a point dynamically after chart load time
8770 * @param {Object} options Point options as given in series.data
8771 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
8772 * @param {Boolean} shift If shift is true, a point is shifted off the start
8773 * of the series as one is appended to the end.
8774 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
8775 * configuration
8777 addPoint: function(options, redraw, shift, animation) {
8778 var series = this,
8779 data = series.data,
8780 graph = series.graph,
8781 area = series.area,
8782 chart = series.chart,
8783 point = (new series.pointClass()).init(series, options);
8785 setAnimation(animation, chart);
8787 if (graph && shift) { // make graph animate sideways
8788 graph.shift = shift;
8790 if (area) {
8791 area.shift = shift;
8792 area.isArea = true;
8795 redraw = pick(redraw, true);
8797 data.push(point);
8798 if (shift) {
8799 data[0].remove(false);
8801 series.getAttribs();
8804 // redraw
8805 series.isDirty = true;
8806 if (redraw) {
8807 chart.redraw();
8812 * Replace the series data with a new set of data
8813 * @param {Object} data
8814 * @param {Object} redraw
8816 setData: function(data, redraw) {
8817 var series = this,
8818 oldData = series.data,
8819 initialColor = series.initialColor,
8820 chart = series.chart,
8821 i = (oldData && oldData.length) || 0;
8823 series.xIncrement = null; // reset for new data
8824 if (defined(initialColor)) { // reset colors for pie
8825 colorCounter = initialColor;
8828 data = map(splat(data || []), function(pointOptions) {
8829 return (new series.pointClass()).init(series, pointOptions);
8832 // destroy old points
8833 while (i--) {
8834 oldData[i].destroy();
8837 // set the data
8838 series.data = data;
8840 series.cleanData();
8841 series.getSegments();
8844 // cache attributes for shapes
8845 series.getAttribs();
8847 // redraw
8848 series.isDirty = true;
8849 chart.isDirtyBox = true;
8850 if (pick(redraw, true)) {
8851 chart.redraw(false);
8856 * Remove a series and optionally redraw the chart
8858 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
8859 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
8860 * configuration
8863 remove: function(redraw, animation) {
8864 var series = this,
8865 chart = series.chart;
8866 redraw = pick(redraw, true);
8868 if (!series.isRemoving) { /* prevent triggering native event in jQuery
8869 (calling the remove function from the remove event) */
8870 series.isRemoving = true;
8872 // fire the event with a default handler of removing the point
8873 fireEvent(series, 'remove', null, function() {
8876 // destroy elements
8877 series.destroy();
8880 // redraw
8881 chart.isDirtyLegend = chart.isDirtyBox = true;
8882 if (redraw) {
8883 chart.redraw(animation);
8888 series.isRemoving = false;
8892 * Translate data points from raw data values to chart specific positioning data
8893 * needed later in drawPoints, drawGraph and drawTracker.
8895 translate: function() {
8896 var series = this,
8897 chart = series.chart,
8898 stacking = series.options.stacking,
8899 categories = series.xAxis.categories,
8900 yAxis = series.yAxis,
8901 data = series.data,
8902 i = data.length;
8904 // do the translation
8905 while (i--) {
8906 var point = data[i],
8907 xValue = point.x,
8908 yValue = point.y,
8909 yBottom = point.low,
8910 stack = yAxis.stacks[(yValue < 0 ? '-' : '') + series.stackKey],
8911 pointStack,
8912 pointStackTotal;
8913 point.plotX = series.xAxis.translate(xValue);
8915 // calculate the bottom y value for stacked series
8916 if (stacking && series.visible && stack && stack[xValue]) {
8917 pointStack = stack[xValue];
8918 pointStackTotal = pointStack.total;
8919 pointStack.cum = yBottom = pointStack.cum - yValue; // start from top
8920 yValue = yBottom + yValue;
8922 if (stacking === 'percent') {
8923 yBottom = pointStackTotal ? yBottom * 100 / pointStackTotal : 0;
8924 yValue = pointStackTotal ? yValue * 100 / pointStackTotal : 0;
8927 point.percentage = pointStackTotal ? point.y * 100 / pointStackTotal : 0;
8928 point.stackTotal = pointStackTotal;
8931 if (defined(yBottom)) {
8932 point.yBottom = yAxis.translate(yBottom, 0, 1, 0, 1);
8935 // set the y value
8936 if (yValue !== null) {
8937 point.plotY = yAxis.translate(yValue, 0, 1, 0, 1);
8940 // set client related positions for mouse tracking
8941 point.clientX = chart.inverted ?
8942 chart.plotHeight - point.plotX :
8943 point.plotX; // for mouse tracking
8945 // some API data
8946 point.category = categories && categories[point.x] !== UNDEFINED ?
8947 categories[point.x] : point.x;
8952 * Memoize tooltip texts and positions
8954 setTooltipPoints: function (renew) {
8955 var series = this,
8956 chart = series.chart,
8957 inverted = chart.inverted,
8958 data = [],
8959 plotSize = mathRound((inverted ? chart.plotTop : chart.plotLeft) + chart.plotSizeX),
8960 low,
8961 high,
8962 tooltipPoints = []; // a lookup array for each pixel in the x dimension
8964 // renew
8965 if (renew) {
8966 series.tooltipPoints = null;
8969 // concat segments to overcome null values
8970 each(series.segments, function(segment){
8971 data = data.concat(segment);
8974 // loop the concatenated data and apply each point to all the closest
8975 // pixel positions
8976 if (series.xAxis && series.xAxis.reversed) {
8977 data = data.reverse();//reverseArray(data);
8980 each(data, function(point, i) {
8982 low = data[i - 1] ? data[i - 1]._high + 1 : 0;
8983 high = point._high = data[i + 1] ? (
8984 mathFloor((point.plotX + (data[i + 1] ?
8985 data[i + 1].plotX : plotSize)) / 2)) :
8986 plotSize;
8988 while (low <= high) {
8989 tooltipPoints[inverted ? plotSize - low++ : low++] = point;
8992 series.tooltipPoints = tooltipPoints;
8999 * Series mouse over handler
9001 onMouseOver: function() {
9002 var series = this,
9003 chart = series.chart,
9004 hoverSeries = chart.hoverSeries;
9006 if (!hasTouch && chart.mouseIsDown) {
9007 return;
9010 // set normal state to previous series
9011 if (hoverSeries && hoverSeries !== series) {
9012 hoverSeries.onMouseOut();
9015 // trigger the event, but to save processing time,
9016 // only if defined
9017 if (series.options.events.mouseOver) {
9018 fireEvent(series, 'mouseOver');
9022 // bring to front
9023 // Todo: optimize. This is one of two operations slowing down the tooltip in Firefox.
9024 // Can the tracking be done otherwise?
9025 if (series.tracker) {
9026 series.tracker.toFront();
9029 // hover this
9030 series.setState(HOVER_STATE);
9031 chart.hoverSeries = series;
9035 * Series mouse out handler
9037 onMouseOut: function() {
9038 // trigger the event only if listeners exist
9039 var series = this,
9040 options = series.options,
9041 chart = series.chart,
9042 tooltip = chart.tooltip,
9043 hoverPoint = chart.hoverPoint;
9045 // trigger mouse out on the point, which must be in this series
9046 if (hoverPoint) {
9047 hoverPoint.onMouseOut();
9050 // fire the mouse out event
9051 if (series && options.events.mouseOut) {
9052 fireEvent(series, 'mouseOut');
9056 // hide the tooltip
9057 if (tooltip && !options.stickyTracking) {
9058 tooltip.hide();
9061 // set normal state
9062 series.setState();
9063 chart.hoverSeries = null;
9067 * Animate in the series
9069 animate: function(init) {
9070 var series = this,
9071 chart = series.chart,
9072 clipRect = series.clipRect,
9073 animation = series.options.animation;
9075 if (animation && !isObject(animation)) {
9076 animation = {};
9079 if (init) { // initialize the animation
9080 if (!clipRect.isAnimating) { // apply it only for one of the series
9081 clipRect.attr( 'width', 0 );
9082 clipRect.isAnimating = true;
9085 } else { // run the animation
9086 clipRect.animate({
9087 width: chart.plotSizeX
9088 }, animation);
9090 // delete this function to allow it only once
9091 this.animate = null;
9097 * Draw the markers
9099 drawPoints: function(){
9100 var series = this,
9101 pointAttr,
9102 data = series.data,
9103 chart = series.chart,
9104 plotX,
9105 plotY,
9107 point,
9108 radius,
9109 graphic;
9111 if (series.options.marker.enabled) {
9112 i = data.length;
9113 while (i--) {
9114 point = data[i];
9115 plotX = point.plotX;
9116 plotY = point.plotY;
9117 graphic = point.graphic;
9119 // only draw the point if y is defined
9120 if (plotY !== UNDEFINED && !isNaN(plotY)) {
9122 /* && removed this code because points stayed after zoom
9123 point.plotX >= 0 && point.plotX <= chart.plotSizeX &&
9124 point.plotY >= 0 && point.plotY <= chart.plotSizeY*/
9126 // shortcuts
9127 pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE];
9128 radius = pointAttr.r;
9130 if (graphic) { // update
9131 graphic.animate({
9132 x: plotX,
9133 y: plotY,
9134 r: radius
9136 } else {
9137 point.graphic = chart.renderer.symbol(
9138 pick(point.marker && point.marker.symbol, series.symbol),
9139 plotX,
9140 plotY,
9141 radius
9143 .attr(pointAttr)
9144 .add(series.group);
9153 * Convert state properties from API naming conventions to SVG attributes
9155 * @param {Object} options API options object
9156 * @param {Object} base1 SVG attribute object to inherit from
9157 * @param {Object} base2 Second level SVG attribute object to inherit from
9159 convertAttribs: function(options, base1, base2, base3) {
9160 var conversion = this.pointAttrToOptions,
9161 attr,
9162 option,
9163 obj = {};
9165 options = options || {};
9166 base1 = base1 || {};
9167 base2 = base2 || {};
9168 base3 = base3 || {};
9170 for (attr in conversion) {
9171 option = conversion[attr];
9172 obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]);
9174 return obj;
9178 * Get the state attributes. Each series type has its own set of attributes
9179 * that are allowed to change on a point's state change. Series wide attributes are stored for
9180 * all series, and additionally point specific attributes are stored for all
9181 * points with individual marker options. If such options are not defined for the point,
9182 * a reference to the series wide attributes is stored in point.pointAttr.
9184 getAttribs: function() {
9185 var series = this,
9186 normalOptions = defaultPlotOptions[series.type].marker ? series.options.marker : series.options,
9187 stateOptions = normalOptions.states,
9188 stateOptionsHover = stateOptions[HOVER_STATE],
9189 pointStateOptionsHover,
9190 seriesColor = series.color,
9191 normalDefaults = {
9192 stroke: seriesColor,
9193 fill: seriesColor
9195 data = series.data,
9197 point,
9198 seriesPointAttr = [],
9199 pointAttr,
9200 pointAttrToOptions = series.pointAttrToOptions,
9201 hasPointSpecificOptions,
9202 key;
9204 // series type specific modifications
9205 if (series.options.marker) { // line, spline, area, areaspline, scatter
9207 // if no hover radius is given, default to normal radius + 2
9208 stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + 2;
9209 stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + 1;
9211 } else { // column, bar, pie
9213 // if no hover color is given, brighten the normal color
9214 stateOptionsHover.color = stateOptionsHover.color ||
9215 Color(stateOptionsHover.color || seriesColor)
9216 .brighten(stateOptionsHover.brightness).get();
9219 // general point attributes for the series normal state
9220 seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults);
9222 // HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius
9223 each([HOVER_STATE, SELECT_STATE], function(state) {
9224 seriesPointAttr[state] =
9225 series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]);
9228 // set it
9229 series.pointAttr = seriesPointAttr;
9232 // Generate the point-specific attribute collections if specific point
9233 // options are given. If not, create a referance to the series wide point
9234 // attributes
9235 i = data.length;
9236 while (i--) {
9237 point = data[i];
9238 normalOptions = (point.options && point.options.marker) || point.options;
9239 if (normalOptions && normalOptions.enabled === false) {
9240 normalOptions.radius = 0;
9242 hasPointSpecificOptions = false;
9244 // check if the point has specific visual options
9245 if (point.options) {
9246 for (key in pointAttrToOptions) {
9247 if (defined(normalOptions[pointAttrToOptions[key]])) {
9248 hasPointSpecificOptions = true;
9255 // a specific marker config object is defined for the individual point:
9256 // create it's own attribute collection
9257 if (hasPointSpecificOptions) {
9259 pointAttr = [];
9260 stateOptions = normalOptions.states || {}; // reassign for individual point
9261 pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {};
9263 // if no hover color is given, brighten the normal color
9264 if (!series.options.marker) { // column, bar, point
9265 pointStateOptionsHover.color =
9266 Color(pointStateOptionsHover.color || point.options.color)
9267 .brighten(pointStateOptionsHover.brightness ||
9268 stateOptionsHover.brightness).get();
9272 // normal point state inherits series wide normal state
9273 pointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, seriesPointAttr[NORMAL_STATE]);
9275 // inherit from point normal and series hover
9276 pointAttr[HOVER_STATE] = series.convertAttribs(
9277 stateOptions[HOVER_STATE],
9278 seriesPointAttr[HOVER_STATE],
9279 pointAttr[NORMAL_STATE]
9281 // inherit from point normal and series hover
9282 pointAttr[SELECT_STATE] = series.convertAttribs(
9283 stateOptions[SELECT_STATE],
9284 seriesPointAttr[SELECT_STATE],
9285 pointAttr[NORMAL_STATE]
9290 // no marker config object is created: copy a reference to the series-wide
9291 // attribute collection
9292 } else {
9293 pointAttr = seriesPointAttr;
9296 point.pointAttr = pointAttr;
9304 * Clear DOM objects and free up memory
9306 destroy: function() {
9307 var series = this,
9308 chart = series.chart,
9309 //chartSeries = series.chart.series,
9310 clipRect = series.clipRect,
9311 issue134 = /\/5[0-9\.]+ (Safari|Mobile)\//.test(userAgent), // todo: update when Safari bug is fixed
9312 destroy,
9313 prop;
9315 // remove all events
9316 removeEvent(series);
9318 // remove legend items
9319 if (series.legendItem) {
9320 series.chart.legend.destroyItem(series);
9323 // destroy all points with their elements
9324 each(series.data, function(point) {
9325 point.destroy();
9327 // destroy all SVGElements associated to the series
9328 each(['area', 'graph', 'dataLabelsGroup', 'group', 'tracker'], function(prop) {
9329 if (series[prop]) {
9331 // issue 134 workaround
9332 destroy = issue134 && prop === 'group' ?
9333 'hide' :
9334 'destroy';
9336 series[prop][destroy]();
9340 // remove from hoverSeries
9341 if (chart.hoverSeries === series) {
9342 chart.hoverSeries = null;
9344 erase(chart.series, series);
9346 // clear all members
9347 for (prop in series) {
9348 delete series[prop];
9353 * Draw the data labels
9355 drawDataLabels: function() {
9356 if (this.options.dataLabels.enabled) {
9357 var series = this,
9360 data = series.data,
9361 options = series.options.dataLabels,
9362 str,
9363 dataLabelsGroup = series.dataLabelsGroup,
9364 chart = series.chart,
9365 inverted = chart.inverted,
9366 seriesType = series.type,
9367 color,
9368 stacking = series.options.stacking,
9369 isBarLike = seriesType === 'column' || seriesType === 'bar',
9370 vAlignIsNull = options.verticalAlign === null,
9371 yIsNull = options.y === null;
9373 if (isBarLike) {
9374 if (stacking) {
9375 // In stacked series the default label placement is inside the bars
9376 if (vAlignIsNull) {
9377 options = merge(options, {verticalAlign: 'middle'});
9380 // If no y delta is specified, try to create a good default
9381 if (yIsNull) {
9382 options = merge(options, {y: {top: 14, middle: 4, bottom: -6}[options.verticalAlign]});
9384 } else {
9385 // In non stacked series the default label placement is on top of the bars
9386 if (vAlignIsNull) {
9387 options = merge(options, {verticalAlign: 'top'});
9390 // If no y delta is specified, set the default
9391 if (yIsNull) {
9392 options = merge(options, {y: -6});
9397 // create a separate group for the data labels to avoid rotation
9398 if (!dataLabelsGroup) {
9399 dataLabelsGroup = series.dataLabelsGroup =
9400 chart.renderer.g('data-labels')
9401 .attr({
9402 visibility: series.visible ? VISIBLE : HIDDEN,
9403 zIndex: 6
9405 .translate(chart.plotLeft, chart.plotTop)
9406 .add();
9409 // determine the color
9410 color = options.color;
9411 if (color === 'auto') { // 1.0 backwards compatibility
9412 color = null;
9414 options.style.color = pick(color, series.color);
9416 // make the labels for each point
9417 each(data, function(point, i){
9418 var barX = point.barX,
9419 plotX = (barX && barX + point.barW / 2) || point.plotX || -999,
9420 plotY = pick(point.plotY, -999),
9421 dataLabel = point.dataLabel,
9422 align = options.align;
9424 // get the string
9425 str = options.formatter.call(point.getLabelConfig());
9426 x = (inverted ? chart.plotWidth - plotY : plotX) + options.x;
9427 y = (inverted ? chart.plotHeight - plotX : plotY) + options.y;
9429 // in columns, align the string to the column
9430 if (seriesType === 'column') {
9431 x += { left: -1, right: 1 }[align] * point.barW / 2 || 0;
9434 // update existing label
9435 if (dataLabel) {
9436 // vertically centered
9437 if (inverted && !options.y) {
9438 y = y + pInt(dataLabel.styles.lineHeight) * 0.9 - dataLabel.getBBox().height / 2;
9440 dataLabel
9441 .attr({
9442 text: str
9443 }).animate({
9444 x: x,
9445 y: y
9447 // create new label
9448 } else if (defined(str)) {
9449 dataLabel = point.dataLabel = chart.renderer.text(
9450 str,
9454 .attr({
9455 align: align,
9456 rotation: options.rotation,
9457 zIndex: 1
9459 .css(options.style)
9460 .add(dataLabelsGroup);
9461 // vertically centered
9462 if (inverted && !options.y) {
9463 dataLabel.attr({
9464 y: y + pInt(dataLabel.styles.lineHeight) * 0.9 - dataLabel.getBBox().height / 2
9470 /*if (series.isCartesian) {
9471 dataLabel[chart.isInsidePlot(plotX, plotY) ? 'show' : 'hide']();
9474 if (isBarLike && series.options.stacking) {
9475 var barY = point.barY,
9476 barW = point.barW,
9477 barH = point.barH;
9479 dataLabel.align(options, null,
9481 x: inverted ? chart.plotWidth - barY - barH : barX,
9482 y: inverted ? chart.plotHeight - barX - barW : barY,
9483 width: inverted ? barH : barW,
9484 height: inverted ? barW : barH
9492 * Draw the actual graph
9494 drawGraph: function(state) {
9495 var series = this,
9496 options = series.options,
9497 chart = series.chart,
9498 graph = series.graph,
9499 graphPath = [],
9500 fillColor,
9501 area = series.area,
9502 group = series.group,
9503 color = options.lineColor || series.color,
9504 lineWidth = options.lineWidth,
9505 dashStyle = options.dashStyle,
9506 segmentPath,
9507 renderer = chart.renderer,
9508 translatedThreshold = series.yAxis.getThreshold(options.threshold || 0),
9509 useArea = /^area/.test(series.type),
9510 singlePoints = [], // used in drawTracker
9511 areaPath = [],
9512 attribs;
9515 // divide into segments and build graph and area paths
9516 each(series.segments, function(segment) {
9517 segmentPath = [];
9519 // build the segment line
9520 each(segment, function(point, i) {
9522 if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
9523 segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i));
9525 } else {
9527 // moveTo or lineTo
9528 segmentPath.push(i ? L : M);
9530 // step line?
9531 if (i && options.step) {
9532 var lastPoint = segment[i - 1];
9533 segmentPath.push(
9534 point.plotX,
9535 lastPoint.plotY
9539 // normal line to next point
9540 segmentPath.push(
9541 point.plotX,
9542 point.plotY
9547 // add the segment to the graph, or a single point for tracking
9548 if (segment.length > 1) {
9549 graphPath = graphPath.concat(segmentPath);
9550 } else {
9551 singlePoints.push(segment[0]);
9554 // build the area
9555 if (useArea) {
9556 var areaSegmentPath = [],
9558 segLength = segmentPath.length;
9559 for (i = 0; i < segLength; i++) {
9560 areaSegmentPath.push(segmentPath[i]);
9562 if (segLength === 3) { // for animation from 1 to two points
9563 areaSegmentPath.push(L, segmentPath[1], segmentPath[2]);
9565 if (options.stacking && series.type !== 'areaspline') {
9566 // follow stack back. Todo: implement areaspline
9567 for (i = segment.length - 1; i >= 0; i--) {
9568 areaSegmentPath.push(segment[i].plotX, segment[i].yBottom);
9571 } else { // follow zero line back
9572 areaSegmentPath.push(
9574 segment[segment.length - 1].plotX,
9575 translatedThreshold,
9577 segment[0].plotX,
9578 translatedThreshold
9581 areaPath = areaPath.concat(areaSegmentPath);
9585 // used in drawTracker:
9586 series.graphPath = graphPath;
9587 series.singlePoints = singlePoints;
9589 // draw the area if area series or areaspline
9590 if (useArea) {
9591 fillColor = pick(
9592 options.fillColor,
9593 Color(series.color).setOpacity(options.fillOpacity || 0.75).get()
9595 if (area) {
9596 area.animate({ d: areaPath });
9598 } else {
9599 // draw the area
9600 series.area = series.chart.renderer.path(areaPath)
9601 .attr({
9602 fill: fillColor
9603 }).add(group);
9607 // draw the graph
9608 if (graph) {
9609 //graph.animate({ d: graphPath.join(' ') });
9610 graph.animate({ d: graphPath });
9612 } else {
9613 if (lineWidth) {
9614 attribs = {
9615 'stroke': color,
9616 'stroke-width': lineWidth
9618 if (dashStyle) {
9619 attribs.dashstyle = dashStyle;
9622 series.graph = renderer.path(graphPath)
9623 .attr(attribs).add(group).shadow(options.shadow);
9630 * Render the graph and markers
9632 render: function() {
9633 var series = this,
9634 chart = series.chart,
9635 group,
9636 setInvert,
9637 options = series.options,
9638 animation = options.animation,
9639 doAnimation = animation && series.animate,
9640 duration = doAnimation ? (animation && animation.duration) || 500 : 0,
9641 clipRect = series.clipRect,
9642 renderer = chart.renderer;
9645 // Add plot area clipping rectangle. If this is before chart.hasRendered,
9646 // create one shared clipRect.
9647 if (!clipRect) {
9648 clipRect = series.clipRect = !chart.hasRendered && chart.clipRect ?
9649 chart.clipRect :
9650 renderer.clipRect(0, 0, chart.plotSizeX, chart.plotSizeY);
9651 if (!chart.clipRect) {
9652 chart.clipRect = clipRect;
9657 // the group
9658 if (!series.group) {
9659 group = series.group = renderer.g('series');
9661 if (chart.inverted) {
9662 setInvert = function() {
9663 group.attr({
9664 width: chart.plotWidth,
9665 height: chart.plotHeight
9666 }).invert();
9669 setInvert(); // do it now
9670 addEvent(chart, 'resize', setInvert); // do it on resize
9672 group.clip(series.clipRect)
9673 .attr({
9674 visibility: series.visible ? VISIBLE : HIDDEN,
9675 zIndex: options.zIndex
9677 .translate(chart.plotLeft, chart.plotTop)
9678 .add(chart.seriesGroup);
9681 series.drawDataLabels();
9683 // initiate the animation
9684 if (doAnimation) {
9685 series.animate(true);
9688 // cache attributes for shapes
9689 //series.getAttribs();
9691 // draw the graph if any
9692 if (series.drawGraph) {
9693 series.drawGraph();
9696 // draw the points
9697 series.drawPoints();
9699 // draw the mouse tracking area
9700 if (series.options.enableMouseTracking !== false) {
9701 series.drawTracker();
9704 // run the animation
9705 if (doAnimation) {
9706 series.animate();
9709 // finish the individual clipRect
9710 setTimeout(function() {
9711 clipRect.isAnimating = false;
9712 group = series.group; // can be destroyed during the timeout
9713 if (group && clipRect !== chart.clipRect && clipRect.renderer) {
9714 group.clip((series.clipRect = chart.clipRect));
9715 clipRect.destroy();
9717 }, duration);
9720 series.isDirty = false; // means data is in accordance with what you see
9725 * Redraw the series after an update in the axes.
9727 redraw: function() {
9728 var series = this,
9729 chart = series.chart,
9730 clipRect = series.clipRect,
9731 group = series.group;
9733 /*if (clipRect) {
9734 stop(clipRect);
9735 clipRect.animate({ // for chart resize
9736 width: chart.plotSizeX,
9737 height: chart.plotSizeY
9741 // reposition on resize
9742 if (group) {
9743 if (chart.inverted) {
9744 group.attr({
9745 width: chart.plotWidth,
9746 height: chart.plotHeight
9750 group.animate({
9751 translateX: chart.plotLeft,
9752 translateY: chart.plotTop
9756 series.translate();
9757 series.setTooltipPoints(true);
9758 series.render();
9762 * Set the state of the graph
9764 setState: function(state) {
9765 var series = this,
9766 options = series.options,
9767 graph = series.graph,
9768 stateOptions = options.states,
9769 lineWidth = options.lineWidth;
9771 state = state || NORMAL_STATE;
9773 if (series.state !== state) {
9774 series.state = state;
9776 if (stateOptions[state] && stateOptions[state].enabled === false) {
9777 return;
9780 if (state) {
9781 lineWidth = stateOptions[state].lineWidth || lineWidth + 1;
9784 if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
9785 graph.attr({ // use attr because animate will cause any other animation on the graph to stop
9786 'stroke-width': lineWidth
9787 }, state ? 0 : 500);
9793 * Set the visibility of the graph
9795 * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED,
9796 * the visibility is toggled.
9798 setVisible: function(vis, redraw) {
9799 var series = this,
9800 chart = series.chart,
9801 legendItem = series.legendItem,
9802 seriesGroup = series.group,
9803 seriesTracker = series.tracker,
9804 dataLabelsGroup = series.dataLabelsGroup,
9805 showOrHide,
9807 data = series.data,
9808 point,
9809 ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
9810 oldVisibility = series.visible;
9812 // if called without an argument, toggle visibility
9813 series.visible = vis = vis === UNDEFINED ? !oldVisibility : vis;
9814 showOrHide = vis ? 'show' : 'hide';
9816 // show or hide series
9817 if (seriesGroup) { // pies don't have one
9818 seriesGroup[showOrHide]();
9821 // show or hide trackers
9822 if (seriesTracker) {
9823 seriesTracker[showOrHide]();
9824 } else {
9825 i = data.length;
9826 while (i--) {
9827 point = data[i];
9828 if (point.tracker) {
9829 point.tracker[showOrHide]();
9835 if (dataLabelsGroup) {
9836 dataLabelsGroup[showOrHide]();
9839 if (legendItem) {
9840 chart.legend.colorizeItem(series, vis);
9844 // rescale or adapt to resized chart
9845 series.isDirty = true;
9846 // in a stack, all other series are affected
9847 if (series.options.stacking) {
9848 each(chart.series, function(otherSeries) {
9849 if (otherSeries.options.stacking && otherSeries.visible) {
9850 otherSeries.isDirty = true;
9855 if (ignoreHiddenSeries) {
9856 chart.isDirtyBox = true;
9858 if (redraw !== false) {
9859 chart.redraw();
9862 fireEvent(series, showOrHide);
9866 * Show the graph
9868 show: function() {
9869 this.setVisible(true);
9873 * Hide the graph
9875 hide: function() {
9876 this.setVisible(false);
9881 * Set the selected state of the graph
9883 * @param selected {Boolean} True to select the series, false to unselect. If
9884 * UNDEFINED, the selection state is toggled.
9886 select: function(selected) {
9887 var series = this;
9888 // if called without an argument, toggle
9889 series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected;
9891 if (series.checkbox) {
9892 series.checkbox.checked = selected;
9895 fireEvent(series, selected ? 'select' : 'unselect');
9900 * Draw the tracker object that sits above all data labels and markers to
9901 * track mouse events on the graph or points. For the line type charts
9902 * the tracker uses the same graphPath, but with a greater stroke width
9903 * for better control.
9905 drawTracker: function() {
9906 var series = this,
9907 options = series.options,
9908 trackerPath = [].concat(series.graphPath),
9909 trackerPathLength = trackerPath.length,
9910 chart = series.chart,
9911 snap = chart.options.tooltip.snap,
9912 tracker = series.tracker,
9913 cursor = options.cursor,
9914 css = cursor && { cursor: cursor },
9915 singlePoints = series.singlePoints,
9916 singlePoint,
9919 // Extend end points. A better way would be to use round linecaps,
9920 // but those are not clickable in VML.
9921 if (trackerPathLength) {
9922 i = trackerPathLength + 1;
9923 while (i--) {
9924 if (trackerPath[i] === M) { // extend left side
9925 trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L);
9927 if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side
9928 trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]);
9933 // handle single points
9934 for (i = 0; i < singlePoints.length; i++) {
9935 singlePoint = singlePoints[i];
9936 trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
9937 L, singlePoint.plotX + snap, singlePoint.plotY);
9940 // draw the tracker
9941 if (tracker) {
9942 tracker.attr({ d: trackerPath });
9944 } else { // create
9945 series.tracker = chart.renderer.path(trackerPath)
9946 .attr({
9947 isTracker: true,
9948 stroke: TRACKER_FILL,
9949 fill: NONE,
9950 'stroke-width' : options.lineWidth + 2 * snap,
9951 visibility: series.visible ? VISIBLE : HIDDEN,
9952 zIndex: 1
9954 .on(hasTouch ? 'touchstart' : 'mouseover', function() {
9955 if (chart.hoverSeries !== series) {
9956 series.onMouseOver();
9959 .on('mouseout', function() {
9960 if (!options.stickyTracking) {
9961 series.onMouseOut();
9964 .css(css)
9965 .add(chart.trackerGroup);
9970 }; // end Series prototype
9974 * LineSeries object
9976 var LineSeries = extendClass(Series);
9977 seriesTypes.line = LineSeries;
9980 * AreaSeries object
9982 var AreaSeries = extendClass(Series, {
9983 type: 'area'
9985 seriesTypes.area = AreaSeries;
9991 * SplineSeries object
9993 var SplineSeries = extendClass( Series, {
9994 type: 'spline',
9997 * Draw the actual graph
9999 getPointSpline: function(segment, point, i) {
10000 var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc
10001 denom = smoothing + 1,
10002 plotX = point.plotX,
10003 plotY = point.plotY,
10004 lastPoint = segment[i - 1],
10005 nextPoint = segment[i + 1],
10006 leftContX,
10007 leftContY,
10008 rightContX,
10009 rightContY,
10010 ret;
10012 // find control points
10013 if (i && i < segment.length - 1) {
10014 var lastX = lastPoint.plotX,
10015 lastY = lastPoint.plotY,
10016 nextX = nextPoint.plotX,
10017 nextY = nextPoint.plotY,
10018 correction;
10020 leftContX = (smoothing * plotX + lastX) / denom;
10021 leftContY = (smoothing * plotY + lastY) / denom;
10022 rightContX = (smoothing * plotX + nextX) / denom;
10023 rightContY = (smoothing * plotY + nextY) / denom;
10025 // have the two control points make a straight line through main point
10026 correction = ((rightContY - leftContY) * (rightContX - plotX)) /
10027 (rightContX - leftContX) + plotY - rightContY;
10029 leftContY += correction;
10030 rightContY += correction;
10032 // to prevent false extremes, check that control points are between
10033 // neighbouring points' y values
10034 if (leftContY > lastY && leftContY > plotY) {
10035 leftContY = mathMax(lastY, plotY);
10036 rightContY = 2 * plotY - leftContY; // mirror of left control point
10037 } else if (leftContY < lastY && leftContY < plotY) {
10038 leftContY = mathMin(lastY, plotY);
10039 rightContY = 2 * plotY - leftContY;
10041 if (rightContY > nextY && rightContY > plotY) {
10042 rightContY = mathMax(nextY, plotY);
10043 leftContY = 2 * plotY - rightContY;
10044 } else if (rightContY < nextY && rightContY < plotY) {
10045 rightContY = mathMin(nextY, plotY);
10046 leftContY = 2 * plotY - rightContY;
10049 // record for drawing in next point
10050 point.rightContX = rightContX;
10051 point.rightContY = rightContY;
10055 // moveTo or lineTo
10056 if (!i) {
10057 ret = [M, plotX, plotY];
10060 // curve from last point to this
10061 else {
10062 ret = [
10063 'C',
10064 lastPoint.rightContX || lastPoint.plotX,
10065 lastPoint.rightContY || lastPoint.plotY,
10066 leftContX || plotX,
10067 leftContY || plotY,
10068 plotX,
10069 plotY
10071 lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later
10073 return ret;
10076 seriesTypes.spline = SplineSeries;
10081 * AreaSplineSeries object
10083 var AreaSplineSeries = extendClass(SplineSeries, {
10084 type: 'areaspline'
10086 seriesTypes.areaspline = AreaSplineSeries;
10089 * ColumnSeries object
10091 var ColumnSeries = extendClass(Series, {
10092 type: 'column',
10093 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
10094 stroke: 'borderColor',
10095 'stroke-width': 'borderWidth',
10096 fill: 'color',
10097 r: 'borderRadius'
10099 init: function() {
10100 Series.prototype.init.apply(this, arguments);
10102 var series = this,
10103 chart = series.chart;
10105 // flag the chart in order to pad the x axis
10106 chart.hasColumn = true;
10108 // if the series is added dynamically, force redraw of other
10109 // series affected by a new column
10110 if (chart.hasRendered) {
10111 each(chart.series, function(otherSeries) {
10112 if (otherSeries.type === series.type) {
10113 otherSeries.isDirty = true;
10120 * Translate each point to the plot area coordinate system and find shape positions
10122 translate: function() {
10123 var series = this,
10124 chart = series.chart,
10125 options = series.options,
10126 stacking = options.stacking,
10127 borderWidth = options.borderWidth,
10128 columnCount = 0,
10129 reversedXAxis = series.xAxis.reversed,
10130 categories = series.xAxis.categories,
10131 stackGroups = {},
10132 stackKey,
10133 columnIndex;
10135 Series.prototype.translate.apply(series);
10137 // Get the total number of column type series.
10138 // This is called on every series. Consider moving this logic to a
10139 // chart.orderStacks() function and call it on init, addSeries and removeSeries
10140 each(chart.series, function(otherSeries) {
10141 if (otherSeries.type === series.type && otherSeries.visible) {
10142 if (otherSeries.options.stacking) {
10143 stackKey = otherSeries.stackKey;
10144 if (stackGroups[stackKey] === UNDEFINED) {
10145 stackGroups[stackKey] = columnCount++;
10147 columnIndex = stackGroups[stackKey];
10148 } else {
10149 columnIndex = columnCount++;
10151 otherSeries.columnIndex = columnIndex;
10155 // calculate the width and position of each column based on
10156 // the number of column series in the plot, the groupPadding
10157 // and the pointPadding options
10158 var data = series.data,
10159 closestPoints = series.closestPoints,
10160 categoryWidth = mathAbs(
10161 data[1] ? data[closestPoints].plotX - data[closestPoints - 1].plotX :
10162 chart.plotSizeX / ((categories && categories.length) || 1)
10164 groupPadding = categoryWidth * options.groupPadding,
10165 groupWidth = categoryWidth - 2 * groupPadding,
10166 pointOffsetWidth = groupWidth / columnCount,
10167 optionPointWidth = options.pointWidth,
10168 pointPadding = defined(optionPointWidth) ? (pointOffsetWidth - optionPointWidth) / 2 :
10169 pointOffsetWidth * options.pointPadding,
10170 pointWidth = mathMax(pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), 1),
10171 colIndex = (reversedXAxis ? columnCount -
10172 series.columnIndex : series.columnIndex) || 0,
10173 pointXOffset = pointPadding + (groupPadding + colIndex *
10174 pointOffsetWidth -(categoryWidth / 2)) *
10175 (reversedXAxis ? -1 : 1),
10176 threshold = options.threshold || 0,
10177 translatedThreshold = series.yAxis.getThreshold(threshold),
10178 minPointLength = pick(options.minPointLength, 5);
10180 // record the new values
10181 each(data, function(point) {
10182 var plotY = point.plotY,
10183 yBottom = point.yBottom || translatedThreshold,
10184 barX = point.plotX + pointXOffset,
10185 barY = mathCeil(mathMin(plotY, yBottom)),
10186 barH = mathCeil(mathMax(plotY, yBottom) - barY),
10187 stack = series.yAxis.stacks[(point.y < 0 ? '-' : '') + series.stackKey],
10188 trackerY,
10189 shapeArgs;
10191 // Record the offset'ed position and width of the bar to be able to align the stacking total correctly
10192 if (stacking && series.visible && stack && stack[point.x]) {
10193 stack[point.x].setOffset(pointXOffset, pointWidth);
10196 // handle options.minPointLength and tracker for small points
10197 if (mathAbs(barH) < minPointLength) {
10198 if (minPointLength) {
10199 barH = minPointLength;
10200 barY =
10201 mathAbs(barY - translatedThreshold) > minPointLength ? // stacked
10202 yBottom - minPointLength : // keep position
10203 translatedThreshold - (plotY <= translatedThreshold ? minPointLength : 0);
10205 trackerY = barY - 3;
10208 extend(point, {
10209 barX: barX,
10210 barY: barY,
10211 barW: pointWidth,
10212 barH: barH
10215 // create shape type and shape args that are reused in drawPoints and drawTracker
10216 point.shapeType = 'rect';
10217 shapeArgs = extend(chart.renderer.Element.prototype.crisp.apply({}, [
10218 borderWidth,
10219 barX,
10220 barY,
10221 pointWidth,
10222 barH
10223 ]), {
10224 r: options.borderRadius
10226 if (borderWidth % 2) { // correct for shorting in crisp method, visible in stacked columns with 1px border
10227 shapeArgs.y -= 1;
10228 shapeArgs.height += 1;
10230 point.shapeArgs = shapeArgs;
10232 // make small columns responsive to mouse
10233 point.trackerArgs = defined(trackerY) && merge(point.shapeArgs, {
10234 height: mathMax(6, barH + 3),
10235 y: trackerY
10241 getSymbol: function(){
10244 /**
10245 * Columns have no graph
10247 drawGraph: function() {},
10250 * Draw the columns. For bars, the series.group is rotated, so the same coordinates
10251 * apply for columns and bars. This method is inherited by scatter series.
10254 drawPoints: function() {
10255 var series = this,
10256 options = series.options,
10257 renderer = series.chart.renderer,
10258 graphic,
10259 shapeArgs;
10262 // draw the columns
10263 each(series.data, function(point) {
10264 var plotY = point.plotY;
10265 if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
10266 graphic = point.graphic;
10267 shapeArgs = point.shapeArgs;
10268 if (graphic) { // update
10269 stop(graphic);
10270 graphic.animate(shapeArgs);
10272 } else {
10273 point.graphic = renderer[point.shapeType](shapeArgs)
10274 .attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE])
10275 .add(series.group)
10276 .shadow(options.shadow);
10283 * Draw the individual tracker elements.
10284 * This method is inherited by scatter and pie charts too.
10286 drawTracker: function() {
10287 var series = this,
10288 chart = series.chart,
10289 renderer = chart.renderer,
10290 shapeArgs,
10291 tracker,
10292 trackerLabel = +new Date(),
10293 cursor = series.options.cursor,
10294 css = cursor && { cursor: cursor },
10295 rel;
10297 each(series.data, function(point) {
10298 tracker = point.tracker;
10299 shapeArgs = point.trackerArgs || point.shapeArgs;
10300 delete shapeArgs.strokeWidth;
10301 if (point.y !== null) {
10302 if (tracker) {// update
10303 tracker.attr(shapeArgs);
10305 } else {
10306 point.tracker =
10307 renderer[point.shapeType](shapeArgs)
10308 .attr({
10309 isTracker: trackerLabel,
10310 fill: TRACKER_FILL,
10311 visibility: series.visible ? VISIBLE : HIDDEN,
10312 zIndex: 1
10314 .on(hasTouch ? 'touchstart' : 'mouseover', function(event) {
10315 rel = event.relatedTarget || event.fromElement;
10316 if (chart.hoverSeries !== series && attr(rel, 'isTracker') !== trackerLabel) {
10317 series.onMouseOver();
10319 point.onMouseOver();
10322 .on('mouseout', function(event) {
10323 if (!series.options.stickyTracking) {
10324 rel = event.relatedTarget || event.toElement;
10325 if (attr(rel, 'isTracker') !== trackerLabel) {
10326 series.onMouseOut();
10330 .css(css)
10331 .add(point.group || chart.trackerGroup); // pies have point group - see issue #118
10339 * Animate the column heights one by one from zero
10340 * @param {Boolean} init Whether to initialize the animation or run it
10342 animate: function(init) {
10343 var series = this,
10344 data = series.data;
10346 if (!init) { // run the animation
10348 * Note: Ideally the animation should be initialized by calling
10349 * series.group.hide(), and then calling series.group.show()
10350 * after the animation was started. But this rendered the shadows
10351 * invisible in IE8 standards mode. If the columns flicker on large
10352 * datasets, this is the cause.
10355 each(data, function(point) {
10356 var graphic = point.graphic,
10357 shapeArgs = point.shapeArgs;
10359 if (graphic) {
10360 // start values
10361 graphic.attr({
10362 height: 0,
10363 y: series.yAxis.translate(0, 0, 1)
10366 // animate
10367 graphic.animate({
10368 height: shapeArgs.height,
10369 y: shapeArgs.y
10370 }, series.options.animation);
10375 // delete this function to allow it only once
10376 series.animate = null;
10381 * Remove this series from the chart
10383 remove: function() {
10384 var series = this,
10385 chart = series.chart;
10387 // column and bar series affects other series of the same type
10388 // as they are either stacked or grouped
10389 if (chart.hasRendered) {
10390 each(chart.series, function(otherSeries) {
10391 if (otherSeries.type === series.type) {
10392 otherSeries.isDirty = true;
10397 Series.prototype.remove.apply(series, arguments);
10400 seriesTypes.column = ColumnSeries;
10402 var BarSeries = extendClass(ColumnSeries, {
10403 type: 'bar',
10404 init: function(chart) {
10405 chart.inverted = this.inverted = true;
10406 ColumnSeries.prototype.init.apply(this, arguments);
10409 seriesTypes.bar = BarSeries;
10412 * The scatter series class
10414 var ScatterSeries = extendClass(Series, {
10415 type: 'scatter',
10418 * Extend the base Series' translate method by adding shape type and
10419 * arguments for the point trackers
10421 translate: function() {
10422 var series = this;
10424 Series.prototype.translate.apply(series);
10426 each(series.data, function(point) {
10427 point.shapeType = 'circle';
10428 point.shapeArgs = {
10429 x: point.plotX,
10430 y: point.plotY,
10431 r: series.chart.options.tooltip.snap
10438 * Create individual tracker elements for each point
10440 //drawTracker: ColumnSeries.prototype.drawTracker,
10441 drawTracker: function() {
10442 var series = this,
10443 cursor = series.options.cursor,
10444 css = cursor && { cursor: cursor },
10445 graphic;
10447 each(series.data, function(point) {
10448 graphic = point.graphic;
10449 if (graphic) { // doesn't exist for null points
10450 graphic
10451 .attr({ isTracker: true })
10452 .on('mouseover', function(event) {
10453 series.onMouseOver();
10454 point.onMouseOver();
10456 .on('mouseout', function(event) {
10457 if (!series.options.stickyTracking) {
10458 series.onMouseOut();
10461 .css(css);
10468 * Cleaning the data is not necessary in a scatter plot
10470 cleanData: function() {}
10472 seriesTypes.scatter = ScatterSeries;
10475 * Extended point object for pies
10477 var PiePoint = extendClass(Point, {
10479 * Initiate the pie slice
10481 init: function () {
10483 Point.prototype.init.apply(this, arguments);
10485 var point = this,
10486 toggleSlice;
10488 //visible: options.visible !== false,
10489 extend(point, {
10490 visible: point.visible !== false,
10491 name: pick(point.name, 'Slice')
10494 // add event listener for select
10495 toggleSlice = function() {
10496 point.slice();
10498 addEvent(point, 'select', toggleSlice);
10499 addEvent(point, 'unselect', toggleSlice);
10501 return point;
10505 * Toggle the visibility of the pie slice
10506 * @param {Boolean} vis Whether to show the slice or not. If undefined, the
10507 * visibility is toggled
10509 setVisible: function(vis) {
10510 var point = this,
10511 chart = point.series.chart,
10512 tracker = point.tracker,
10513 dataLabel = point.dataLabel,
10514 connector = point.connector,
10515 method;
10517 // if called without an argument, toggle visibility
10518 point.visible = vis = vis === UNDEFINED ? !point.visible : vis;
10520 method = vis ? 'show' : 'hide';
10522 point.group[method]();
10523 if (tracker) {
10524 tracker[method]();
10526 if (dataLabel) {
10527 dataLabel[method]();
10529 if (connector) {
10530 connector[method]();
10532 if (point.legendItem) {
10533 chart.legend.colorizeItem(point, vis);
10538 * Set or toggle whether the slice is cut out from the pie
10539 * @param {Boolean} sliced When undefined, the slice state is toggled
10540 * @param {Boolean} redraw Whether to redraw the chart. True by default.
10542 slice: function(sliced, redraw, animation) {
10543 var point = this,
10544 series = point.series,
10545 chart = series.chart,
10546 slicedTranslation = point.slicedTranslation,
10547 translation;
10549 setAnimation(animation, chart);
10551 // redraw is true by default
10552 redraw = pick(redraw, true);
10554 // if called without an argument, toggle
10555 sliced = point.sliced = defined(sliced) ? sliced : !point.sliced;
10557 translation = {
10558 translateX: (sliced ? slicedTranslation[0] : chart.plotLeft),
10559 translateY: (sliced ? slicedTranslation[1] : chart.plotTop)
10561 point.group.animate(translation);
10562 if (point.shadowGroup) {
10563 point.shadowGroup.animate(translation);
10570 * The Pie series class
10572 var PieSeries = extendClass(Series, {
10573 type: 'pie',
10574 isCartesian: false,
10575 pointClass: PiePoint,
10576 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
10577 stroke: 'borderColor',
10578 'stroke-width': 'borderWidth',
10579 fill: 'color'
10583 * Pies have one color each point
10585 getColor: function() {
10586 // record first color for use in setData
10587 this.initialColor = colorCounter;
10591 * Animate the column heights one by one from zero
10592 * @param {Boolean} init Whether to initialize the animation or run it
10594 animate: function(init) {
10595 var series = this,
10596 data = series.data;
10598 each(data, function(point) {
10599 var graphic = point.graphic,
10600 args = point.shapeArgs,
10601 up = -mathPI / 2;
10603 if (graphic) {
10604 // start values
10605 graphic.attr({
10606 r: 0,
10607 start: up,
10608 end: up
10611 // animate
10612 graphic.animate({
10613 r: args.r,
10614 start: args.start,
10615 end: args.end
10616 }, series.options.animation);
10620 // delete this function to allow it only once
10621 series.animate = null;
10625 * Do translation for pie slices
10627 translate: function() {
10628 var total = 0,
10629 series = this,
10630 cumulative = -0.25, // start at top
10631 precision = 1000, // issue #172
10632 options = series.options,
10633 slicedOffset = options.slicedOffset,
10634 connectorOffset = slicedOffset + options.borderWidth,
10635 positions = options.center.concat([options.size, options.innerSize || 0]),
10636 chart = series.chart,
10637 plotWidth = chart.plotWidth,
10638 plotHeight = chart.plotHeight,
10639 start,
10640 end,
10641 angle,
10642 data = series.data,
10643 circ = 2 * mathPI,
10644 fraction,
10645 smallestSize = mathMin(plotWidth, plotHeight),
10646 isPercent,
10647 radiusX, // the x component of the radius vector for a given point
10648 radiusY,
10649 labelDistance = options.dataLabels.distance;
10651 // get positions - either an integer or a percentage string must be given
10652 positions = map(positions, function(length, i) {
10654 isPercent = /%$/.test(length);
10655 return isPercent ?
10656 // i == 0: centerX, relative to width
10657 // i == 1: centerY, relative to height
10658 // i == 2: size, relative to smallestSize
10659 // i == 4: innerSize, relative to smallestSize
10660 [plotWidth, plotHeight, smallestSize, smallestSize][i] *
10661 pInt(length) / 100:
10662 length;
10665 // utility for getting the x value from a given y, used for anticollision logic in data labels
10666 series.getX = function(y, left) {
10668 angle = math.asin((y - positions[1]) / (positions[2] / 2 + labelDistance));
10670 return positions[0] +
10671 (left ? -1 : 1) *
10672 (mathCos(angle) * (positions[2] / 2 + labelDistance));
10675 // set center for later use
10676 series.center = positions;
10678 // get the total sum
10679 each(data, function(point) {
10680 total += point.y;
10683 each(data, function(point) {
10684 // set start and end angle
10685 fraction = total ? point.y / total : 0;
10686 start = mathRound(cumulative * circ * precision) / precision;
10687 cumulative += fraction;
10688 end = mathRound(cumulative * circ * precision) / precision;
10690 // set the shape
10691 point.shapeType = 'arc';
10692 point.shapeArgs = {
10693 x: positions[0],
10694 y: positions[1],
10695 r: positions[2] / 2,
10696 innerR: positions[3] / 2,
10697 start: start,
10698 end: end
10701 // center for the sliced out slice
10702 angle = (end + start) / 2;
10703 point.slicedTranslation = map([
10704 mathCos(angle) * slicedOffset + chart.plotLeft,
10705 mathSin(angle) * slicedOffset + chart.plotTop
10706 ], mathRound);
10708 // set the anchor point for tooltips
10709 radiusX = mathCos(angle) * positions[2] / 2;
10710 radiusY = mathSin(angle) * positions[2] / 2;
10711 point.tooltipPos = [
10712 positions[0] + radiusX * 0.7,
10713 positions[1] + radiusY * 0.7
10716 // set the anchor point for data labels
10717 point.labelPos = [
10718 positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector
10719 positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a
10720 positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie
10721 positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a
10722 positions[0] + radiusX, // landing point for connector
10723 positions[1] + radiusY, // a/a
10724 labelDistance < 0 ? // alignment
10725 'center' :
10726 angle < circ / 4 ? 'left' : 'right', // alignment
10727 angle // center angle
10731 // API properties
10732 point.percentage = fraction * 100;
10733 point.total = total;
10737 this.setTooltipPoints();
10741 * Render the slices
10743 render: function() {
10744 var series = this;
10746 // cache attributes for shapes
10747 //series.getAttribs();
10749 this.drawPoints();
10751 // draw the mouse tracking area
10752 if (series.options.enableMouseTracking !== false) {
10753 series.drawTracker();
10756 this.drawDataLabels();
10758 if (series.options.animation && series.animate) {
10759 series.animate();
10762 series.isDirty = false; // means data is in accordance with what you see
10766 * Draw the data points
10768 drawPoints: function() {
10769 var series = this,
10770 chart = series.chart,
10771 renderer = chart.renderer,
10772 groupTranslation,
10773 //center,
10774 graphic,
10775 group,
10776 shadow = series.options.shadow,
10777 shadowGroup,
10778 shapeArgs;
10781 // draw the slices
10782 each(series.data, function(point) {
10783 graphic = point.graphic;
10784 shapeArgs = point.shapeArgs;
10785 group = point.group;
10786 shadowGroup = point.shadowGroup;
10788 // put the shadow behind all points
10789 if (shadow && !shadowGroup) {
10790 shadowGroup = point.shadowGroup = renderer.g('shadow')
10791 .attr({ zIndex: 4 })
10792 .add();
10795 // create the group the first time
10796 if (!group) {
10797 group = point.group = renderer.g('point')
10798 .attr({ zIndex: 5 })
10799 .add();
10802 // if the point is sliced, use special translation, else use plot area traslation
10803 groupTranslation = point.sliced ? point.slicedTranslation : [chart.plotLeft, chart.plotTop];
10804 group.translate(groupTranslation[0], groupTranslation[1]);
10805 if (shadowGroup) {
10806 shadowGroup.translate(groupTranslation[0], groupTranslation[1]);
10810 // draw the slice
10811 if (graphic) {
10812 graphic.animate(shapeArgs);
10813 } else {
10814 point.graphic =
10815 renderer.arc(shapeArgs)
10816 .attr(extend(
10817 point.pointAttr[NORMAL_STATE],
10818 { 'stroke-linejoin': 'round' }
10820 .add(point.group)
10821 .shadow(shadow, shadowGroup);
10824 // detect point specific visibility
10825 if (point.visible === false) {
10826 point.setVisible(false);
10834 * Override the base drawDataLabels method by pie specific functionality
10836 drawDataLabels: function() {
10837 var series = this,
10838 data = series.data,
10839 point,
10840 chart = series.chart,
10841 options = series.options.dataLabels,
10842 connectorPadding = pick(options.connectorPadding, 10),
10843 connectorWidth = pick(options.connectorWidth, 1),
10844 connector,
10845 connectorPath,
10846 outside = options.distance > 0,
10847 dataLabel,
10848 labelPos,
10849 labelHeight,
10850 lastY,
10851 centerY = series.center[1],
10852 quarters = [// divide the points into quarters for anti collision
10853 [], // top right
10854 [], // bottom right
10855 [], // bottom left
10856 [] // top left
10860 visibility,
10861 overlapping,
10862 rankArr,
10863 secondPass,
10864 sign,
10865 lowerHalf,
10866 sort,
10867 i = 4,
10870 // run parent method
10871 Series.prototype.drawDataLabels.apply(series);
10873 // arrange points for detection collision
10874 each(data, function(point) {
10875 var angle = point.labelPos[7],
10876 quarter;
10877 if (angle < 0) {
10878 quarter = 0;
10879 } else if (angle < mathPI / 2) {
10880 quarter = 1;
10881 } else if (angle < mathPI) {
10882 quarter = 2;
10883 } else {
10884 quarter = 3;
10886 quarters[quarter].push(point);
10888 quarters[1].reverse();
10889 quarters[3].reverse();
10891 // define the sorting algorithm
10892 sort = function(a,b) {
10893 return a.y > b.y;
10895 /* Loop over the points in each quartile, starting from the top and bottom
10896 * of the pie to detect overlapping labels.
10898 while (i--) {
10899 overlapping = 0;
10901 // create an array for sorting and ranking the points within each quarter
10902 rankArr = [].concat(quarters[i]);
10903 rankArr.sort(sort);
10904 j = rankArr.length;
10905 while (j--) {
10906 rankArr[j].rank = j;
10909 /* In the first pass, count the number of overlapping labels. In the second
10910 * pass, remove the labels with lowest rank/values.
10912 for (secondPass = 0; secondPass < 2; secondPass++) {
10913 lowerHalf = i % 3;
10914 lastY = lowerHalf ? 9999 : -9999;
10915 sign = lowerHalf ? -1 : 1;
10917 for (j = 0; j < quarters[i].length; j++) {
10918 point = quarters[i][j];
10920 dataLabel = point.dataLabel;
10921 if (dataLabel) {
10922 labelPos = point.labelPos;
10923 visibility = VISIBLE;
10924 x = labelPos[0];
10925 y = labelPos[1];
10928 // assume all labels have equal height
10929 if (!labelHeight) {
10930 labelHeight = dataLabel && dataLabel.getBBox().height;
10933 // anticollision
10934 if (outside) {
10935 if (secondPass && point.rank < overlapping) {
10936 visibility = HIDDEN;
10937 } else if ((!lowerHalf && y < lastY + labelHeight) ||
10938 (lowerHalf && y > lastY - labelHeight)) {
10939 y = lastY + sign * labelHeight;
10940 x = series.getX(y, i > 1);
10941 if ((!lowerHalf && y + labelHeight > centerY) ||
10942 (lowerHalf && y -labelHeight < centerY)) {
10943 if (secondPass) {
10944 visibility = HIDDEN;
10945 } else {
10946 overlapping++;
10952 if (point.visible === false) {
10953 visibility = HIDDEN;
10956 if (visibility === VISIBLE) {
10957 lastY = y;
10960 if (secondPass) {
10962 // move or place the data label
10963 dataLabel
10964 .attr({
10965 visibility: visibility,
10966 align: labelPos[6]
10967 })[dataLabel.moved ? 'animate' : 'attr']({
10968 x: x + options.x +
10969 ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0),
10970 y: y + options.y
10972 dataLabel.moved = true;
10974 // draw the connector
10975 if (outside && connectorWidth) {
10976 connector = point.connector;
10978 connectorPath = [
10980 x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
10982 x, y, // first break, next to the label
10984 labelPos[2], labelPos[3], // second break
10986 labelPos[4], labelPos[5] // base
10989 if (connector) {
10990 connector.animate({ d: connectorPath });
10991 connector.attr('visibility', visibility);
10993 } else {
10994 point.connector = connector = series.chart.renderer.path(connectorPath).attr({
10995 'stroke-width': connectorWidth,
10996 stroke: options.connectorColor || '#606060',
10997 visibility: visibility,
10998 zIndex: 3
11000 .translate(chart.plotLeft, chart.plotTop)
11001 .add();
11012 * Draw point specific tracker objects. Inherit directly from column series.
11014 drawTracker: ColumnSeries.prototype.drawTracker,
11017 * Pies don't have point marker symbols
11019 getSymbol: function() {}
11022 seriesTypes.pie = PieSeries;
11025 // global variables
11026 win.Highcharts = {
11027 Chart: Chart,
11028 dateFormat: dateFormat,
11029 pathAnim: pathAnim,
11030 getOptions: getOptions,
11031 numberFormat: numberFormat,
11032 Point: Point,
11033 Color: Color,
11034 Renderer: Renderer,
11035 seriesTypes: seriesTypes,
11036 setOptions: setOptions,
11037 Series: Series,
11039 // Expose utility funcitons for modules
11040 addEvent: addEvent,
11041 createElement: createElement,
11042 discardElement: discardElement,
11043 css: css,
11044 each: each,
11045 extend: extend,
11046 map: map,
11047 merge: merge,
11048 pick: pick,
11049 extendClass: extendClass,
11050 version: '2.1.5'
11052 }());